转载自:https://blog.youkuaiyun.com/nan_lei/article/details/81636473
一、fork()函数
在Linux系统内,创建子进程的方法是使用系统调用fork()函数。fork()函数是Linux系统内一个非常重要的函数,它与我们之前学过的函数有一个显著的区别:fork()函数调用一次却会得到两个返回值。
函数fork()
所需头文件:#include<sys/types.h>
#include<unistd.h>
函数原型: pid_t fork() 函数参数:无
函数返回值:
0 子进程
大于0 父进程,返回值为创建出的子进程的PID
-1 出错
fork()函数用于从一个已经存在的进程内创建一个新的进程,新的进程称为“子进程”,相应地称创建子进程的进程为“父进程”。使用fork()函数得到的子进程是父进程的复制品,子进程完全复制了父进程的资源,包括进程上下文、代码区、数据区、堆区、栈区、内存信息、打开文件的文件描述符、信号处理函数、进程优先级、进程组号、当前工作目录、根目录、资源限制和控制终端等信息,而子进程与父进程的区别有进程号、资源使用情况和计时器等。
由于复制父进程的资源需要大量的操作,十分浪费时间与系统资源,因此Linux内核采取了写时拷贝技术(copy on write)来提高效率。
由于子进程几乎对父进程完全复制,因此父子进程会同时运行同一个程序。因此我们需要某种方式来区分父子进程。区分父子进程常见的方法为查看fork()函数的返回值或区分父子进程的PID。
示例:使用fork()函数创建子进程,父子进程分别输出不同的信息
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main()
{
pid_t pid;
pid = fork();//获得fork()的返回值,根据返回值判断父进程/子进程
if(pid==-1)//若返回值为-1,表示创建子进程失败
{
perror("cannot fork");
return -1;
}
else if(pid==0)//若返回值为0,表示该部分代码为子进程
{
printf("This is child process\n");
printf("pid is %d, My PID is %d\n",pid,getpid());
}
else//若返回值>0,则表示该部分为父进程代码,返回值是子进程的PID
{
printf("This is parent process\n");
printf("pid is %d, My PID is %d\n",pid,getpid()); //getpid()获得的是自己的进程号
}
return 0;
}
第一次使用fork()函数的同学可能会有一个疑问:fork()函数怎么会得到两个返回值,而且两个返回值都使用变量pid存储,这样不会冲突么?
在使用fork()函数创建子进程的时候,我们的头脑内始终要有一个概念:在调用fork()函数前是一个进程在执行这段代码,而调用fork()函数后就变成了两个进程在执行这段代码。两个进程所执行的代码完全相同,都会执行接下来的if-else判断语句块。
当子进程从父进程内复制后,父进程与子进程内都有一个"pid"变量:在父进程中,fork()函数会将子进程的PID返回给父进程,即父进程的pid变量内存储的是一个大于0的整数;而在子进程中,fork()函数会返回0,即子进程的pid变量内存储的是0;如果创建进程出现错误,则会返回-1,不会创建子进程。
fork()函数一般不会返回错误,若fork()函数返回错误,则可能是当前系统内进程已经达到上限,或者内存不足。
注意:父子进程的运行先后顺序是完全随机的(取决于系统的调度),也就是说在使用fork()函数的默认情况下,无法控制父进程在子进程前进行还是子进程在父进程前进行。
vfork()函数
fork()函数还有一个兄弟函数:vfork()。
函数vfork()
所需头文件:#include<sys/types.h>
#include<unistd.h>
函数原型:pid_t vfork()
函数返回值:同fork()函数
0 子进程
大于0 父进程,返回值为创建出的子进程的PID
-1 出错
vfork()函数功能与fork()函数功能类似不过更加彻底:内核不再给子进程创建虚拟空间,直接让子进程共享父进程的虚拟空间。当父子进程中有更改相应段的行为发生时,再为子进程相应的段创建虚拟空间并分配物理空间。在vfork()函数创建子进程后父进程会阻塞,保证子进程先行运行。
vfork()函数创建的子进程会与父进程(在调用exec函数族函数或exit()函数前)共用地址空间,此时子进程如果使用变量则会直接修改父进程的变量值。因此,vfork()函数创建的子进程可能会对父进程产生干扰。另外,如果子进程未调用exec函数族函数或exit()函数,则父子进程会出现死锁现象。
举个例子,vfork()函数创建了一个“儿子”暂时“霸占”“老爹”的房产,此时需要委屈老爹一下,让老爹歇息(阻塞)。当儿子买房了(执行exec函数族函数)或者儿子死了(执行exit()退出),就相当于分家了,此时老爹得到自己的房产。
fork()函数与vfork()函数的主要区别如下:
1.vfork()函数保证子进程先行运行,在子进程调度exec函数族函数或者exit()函数后父进程才会被调度运行。如果子进程需要依赖父进程的进一步动作,则会产生死锁
2.fork()函数需要拷贝父进程的进程环境,而vfork()函数则不需要完全拷贝父进程的进程环境,在子进程调用exec函数族函数或者exit()函数之前,子进程与父进程共享进程环境(此时子进程相当于线程),父进程阻塞等待。
vfork()函数end
练习1:修改示例代码,首先让父进程输出一段信息,3秒后让子进程输出一段信息,再3秒后让父进程输出另一段信息(使用sleep()函数控制)
答案:
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main()
{
pid_t pid;
pid = fork();
if(pid==-1)
{
perror("cannot fork");
return -1;
}
else if(pid==0)
{
sleep(3);
printf("This is child process\n");
printf("pid is %d\n, My PID is %d\n",pid,getpid());
}
else
{
printf("This is parent process\n");
printf("pid is %d\n, My PID is %d\n",pid,getpid());
sleep(6);
printf("This is parent process\n");
}
return 0;
}
练习2(选做):在练习2的基础上,添加“文件锁”,使得父子进程分别对文件进行写操作
答案:
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
#include<sys/stat.h>
#include<fcntl.h>
#define MAX 26
int lock_set(int fd, int type)
{
struct flock lock;
lock.l_whence = SEEK_SET;
lock.l_start = 0;
lock.l_len = 0;
lock.l_type = type;
lock.l_pid = -1;
switch(type)
{
case F_RDLCK:
case F_WRLCK:
if((fcntl(fd,F_SETLKW,&lock))<0)
{
printf("lock failed:type=%d\n",lock.l_type);
return -1;
}
break;
case F_UNLCK:
if((fcntl(fd,F_SETLKW,&lock))<0)
{
printf("unlock failed\n");
return -1;
}
break;
default:
printf("input error\n");
}
return 0;
}
int main(int argc, const char *argv[])
{
int fd;
pid_t pid;
int i;
char buffer[2];//write()函数缓冲区
if((fd=(open(argv[1],O_RDWR|O_CREAT|O_TRUNC,0666)))<0)
{
perror("cannot open file");
return -1;
}
pid = fork();
if(pid==-1)
{
perror("cannot fork");
return -1;
}
else if(pid==0)//子进程
{
lock_set(fd,F_WRLCK);
printf("child process begin\n");
for(i=0;i<26;i++)
{
sprintf(buffer,"%c ",'A'+i);
write(fd,buffer,2);
printf("child process write %c\n",'A'+i);
sleep(1);//延时1秒
}
printf("child process over\n");
lock_set(fd,F_UNLCK);
}
else//父进程
{
lock_set(fd,F_WRLCK);
printf("parent process begin\n");
for(i=0;i<26;i++)
{
sprintf(buffer,"%c ",'a'+i);
write(fd,buffer,2);
printf("parent process write %c\n",'a'+i);
sleep(1);//延时1秒
}
printf("parent process over\n");
lock_set(fd,F_UNLCK);
wait(NULL);
}
close(fd);
return 0;
}
二、exec函数族
如果我们使用fork()函数创建一个子进程,则该子进程几乎复制了父进程的全部内容,也就是说,子进程与父进程在执行同一个可执行程序。那么我们能否让子进程不执行父进程正在执行的程序呢?
exec函数族提供了让进程运行另一个程序的方法。exec函数族内的函数可以根据指定的文件名或目录名找到可执行程序,并加载新的可执行程序,替换掉旧的代码区、数据区、堆区、栈区与其他系统资源。这里的可执行程序既可以是二进制文件,也可以是脚本文件。在执行exec函数族函数后,除了该进程的进程号PID,其他内容都被替换了。
通常情况下,我们首先使用fork()函数创建一个子进程,然后调用exec函数族内函数将子进程内程序替换成其他的可执行程序,这样看起来就像父进程诞生了一个新的且完全不同于父进程的子进程。
exec函数族有6个函数,这些函数的函数名、函数功能、函数参数列表有相似之处,我们在使用的过程中一定要仔细区分这些函数的区别避免混淆。有关exec函数族的更多使用方法内容请查阅man手册。
exec函数族函数
所需头文件:#include<unistd.h>
函数原型:
int execl(const char *path, const char *arg,…)
int execlp(const char *file, const char *arg,…)
int execle(const char *path, const char *arg,…, char *const envp[])
int execv(const char *path, char *const argv[])
int execvp(const char *file, char *const argv[])
int execve(const char *path, char *const argv[], char *const envp[])
execl(完整的路径名,列表……);
execlp(文件名,列表……);
execle(完整的路径,列表……,环境变量的向量表)
execv(完整的路径名,向量表);
execvp(文件名,向量表);
execve(完整的路径,向量表,环境变量的向量表)
函数参数:
path:文件路径,使用该参数需要提供完整的文件路径
file:文件名,使用该参数无需提供完整的文件路径,终端会自动根据$PATH的值查找文件路径
arg:以逐个列举方式传递参数
argv:以指针数组方式传递参数
envp:环境变量数组
返回值:-1(通常情况下无返回值,当函数调用出错才有返回值-1)
这6个函数的函数功能类似,但是在使用语法规则上有细微区别。我们可以看出,其实exec函数族的函数都是exec+后缀来命名的,具体的区别如下:
区别1:参数传递方式(函数名含有l还是v)
exec函数族的函数传参方式有两种:逐个列举或指针数组。
若函数名内含有字母'l'(表示单词list),则表示该函数是以逐个列举的方式传参,每个成员使用逗号分隔,其类型为const char *arg,成员参数列表使用NULL结尾
若函数名内含有字母'v'(表示单词vector),则表示该函数是以指针数组的方式传参,其类型为char *const argv[],命令参数列表使用NULL结尾
区别2:查找可执行文件方式(函数名是否有p)
我们可以看到这几个函数的形参有些为path,而有些为file。其中:
若函数名内没有字母'p',则形参为path,表示我们在调用该函数时需要提供可执行程序的完整路径信息
若函数名内含有字母'p',则形参为file,表示我们在调用该函数时只需给出文件名,系统会自动按照环境变量$PATH的内容来寻找可执行程序
区别3:是否指定环境变量(函数名是否有e)
exec可以使用默认的环境变量,也可以给函数传入具体的环境变量。其中:
若函数名内没有字母'e',则使用系统当前环境变量
若函数名内含有字母'e'(表示单词environment),则可以通过形参envp[]传入当前进程使用的环境变量
exec函数族简单命名规则如下:
后缀 能力
l 接收以逗号为分隔的参数列表,列表以NULL作为结束标志
v 接收一个以NULL结尾的字符串数组的指针
p 提供文件的完整的路径信息 或 通过$PATH查找文件
e 使用系统当前环境变量 或 通过envp[]传递新的环境变量
这6个exec函数族的函数,execve()函数属于系统调用函数,其余5个函数属于库函数。
示例1:使用execl()函数,在子进程内运行ps -ef命令
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
pid_t pid;
pid = fork();
if(pid==-1)
{
perror("cannot fork");
return -1;
}
else if(pid==0)//子进程
{
printf("This is Child process\n");
if(execl("/bin/ps","ps","-ef",NULL)<0)//子进程执行ps -ef,注意参数的写法,且需要使用NULL结尾
{
perror("cannot exec ps");
}
}
else//父进程
{
printf("This is Parent process\n");
sleep(1);//父进程延时1s,让子进程先运行
}
return 0;
}
运行该程序会发现,子进程会运行ps -ef命令,这与我们在终端直接输入ps -ef得到的结果是相同的。
注意我们在调用exec函数族的函数时,一定要加上错误判断语句。当exec函数族函数执行失败时,返回值为-1,并且报告给内核错误码,我们可以通过perror将这个错误码的对应错误信息输出。常见的exec函数族函数执行失败的原因有:
1.找不到文件或路径
2.参数列表arg、数组argv和环境变量数组列表envp未使用NULL指定结尾
3.该文件没有可执行权限
示例2:使用execlp()函数完成示例1的代码,注意execlp()与execl()函数的参数的区别
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
pid_t pid;
pid = fork();
if(pid==-1)
{
perror("cannot fork");
return -1;
}
else if(pid==0)//子进程
{
printf("This is Child process\n");
if(execlp("ps","ps","-ef",NULL)<0)//第一个参数只需要写ps即可,系统会根据环境变量自行寻找ps程序的位置
{
perror("cannot exec ps");
}
}
else//父进程
{
printf("This is Parent process\n");
sleep(1);//父进程延时1s,让子进程先运行
}
return 0;
}
示例3:使用execvp()函数完成示例2的代码,注意execvp()与execlp()函数的参数的区别
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
pid_t pid;
char *arg[]={"ps","-ef",NULL};//设定参数向量表,注意使用NULL结尾
pid = fork();
if(pid==-1)
{
perror("cannot fork");
return -1;
}
else if(pid==0)//子进程
{
printf("This is Child process\n");
if(execvp("ps",arg)<0)//注意该函数的参数与execlp()函数的区别
{
perror("cannot exec ps");
}
}
else//父进程
{
printf("This is Parent process\n");
sleep(1);//父进程延时1s,让子进程先运行
}
return 0;
}
接下来我们看如何使用execle()或execve()传递新的环境变量
示例4:使用execle()函数将一个新的环境变量添加到子进程中,并使用env命令查看
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
pid_t pid;
char *envp[]={"PATH=/tmp","USER=liyuge",NULL};//设定新的环境变量,注意使用NULL结尾
pid = fork();
if(pid==-1)
{
perror("cannot fork");
return -1;
}
else if(pid==0)//子进程
{
printf("This is Child process\n");
if(execle("/usr/bin/env","env",NULL,envp)<0)
{
perror("cannot exec env");
}
}
else//父进程
{
printf("This is Parent process\n");
sleep(1);//父进程延时1s,让子进程先运行
}
return 0;
}
运行该程序,我们可以看到输出了两个新的环境变量信息:PATH和USER,这两个新的环境变量与旧的环境变量(父进程)是不同的,有兴趣的同学可以将父进程的环境变量也输出作比较。
示例5:使用execve()函数完成示例2的代码,注意execve()与execle()函数的参数的区别
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
pid_t pid;
char *arg[]={"env",NULL};//设定参数向量表,注意使用NULL结尾
char *envp[]={"PATH=/tmp","USER=liyuge",NULL};//设定新的环境变量,注意使用NULL结尾
pid = fork();
if(pid==-1)
{
perror("cannot fork");
return -1;
}
else if(pid==0)//子进程
{
printf("This is Child process\n");
if(execve("/usr/bin/env",arg,envp)<0)
{
perror("cannot exec env");
}
}
else//父进程
{
printf("This is Parent process\n");
sleep(1);//父进程延时1s,让子进程先运行
}
return 0;
}
练习:使用exec函数族函数,在子进程内执行自己编译的可执行程序a.out文件
答案:使用execl()函数,其余函数的用法请同学们自己思考
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
pid_t pid;
pid = fork();
if(pid==-1)
{
perror("cannot fork");
return -1;
}
else if(pid==0)//子进程
{
printf("This is Child process\n");
if(execl("/home/linux/a.out","./a.out",NULL)<0)//使用execl()函数
{
perror("cannot exec a.out");
}
}
else//父进程
{
printf("This is Parent process\n");
sleep(1);//父进程延时1s,让子进程先运行
}
return 0;
}
三、exit()函数与_exit()函数
当我们需要结束一个进程的时候,我们可以使用exit()函数或_exit()函数来终止该进程。当程序运行到exit()函数或_exit()函数时,进程会无条件停止剩下的所有操作,并进行清理工作,最终将进程停止。
函数exit()
所需头文件:#include<stdlib.h>
函数原型:
void exit(int status)
函数参数:
status 表示让进程结束时的状态(会由主进程的wait();负责接收这个返回值【也可以不接收】-->类似函数的返回值),默认使用0表示正常结束
返回值: 无
函数_exit()
所需头文件:#include<unistd.h>
函数原型:
void _exit(int status)
函数参数:
status 同exit()函数
返回值: 无
exit()函数与_exit()函数用法类似,但是这两个函数还是有很大的区别的:
_exit()函数直接使进程停止运行,当调用_exit()函数时,内核会清除该进程的内存空间,并清除其在内核中的各种数据。
exit()函数则在_exit()函数的基础上进行了升级,在退出进程之间增加了若干工序。exit()函数在终止进程之前会检测进程打开了哪些文件,并将缓冲区内容写回文件。
因此,exit()函数与_exit()函数最主要的区别就在于是否会将缓冲区数据保留并写回。_exit()函数不会保留缓冲区数据,直接将缓冲区数据丢弃,直接终止进程运行;而exit()函数会将缓冲区内数据写回,待缓冲区清空后再终止进程运行。
下面的两段示例代码演示了exit()函数与_exit()函数的区别
示例1:使用exit()函数终止进程
#include<stdio.h>
#include<stdlib.h>
int main()
{
printf("Using exit()\n");
printf("This is the content in buffer");
exit(0);
}
示例2:使用_exit()函数终止进程
#include<stdio.h>
#include<unistd.h>
int main()
{
printf("Using exit()\n");
printf("This is the content in buffer");
_exit(0);
}
在两个示例程序中,示例1会输出"This is the content in buffer",而示例2不会输出
四、wait()函数与waitpid()函数
使用wait()函数与waitpid()函数让父进程回收子进程的系统资源,两个函数的功能大致类似,waitpid()函数的功能要比wait()函数的功能更多。
函数wait()
所需头文件:#include<sys/types.h>
#include<sys/wait.h>
函数原型:
pid_t wait(int *status)
函数参数:
status 保存子进程结束时的状态(由exit();返回的值)。使用地址传递,父进程获得该变量。若无需获得状态,则参数设置为NULL
返回值:
成功:已回收的子进程的PID
失败:-1
函数waitpid()
所需头文件:#include<sys/types.h>
#include<sys/wait.h>
函数原型:
pid_t waitpid(pid_t pid, int *status, int options)
函数参数:
pid pid是一个整数,具体的数值含义为:
pid>0 回收PID等于参数pid的子进程
pid==-1 回收任何一个子进程。此时同wait()
pid==0 回收其组ID等于调用进程的组ID的任一子进程
pid<-1 回收其组ID等于pid的绝对值的任一子进程
status 同wait()
options
0:同wait(),此时父进程会阻塞等待子进程退出
WNOHANG:若指定的进程未结束,则立即返回0(不会等待子进程结束)
返回值:
>0 已经结束运行的子进程号
0 使用WNOHANG选项且子进程未退出
-1 错误
当进程结束时,该进程会向它的父进程报告。wait()函数用于使父进程阻塞,直到父进程接收到一个它的子进程已经结束的信号为止。如果该进程没有子进程或所有子进程都已结束,则wait()函数会立即返回-1。
waitpid()函数的功能与wait()函数一样,不过waitpid()函数有若干选项,所以功能也比wait()函数更加强大。实际上,wait()函数只是waitpid()函数的一个特例而已,Linux内核总是调用waitpid()函数完成相应的功能。
wait(NULL)等价于waitpid(-1,NULL,0)。
示例1:使用wait()函数,让父进程在子进程结束后再运行
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t pid;
pid = fork();
if(pid==-1)
{
perror("cannot fork");
return -1;
}
else if(pid==0)//子进程
{
printf("This is Child process\n");
printf("Child process ID is %d\n",getpid());
printf("Child process will exit\n");
}
else//父进程
{
pid = wait(NULL);//等待子进程结束
printf("This is Parent process\n");
printf("Child process %d is over\n",pid);
}
return 0;
}
示例2:使用waitpid()函数,让父进程回收子进程。参数使用WNOHANG使父进程不会阻塞,若子进程暂时未退出,则父进程在1s后再次尝试回收子进程
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t pid;
pid = fork();
if(pid<0)
{
perror("cannot fork");
return -1;
}
else if(pid==0)//子进程
{
printf("This is Child process\n");
sleep(5);//模拟子进程运行5s
exit(0);//子进程正常退出
}
else//父进程
{
int ret;
do//循环直至子进程退出为止
{
ret = waitpid(pid,NULL,WNOHANG);//回收子进程,使用WNOHANG选项参数
if(ret==0)
{
printf("The Child process is running, can't be exited\n");
sleep(1);//1秒后再次尝试
}
}while(ret==0);
if(pid==ret)//如果检测到子进程退出
{
printf("Child process exited\n");
}
else
{
printf("Some error occured\n");
}
}
return 0;
}
练习:若将示例2的程序内的waitpid()函数去掉WNOHANG选项参数,会出现什么效果?编程验证自己的猜想。
答案:父进程会一直阻塞等待子进程结束为止。在终端上只会输出1个"Child process exited\n",而不会输出"The Child process is running, can’t be exited\n"
综合练习:使用fork()函数、exec函数族函数、exit()函数、waitpid()函数完成以下功能:
该程序有3个进程,其中1个为父进程,另外2个为子进程。其中一个子进程运行"ls -l"命令,另外一个子进程延时5秒后退出。父进程首先使用阻塞的方式等待第一个子进程结束,再采用非阻塞的方式等待第二个子进程结束。待两个子进程都退出后,父进程退出。
该练习题给出两种答案,一种正确,一种错误,请观察两段代码的区别,并指出错误的代码产生错误的原因
答案1(错误答案):
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>
int main()
{
pid_t child1,child2,child;
child1 = fork();//?
child2 = fork();//?
if(child1<0)
{
printf("Child1 fork error\n");
exit(1);
}
else if(child1==0)
{
printf("In Child1 execute 'ls -l'\n");
if(execlp("ls","ls","-l",NULL)<0)
{
perror("child1 execlp error");
}
}
if(child2<0)
{
printf("Child2 fork error\n");
exit(1);
}
else if(child2==0)
{
printf("In Child2 sleep 5s\n");
sleep(5);
exit(0);
}
else
{
printf("In parent process:\n");
child = waitpid(child1,NULL,0);
if(child==child1)
{
printf("Get child1 exit\n");
}
else
{
printf("Error occured\n");
}
do
{
child = waitpid(child2,NULL,WNOHANG);
if(child==0)
{
printf("The child2 process hasn't exited\n");
sleep(1);
}
}while(child==0);
if(child==child2)
{
printf("Get child2 exit\n");
}
else
{
printf("Error occured\n");
}
}
exit(0);
}
答案2(正确答案):
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>
int main()
{
pid_t child1,child2,child;
child1 = fork();
if(child1<0)
{
printf("Child1 fork error\n");
exit(1);
}
else if(child1==0)
{
printf("In Child1 execute 'ls -l'\n");
if(execlp("ls","ls","-l",NULL)<0)
{
perror("child1 execlp error");
}
}
else
{
child2 = fork();
if(child2<0)
{
printf("Child2 fork error\n");
exit(1);
}
else if(child2==0)
{
printf("In Child2 sleep 5s\n");
sleep(5);
exit(0);
}
else
{
printf("In parent process:\n");
child = waitpid(child1,NULL,0);
if(child==child1)
{
printf("Get child1 exit\n");
}
else
{
printf("Error occured\n");
}
do
{
child = waitpid(child2,NULL,WNOHANG);
if(child==0)
{
printf("The child2 process hasn't exited\n");
sleep(1);
}
}while(child==0);
if(child==child2)
{
printf("Get child2 exit\n");
}
else
{
printf("Error occured\n");
}
}
}
exit(0);
}
一、守护进程概述
守护进程(daemon,也被译为“精灵进程”)是一类在后台工作的特殊进程,通常情况下,守护进程用于执行特定的系统任务。守护进程的生命周期较长,一些守护进程在系统引导时启动,一直运行至系统关闭;另一些守护进程在需要工作的时候启动,完成任务后就自动结束。在操作系统内,许多的系统服务都是通过守护进程实现的。
由于守护进程运行在后台,因此守护进程必须脱离前台(终端)运行。通常情况下,每一个从终端启动的进程都是依附于该终端的,当该终端关闭时,依附于该终端的进程也会自动结束。而守护进程却能够(或者说,必须)突破终端的限制,不受终端的影响在后台工作。从另一个角度说,若我们希望某些任务不因用户、终端或其他因素的变化而受影响,则必须把这个进程变成守护进程。
常见的守护进程有以下几类:
1.系统守护进程:syslogd、login、crond、at等
2.网络守护进程:sendmail、httpd、xinetd等
3.独立启动的守护进程:httpd、named、xinetd等
4.被动守护进程:telnet、finger、ktalk等
一些守护进程简介
1、syslogd:用于记录设备的运行情况,通常情况下系统内设备的运行情况都保存在/var/log目录下
2、login:改变当前登陆用户的ID并调用该用户使用的Shell终端
3、crond:Linux内用户周期性地执行某种任务或处理某些事件的守护进程,类似windows系统下的计划任务
4、at:定时任务,指定在某个时间点执行某个任务
5、httpd:Apache相关的Http服务器进程
6、xinetd:inetd进程的升级版,负责internet服务相关的守护进程,支持对TCP/UDP/RPC的服务,支持时间片访问,有效防止DoS攻击,提供功能完备的日志文件等功能。
7、telnet:internet远程登陆的标准协议与主要方式,使用telnet可以连接服务器,使得可以在本地访问/控制服务器
查看当前系统内正在工作的守护进程:命令ps -axj
二、编写守护进程
守护进程看似复杂,实际编写一个守护进程是有固定的流程的。只要遵循特定的流程编写即可。
我们在编写守护进程的时候,要特别注意守护进程是独立于终端存在的,因此需要尽可能保证该进程脱离终端控制。
编写一个守护进程的步骤如下:
1.创建子进程,父进程退出
2.在子进程内创建新会话
3.改变工作目录
4.重设文件权限掩码
5.关闭文件描述符
1、创建子进程,父进程退出
由于守护进程是脱离终端的,因此我们创建一个进程后,让它的父进程退出,子进程继续运行。这样就会在终端里造成一个“该进程已经运行完毕”的假象,让所有的守护进程的功能都在子进程内执行,而用户可以在终端输入其他命令。这样从形式上就做到了让子进程转入后台工作。
当父进程退出后,子进程变成了孤儿进程。在Linux内,如果出现了孤儿进程,则自动由1号进程(init进程)收养该进程,也就是说变成了1号进程(init进程)的子进程了。
2、在子进程内创建新会话
这个步骤是创建守护进程最重要的步骤,虽然实现特别简单,但是其背后的意义十分重大。
这里我们首先需要了解两个概念:进程组与会话期
1)进程组(Process Group)
进程组是一个或多个进程的集合。进程组号由PGID(Process Group ID)唯一识别。
进程不是孤立的,每个进程都存在父子、兄弟关系,如果某些进程关系相近或者功能相似,那么我们可以将这些进程编成一个进程组。每个进程必定属于一个进程组,也只能属于一个进程组。
那么Linux为什么要设置进程组呢?其实设置进程组是为了方便管理多个进程。例如我们完成某个任务需要100个进程,当任务结束后需要一个一个杀死每个进程,十分繁琐;如果我们将这100个进程编成一个进程组,则直接针对进程组发起“杀死进程”的操作,这样可以一次性杀死100个进程。
每个进程除了进程号PID之外,进程组号PGID也是一个必备的属性。
每个进程组都有一个组长进程,负责整个进程组的调度工作。组长进程的PID等于进程组的PGID,不过PGID不会因为组长进程的退出而改变。
2)会话期(Session)
会话期(简称会话)是一个或多个进程组的集合。通常情况下,一个会话期开始于用户登录,终止于用户退出;或者开始于终端打开,结束于终端关闭。会话期也有自己的会话期号,会话期由SID(Session ID)唯一识别。
Linux是一个多任务的操作系统,必须要支持多个用户同时登录同一个操作系统的操作。当一个用户登录一次时,操作系统就为这个用户创建一个新会话。每个会话期都有一个会话组长(Session Leader),也称为会话期首进程,即创建该会话的进程,会话期的SID就是会话组长的PID。
一个会话可以包含多个进程组,这些进程组被分为一个前台进程组和多个后台进程组。前台进程是操作系统与用户进行交互的进程组,注意前台进程组只能有一个。而后台进程组则无需与用户进行交互,一些后台工作的进程或者守护进程就被分配成为了后台进程组。
那么我们为什么要在子进程中创建一个新会话呢?
我们通过fork()函数创建了子进程,通过前面的学习我们知道,子进程复制了父进程的资源,其中就包括父进程的进程组、会话期与控制终端。虽然在第一步时我们将父进程退出,但是父进程的进程组、会话期与控制终端没有改变,还保留在子进程内,因此,这时的子进程还不是真正意义上的独立。
我们可以使用setsid()函数来创建一个新会话,并让该进程担任会话组长。调用setsid()函数主要有三个功能:
1.让进程摆脱原会话控制
2.让进程摆脱原进程组控制
3.让进程摆脱原控制终端控制
setsid()函数用法如下:
函数setsid()
所需头文件:#include<sys/types.h>
#include<unistd.h>
函数原型: pid_t setsid()
函数参数: 无
函数返回值:
成功:该会话期的SID
失败:-1
3、改变工作目录
使用fork()函数创建的子进程复制了父进程的工作目录。由于在进程运行过程中,当前进程的工作目录所在的文件系统是无法卸载的,这会对后续的工作造成一定麻烦(例如需要切换到单用户模式),因此在后台工作的守护进程的工作目录必须改变成其他的不会受到干扰的工作目录。通常情况下,我们将守护进程的工作目录设定为根目录"/",除了根目录之外,某些情况下还可以放置在"/tmp"目录下。
改变工作目录的函数时chdir()。
函数chdir()
所需头文件:#include<unistd.h>
函数原型: int chdir(const char *path)
函数参数: path:需要改变工作目录的路径
函数返回值:
成功:0
失败:-1
4、重设文件权限掩码
文件权限掩码的作用是屏蔽文件权限码中对应的位,通常情况下用8进制数表示。例如,如果文件权限掩码是050,则表示屏蔽了文件组用户的可读与可写权限。
使用fork()函数创建的子进程复制了父进程的文件权限码,这样守护进程在工作时可能会受到一定影响。因此,我们重设文件权限,使得守护进程的进行不受阻碍,更加灵活。
设置文件权限掩码的函数为umask(),通常使用umask(0),即将该进程的文件权限码设为0777
函数umask()
所需头文件:#include<sys/types.h>
#include<sys/stat.h>
函数原型: mode_t umask(mode_t mask)
函数参数: mask:需设定的文件权限掩码。常用为0
函数返回值:无需返回值(总是成功)
5、关闭文件描述符
同文件权限码一样,使用fork()函数创建的子进程复制了父进程已打开文件的文件描述符。这些被打开的文件可能永远不会被守护进程访问,但是仍然在占用系统资源,而且还可能导致该文件无法被关闭等情况。
特别要指出,由于守护进程在后台运行,和终端无关,因此终端的标准输入流、标准输出流、标准错误流已经失去了存在的价值,因此需要关闭这3个流。
通常情况下我们可以使用getdtablesize()函数获取该进程已经打开的所有文件描述符,并使用循环语句逐个关闭这些文件描述符。
函数getdtablesize()
所需头文件:#include<unistd.h>
函数原型: int getdtablesize()
函数参数: 无
函数返回值:当前进程打开的文件描述符总数
示例:创建一个守护进程,该守护进程的功能是每隔2秒向/tmp/daemon.log文件内写入一行字符串
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#include<string.h>
#include<fcntl.h>
#include<sys/types.h>
#include<unistd.h>
#include<sys/stat.h>
#include<sys/wait.h>
int main()
{
pid_t pid;
int i,fd;
char writebuf[128]={0};
time_t t;
pid = fork();
if(pid<0)
{
perror("cannot fork");
exit(1);
}
else if(pid>0)//父进程
{
exit(0);//第一步:父进程退出
}
setsid();//第二步:子进程创建新会话
chdir("/tmp");//第三步:改变工作目录为/tmp
//虽然经常设置工作目录为根目录,但由于本题需要向/tmp/daemon.log内写入数据,因此直接设置工作目录为/tmp
umask(0);//第四步:重设文件权限掩码
for(i=0;i<getdtablesize();i++)//第五步:关闭所有文件描述符,尤其是3个默认流的文件描述符
{
close(i);
}
//此时守护进程创建完毕,下面开始守护进程的工作
while(1)
{
if((fd=open("daemon.log",O_WRONLY|O_CREAT|O_TRUNC,0600))<0)
{
perror("cannot open file daemon.log");
exit(1);
}
strcpy(writebuf,"This is Daemon Process\n");
write(fd,writebuf,strlen(writebuf));
bzero(writebuf,sizeof(writebuf));
t = time(NULL);
sprintf(writebuf,"Write Time:%s\n",asctime(localtime(&t)));//使用asctime获取时间
write(fd,writebuf,strlen(writebuf));
bzero(writebuf,sizeof(writebuf));
close(fd);//写毕关闭文件
sleep(2);//延时2s
}
exit(0);
}
该程序每隔2秒向/tmp/daemon.log内写入数据。我们可以使用ps命令看到守护进程正在运行