[TOC]
参考资料
Linux 之守护进程、僵死进程与孤儿进程
【概述】-Linux内核三驾马车之-进程管理
【Linux编程】守护进程(daemon)详解与创建
守护进程
Linux 进程、进程组、会话周期、控制终端
Linux中fork,vfork和clone详解(区别与联系)
1. 进程的基本概念
1.1. 进程与线程
进程是资源封装的单位。进程封装的资源包括:内存、文件、文件系统、信号、控制台等等。一个进程区别于另外一个进程的标记就是占有的资源完全不一样。那么在任何一个OS中描述进程,我们只需要去描述它的资源即可。通常我们使用一个数据结构PCB(Process Control Block)进程控制块来描述一个进程。在不同的OS中表现是不一样的,对于Linux操作系统,我们使用task_struct描述一个进程。
上图显示,包含的资源有:
- pid表示一个进程的ID资源
- mm_struct表示内存资源
- fs_struct表示文件系统资源
- files_struct表示打开的文件资源
- signal_struct表示信号资源
线程则是Linux的调度单位,共享同一个进程下的资源。Linux内核调度器是以线程为单位进行调度和上下文切换的。
在Linux内核中创建进程和线程,入口是统一的均是do_fork(),但是pthread_create=>clone=>do_fork会传入以下clone_flags参数,标识这些资源需要共享,这就成了创建线程的概念了。
clone_flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|...
进程和线程的更多区别可见进程和线程
1.2. 守护进程
守护进程,也就是通常说的Daemon进程,是Linux中的后台服务进程。它是一个生存期较长的进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。守护进程是脱离于终端并且在后台运行的进程。守护进程脱离于终端是为了避免进程在执行过程中的信息在任何终端上显示并且进程也不会被任何终端所产生的终端信息所打断。
1.3. 进程组
每个进程都属于一个进程组。每个进程组都有一个组长进程,组长进程的进程号等于进程组ID。只要某个进程组中有一个进程存在,则该进程组就存在,与组长进程是否终止无关。从进程组创建开始到其中最后一个进程离开为止的时间区间成为进程组的生存期。进程组中最后一个进程可以终止或者转移到另一个进程组中。
1.4. 会话
会话是一个或多个进程组的集合。通常,一个会话开始于用户登录,终止于用户退出,在此期间该用户运行的所有进程都属于这个会话期。
1.5. 控制终端
会话的领头进程打开一个终端之后, 该终端就成为该会话的控制终端 (SVR4/Linux) 与控制终端建立连接的会话领头进程称为控制进程 (session leader) 。一个会话只能有一个控制终端 ,产生在控制终端上的输入和信号将发送给会话的前台进程组中的所有进程 ,终端上的连接断开时 (比如网络断开或 Modem 断开), 挂起信号将发送到控制进程(session leader) 。平时在X-window下是使用的terminal称为伪终端,但它也是一个控制终端。
1.6. 僵尸进程
在fork()/execve()过程中,假设子进程结束时父进程仍存在,而父进程fork()之前既没安装SIGCHLD信号处理函数调用waitpid()等待子进程结束,又没有显式忽略该信号,则子进程成为僵死进程,无法正常结束,此时即使是root身份kill -9也不能杀死僵死进程。补救办法是杀死僵尸进程的父进程(僵死进程的父进程必然存在),僵死进程成为”孤儿进程”,过继给1号进程init,init始终会负责清理僵死进程。
我们知道在unix/linux中,正常情况下,子进程是通过父进程创建的,子进程在创建新的进程。子进程的结束和父进程的运行是一个异步过程,即父进程永远无法预测子进程 到底什么时候结束。 当一个 进程完成它的工作终止之后,它的父进程需要调用wait()或者waitpid()系统调用取得子进程的终止状态。
1.7. 孤儿进程
一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。
由于孤儿进程会被init进程给收养,所以孤儿进程不会对系统造成危害。
2. 僵尸进程的危害与预防
2.1. 僵尸进程的危害
unix提供了一种机制可以保证只要父进程想知道子进程结束时的状态信息, 就可以得到。这种机制就是: 在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。 但是仍然为其保留一定的信息(包括进程号the process ID,退出状态the termination status of the process,运行时间the amount of CPU time taken by the process等)。直到父进程通过wait / waitpid来取时才释放。 但这样就导致了问题,如果进程不调用wait / waitpid的话, 那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程. 此即为僵尸进程的危害,应当避免。
任何一个子进程(init除外)在exit()之后,并非马上就消失掉,而是留下一个称为僵尸进程(Zombie)的数据结构,等待父进程处理。这是每个 子进程在结束时都要经过的阶段。如果子进程在exit()之后,父进程没有来得及处理,这时用ps命令就能看到子进程的状态是“Z”。如果父进程能及时 处理,可能用ps命令就来不及看到子进程的僵尸状态,但这并不等于子进程不经过僵尸状态。 如果父进程在子进程结束之前退出,则子进程将由init接管。init将会以父进程的身份对僵尸状态的子进程进行处理。
一个进程如果只复制fork子进程而不负责对子进程进行wait()或是waitpid()调用来释放其所占有资源的话,那么就会产生很多的僵死进程,如果要消灭系统中大量的僵死进程,只需要将其父进程杀死,此时所有的僵死进程就会编程孤儿进程,从而被init所收养,这样init就会释放所有的僵死进程所占有的资源,从而结束僵死进程。
2.2. 如何防止僵尸进程
在fork()/execve()过程中,假设子进程结束时父进程仍存在,而父进程fork()之前既没安装SIGCHLD信号处理函数调用waitpid()等待子进程结束,又没有显式忽略该信号,则子进程成为僵死进程,无法正常结束,此时即使是root身份kill -9也不能杀死僵死进程。补救办法是杀死僵尸进程的父进程(僵死进程的父进程必然存在),僵死进程成为”孤儿进程”,过继给1号进程init,init始终会负责清理僵死进程。
3. 守护进程的创建
主要参考守护进程
3.1. 创建子进程,父进程退出
编写守护进程第一步,就是要使得进程独立于终端后台运行。为避免终端挂起,将父进程退出,造成程序已经退出的假象,而后面的工作都在子进程完成,这样控制终端也可以继续执行其他命令,从而在形式上脱离控制终端的控制。
由于父进程先于子进程退出,子进程就变为孤儿进程,并由 init 进程作为其父进程收养。
3.2. 子进程创建新会话
经过上一步,子进程已经后台运行,然而系统调用 fork 创建子进程,子进程便复制了原父进程的进程控制块(PCB),相应地继承了一些信息,包括会话、进程组、控制终端等信息。尽管父进程已经退出,但子进程的会话、进程组、控制终端的信息没有改变。为使子进程完全摆脱父进程的环境,需要调用 setsid 函数(注意:组长进程调用 setsid ,则出错返回,无法新建会话。)。
通过调用 setsid 函数可以创建一个新会话,调用进程担任新会话的首进程,其作用有:
- 使当前进程脱离原会话的控制
- 使当前进程脱离原进程组的控制
- 使当前进程脱离原控制终端的控制
这样,当前进程才能实现真正意义上完全独立出来,摆脱其他进程的控制。注意非组长进程调用 setsid 才能创建一个新会话,对应的变化有:
- 该进程变成新会话的首进程
- 该进程成为一个新进程组的组长进程
- 该进程没有控制终端,如果之前有,则会被中断(会话过程对控制终端的独占性)
也就是说:组长进程不能成为新会话首进程,新会话首进程必定成为组长进程。
3.3. 改变当前目录
使用fork创建的子进程继承了父进程的当前工作目录。由于在进程运行中,当前目录所在的文件系统(如“/mnt/usb”)是不能卸载的,这对以后的使用会造成诸多的麻烦(比如系统由于某种原因要进入单用户模式)。因此通常的做法是让根目录("/")作为守护进程的当前工作目录,这样就可以避免上述的问题。
当然,如有特殊需要,也可以把当前工作目录换成其他的路径,如/tmp。改变工作目录的常见函数是chdir。
3.4. 重设文件权限掩码
fork 函数创建的子进程,继承了父进程的文件操作权限,为防止对以后使用文件带来问题,需要重设文件权限掩码。
文件权限掩码,设定了文件权限中要屏蔽掉的对应位。这个跟文件权限的八进制数字模式表示差不多,将现有存取权限减去权限掩码(或做异或运算),就可产生新建文件时的预设权限。
调用 umask 设置文件权限掩码,通常是重设为 0,清除掩码,这样可以大大增强守护进程的灵活性。
3.5. 关闭文件描述符
同文件权限掩码一样,子进程可能继承了父进程打开的文件,而这些文件可能永远不会被用到,但它们一样消耗系统资源,而且可能导致所在的文件系统无法卸下,因此需要一一关闭它们。由于守护进程脱离了终端运行,因此标准输入、标准输出、标准错误输出这3个文件描述符也要关闭。