ILD

Linux编程接口:Processes
作者:Yuan Jianpeng 邮箱:yuanjp89@163.com
发布时间:2021-2-27 站点:Inside Linux Development

Process create

fork,execve, exit, _exit, wait

这个就不讲了,其它很多书籍都讲了。


vfork是一种更快的fork,它和父进程共享内存,不创建page table,没有自己的虚拟地址空间,通常vfork之后立即执行execve。


Process Termination

进程有两种退出方式,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(),否则会执行清理工作,导致父进程异常。


Monitor child process

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信号。


如果父进程显示的忽略SIGCHLD信号,那么wait等不带任何信息,直接返回-1,ECHILD。且子进程不会成为僵死进程,直接被系统收回。


program executing

int execve(const char *pathname, char *const argv[], char *const envp[]);

pathanme可是绝对路径,也可以是相对于CWD的路径。如果execve成功,那么这个函数绝不返回,如果失败总是返回-1,所以我们永远不需要检查execve的返回值。


exec() library function

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 script

#! 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


system()

int system(const char *command);

system创建一个子进程调用shell来执行命令。所以command可以是任何shell命令。


system的返回值:

如果command是NULL,如果有shell可用返回非0值,如果没有shell可用,返回0。如果command不是NULL则返回值是以下:


后两种情况,返回的是waitpid返回的status一样的格式。


bash的返回值是0-255:


system实现细节

system实现的复杂性是信号的处理。执行system就好像在交互shell执行命令一样,调用者就像命令行所在的shell一样表现:


如果调用者安装了SIGCHLD,并且在信号处理函数中执行了wait,那么system自己就等待不了儿子了,所以system会block掉SIGCHLD。


执行system时,调用者进程忽略SIGINT和SIGQUIT信号。否则按ctrl+c会杀死调用者,命令,只会留下对应的shell。


调用system的程序不应该忽略SIGCHLD,否则,system无法等待儿子。

process group

当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:


session

通常一个交互式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。


Controlling terminal and Controlling process

当一个没有控制终端的session leader,open一个不是其它session的终端的controlling terminal时,且open没有指定O_NOCTTY选项,那么自动打开一个控制终端。子进程自动继承控制终端,即使子进程和控制终端相关的所有描述符已经关闭,仍然可以通过/dev/tty打开控制终端。getpass程序就是通过这种方式从控制终端读取输入的。如果一个进程没有控制终端,打开/dev/tty将以ENXIO失败。


使用ioctl(fd, TIOCNOTTY)移除进程和控制终端的关系。如果是控制进程丢失了这个关系,那么:


char *ctermid(char *ttyname);

这个接口可以获得控制终端的路径。


对于Linux,只有session leader可以获得控制终端。


Foreground and Background Process Groups

每个session有一个前台进程组和多个后台进程组。session leader负责维护前台进程和后台进程,通过下面的接口:


pid_t tcgetpgrp(int fd);

int tcsetpgrp(int fd, pid_t pgid);


fd应该是打开的控制终端。在Linux上,这两个接口是通过ioctl选项TIOCGPGRP and TIOCSPGRP实现的。


SIGHUP信号

session leader是一个terminal的controlling process。当terminal disconnect的时候,内核给controlling process发送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。


Job control

当一个命令以 & 结尾时,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同理。

Orphaned process groups

如果组中的每一个成员满足:父亲是本组成员,或者父亲不是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错误。


Daemons

创建一个守护进程,通常可以通过下列步骤实现:


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重新加载配置。


Copyright © linuxdev.cc 2017-2024. Some Rights Reserved.