进程是操作系统的一个核心概念。每个进程都有自己唯一的标识:进程ID,也有自己的生命周期。
- 父进程,子进程
进程都有父进程,父进程也有父进程。
父进程:自己调用fork()来创建一个或多个子进程的进程,所有的进程只有一个父进程
子进程:被创建的进程,子进程所有的资源都继承父进程,但是两个不共享地址,继而变量也不会共享
- 孤儿进程,僵尸进程
在 Unix/Linux 系统中,子进程是通过父进程创建的,且两者的运行是相互独立的,父进程永远无法预测子进程到底什么时候结束。为了保证父进程可以得到子进程结束时的状态信息,当一个进程调用 exit 命令结束自己的生命时,它并没有真正的被销毁。内核只是释放了该进程的所有资源,包括打开的文件、占用的内存等,但是留下一个称为僵尸进程的数据结构,这个结构保留了一定的信息(包括进程号 the process ID,退出状态,运行时间),这些信息直到父进程通wait()/waitpid() 来取时才释放。
一个父进程退出,而它的一个或多个子进程还在运行,那么这些子进程将成为孤儿进程。孤儿进程将被 init 进程(进程号为1)所收养,并由 init 进程对它们完成状态收集工作。
孤儿进程是没有父进程的进程,孤儿进程这个重任就落到了 init 进程身上,init 进程就好像是一个民政局,专门负责处理孤儿进程的善后工作。每当出现一个孤儿进程的时候,内核就把孤儿进程的父进程设置为 init,而 init 进程会循环地 wait() 它的已经退出的子进程。这样,当一个孤儿进程凄凉地结束了其生命周期的时候,init 进程就会出面处理它的一切善后工作。因此孤儿进程并不会有什么危害。
一个进程使用 fork 创建子进程,如果子进程退出,而父进程并没有调用 wait 或 waitpid 获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中,这种进程称之为僵死进程。
僵尸进程虽然不占有任何内存空间,但如果父进程不调用 wait() / waitpid() 的话,那么保留的信息就不会释放,其进程号就会一直被占用,而系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程。
僵死进程解决方法:
父进程通过 wait 和 waitpid 等函数等待子进程结束:这会导致父进程挂起,所以这并不是一个好办法,父进程如果不能和子进程并发执行的话,那我们创建子进程就没有意义。并且一个 wait 只能解决一个子进程,如果有多个子进程就要用到多个 wait。
通过信号机制:子进程退出时,向父进程发送 SIGCHILD 信号,父进程处理 SIGCHILD 信号,在信号处理函数中调用 wait 处理僵尸进程。
fork两次:原理是使进程成为孤儿进程,从而其父进程变为 init 进程,通过 init 进程处理僵尸进程。具体操作为:父进程一次 fork() 后产生一个子进程随后立即执行 wait(NULL) 来等待子进程结束,然后子进程再次 fork() 后产生孙子进程,随后立即exit(0)。这样子进程顺利终止(父进程仅仅给子进程收尸,并不需要子进程的返回值),然后父进程继续执行。这时的孙子进程由于失去了它的父进程(即是父进程的子进程),将被转交给Init进程托管。于是父进程与孙子进程无继承关系了,它们的父进程均为Init,Init进程在其子进程结束时会自动收尸,这样也就不会产生僵死进程了。
kill 父进程:严格地来说,僵尸进程并不是问题的根源,罪魁祸首是产生出大量僵尸进程的那个父进程。因此可以把产生大量僵尸进程的那个元凶枪毙掉(也就是通过 kill 发送 SIGTERM 或者 SIGKILL 信号)。父进程被结束之后,它产生的僵死进程就变成了孤儿进程,这些孤儿进程会被 init 进程接管,init 进程会 wait() 这些孤儿进程,释放它们占用的系统进程表中的资源。
进程回收
进程回收函数分别是wait函数和waitpid函数,调用一次只能回收一个子进程
阻塞并等待子进程退出
回收子进程残留资源
获取子进程结束状态
pid_t wait(int *status);
返回值
成功返回子进程的ID,失败返回-1。
参数
status:
通过status获取子进程的退出状态
WIFEXITED(status),若为正常结束子进程返回的状态,则为真;对于这种情况可执行 WEXITSTATUS(status),取子进程传给exit的低8位。
WEXITSTATUS(status),取得子进程exit()返回的结束代码,一般会先用 WIFEXITED 来判断是否正常结束才能使用此宏。
WIFSIGNALED(status),若为异常结束子进程返回的状态,则为真;对于这种情况可执行WTERMSIG(status),取使子进程结束的信号编号。
WTERMSIG(status),取得子进程因信号而中止的信号代码,一般会先用 WIFSIGNALED 来判断后才使用此宏。
WIFSTOPPED(status),若为当前暂停子进程返回的状态,则为真;对于这种情况可执行WSTOPSIG(status),取使子进程暂停的信号编号。
WSTOPSIG(status),取得引发子进程暂停的信号代码,一般会先用 WIFSTOPPED 来判断后才使用此宏。
waitpid
pid_t waitpid(pid_t pid, int *status, int options);
参数
pid:
pid<-1 等待进程组号为pid绝对值的任何子进程
pid=-1 等待任何子进程,此时的waitpid()函数就退化成了普通的wait()函数
pid=0 等待进程组号与目前进程相同的任何子进程
pid>0 等待进程号为pid的子进程(一般是这个比较常用)
options:
WNOHANG,若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若结束,则返回该子进程的ID。
WUNTRACED,若子进程进入暂停状态,则马上返回,但子进程的结束状态不予以理会。通过WIFSTOPPED(status)宏确定返回值是否对应一个暂停的子进程。
- 进程、进程组,会话
一个进程会有如下ID:
PID:进程ID,进程的唯一标识。对于多线程的进程而言,所有线程调用getpid函数会返回相同的值。
PGID:进程组ID。每个进程都会有进程组ID,表示该进程所属的进程组。默认情况下新创建的进程会继承父进程的进程组ID。可以调用函数getpgrp或getpgid获取进程组ID。可以调用函数setpgid来修改进程的组ID或建立新的进程组。
SID:会话ID。每个进程也都有会话ID。默认情况下,新创建的进程会继承父进程的会话ID。可以调用函数getsid获取进程的会话ID。可以调用函数setsid来创建会话。
进程组和会话在进程之间形成了两级的层次:
进程组是一组协同工作或关联进程的集合,每个进程有进程组ID(PGID);每个进程属于一个进程组,每一个进程组有一个进程组长,该进程组长的进程ID(PID)与进程组ID(PGID)相同;一个信号可以发送给进程组的所有进程、让所有进程终止、暂停或继续运行。
会话是一个或多个进程组的集合。当用户登录系统时,登录进程会为这个用户创建一个新的会话(session);shell进程(如bash)作为会话的第一个进程,称为会话进程(session leader);
会话的SID:等于会话首进程的PID;会话会分配给用户一个控制终端(只能有一个),用于处理用户的输入输出;一个会话包括了该登录用户的所有活动;会话中的进程由一个前台进程组和N个后台进程组构成。
- 守护进程
守护进程(Daemon)是一种运行在后台的一种特殊的进程,它独立于控制终端并且周期性的执行某种任务或等待处理某些发生的事件。由于在linux中,每个系统与用户进行交流的界面成为终端,每一个从此终端开始运行的进程都会依附于这个终端,这个终端被称为这些进程的控制终端,当控制终端被关闭的时候,相应的进程都会自动关闭。但是守护进程却能突破这种限制,它脱离于终端并且在后台运行,并且它脱离终端的目的是为了避免进程在运行的过程中的信息在任何终端中显示并且进程也不会被任何终端所产生的终端信息所打断。它从被执行的时候开始运转,知道整个系统关闭才退出(当然可以认为的杀死相应的守护进程)。如果想让某个进程不因为用户或中断或其他变化而影响,那么就必须把这个进程变成一个守护进程。
守护进程实现的步骤:
(1) 创建子进程,父进程退出(使子进程成为孤儿进程):这是编写守护进程的第一步,由于守护进程是脱离终端的,因此完成第一步后就会在shell终端里造成一个程序已经运行完毕的假象。之后的所有工作在子进程中完成,而用户在shell终端里则可以执行其他命令,从而在形式上做到了与控制终端脱离。实现的语句如下:if(pid=fork()){exit(0);}是父进程就结束,然后子进程继续执行。
(2) 在子进程中创建新的会话(脱离控制终端):
这步是创建守护进程中最重要的一步,虽然实现起来很简单,但是它的意义非常重要,在这里使用的是系统函数setsid()来创建一个新的会话,并且担任该会话组的组长。会话组组长可以重新申请打开一个控制终端。可以通过使进程不再成为会话组长来禁止进程重新打开控制终端;
setsid()函数的作用:创建一个新的会话,并且担任该会话组的组长。具体作用包括:让一个进程摆脱原会话的控制,让进程摆脱原进程的控制,让进程摆脱原控制终端的控制。
创建守护进程要调用setsid()函数的原因:由于创建守护进程的第一步是调用fork()函数来创建子进程,再将父进程退出。由于在调用了fork()函数的时候,子进程拷贝了父进程的会话期、进程组、控制终端等资源、虽然父进程退出了,但是会话期、进程组、控制终端等并没有改变,因此,需要用setsid()函数来使该子进程完全独立出来,从而摆脱其他进程的控制。
(3) 改变当前目录为根目录:
使用fork()创建的子进程是继承了父进程的当前工作目录,由于在进程运行中,当前目录所在的文件系统是不能卸载的,这对以后使用会造成很多的麻烦。因此通常的做法是让“/”作为守护进程的当前目录,当然也可以指定其他的别的目录来作为守护进程的工作目录。
(4) 重设文件权限掩码:
文件权限掩码是屏蔽掉文件权限中的对应位。由于使用fork()函数新创建的子进程继承了父进程的文件权限掩码,这就给该子进程使用文件带了很多的麻烦(比如父进程中的文件没有执行文件的权限,然而在子进程中希望执行相应的文件这个时候就会出问题)。因此在子进程中要把文件的权限掩码设置成为0,即在此时有最大的权限,这样可以大大增强该守护进程的灵活性。设置的方法是:umask(0)。
(5) 关闭文件描述符:
同文件权限码一样,用fork()函数新建的子进程会从父进程那里继承一些已经打开了的文件。这些文件被打开的文件可能永远不会被守护进程读写,如果不进行关闭的话将会浪费系统的资源,造成进程所在的文件系统无法卸下以及引起预料的错误。
(6) 守护进程的退出:
上面建立了守护进程,当用户需要外部停止守护进程运行时,往往需要使用kill命令来停止该守护进程,所以守护进程中需要编码来实现kill发出的signal信号处理,达到进程的正常退出。实现该过程的函数是signal函数:
signal(SIGTERM, sigterm_handler);
void sigterm_handler(int arg)
{
//进行相应处理的函数
}
功能是:将一个给定的函数和一个特定的信号联系起来,即在收到特定的信号的时候执行相应的函数。
守护进程实现的一个简单的例子:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <assert.h>
#include <time.h>
#include <sys/stat.h>
int main()
{
// 创建子进程,父进程退出(使子进程成为孤儿进程)
pid_t pid=fork();
if(pid!=0)
{
exit(0);
}
// 在子进程中创建新的会话
setsid();
// 再次创建子进程,父进程退出使进程不再成为会话组长来禁止进程重新打开控制终端
pid_t pid=fork();
if(pid!=0)
{
exit(0);
}
// 改变当前目录为根目录
chdir("/");
// 重设文件权限掩码
umask(0);
// 关闭文件描述符
int maxfd = getdtablesize();
for(int i=0;i<maxfd;i++)
{
close(i);
}
while(1)
{
FILE *fp=fopen("/tmp/deamon.log","a");
if(fp==NULL)
{
break;
}
time_t tv;
time(&tv);
fprintf(fp,"Time is %sn",asctime(localtime(&tv)));
fclose(fp);
sleep(5);
}
}