Linux进程编程- 3.1 fork系统调用
- 3.1.1 fork工作原理
- 3.1.2 fork函数
- 3.1.3 fork编程示例
- 3.1.4 小结
- 3.2 exec系统调用
- 3.2.1 exec函数族作用
- 3.2.2 exec函数族
- 3.2.3 exec编程示例
- 3.2.4 小结
- 3.3 exit系统调用
- 3.3.1 exit工作原理
- 3.3.2 exit函数
- 3.4 wait系统调用
- 3.4.1 wait工作原理
- 3.4.2 wait函数
- 3.4.3 进程管理综合示例
- 3.1.1 fork工作原理
- 3.1.2 fork函数
- 3.1.3 fork编程示例
- 3.1.4 小结
- 3.2.1 exec函数族作用
- 3.2.2 exec函数族
- 3.2.3 exec编程示例
- 3.2.4 小结
- 3.3.1 exit工作原理
- 3.3.2 exit函数
- 3.4.1 wait工作原理
- 3.4.2 wait函数
- 3.4.3 进程管理综合示例
3.1 fork系统调用
3.1.1 fork工作原理
fork系统调用是创建子进程的唯一方法。执行过程如下:
- Linux内核在进程表中为子进程分配一个表项,然后分配PID。子进程表项的内容来自父进程,fork会将父进程的表项复制为副本,并分配给子进程;
- Linux内核使父进程的文件表和索引表的节点自增1,创建用户及上下文;
- 将父进程上下文复制到子进程上下文空间中;
- fork调用结束后子进程的PID将返回给父进程,而子进程获得的值为0。
3.1.2 fork函数
头文件:unistd.h
函数原型:pid_fork(void)
功能:创建一个与原来进程几乎完全相同的进程,即两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同,两个进程也可以做不同的事。一个进程调用fork函数后,系统先给新的进程分配资源,例如,存储数据和代码的空间。然后把原来的进程所有值都复制到新的进程中,只有少数值与原来的进程的值不同。相当于克隆了一个自己。
返回值:fork被调用一次却能够返回两次且可能有三种不同的返回值:
- 在父进程中,fork返回新创建子进程的进程ID(通常为父进程PID 1);
- 在子进程中,fork返回0;
- 如果出现错误,fork返回一个负值。 fork出错可能有两种原因:
- 当前的进程数已经达到了系统规定的上限,这时errno的值被设置为EAGAIN;
- 系统内存不足,这时errno的值被设置为ENOMEM。 两个进程有不同的PID,但执行顺序由进程调度策略决定。
3.1.3 fork编程示例
- 打开Ubuntu终端,切换用户到root,新建一个process文件夹用于存放实验文件,进入该目录下,输入sudo vi forkProcess.c使用vi文本编辑器编辑forkProcess.c文件;
- 按下i键进入编辑模式,输入fork编程示例,该示例创建一个子进程,通过fork()函数返回值判断进程是子进程还是父进程,并打印信息。
- 按下ESC键退出编辑模式,输入“:wq”回到命令行,使用gcc编译器编译forkProcess.c文件生成可执行文件forkProcess.
- 输入./forkProcess执行该文件,得到结果如下:
3.1.4 小结
其实,fork底层是调用了内核的函数来实现fork的功能的,即先create()先创建进程,此时进程内容为空,然后clone()复制父进程的内容到子进程中,此时子进程就诞生了,接着父进程就return返回了。而子进程诞生后,是直接运行return返回的,然后接着执行后面的程序,这里注意:子进程是不会执行前面父进程已经执行过的程序了得,因为PCB中记录了当前进程运行到哪里,而子进程又是完全拷贝过来的,所以PCB的程序计数器也是和父进程相同的,所以是从fork()后面的程序继续执行。此时就按照前面的规则进行判断返回[1]。如下图所示:
3.2 exec系统调用
3.2.1 exec函数族作用
exec函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的内容,换句话说,就是在调用进程内部执行一个可执行文件。这里的可执行文件既可以是二进制文件,也可以是任何Linux下可执行的脚本文件。
与一般情况不同,exec函数族的函数执行成功后不会返回,因为调用进程的实体,包括代码段,数据段和堆栈等都已经被新的内容取代,只留下进程ID等一些表面上的信息仍保持原样,颇有些神似"三十六计"中的"金蝉脱壳"。看上去还是旧的躯壳,却已经注入了新的灵魂。只有调用失败了,它们才会返回一个-1,从原程序的调用点接着往下执行。
我们应该明白了,Linux下是如何执行新程序的,每当有进程认为自己不能为系统和用户做出任何贡献了,他就可以发挥最后一点余热,调用任何一个exec,让自己以新的面貌重生;或者,更普遍的情况是,如果一个进程想执行另一个程序,它就可以fork出一个新进程,然后调用任何一个exec,这样看起来就好像通过执行应用程序而产生了一个新进程一样[2]。
3.2.2 exec函数族
头文件:unistd.h
函数原型:
int execl(const char *path, const char *arg, …);
int execv(const char *path, char *const argv[]);
int execle(const char *path, const char *arg,…, char * const envp[]);
int execve(const char *file, char *const argv[]);
int execlp(const char *file, const char *arg, …);
int execvp(const char *file, char *const argv[]);
选项:
l:希望接收一个以逗号分隔的参数列表,列表以NULL指针作为结束标志;
v:希望接收一个以NULL结尾的字符串数组的指针;
p:是一个以NULL结尾的字符串数组指针,函数可以利用PATH变量查找子程序文件;
e:函数传递指定参数envp,允许改变子进程的环境,无后缀e时子进程使用当前程序的环境。
参数:
path:可执行文件的路径名字;
arg:可执行程序所带的参数,第一个参数为可执行文件名字,没有带路径且arg必须以NULL结束;
file:如果参数file中包含/,就将其视为路径名,否则就按PATH环境变量,在它所指定的各目录中搜寻可执行文件。
功能:
以新进程代替原有进程,但PID保持不变。
返回值:
执行成功不返回,出错则返回-1,失败原因记录在errno中。
六个函数的区别:
- 查找方式不同:前四个函数的查找方式都是完整的文件目录路径,而后两个(以p结尾的两个函数)可以只给出文件名,系统会自动从环境变量“$PATH”所指的路径中进行查找;
- 参数传递方式不同:exec函数族的参数传递有两种方式:函数名的第五位字母为“l”(list)表示逐个列举的方式、函数名第五位字母为“v”(vector)的表示将所有参数整体构造成指针数组传递。然后将该数组的首地址当作参数传给它,数组中的最后一个指针要求是NULL;
- 环境变量不同:以“e”(environment)结尾的两个函数execl、execve可以在envp[]中指定当前进程所使用的的环境变量替换掉该进程继承的环境变量,其他函数把调用进程的环境传递给新进程。
3.2.3 exec编程示例
execl 实现ls指令
execv 实现获取系统时间
3.2.4 小结
执行exec系统调用,一般都是这样,用fork()函数新建立一个进程,然后让进程去执行exec调用。我们知道,在fork()建立新进程之后,父进程与子进程共享代码段,但数据空间是分开的,但父进程会把自己数据空间的内容copy到子进程中去,还有上下文也会copy到子进程中去。而为了提高效率,采用一种写时copy的策略,即创建子进程的时候,并不copy父进程的地址空间,父子进程拥有共同的地址空间,只有当子进程需要写入数据时(如向缓冲区写入数据),这时候会复制地址空间,复制缓冲区到子进程中去。从而父子进程拥有独立的地址空间。而对于fork()之后执行exec后,这种策略能够很好的提高效率,如果一开始就copy,那么exec之后,子进程的数据会被放弃,被新的进程所代替。
3.3 exit系统调用
3.3.1 exit工作原理
exit系统调用的执行发生以下事件:
- 清除当前进程的所有信号处理函数。
- 如果当前进程是和终端关联的“进程组组长”,则会向每个组内进程发送hang-up signal,并且把这些成员的进程组设置为0。
- 通过内核内部算法关闭当前进程所有打开的文件描述符,并且释放当前目录所关联的inode;如果存在current (charged)root,也将其释放通过算法iput。
- 为进程释放所有的region以及关联的memory。
- 计算进程机器子进程执行的时间(user mode 和kernel mode),并把记录写到一个全局的accounting file。
- 将进程的状态改变为zombie,并将自己的所有的子进程的父进程ID设置为1(init);如果有孩子的状态是zombie,向init进程发SIGCHLD信号,以清除子进程的process table slot。
- exiting进程向自己的父进程发送SIGCHLD信号。
- 进行context switch,调度其他非zombie进程(本进程已经是zombie)。
3.3.2 exit函数
函数原型1:void _exit(int status)
头文件:unistd.h
函数原型2:void exit(int status)
头文件:stdlib.h
功能:终止发出调用的进程,status是返回给父进程的状态值,父进程可通过wait系统调用获得。
exit()和_exit()的区别:
l _exit()的作用最简单:直接使进程停止运行,清除其使用的内存空间,并销毁其在内核中的各种数据结构;
l exit()在终止进程之前要检查文件的打开情况,把文件缓冲区中的内容写回文件,即清理“I/O缓冲”;
l 两者最终都要将控制权交给内核,旖旎次,要想保证数据的完整性,就一定要使用exit()。
3.4 wait系统调用
3.4.1 wait工作原理
进程一旦调用了wait,就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。
3.4.2 wait函数
头文件:sys/types.h和sys/wait.h
函数原型1:pid_t wait(int *status)
函数原型2:pid_t waitpid(pid_t pid, int *status, int options)
参数:
status:返回子进程退出时的状态;
pid:pid>0时:等待进程号为pid的子进程结束、pid=0时:等待组ID等于调用进程组ID的子进程结束、pid=-1时:等待任一子进程结束,等价于调用wait()、pid<-1时:等待组ID等于PID的绝对值的任一子进程结束;
options:WNOHANG:若pid指定的子进程没有结束,则waitpid()不阻塞而立即返回,此时的返回值为0、WUNTRACED:为了实现某种操作,由pid指定的任一进程已被暂停,且其状态自暂停以来还未报告过,则返回其状态、0:同wait(),阻塞父进程,等待子进程退出。
wait()与waitpid()区别:wait()函数等待所有子进程的僵死状态,waitpid()函数等待PID与参与pid相关的子进程的僵死状态。
检查子进程的返回状态码status:
WIFEXITED(status):进程中通过调用_exit()或exit()正常退出,该宏值为非0;
WIFSIGNALED(status):子进程因得到的信号没有被捕捉导致退出,该宏值为非0;
WIFSTOPPED(status):子进程没有终止但停止了,并可重新执行时,该宏值为非0,这种情况仅出现在waitpid()调用中使用了WUNTRACED选项;
WEXITSTATUS(status):如果WIFEXITED(status)返回非0,该宏返回由子进程调用_exit(status)或exit(status)时设置的调用参数status值;
WTERMSIG(status):如果WIFSIGNALED(status)返回非0,该宏返回导致子进程退出的信号的值;
WSTOPSIG(status):如果WIFSTOPPED(status)返回非0,该宏返回导致子进程停止的信号的值。