先来跟鸭鸭理清几个名词吧~
多用户:多个用户同一时间使用计算机
多任务:同时执行几个任务,并且可以在还没有执行完一个任务的时候执行另一个任务
程序:静态,保存在硬盘上的可执行代码
进程:动态,运行中的程序,表示过程(操作系统资源管理的最小单位)
线程:在进程内部,比进程更小的能独立运行的基本单位
PS.与同属一个进程的其他线程共享进程拥有的全部资源
一个线程可以创建和撤销另一个线程,同进程的多个线程可以并行执行(注意并行与并发的区别)
进程标识:ID(唯一的为非负数)
实际用户:标识运行该进程的用户
有效用户:标识来运行该进程的用户身份
父进程和子进程:相当于父亲和儿子
那么如何获取进程标识符呢?
可以使用下面这几个函数:
pid_t getgid(id) //进程id
pid_t getppid(id) //父进程id
pid_t getuid(id) //实际用户id
pid_t geteuid(id) //有效用户id
pid_t getgid(id) //实际组id
pid_t getegid(id) //有效组id
Linux进程的结构和状态是这样的:
Linux中一个进程由三部分组成:代码段,数据段,堆栈段
从内存的低地址到高地址依次为:
代码段:二进制机器代码
数据段:存储已被初始化的变量,包括全局变量和已被初始化的静态变量
未初始化数据段:存储未被初始化的静态变量,又称BBS
堆:用于存放程序运行中动态分配的变量
栈:用于函数调用,保存函数的返回地址,函数的参数,函数内部定义的局部变量
PS.高地址还存储了命令行参数和环境变量
进程有以下状态:
运行状态、可中断等待状态、不可中断等待状态、僵死状态、停止状态
PS.用ps命令查看进程的当前状态
ps运行结果的后缀字符:
< 高优先级进程、N 低优先级进程、L 内存锁页、
s 会话首进程、l 多线程进程、+ 前台进程组
Linux下程序是如何转化为进程的呢?
- 内核将程序读入内存,为程序分配内存空间
- 内核为该进程分配进程标识符pid和其他所需资源
- 内核为该进程保存PID及相应的状态信息,把进程放到运行队列中等待执行.程序转化为进程后就可以被操作系统的调度程序调度执行了
进程的内存影像:
是指内核在内存中如何存放可执行程序文件.在将程序转化为进程的过程中,操作系统将可执行程序由硬盘复制到内存中
可执行程序和内存映像的区别:
- 可执行程序位于硬盘,内存映像位于内存
- 可执行程序没有堆栈(程序被加载到内存中才会分配堆栈)
- 可执行程序中未初始化数据段并不储存在位于硬盘中的可执行文件中
- 可执行程序是静态的,不变的,内存映像动态变化
快来创建一个进程吧~
①由操作系统创建:进程之间平等,不存在资源继承
PS.在系统启动时,操作系统会创建一些进程,承担着管理和分配系统资源的任务,通常被称为系统进程
②由父进程创建:子进程,继承父进程
fork函数:
#include<sys/types.h>
#include<unistd.h>
pid_t fork(void);
这个函数可以说是很特殊了,它有两个返回值,调用一次返回两次。其实就相当于fork一个进程之后,当前进程分裂为两个,一个父进程,一个刚刚fork的子进程;一个返回值是父进程调用fork函数后的返回值,即刚刚创建的子进程的ID,另一个是子进程中fork函数的返回值,该返回值是0
若进程创建失败,则只返回一个-1,失败原因通常是父进程拥有的子进程的个数超过了规定的限制(超生吗哈哈哈..),此时errno为EAGAIN;可供使用的内存不足也会导致进程创建失败(没钱养孩子惹…),此时errno为ENOMEN
所以鸭鸭可以通过返回值来区别父进程和子进程:父进程返回子进程ID,子进程返回0
vfork函数:
为什么会有vfork呢?
因为以前的fork当它创建一个子进程时,将会创建一个新的地址空间,并且拷贝父进程的资源,然后将会有两种行为:
① 执行从父进程那里拷贝过来的代码段
② 调用一个exec执行一个新的代码段
当进程调用exec函数时,一个新程序替换了当前进程的正文,数据,堆和栈段。这样,前面的拷贝工作就是白费力气了,这种情况下,聪明的人就想出了vfork。vfork并不复制父进程的进程环境,子进程在父进程的地址空间中运行,所以子进程不能进行写操作,并且在儿子“霸占”着老子的房子时候,要委屈老子一下了,让他在外面歇着(阻塞等待),一旦儿子执行了exec或者exit后,相当于儿子买了自己的房子了,这时候就相当于分家了。因此,如果创建子进程是为了调用exec执行一个新的程序的时候,就应该使用vfork
fork与vfork的区别:
① vfork保证子进程先运行,在它调用exec或exit之后父进程才可能被调度运行。如果在调用这两个函数之前子进程依赖于父进程的某个行为,则会导致死锁。
② fork要拷贝父进程的进程环境;哪个进程先运行取决于系统的调度算法;而vfork则不需要完全拷贝父进程的进程环境,在子进程没有调用exec和exit之前,子进程与父进程共享进程环境,相当于线程的概念,此时父进程阻塞等待。
什么是孤儿进程呢?
如果一个子进程的父进程先于子进程结束,子进程就成为一个孤儿,它有init进程收养,成为其子进程
什么是守护进程呢?
指在后台运行的,没有控制终端与之相连的进程。它独立于控制终端,通常周期性地执行某种任务。Linux的大多数服务器都是使用守护进程的方式实现。
如何创建一个守护进程?
首先我们需要理解一些基本概念:
进程组(process group): 一个或多个进程的集合,每个进程都有一个进程组ID,这个ID就是进程组长的进程ID
会话期(session): 一个或多个进程组的集合,每个会话有唯一一个会话首进程(session leader),会话ID为会话首进程ID
控制终端(controlling terminal) :每一个会话可以有一个单独的控制终端,与控制终端连接的会话首进程就是控制进程(controlling process)。 这时候,与当前终端交互的就是前台进程组,其他的都是后台进程组。
需要以下操作:
需要两次fork的原因:
1. 第一次fork的作用是让shell认为本条命令已经终止,不用挂在终端输入上。还有一个作用是为后面setsid服务,setsid的调用者不能是进程组组长(group leader),此时父进程是进程组组长。
2. fork第二次主要目的是防止进程再次打开一个控制终端。因为打开一个控制终端的前提条件是该进程必须是会话组长。再fork一次,子进程ID != sid(sid是进程父进程的sid),所以也无法打开新的控制终端。
进程退出:
- 正常退出:在main函数中执行return;调用exit函数;调用_exit函数
- 异常退出:调用about函数;进程受到某个信号使程序终止
最终都会执行内核中的同一段代码,用来关闭进程已打开的文件描述符,释放它所占用的内存和其他资源
比较:
1. exit和return的区别:exit是一个有参数的函数,而return是函数执行完后的返回.exit把控制权交给系统,而return交给调用函数
2. exit和about:exit是正常终止进程,而about是异常终止
3. exit(0)正常终止,exit(1)异常终止
4. exit()和_exit()的区别:exit在头文件stdlib.h中声明,而_exit()声明在unistd.h中,两个函数均能正常终止进程,但是_exit()会执行后立即返回给内核,而exit()要先执行一些清楚操作,然后把控制权交给内核
什么是僵尸进程?
当子进程先于父进程终止,而父进程又没有调用wait函数等待子进程结束,子进程进入僵死状态,并且会一直保持下去除非系统重启.子进程处于僵死状态,内核只保存该进程的一些必要信息以备父进程所需.此时子进程始终占用着资源,同时也减少了系统可以创建的最大进程数;如果子进程先于父进程终止,且父进程调用了wait或waitpid函数,则父进程会等待子进程结束
执行新程序:
使用fork或vfork创建子进程后,子进程通常会调用exec函数来执行另一个程序.系统调用exec用于执行一个可执行程序以代替当前进程的执行映像;一个进程一旦调用exec函数,它本身就死亡了,系统把代码段替换成新的程序的代码,废弃原有的数据段和堆栈段,并为新程序分配新的数据段与堆栈段,惟一保留的就是进程ID,对系统而言还是一个进程,不过执行的已经是另一个程序了
exexc函数族:
#include<stdio.h>
int execve(const char *path,char * const argv[],char *const envp[]);
int execv(const char *path,char *const envp[]);
int execle(const char *path,const char *arg,...);
int execl(const char *path,const char *arg,...);
int execvp(const char *file,char* const argv[]);
int execlp(const char *file,const char *arg,...);
exec函数错误表:
errno | 错误描述 |
---|---|
EACCES | 指向的文件或脚本文件没有设置可执行位,即指定的文件是不可执行的 |
E2BIG | 新程序的命令行参数与环境变量容量之和超过ARG_MAX |
ENOEXEC | 由于没有正确的格式,指定的文件无法执行 |
ENOMEN | 没有足够的内存空间来执行指定的程序 |
ETXTBUSY | 指定文件被一个或多个进程以可写的方式打开 |
EIO | 从文件系统读入文件时发生I/O错误 |
执行新程序后的进程除了保存原来的进程ID,父进程ID,实际用户ID,实际组ID之外,进程还保持了许多原有特征,主要有:
1. 当前工作目录
2. 根目录
3. 创建文件时使用的屏蔽字
4. 进程信号屏蔽字
5. 未决警告
6. 和进程相关的使用处理器的时间
7. 控制终端
8. 文件锁
等待进程结束:
当子进程先于父进程退出时,如果父进程没有调用wait和waitpid函数,子进程就会进入僵死状态,如果父进程调用了waitpid函数,就不会使子进程变为僵尸进程
#include<sys/wait.h>
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid,int *statloc,int options);
wait函数使父进程暂停执行,直到它的一个子进程结束为止,该函数的返回值是终止运行的子进程PID,参数statloc所指向的变量存放子进程的退出码,即从子进程的main函数返回的值或子进程中exit函数的参数;如果statloc不是一个空指针,状态信息将被写入它指向的变量
waitpid也用来等待子进程的结束,但它用于等待某个特定进程结束,参数pid指明要等待的子进程PID.statloc的含义与wait函数中相同,options允许用户改变waitpid的行为,若将该参数赋值为WNOHANG,则使父进程不被挂起而立即返回并执行其后的代码
还有几个函数~
- setuid和setgid
#include<sys/types.h>
#include<unistd.h>
int setuid(uid_t uid); //设置实际用户有效用户ID
int setgid(gid_t gid); //设置实际组有效组用户ID
只有root用户才能更改实际用户ID,所以一个非特权用户进程是不能通过setuid或setgid得到root用户权限的.
但是su命令为什么可以把一个普通用户变成root用户呢?
因为su是一个set_uid程序,执行了一个设置了set_uid位的程序时,内核将进程的有效用户ID设置为文件属猪(啊呸!属主……)的ID.而内核检查一个进程是否具有访问某文件的权限时,是使用进程的有效用户ID进行检查的,su程序的文件属主是root,普通用户运行su命令时,su进程的权限是root用户
- nice
#include<unistd.h>
int nice(int increment);
nice函数可以改变进程的优先级
- getpriprity和setpriority
#include<sys/resource.h>
int getpriority(int which,int who);
int setpriority(int which,int who,int prio);
getpriority函数返回一组进程的优先级
which | who | |
---|---|---|
PRIO_PROCESS | 一个特定的进程 | 进程ID |
PRIO_PGRP | 一个进程组的所有进程 | 进程组ID |
PRIO_USER | 一个用户拥有的所有进程 | 实际用户ID |
如果getpriority函数调用成功,返回指定进程的优先级,如果出错返回-1,并设置errno的值,errno可能取值如下:
ESRCH:which和who的组合与现存的所有进程均不匹配
EINVAL:which是个无效的值
当指定的一组进程的优先级不同时,getpriority将返回其中优先级最低的一个
setpriority函数用来设置指定进程的优先级