进程的层次关系

pid是进程ID,pgid是进程组ID,sid是会话ID。默认情况下,新创建的进程会继承父进程的进程组ID和会话ID。

比喻:会话是公司,进程组是部门,进程是员工。

进程组方便管理一组进程,比如发送信号可以发给一个进程组。

对于shell会话来说,登录时的进程为会话首进程。之后启动的进程分为前台进程组和后台进程组的概念。只有一个前台进程组,可以有多个后台进程组。前台进程组的进程在控制终端读取输入,比如ctrl+c

fork

fork以后会用写时复制技术复制父进程的数据段、代码段、栈、堆。

写实复制技术利用了Linux的多级页表机制,在fork时拷贝的是父进程的页表,而不是物理内存页。在父子进程对页表进程修改时,才真正触发拷贝物理内存页。

实现上将页表设置成写保护,父子进程在尝试修改写保护的页面时,会引发缺页中断,内核会负责创建副本,也就是真正的拷贝。

文件描述符在fork的时候也发生了复制,但是复制的只是fdtable里保存的fd指针,因此父子进程还是共享fd。

复制过程略复杂,会设计到子进程初始化files_struct以及重新alloc_fdtable,最后将父进程的struct file类型指针拷贝到子进程对应的数据结构中。

为了避免暴露fd给子进程,Linux引入了close on exec机制。只要设置了FD_CLOSEXEC标志位的文件,在子进程调用exec时会将文件关闭。可以在open时设置这个flag,也可以在open后调用fcntl修改这个flag。建议在open时设置,因为可能在修改flag之前子进程已经被fork出来了。

vfork父子进程共享内存。一般不要用vfork。

vfork创建的子进程不要用return退出,会有奇怪的bug。

daemon进程创建一般使用两次fork

  1. fork子进程,父进程退出

  2. 子进程修改workdir, setsid, umask(0)

  3. 再次fork子进程,父进程退出。让daemon进程不是会话组的首进程,因为首进程可能打开终端设备

  4. 关闭stderr, stdout, stdin

exit和wait

退出有三种方式:_exit n,exit n, return n

exit会处理用户定义的回调以及flush IO缓冲区,最后调用_exit

return等于exit

子进程退出后,pid和状态信息并不会立即释放掉。这样设计是为了记录一些进程运行过程中的状态。这样的进程就成为了僵尸进程

当父进程调用wait给子进程收尸后,僵尸进程的资源才会释放掉。另外在子进程死掉的时候,会给父进程发送SIGCHLD信号,如果父进程回应一个SIG_IGN,那么子进程的pid和状态信息也会立即释放掉。

wait族函数有三个:

  • wait阻塞父进程,获取任意一个退出的子进程的状态信息

  • waitpid可以精确控制给哪一个pid/pgid的进程收尸,还可以设置为非阻塞,还可以获取子进程的暂停和继续的状态

  • waitid是加强版waitpid,特色功能是可以只获取子进程的状态信息,而不会释放子进程的状态信息。wait和waitpid在获取后内核都会释放子进程的资源。

exit和wait的内核实现

在进程退出以后,还有两件事情要做:

  1. 作为父进程,需要为子进程寻找新的父进程

  2. 作为子进程,需要通知父进程为自己收尸

对于1来说,首先是寻找同一线程组中的其他线程作为新的父进程(假设是多线程下某个线程退出了),如果没找到,就交给init进程。父进程这时候也可以发送信号给子进程。

对于2来说,多线程下只有线程组主线程会通知父进程(或者主线程先退出了,线程组中最后一个线程也会通知父进程)。通知方式有两种:1.子进程可以发送SIGCHILD信号给父进程处理,父进程在信号处理函数中调用waitpid处理。2.父进程调用wait主动等待,将一个结构体加入到wait_childexit等待队列中。子进程在退出时会调用_wake_up_parent唤醒等待队列中的父进程。

exec

OS提供的系统调用是execve,原型是int execve(cons char *path, char *const argv[], char *const envp[])

path是可执行文件路径,argv[]是参数数组。argv[0]一般设为可执行文件名,在获取参数时会跳过。evp是环境变量表。

glibc封装了6个函数,execl execle execlp execv execve execvp

区别在于l代表参数是列表list(char *arg…),v代表参数是数组vector(char *argv[]),p代表会自动解析PATH变量,e代表需要手动传入环境变量。

在内核实现上,首先会打开文件,然后把参数和环境变量复制到bprm中,然后在linux_bfmt链表中寻找可以执行该文件的解释器,如果找不到任何一个可用的解释器且没有动态加载的解释器,那么就报错。

常见的解释器有Linux的二进制文件ELF,脚本文件SCRIPT,用户自定义的MISC

脚本开头的sheba标识也是在SCRIPT的解释器下寻找到真正的解释器,比如#!/usr/bin/env python

可以自己在MISC下面注册新的解释器给OS,用magic number或者扩展名标识

exec后跟不会设置pid相关的值,但是信号处理会重置为SIG_DLF(除了SIG_IGN在Linux会保留)

system

可以使用system函数运行一条shell命令。过程是创建一个shell进程,然后在创建一个或多个命令进程。