进程可以简单的理解为一个正在执行的程序,它是计算机系统中拥有资源和独立运行的最小单位。多个进程同时运行从宏观看是并行,从微观上看是串行。举个例子,现有一个CPU以及两个同时运行的线程a和b,CPU实际上是用极小的时间碎片来交替执行a和b,以达到肉眼觉得CPU在同时执行两个进程的效果。
进程有三个状态,分别是就绪态、运行态、阻塞态。顾名思义,就绪态就是进程万事俱备只等CPU来执行它了;运行态便是CPU正在执行该进程;阻塞态是线程还没准备好被CPU执行。当然在这三个状态之上又衍生出许多状态,这里不多做介绍。
另外,每一个进程都有自己的编号,称之为pid(process identity document)。在进程中可以通过getpid()获得当前进程pid,也可以通过getppid()获得当前进程父进程的pid。
man 2 fork
pid_t pid fork();
返回值:成功则返回子进程的pid,失败则返回负值。
用fork创建的子进程会和父进程执行同一个可执行文件,但子进程会从fork函数之后才开始执行。如图所示:
这里值得注意的是,程序的编译会经历四个步骤,即预处理、编译、汇编、链接。只有经过这四个步骤之后程序才会变成一个可执行文件,而由于这四个步骤会处理好程序的各种变量、头文件、宏定义等内容,所以不会导致子进程从fork开始执行下去会因为缺少一些变量定义之类的而产生报错。
如果仅仅使用fork让子进程执行父进程的代码,这将使子进程显得毫无意义,而为了给子进程添加新的任务,exec函数族便被发明出来。从说明书可以看到exec有六个函数。
man execl
比较常用的使execl,通过用execl函数让子进程去执行其他的可执行文件,以达到给子进程添加新任务的目的。其函数原型长这样:
int execl(const char *path, const char *arg, ... /* (char *) NULL*/);
它的参数应当如何设置,我先直接贴一段manual的原文上来。
The const char *arg and subsequent ellipses in the execl(), execlp(),and execle() functions can be thought of as arg0, arg1, ..., argn.
Together they describe a list of one or more pointers to null-terminated strings that represent the argument list available to the executed program.
The first argument, by convention, should point to the filename associated with the file being executed.
The list of arguments must be terminated by a null pointer, and, since these are variadic functions, this pointer must be cast (char *) NULL.
这段话大概的意思是,execl可以有无数个参数,具体取决了即将调用的可执行文件的需要。但除了char *path之外的第一个参数是可执行文件的名字,最后一个参数是NULL。
char *path:可执行文件所在的目录(包含可执行文件的名字)。
char *arg1:可执行文件的名字。
…
char *argn:NULL
比如我们想在子进程中执行ls。execl可以这个写:
execl(“/bin/ls”,“ls”,NULL); //仅列出当前目录可见文件
execl(“/bin/ls”,“ls”, "-l", NULL); //列出当前目录可见文件详细信息
execl(“/bin/ls”,“ls”, "-i", "-l", NULL); //显示文件的inode信息
...
为什么父进程的getpid()和子进程getppid()得到的值不一样,可以参考下面这篇文章:父进程中getpid()值与子进程中getppid()值不相同的问题及解释
以上这三种进程中,孤儿进程是可以成为进入守护进程的前提,而守护进程又在许多情况下挥发巨大的作业,那么只剩下僵尸进程是程序不愿意看到的。
避免僵尸进程可以用wait系列函数函数,我们来看看它的函数说明:
man 2 wait
wait的函数原型是:
pid_t wait(int *status);
当调用wait()函数,父进程会自动检查子进程的状态,无需我们再干预。
int *status:是一个32位的整形数据,其中包含了退出码、终止信号等信息。通常通过一些宏函数来读取status中的具体信息。当然,如果你压根不想要读取这些信息,只想默默收尸走人,那这个参数可以是NULL。
返回值:如果成功,则返回子进程的pid,反之返回-1。
读取status的宏:
此外,还有waitpid,waitid等函数,前者常用于等待回收某个具体的子进程,后者我也不太懂了。。。
贴一段代码来看看wait怎么用:
#include
#include
#include
#include
#include
int main()
{
pid_t pid = fork(); //create a child process
if(pid)
{
int status;
wait(&status); //waiting for the child to terminate and recliam its resources
if(WIFEXITED(status))
{
printf("The exit code is: %d\n", WEXITSTATUS(status));
}
}
else
{
printf("This is the child process.\n");
sleep(2);
exit(3);
}
return 0;
}
前文提到,守护进程是托管在 i n i t init init下的子进程,且脱离控制终端独立运行于后台。由此引出创建一个守护进程的两个必要步骤:
这两个步骤使创建一个守护进程的必要步骤,再次也先暂停下来解释何为进程组和会话。
所谓进程组,顾名思义就是许多个进程组成的一个小组,该小组的id(Group Identity Document: GID)就是小组组长的pid。接着,会话中又会聚集了许多个小组,同理,会话id(Session Identity Dccument: SID)便是作为翘楚的进程组id(GID)。一般而言,一个会话使用一个控制终端,不过也有特殊,比如对于为守护进程所创建的新会话,我们不希望它有一个控制终端。
注:控制终端就是我们敲命令行的那个窗口,也称终端或终端窗口。在Ubuntu中直接叫terminal(终端),一个terminal对应一个shell进程。而shell是一个解释器,为终端和系统之间的交互提供桥梁。参考:link
接下来,添加几个步骤让讲守护进程的更具备撸棒性(robust)。
最后,就可以在守护进程中添加我们需要执行的代码了。
守护进程一般生命周期比较长,由于其脱离了控制终端,所以想要关闭守护进程只能等到系统完全关闭或者手动杀死它。比如用kill:
kill -9 [the pid of the daemon process]
本次练习任务:
#include
#include
#include
#include
#include
int main()
{
pid_t pid = fork();
if(pid) //enter the parent process
{
printf("the pid of the parent processs is: %d.\n",getpid());
exit(1);
}
else //enter the child process
{
sleep(1); //waiting for the parent process to terminate
printf("the pid of the child processs is: %d, and parent is: %d. \n",getpid(),getppid());
setsid(); //create and enter a new session
chdir("/"); //change the working directory
umask(0); //change the umask
for(int i=0;i<3;i++){close(i);} //close the file descriptor
while(1)
{
//you can put any programs you like into this field.
}
}
return 0;
}
输出结果是:
the pid of parent process is: 2600.
the pid of child process is: 2601, and its parent is: 1420.
可以看到子进程的父进程已经和原来创建它的父进程pid不一样了,我们通过搜索看看是谁托管了这个子进程。
ps -aux | grep 1420
可以看到是init进程托管了这个子进程。此外,当该孤儿进程使用setsid()函数变成守护进程之后,如果再使用printf()之类的函数将失去效果。因为守护进程没有其对应的控制终端,自然无法让printf()发挥作用。