本文基于linux环境编程之守护进程 - 灰信网(软件开发博客聚合)进行实践和完善。
计算机实际上可以做的事情实质上非常简单,比如计算两个数的和,再比如在内存中寻找到某个地址等等。这些最基础的计算机动作被称为指令 (instruction)。所谓的程序(program),就是这样一系列指令的所构成的集合。通过程序,我们可以让计算机完成复杂的操作。程序大多数时候被存储为可执行的文件。这样一个可执行文件就像是一个菜谱,计算机可以按照菜谱作出可口的饭菜。
那么,程序和进程(process)的区别又是什么呢?
进程是程序的一个具体实现。只有食谱没什么用,我们总要按照食谱的指点真正一步步实行,才能做出菜肴。进程是执行程序的过程,类似于按照食谱,真正去做菜的过程。同一个程序可以执行多次,每次都可以在内存中开辟独立的空间来装载,从而产生多个进程。不同的进程还可以拥有各自独立的IO接口。
操作系统的一个重要功能就是为进程提供方便,比如说为进程分配内存空间,管理进程的相关信息等等,就好像是为我们准备好了一个精美的厨房。
首先,我们可以使用 p s 命 令 来 查 询 正 在 运 行 的 进 程 , 比 如 ‘ ps命令来查询正在运行的进程,比如` ps命令来查询正在运行的进程,比如‘ps -eo pid,comm,cmd`,下图为执行结果:
(-e表示列出全部进程,-o pid,comm,cmd表示我们需要PID,COMMAND,CMD信息)
ok@u20:~/桌面$ ps -eo pid,comm,cmd
PID COMMAND CMD
1 systemd /sbin/init splash
2 kthreadd [kthreadd]
3 rcu_gp [rcu_gp]
4 rcu_par_gp [rcu_par_gp]
5 netns [netns]
7 kworker/0:0H-ev [kworker/0:0H-events_highpri]
10 mm_percpu_wq [mm_percpu_wq]
...
9995 systemd-udevd /lib/systemd/systemd-udevd
11641 systemd-logind /lib/systemd/systemd-logind
11672 gdm-session-wor gdm-session-worker [pam/gdm-launch-environment]
11681 systemd /lib/systemd/systemd --user
11682 (sd-pam) (sd-pam)
15543 kworker/0:1-cgr [kworker/0:1-cgroup_destroy]
15546 kworker/2:1-rcu [kworker/2:1-rcu_par_gp]
15564 bash bash test.sh
15566 a.out ./a.out
15574 kworker/3:1-cgr [kworker/3:1-cgroup_destroy]
15579 nautilus /usr/bin/nautilus --gapplication-service
15602 bash bash
15614 kworker/0:2-eve [kworker/0:2-events]
15667 bash bash
15738 kworker/3:0-eve [kworker/3:0-events]
每一行代表了一个进程。每一行又分为三列。第一列PID(process IDentity)是一个整数,每一个进程都有一个唯一的PID来代表自己的身份。第二列COMMAND是这个进程的简称。第三列CMD是进程所对应的程序以及运行时所带的参数。
(第三列有一些由中括号[]括起来的。它们是内核的一部分功能,被打扮成进程的样子以方便操作系统管理。我们不必考虑它们。)
实际上,当计算机开机的时候,内核(kernel)只建立了一个systemd进程。Linux内核并不提供直接建立新进程的系统调用。剩下的所有进程都是init进程通过fork机制建立的。新的进程要通过老的进程复制自身得到,这就是fork。fork是一个系统调用。进程存活于内存中。每个进程都在内存中分配有属于自己的一片空间 (address space)。当进程fork的时候,Linux在内存中开辟出一片新的内存空间给新的进程,并将老的进程空间中的内容复制到新的空间中,此后两个进程同时运行。
老进程成为新进程的父进程(parent process),而相应的,新进程就是老的进程的子进程(child process)。一个进程除了有一个PID之外,还会有一个PPID(parent PID)来存储的父进程PID。如果我们循着PPID不断向上追溯的话,总会发现其源头是systemd进程。所以说,所有的进程也构成一个以systemd为根的树状结构。
如下,我们查询当前shell下的进程:
ok@u20:~/桌面$ ps -o pid,ppid,cmd
PID PPID CMD
15667 13862 bash
16323 15667 ps -o pid,ppid,cmd
ok@u20:~/桌面$
还可以用pstree命令来显示整个进程树:
ok@u20:~/桌面$ pstree
systemd─┬─ModemManager───2*[{ModemManager}]
├─NetworkManager───2*[{NetworkManager}]
├─VGAuthService
├─accounts-daemon───2*[{accounts-daemon}]
├─acpid
├─at-spi-bus-laun─┬─dbus-daemon
│ └─3*[{at-spi-bus-laun}]
├─at-spi2-registr───2*[{at-spi2-registr}]
├─avahi-daemon───avahi-daemon
├─bluetoothd
├─colord───2*[{colord}]
├─cron
├─cups-browsed───2*[{cups-browsed}]
├─cupsd
├─dbus-daemon
├─gdm3─┬─gdm-session-wor─┬─gdm-x-session─┬─Xorg───{Xorg}
│ │ │ ├─dbus-run-sessio─┬─dbus-daemo+
│ │ │ │ └─gnome-sess+
│ │ │ └─2*[{gdm-x-session}]
│ │ └─2*[{gdm-session-wor}]
│ ├─gdm-session-wor─┬─gdm-x-session─┬─Xorg───{Xorg}
│ │ │ ├─gnome-session-b─┬─ssh-agent
│ │ │ │ └─2*[{gnome-+
│ │ │ └─2*[{gdm-x-session}]
│ │ └─2*[{gdm-session-wor}]
│ └─2*[{gdm3}]
├─gjs───6*[{gjs}]
├─gnome-keyring-d───3*[{gnome-keyring-d}]
├─gsd-printer───2*[{gsd-printer}]
├─ibus-daemon─┬─ibus-engine-lib───3*[{ibus-engine-lib}]
│ ├─ibus-engine-sim───2*[{ibus-engine-sim}]
│ ├─2*[kerneloops]
├─networkd-dispat
├─polkitd───2*[{polkitd}]
├─rsyslogd───3*[{rsyslogd}]
├─rtkit-daemon───2*[{rtkit-daemon}]
├─snapd───13*[{snapd}]
├─supervisord───bash───a.out
├─switcheroo-cont───2*[{switcheroo-cont}]
├─systemd─┬─(sd-pam)
│ ├─dbus-daemon
│ ├─goa-daemon───3*[{goa-daemon}]
├─systemd─┬─(sd-pam)
│ ├─at-spi-bus-laun─┬─dbus-daemon
│ │ └─3*[{at-spi-bus-laun}]
│ ├─at-spi2-registr───2*[{at-spi2-registr}]
│ ├─gnome-session-c───{gnome-session-c}
│ ├─gnome-shell───8*[{gnome-shell}]
│ ├─gnome-shell-cal───5*[{gnome-shell-cal}]
│ ├─gnome-terminal-─┬─2*[bash]
│ │ ├─bash───pstree
│ │ └─4*[{gnome-terminal-}]
│ ├─goa-daemon───3*[{goa-daemon}]
│ ├─xdg-document-po───5*[{xdg-document-po}]
│ └─xdg-permission-───2*[{xdg-permission-}]
├─systemd-journal
├─systemd-logind
├─systemd-network
├─wpa_supplicant
└─xdg-permission-───2*[{xdg-permission-}]
ok@u20:~/桌面$
fork通常作为一个函数被调用。这个函数会有两次返回,将子进程的PID返回给父进程,0返回给子进程。实际上,子进程总可以查询自己的PPID来知道自己的父进程是谁,这样,一对父进程和子进程就可以随时查询对方。
通常在调用fork函数之后,程序会设计一个if选择结构。
由此,就可以在子进程建立之后,让它执行与父进程不同的功能。
当子进程终结时,它会通知父进程,并清空自己所占据的内存,并在内核里留下自己的退出信息(exit code,如果顺利运行,为0;如果有错误或异常状况,为>0的整数)。在这个信息里,会解释该进程为什么退出。父进程在得知子进程终结时,有责任对该子进程使用wait系统调用。这个wait函数能从内核中取出子进程的退出信息,并清空该信息在内核中所占据的空间。但是,如果父进程早于子进程终结,子进程就会成为一个孤儿(orphand)进程。孤儿进程会被过继给init进程,init进程也就成了该进程的父进程。init进程负责该子进程终结时调用wait函数。
当然,一个糟糕的程序也完全可能造成子进程的退出信息滞留在内核中的状况(父进程不对子进程调用wait函数),这样的情况下,子进程成为僵尸(zombie)进程。当大量僵尸进程积累时,内存空间会被挤占。
尽管在UNIX中,进程与线程是有联系但不同的两个东西,但在Linux中,线程只是一种特殊的进程。多个线程之间可以共享内存空间和IO接口。所以,进程是Linux程序的唯一的实现方式。
总结:
在fork()/execve()
过程中,假设子进程结束时父进程仍存在,而父进程fork()
之前既没安装SIGCHLD
信号处理函数调用waitpid()/wait()
等待子进程结束,又没有显式忽略该信号,则子进程成为僵尸进程,无法正常结束,此时即使是root
身份kill -9
也不能杀死僵尸进程。补救办法是杀死僵尸进程的父进程(僵尸进程的父进程必然存在,否则子进程就会因为父进程的结束而变成了孤儿进程了),僵尸进程成为”孤儿进程”,过继给1号进程init,init始终会负责清理僵尸进程。
每个进程都会属于一个进程组(process group,pg),每个进程组中可以包含多个进程。进程组会有一个进程组领导进程 (process group leader),也叫进程组长。进程组长的PID成为进程组的ID (process group ID, PGID),以识别进程组。
一般情况下,一个进程组是由一个进程 fork 出来的,之后,它的子进程再去 fork ,最后,得到了一个进程组。当然,单个的进程也是一个进程组。进程组有进程组 id ,它通常是第一个进程的 pid ,也就是组长进程的 pid (以识别进程组)。但是一个进程组的组长进程并不是一定要一直存在,当进程组长运行结束后,该进程组仍然存在,且进程组的id不变。
$ps -o pid,pgid,ppid,comm | cat
PID PGID PPID COMMAND
17763 17763 17751 bash
18534 18534 17763 ps
18535 18534 17763 cat
PID为进程自身的ID,PGID为进程所在的进程组的ID, PPID为进程的父进程ID。从上面的结果,我们可以推测出如下关系:ps和cat进程构成18534进程组,组长为ps进程;bash单个进程构成一个进程组。同时,bash进程是ps和cat的父进程。进程组的领导进程的PID成为进程组ID。领导进程可以先终结。此时进程组依然存在,并持有相同的PGID,直到进程组中最后一个进程终结。
创建进程组的目的是用于简化向组内所有进程发送信号的操作,即如果一个信号是发给一个进程组,则这个组内的所有进程都会受到该信号【方便管理】。
进程组ID和进程ID相似:它们是正整型,并以pid_t类型存储。函数getpgrp返回调用进程的进程组ID。
#include
/***********************
*功能:获取进程组ID
*返回值:成功返回进程组ID,失败返回-1并设置errno
* *********************/
pid_t getpgrp(void);
在BSD后代系统的早期版本,getpgrp函数接受一个pid参数并返回这个进程的进程组。SUS定义getpgid函数作为一个XSI扩展来效仿这种行为。
/**************************
* 功能:获取进程GID
* 参数:pid ---- 进程ID
* 如果pid为0,返回调用进程的进程组ID。因而getpgid(0)等价于getpgrp();
* 返回值:成功返回进程组ID,失败返回-1并设置errno
* ***********************/
pid_t getpgid(pid_t pid);
通过调用setpgid,可以让一个进程加入一个存在的进程组或创建一个新的进程组。
/*****************************
* 功能: 设置进程组ID
* 参数: pid进程ID
* pgid进程组ID
* 如果pid的实参为0,setpgid(0, 5)等价于setpgid(getpid(), 5)
* 如果pgid为0,setpgid( 5, 0)等价于setpgid( 5, getpid())
* 返回值:成功返回0,错误返回-1
* 注意: 一个进程只能为它自己或它的子进程设置进程组ID
* 在它的子进程调用了exec后,它就不再能改变该子进程的进程组ID
****************************/
int setpgid(pid_t pid, pid_t pgid);
KILL 进程组
一个进程所创建的子进程,都会被包含到一个进程组中。所以,我们可以用进程组杀死某个进程及其fork出的所有子进程。
ok@u20:~/桌面$ ps -eo pid,ppid,pgid,comm
pid ppid pgid comm
15564 989 15564 bash
15566 15564 15564 a.out
15579 12965 12978 nautilus
我们以15564进程组为例,确实将该进程组的进程杀掉了。
$ kill -- -15564
或者$ kill -SIGTERM -- -15564
ok@u20:~/桌面$ kill -- -15564
ok@u20:~/桌面$ ps -eo pid,ppid,pgid,comm
PID PPID PGID COMMAND
1 0 1 systemd
2 0 0 kthreadd
...
15312 1 15312 snapd
15476 1 15476 systemd-resolve
15579 12965 12978 nautilus
15602 13862 15602 bash
15614 2 0 kworker/0:2-rcu_par_gp
15667 13862 15667 bash
POSIX中的进程组
显示子进程与父进程的进程组id。
#include
#include
#include
int main()
{
pid_t pid;
printf("start fork......\n");
if((pid=fork())<0){//第一次创建子进程
perror("fork");
exit(1);
}else if(pid==0){//子进程
printf("The child process PID is %d.\n",getpid());
printf("The Group ID is %d.\n",getpgrp());
printf("The Group ID is %d.\n",getpgid(0));
printf("The Group ID is %d.\n",getpgid(getpid()));
}else{//父进程
sleep(3);
printf("The parent process PID is %d.\n",getpid());
printf("The Group ID is %d.\n",getpgrp());
}
exit(0);
}
ok@u20:~/test/forkTest$ ./fork1
start fork......
The child process PID is 15700.
The Group ID is 15699.
The Group ID is 15699.
The Group ID is 15699.
The parent process PID is 15699.
The Group ID is 15699.
僵尸进程和孤儿进程的测试也可以在上述代码中展开,只不过需要修改一下结束顺序。这里面因为父进程后结束,而子进程结束,但是父进程没有设置wait()函数等接收子进程的结束信号,所以当子进程结束而父进程没有结束的时候,子进程会变成僵尸进程:
16851 15602 16851 fork1 ./fork1
16852 16851 16851 fork1 [fork1]
16853 13892 16853 ps ps -eo pid,ppid,pgid,comm,cmd
但是当父进程也结束后,两个进程都正常结束,应该是父进程正常退出,僵尸子进程变成1号进程的子进程,由1号进程进行了资源回收处理。
16833 16830 16830 a.out ./a.out
16872 2 0 kworker/0:2-cgr [kworker/0:2-cgroup_destroy]
16876 13892 16876 ps ps -eo pid,ppid,pgid,comm,cmd
ok@u20:~/桌面$
下面子进程使用setpgid()创建了一个新的进程组,并且进程组的组长就是自己。从而脱离了父进程的进程组:
#include
#include
#include
#include
#include
int main()
{
pid_t pid;
printf("start fork......\n");
if((pid=fork())<0){
perror("fork");
exit(1);
}else if(pid==0){
printf("Child:before setpgid,my pid=%d,My pgid=%d\n",getpid(),getpgrp());
setpgid(getpid(),getpid());
printf("Child:after setpgid,my pid=%d,My pgid=%d\n",getpid(),getpgrp());
}else{
wait(NULL);//等待子进程先结束,并处理其发送的sigchild信号
printf("child exit\n");
printf("Father:My pid=%d,My pgid=%d\n",getpid(),getpgid(getpid()));
}
exit(0);
}
ok@u20:~/test/forkTest$ ./fork2
start fork......
Child:before setpgid,my pid=17007,My pgid=17006
Child:after setpgid,my pid=17007,My pgid=17007
child exit
Father:My pid=17006,My pgid=17006
ok@u20:~/test/forkTest$
可以看出子进程可以设置自己为新的进程组长。
#include
#include
#include
#include
#include
int main()
{
pid_t pid;
printf("start fork......\n");
if((pid=fork())<0){
perror("fork");
exit(1);
}else if(pid==0){
printf("Child:before setpgid,my pid=%d,My pgid=%d\n",getpid(),getpgrp());
sleep(2);
printf("Child:after setpgid,my pid=%d,My pgid=%d\n",getpid(),getpgrp());
}else{
sleep(1);
setpgid(pid,pid);
wait(NULL);
printf("child exit\n");
printf("Father:My pid=%d,My pgid=%d\n",getpid(),getpgid(getpid()));
}
exit(0);
}
ok@u20:~/test/forkTest$ ./fork3
start fork......
Child:before setpgid,my pid=17033,My pgid=17032
Child:after setpgid,my pid=17033,My pgid=17033
child exit
Father:My pid=17032,My pgid=17032
ok@u20:~/test/forkTest$
多个进程组构成一个「会话」,建立会话的进程是会话的领导进程,也叫会话组长,该进程 ID 为会话的 SID。会话中的每个进程组称为一个「作业」。会话可以有一个进程组称为会话的「前台作业」,其它进程组为「后台作业」
一个会话可以有一个控制终端,当控制终端有输入和输出时都会传递给前台进程组,比如Ctrl + Z。会话的意义在于能将多个作业通过一个终端控制,一个前台操作,其它后台运行。
进程组(工作)的概念较为简单易懂。而会话主要是针对一个终端建立的。当我们打开多个终端窗口时,实际上就创建了多个终端会话。每个会话都会有自己的前台工作和后台工作。这样,我们就为进程增加了管理和运行的层次。在没有图形化界面的时代,会话允许用户通过shell进行多层次的进程发起和管理。比如说,我可以通过shell发起多个后台工作,而此时标准输入输出并不被占据,我依然可以继续其它的工作。如今,图形化界面可以帮助我们解决这一需求,但工作组和会话机制依然在Linux的许多地方应用。
1、一个进程通过调用setsid函数来建立一个新的会话。
#include
pid_t setsid(void);
//返回值:成功返回会话ID;出错返回-1
①如果调用此函数的进程不是一个进程组组长,则此函数创建一个新会话:
此进程变成该新会话的会话首进程(session leader,会话首进程是创建该会话的进程,也就是会话组长)。此时,该进程是该会话中的唯一进程
此进程成为一个新进程组的组长进程。新进程组ID是该调用进程的进程ID
此进程没有控制终端。如果在调用setsid之前此进程有一个控制终端,那么这种联系也被解除
②如果调用此函数的进程是一个进程组组长,则此函数出错返回
为了保证不处于这种情况,通常先调用fork,然后使其父进程终止,而子进程则继续。因为子进程继承了父进程的进程组ID, 而其进程ID则是新分配的,两者不可能相等,所以这就保证了子进程不是一个进程组的组长
组长进程不能成为新会话首进程,新会话首进程必定会成为组长进程…
2、获取会话首进程的进程组ID(getsid函数)
#include
//返回值:成功返回会话首进程的会话ID;失败返回-1
pid_t getsid(pid_t pid);
会话ID
例如:下面的一个会话中,有3个进程组
通常是由shell的管道线将几个进程编成一组的,则上图的安排可能是由下列形式的shell命令形成的
一个命令可以通过在末尾加上&方式让它在后台运行:
$ ping localhost > log &
[1] 4345 # 括号中的1表示工作号,而10141为PGID
$ ps -o pid,pgid,ppid,sid,tty,comm
PID PGID PPID SID TT COMMAND
3772 3772 3764 3772 pts/0 bash
4345 4345 3772 3772 pts/0 ping
4355 4355 3772 3772 pts/0 ps
信号可以通过kill的方式来发送给工作组。上面的两个命令,一个是发送给PGID(通过在PGID前面加-来表示是一个PGID而不是PID),一个是发送给工作1(%1),两者等价。
$kill -SIGTERM -4345
或者
$kill -SIGTERM %1
一个工作可以通过$fg从后台工作变为前台工作:
$cat > log &
$fg %1
当我们运行第一个命令后,由于工作在后台,我们无法对命令进行输入,直到我们将工作带入前台,才能向cat命令输入。在输入完成后,按下CTRL+D来通知shell输入结束。