fork,execve, exit, _exit, wait
这个就不讲了,其它很多书籍都讲了。
vfork是一种更快的fork,它和父进程共享内存,不创建page table,没有自己的虚拟地址空间,通常vfork之后立即执行execve。
进程有两种退出方式,1种是信号异常退出,另外一种是通过_exit系统调用正常退出:
void _exit(int status);
status表明退出状态,父进程可以通过wait()读取到,父进程只能读取status的低8位。_exit绝不会返回,它总是结束进程。
通常调用exit()库函数退出进程,它在调用_exit系统调用前,执行一些操作:
1 执行Exit handler
2 flush stdio streams
3 调用_exit
还有另外一种方式,从main函数返回,等价于exit(n)。fork出来的子进程,通常不要调用exit,而是调用_exit(),否则会执行清理工作,导致父进程异常。
1 wait()系统调用
pid_t wait(int *status);
如果没有任何子进程结束(包括之前的),则wait等待,之后,任何一个子进程结束,wait就返回。
如果失败,返回-1,一个原因是没有可等待的儿子,错误码ECHLD。
2 waitpid系统调用
pid_t waitpid(pid_t pid, int *status, int options);
pid如果大于0,则等待特定进程。
pid等于0,等待和调用者同进程组的进程结束。
pid等于-1,则等待所有儿子。
pid小于-1,等待-pid进程组。
options
WUNTRACED,除了等待结束的进程,还等待停止的进程。
WCONTINUED,等待continue的进程。
WNOHANG,不阻塞。
status的值只有低2个字节有效,有4种情况:
正常退出:最低字节为0,第二个字节为退出status。
信号退出:最低字节的0-7位是退出信号,第8位是core dump标志,第二字节为0。
stopped:最低字节为0x7f,第二字节为停止信号。
continued:低2字节为0xffff
<sys/wait.h>有一些接来获取status的字段:
WIFEXITED(status)
如果是正常退出,返回true,使用WEXITSTATUS(status)来获取退出码
WIFSIGNALED(status)
如果是默认信号处理方式导致的退出,返回true,WTERMSIG(status)返回信号号。
WIFSTOPPED(status)
如果是停止,则返回true,使用WSTOPSIG(status)返回导致停止的信号号。
WIFCONTINUED(status)
如果是continue,则返回true。
注意:wait和waitpid只会等儿子,不会等孙子。
当子进程退出的时候,将给父进程发送SIGCHLD信号。
如果父进程显示的忽略SIGCHLD信号,那么wait等不带任何信息,直接返回-1,ECHILD。且子进程不会成为僵死进程,直接被系统收回。
int execve(const char *pathname, char *const argv[], char *const envp[]);
pathanme可是绝对路径,也可以是相对于CWD的路径。如果execve成功,那么这个函数绝不返回,如果失败总是返回-1,所以我们永远不需要检查execve的返回值。
int execle(const char *pathname, const char *arg, ...
/* , (char *) NULL, char *const envp[] */ );
int execlp(const char *filename, const char *arg, ...
/* , (char *) NULL */);
int execvp(const char *filename, char *const argv[]);
int execv(const char *pathname, char *const argv[]);
int execl(const char *pathname, const char
后缀l,表示是可变参数,v表示是数组,e表示带环境变量,
p表示是支持filename,如果filename带/,表示是绝对或相对路径,如果不带,则从PATH环境变量中去搜索。
如果PATH没定义,则默认为/usr/bin和/bin
不带e的,继承父亲的环境变量。
int fexecve(int fd, char *const argv[], char *const envp[]);
从文件描述符去执行。
#! interpreter-path [ optional-arg ]
最终执行的程序是:
interpreter-path "optional-arg" script-path script arg.
如果test.sh的内容是
#!/usr/bin/printarg a b
那么执行./test.sh,调用的是
$ ./test.sh 1 2
args: 4
0(3): a sp b
1(9): . / t e s t . s h
2(1): 1
3(1): 2
int system(const char *command);
system创建一个子进程调用shell来执行命令。所以command可以是任何shell命令。
system的返回值:
如果command是NULL,如果有shell可用返回非0值,如果没有shell可用,返回0。如果command不是NULL则返回值是以下:
如果不能创建子进程,或者读取不到子进程的状态,system返回-1
如果不能执行shell,则返回类似子进程返回_exit(127)的返回值。
如果system成功,那么返回shell的返回值(通常是最后一条命令的返回值)
后两种情况,返回的是waitpid返回的status一样的格式。
bash的返回值是0-255:
如果最后一条命令正常退出,则返回0-127(取status的第一个字节的低7位)
如果最后一条命令是信号导致的跳出,则返回128+N。
如果命令不存在,返回127
如果命令不可执行,返回126
system实现细节
system实现的复杂性是信号的处理。执行system就好像在交互shell执行命令一样,调用者就像命令行所在的shell一样表现:
如果调用者安装了SIGCHLD,并且在信号处理函数中执行了wait,那么system自己就等待不了儿子了,所以system会block掉SIGCHLD。
执行system时,调用者进程忽略SIGINT和SIGQUIT信号。否则按ctrl+c会杀死调用者,命令,只会留下对应的shell。
调用system的程序不应该忽略SIGCHLD,否则,system无法等待儿子。
当shell执行单个命令,或者pipeline 命令时,shell都创建一个新的进程组。每个进程都有一个进程组ID。pipeline中的命令都属于同一个进程组。进程组有一个leader,进程组ID是leader的PID,即使leader退出,进程组中的其它进程的组ID也不变。子进程继承父进程的进程组ID。所以所有的子进程和父进程都属于同一个进程组。
接口:
int setpgid(pid_t pid, pid_t pgid);
pid_t getpgid(pid_t pid);
对于setpgid设置进程的进程组ID:
如果pid是0,则设置本进程
如果pgid是0,或者等于pid,则创建了一个新的进程组,这个进程是新进程组的leader。
pid只能指定自己和儿子,指定其它进程返回ESRCH
调用者进程,要修改的进程、目标进程组ID对应的同ID进程,必须是同一个session,否则返回EPERM。
pid不能指定为一个session leader,否则EPERM。
如果儿子已经执行了execv,则不可再改变儿子的pgid,否则返回EACCES。
通常一个交互式shell是一个session的leader。一个session可能包含多个进程组。leader的pid是session id。使用下面的接口创建一个新的session:
pid_t setsid(void);
这个接口创建一个新的session,session id是调用者进程的PID,并且把pgid也修改为调用者进程的pid。
同时会关闭之前的control terminal,因为新创建的session暂时没有control terminal,如果打开/dev/tty将失败。
这个接口要求调用者进程不是一个进程组的leader,否则以EPERM失败,因为如果是一个进程组的leader的话,那进程组中其它进程的session和该进程的session不同,这违反了一个进程组属于一个session的规定。
当内核创建新的进程时,保证进程id匹配于任何存在的进程组id和session id。
当一个没有控制终端的session leader,open一个不是其它session的终端的controlling terminal时,且open没有指定O_NOCTTY选项,那么自动打开一个控制终端。子进程自动继承控制终端,即使子进程和控制终端相关的所有描述符已经关闭,仍然可以通过/dev/tty打开控制终端。getpass程序就是通过这种方式从控制终端读取输入的。如果一个进程没有控制终端,打开/dev/tty将以ENXIO失败。
使用ioctl(fd, TIOCNOTTY)移除进程和控制终端的关系。如果是控制进程丢失了这个关系,那么:
session中的所有进程都丢失了和控制终端的关系
控制终端没有了对应的控制进程,其它session可以和这个控制终端建立关系。
kernel发送SIGHUP给前台进程。
char *ctermid(char *ttyname);
这个接口可以获得控制终端的路径。
对于Linux,只有session leader可以获得控制终端。
每个session有一个前台进程组和多个后台进程组。session leader负责维护前台进程和后台进程,通过下面的接口:
pid_t tcgetpgrp(int fd);
int tcsetpgrp(int fd, pid_t pgid);
fd应该是打开的控制终端。在Linux上,这两个接口是通过ioctl选项TIOCGPGRP and TIOCSPGRP实现的。
session leader是一个terminal的controlling process。当terminal disconnect的时候,内核给controlling process发送SIGHUP信号。
当控制进程退出的时候,内核:
解除session中所有进程和终端的关系。
解除终端和session的关系
给所有的前台进程发送SIGHUP信号,通知它们丢失了终端。
根据bash手册:当一个bash收到SIGHUP信号时,默认的动作是结束bash进程。若是一个交互式的bash进程,会重新发送SIGHUP给所有的jobs,包括running和stopped的jobs。停止的jobs会先发送SIGCONT。为了阻止shell发送SIGHUP给job,可以使用disown内置命令将进程(组)从job tables中移除,或者使用disown -h将job标记为不发送SIGHUP。上述只是在bash收到SIGHUP才有的动作,正常退出不会发送SIGHUP。使用huponexit选项,可以让BASH在退出的时候发送SIGHUP。
当一个命令以 & 结尾时,bash将其放入后台运行。使用jobs可以查看所有的jobs
$ sleep 100 &
[1] 13981
$ jobs
[1]+ Running sleep 100 &
每个job有一个编号,使用fg [job_spec],将后台进程变成前台进程执行:
$ fg 1
sleep 100
前台进程按Control+Z,变成一个暂停的后台进程。
$ sleep 100
^Z
[1]+ Stopped sleep 100
使用bg [job_spec]让停止的后台进程继续在后台运行:
$ bg 1
[1]+ sleep 100 &
$ jobs
[1]+ Running sleep 100 &
当一个后台运行的进程尝试读取终端时,驱动将发送SIGTTIN信号,这个信号默认是停止进程,这样shell将整个job变成stopped状态。
默认情况下后台进程可以输出到终端,但是终端开启了TOSTOP标志:
stty tostop
那么当后台进程输出数据到终端时,将收到SIGTTOU信号,进程将被暂停,bash检测到进程被暂停,将jobs变成stopped状态。
当后台进程block或者ignore SIGTTIN信号时,从终端read返回EIO,同时不产生SIGTTIN信号。SIGTTOU同理。
如果组中的每一个成员满足:父亲是本组成员,或者父亲不是group所在session的成员。那么这个进程组是孤儿进程组。
执行一个命令,命令fork之后退出,那么child所在的组,就是一个孤儿进程组。因为父亲退出后child被init进程收养。init进程显然是另外一个session。孤儿进程组已经逃脱了session的控制进程的管辖,因为控制进程意识不到儿子的存在,父亲已经退出了,儿子也不在jobs中。
孤儿进程的最大问题是,那个不在组成员的父进程是另外一个session,如果孤儿进程是stopped,那么没人能恢复它(父亲也没有权限,不考虑root,而且如果是init进程,那么就更不会了,init进程只是wait,阻止僵尸进程)。
因此,当一个孤儿进程中的任何一个进程变成stopped时,所有成员被发送一个SIGHUP信号,跟着一个SIGCONT信号。
如下代码,将产生SIGHUP信号:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | #include <stdio.h> #include <unistd.h> #include <signal.h> #include <string.h> void handler( int sig) { printf ( "sig %s\n" , strsignal(sig)); } int main() { if (fork() == 0) { signal (SIGHUP, handler); raise (SIGSTOP); while (1) sleep(100); } } |
对于孤儿进程组,读写终端不产生SIGTTIN or SIGTTOU信号,read/write返回EIO错误。
创建一个守护进程,通常可以通过下列步骤实现:
1 执行fork,然后父进程退出,子进程继续运行。这么做有两个考虑点:
a. 如果在命令行执行,那父进程退出了,子进程在后台执行,shell返回。
b. 子进程不是一个进程组leader,下一步骤才可以执行。
2 调用setsid,创建一个新的session,并且解除自己和之前的控制终端的关系。
3 如果后续不打开控制终端,那么不用担心收到任何控制终端的信号,如果需要打开控制终端,可以:
a. 指定O_NOCTTY选项
b. 在setsid之后,再次fork,这样就不是一个session leader了,这样就不能获取控制终端了。
4 清除umask
5 将cwd设置成跟目录,这样做是释放相应的文件引用,否则unmount文件系统时会失败。
或者将cwd设置成对应的配置文件的目录。
6 关闭所有从父亲继承过来的文件描述符。
7 将0/1/2描述符打开到/dev/null
关闭系统时,init进程先发送SIGTERM给所有进程,如果没有结束5s后发送SIGKILL。所以守护进程应该在SIGTERM信号快速清理资源退出。守护进程还可以捕获SIGHUO重新加载配置。