用fork函数启动一个子进程时,子进程就有了它自己的生命并将独立运行;如果父进程先于子进程退出,则子进程成为孤儿进程,此时将自动被PID为1的进程(即init)接管。孤儿进程退出后,它的清理工作由祖先进程init自动处理。但在init进程清理子进程之前,它一直消耗系统的资源,所以要尽量避免
fork_lone.c | |
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h> //exit()
int main()
{
pid_t pid;
pid=fork(); //fork函数返回pid_t类型变量
if(pid==0)
{
printf("I am son , pid=%d , ppid=%d\n", getpid() , getppid());
while(1); //这里不能是exit(0);
}
else
{
printf("I am father , pid=%d , ppid=%d\n", pid ,getppid());
exit(0);
}
return 0;
}
|
//ppid变成1了,不是3277 ps -elf | grep a.out |
通过 ps –elf 就可以看到此时子进程一直在运行,并且父进程是1号进程。如果子进程先退出,系统不会自动清理掉子进程的环境,而必须由父进程调用 wait 或 waitpid 函数来完成清理工作,如果父进程不做清理工作,则已经退出的子进程将成为僵尸进程(defunct),在系统中如果存在的僵尸(zombie)进程过多,将会影响系统的性能,所以必须对僵尸进程进行处理。
fork_zombie.c | |
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
pid_t pid;
int i=3;
pid=fork();
if(pid==0)
{
printf("I am son , pid=%d , ppid=%d\n", getpid() , getppid());
exit(0);
}
else
{
printf("I am father , pid=%d , ppid=%d\n", pid ,getppid());
while(1);
}
return 0;
}
|
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status); // NULL表示等待所有进程
pid_t waitpid(pid_t pid, int *status, int options); //后面两个参数通常写NULL,0,NULL表示不接受子进程退出值 ( (-1,NULL,0)表示等待任何进程,并阻塞 )
//这个主要用于自己创建了多个子进程,但是目的只要等某一个进程。这是属于特殊情况,正常情况都要等待退出
wait 和 waitpid 都将暂停父进程,等待一个已经退出的子进程,并进行清理工作;
wait 函数随机地等待一个已经退出的子进程,并返回该子进程的pid;NULL表示等待所有进程
waitpid 等待指定pid的子进程;如果为-1表示等待所有子进程。
status 参数是传出参数,存放子进程的退出状态;
通常用下面的两个宏来获取状态信息:
WIFEXITED(status) 查看子进程是否正常退出;如果子进程正常结束,它就取一个非0值。
WEXITSTATUS(status) 如果WIFEXITED非零(异常退出),它返回子进程的退出码(如果没有上述条件的子进程)
fork_wait.c | |
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t pid;
int i=3;
pid=fork();
if(pid==0)
{
sleep(5); //让子进程先睡眠五秒,方便查看一开始进程状态(都在睡觉)
printf("I am child,my pid=%d,my ppid=%d\n",getpid(),getppid());
exit(0);
}
else
{
printf("child pid=%d\n",pid);
wait(NULL);
printf("I am wake\n");
exit(0);
}
return 0;
}
|
fork_wait_status.c | |
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t pid;
pid=fork();
if(pid==0)
{
sleep(5);
printf("I am child , my pid =%d ppid =%d\n",getpid(),getppid());
exit(1); //子进程正常退出(这里退出参数是多少,后面的WEXITSTATUS(status)就是多少)
}
else
{
printf("I am father , my pid =%d ppid =%d\n",getpid(),getppid());
int status;
wait(&status); //waitpid(pid,&status,0) | waitpid(-1,&status,0) 危险
printf("I am wake\n");
if(WIFEXITED(status))
{
printf("child terminated normally , exited value %d\n",WEXITSTATUS(status));
}
else
{
printf("child terminated abnormally , exited value %d\n",WEXITSTATUS(status));
}
exit(0);
}
return 0;
}
|
//fork_waitpid_WNOHANG.c
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t pid;
pid = fork();
if( 0==pid )
{
printf("I am child , my pid =%d ppid =%d\n",getpid(),getppid());
exit(0);
//while(1);
}
else
{
printf("I am father , my pid =%d ppid =%d\n",getpid(),getppid());
int status;
printf("sleep\n");
sleep(10);
printf("sleep over\n");
//printf("no sleep\n");
waitpid(pid,&status,WNOHANG);
printf("I am wake\n");
if( WIFEXITED(status) )
{
printf("child terminated normally , exited value %d\n",WEXITSTATUS(status));
}
else
{
printf("child terminated abnormally , exited value %d\n",WEXITSTATUS(status));
}
exit(0);
}
return 0;
}
|
//子进程先退出,成为僵尸进程,后面执行到waitpid回收了子进程 //执行到waitpid的时候子进程没有退出,就直接退出父进程,子进程成为孤儿进程; |
fork_wait_status.c //子进程异常结束,被kill | |
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t pid;
pid=fork();
if(pid==0)
{
printf("I am child , my pid =%d ppid =%d\n",getpid(),getppid());
while(1);
}
else
{
printf("child pid=%d\n",pid);
int status;
wait(&status);
printf("I am wake\n");
if( WIFEXITED(status) )
{
printf("child terminated normally , exited value %d\n",WEXITSTATUS(status));
}
else
{
printf("child terminated abnormally , exited value %d\n",WEXITSTATUS(status));
}
exit(0);
}
return 0;
}
|
//父进程等待子进程结束 //kill子进程 //kill父进程 |
fork_waitpid.c | |
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t pid;
pid = fork();
if( 0==pid )
{
printf("I am child , my pid =%d ppid =%d\n",getpid(),getppid());
exit(1);
}
else
{
printf("I am father , my pid =%d ppid =%d\n",getpid(),getppid());
int status;
printf("status = %d\n",status);
waitpid(-1,NULL,0); //NULL表示不接受子进程退出的返回值( WEXITSTATUS(status)得到的值就是0,就是定义status的默认值就是0 )
//->wait(NULL);
printf("I am wake\n");
if( WIFEXITED(status) )
{
printf("child terminated normally , exited value %d\n",WEXITSTATUS(status));
}
else
{
printf("child terminated abnormally , exited value %d\n",WEXITSTATUS(status));
}
exit(0);
}
return 0;
}
|
进程的终止有5种方式:
l main函数的自然返回;(return 0)
l 调用exit函数
l 调用_exit函数
l 调用abort函数
l 接收到能导致进程终止的信号ctrl+c (SIGINT) ctrl+\ (SIGQUIT)
前3种方式为正常的终止,后2种为非正常终止。但是无论哪种方式,进程终止时都将执行相同的关闭打开的文件,释放占用的内存等资源。只是后两种终止会导致程序有些代码不会正常的执行比如对象的析构、atexit函数的执行等。
exit和_exit函数都是用来终止进程的。当程序执行到exit和_exit时,进程会无条件的停止剩下的所有操作,清除包括PCB在内的各种数据结构,并终止本程序的运行。但是它们是有区别的,exit和_exit的区别如图所示:
exit 函数在退出之前会检查文件的打开情况,把文件缓冲区中的内容写回文件,就是图中的“清理I/O缓冲
由于linux的标准函数库中,有一种被称作“缓冲I/O”操作,其特征就是对应每一个打开的文件,在内存中都有一片缓冲区。每次读文件时,会连续读出若干条记录,这样在下次读文件时就可以直接从内存的缓冲区中读取;同样,每次写文件的时候,也仅仅是写入内存中的缓冲区,等满足一定的条件(如达到一定数量或遇到特定字符等),再将缓冲区中的内容一次性写入文件。这种技术大大增加了文件读写的速度,但也为编程带来了麻烦。比如有一些数据,认为已经写入文件,实际上因为没有满足特定的条件,它们还只是保存在缓冲区内,这时用_exit函数直接将进程关闭,缓冲区中的数据就会丢失。因此,如想保证数据的完整性,建议使用exit函数。 |
exit和_exit函数的原型:
#include <stdlib.h> //exit的头文件
#include <unistd.h> //_exit的头文件
void exit(int status);
void _exit(int status); //status是一个整型的参数,可以利用这个参数传递进程结束时的状态。一般来说,0表示正常结束;其他的数值表示出现了错误,进程非正常结束。
exit.c | _exit.c | abort.c |
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
printf("Using exit...\n"); //'\n'会刷新缓冲区
printf("This is the content in buffer");
exit(0);
}
|
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
printf("Using exit...\n");
printf("This is the content in buffer");
_exit(0);
}
|
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
abort();
}
|
进程间打开文件的继承
用fork继承打开的文件
fork以后的子进程自动继承了父进程的打开的文件,继承以后,父进程关闭打开的文件不会对子进程造成影响。
|
Daemon运行在后台也称作“后台服务进程”。 它是没有控制终端与之相连的进程。它独立于控制终端、会话周期的执行某种任务。那么为什么守护进程要脱离终端后台运行呢?守护进程脱离终端是为了避免进程在执行过程中的信息在任何终端上显示并且进程也不会被任何终端所产生的任何终端信息所打断。那么为什么要引入守护进程呢?由于在linux中,每一个系统与用户进行交流的界面称为终端,每一个从此终端开始运行的进程都会依赖这个终端,这个终端就称为这些进程的控制终端。当控制终端被关闭时,相应的进程都会自动关闭。但是守护进程却能突破这种限制,它被执行开始运转,直到整个系统关闭时才退出。几乎所有的服务器程序,如Apache和wu-FTP,都用daemon进程的形式实现。很多Linux下常见的命令如inetd和ftpd,末尾的字母d通常就是指daemon。 |
守护进程的特性:
1> 守护进程最重要的特性是后台运行。
2> 其次,守护进程必须与其运行前的环境隔离开来。这些环境包括未关闭的文件描述符、控制终端、会话和进程组、工作目录以及文件创建掩码等。这些环境通常是守护进程从父进程那里继承下来的。
3> 守护进程的启动方式
daemon进程的编程规则
创建子进程,父进程退出:
调用fork产生一个子进程,同时父进程退出。我们所有后续工作都在子进程中完成。这样做我们可以交出控制台的控制权,并为子进程作为进程组长作准备;由于父进程已经先于子进程退出,会造成子进程没有父进程,变成一个孤儿进程(orphan)。每当系统发现一个孤儿进程,就会自动由1号进程收养它,这样,原先的子进程就会变成1号进程的子进程。代码如下:
pid = fork();
if(pid>0)
exit(0);
|
#include <unistd.h>
pid_t getpgid(pid_t pid); 获得进程组id
int setpgid(pid_t pid, pid_t pgid);成立一个新的进程组//如果要独立成立一个组,就都填自己
pid_t getsid(pid_t pid); 获得会话组id
pid_t setsid(void); 成为新进程组组长和新会话领导,脱离控制终端
|
在子进程中创建新会话:
使用系统函数setsid()。由于创建守护进程的第一步调用了fork函数来创建子进程,再将父进程退出。由于在调用fork函数的时候,子进程全盘拷贝了父进程的会话期、进程组、控制终端等,虽然父进程退出了,但
会话期、进程组、控制终端并没有改变,因此,还不是真正意义上的独立开来。而调用setsid函数会创建一个新的会话并自任该会话的组长,调用setsid函数有下面3个作用:
让进程摆脱原会话的控制,让进程摆脱原进程组的控制,让进程摆脱原控制终端的控制;
会话周期( 会话组 ):
会话期是一个或多个进程组的集合。通常,一个会话开始于用户登录,终止于用户退出,在此期间该用户运行的所有进程都属于这个会话期。
进程组:
是一个或多个进程的集合。进程组有进程组ID来唯一标识。除了进程号(PID)之外,进程组ID(GID)也是一个进程的必备属性。每个进程都有一个组长进程,其组长进程的进程号等于进程组ID。且该进程组ID不会因为组长进程的退出而受影响。
控制终端:
由于在linux中,每一个系统与用户进行交流的界面称为终端,每一个从此终端开始运行的进程都会依赖这个控制终端
会话周期包含进程组包含控制终端
改变当前目录为根目录:
使用fork函数创建的子进程继承了父进程的当前工作目录。由于在进程运行中,当前目录所在的文件是不能卸载的,这对以后的使用会造成很多的不便。利用chdir("/");把当前工作目录切换到根目录。
重设文件权限掩码:
umask(0);将文件权限掩码设为0,Deamon创建文件不会有太大麻烦;(如果没有文件操作的话就可以不要这一步了,创建文件的时候umask==0,到时候文件是什么权限就是什么权限,不会再减去掩码)
#include <sys/types.h>
#include <sys/stat.h>
mode_t umask(mode_t mask);
|
关闭所有不需要的文件描述符:
新进程会从父进程那里继承一些已经打开了的文件。这些被打开的文件可能永远不会被守护进程读写,而它们一直消耗系统资源。另外守护进程已经与所属的终端失去联系,那么从终端输入的字符不可能到达守护进程,守护进程中常规方法(如printf)输出的字符也不可能在终端上显示。所以通常关闭从0到MAXFILE的所有文件描述符。(不要终端了,所以就要关闭0,1,2了)
for(i=0;i<MAXFD;i++) //MAXFD视情况自己确定
close(i);
(注:有时还要处理SIGCHLD信号signal(SIGCHLD, SIG_IGN);防止僵尸进程(zombie))
|
下面就可以添加任何你要daemon做的事情
daemon.c | |
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
int main()
{
if(fork()>0)
{
exit(0); //父进程退出,子进程由1号进程接管
}
setsid(); //成为新进程组的组长和会话领导,脱离控制终端、原会话组和进程组
chdir("/"); //设置工作目录为根目录,防止当前目录删除,子程序由涉及到当前目录的一些操作引起异常
umask(0000); //设置文件访问权限掩码
int i;
for(i=0;i<3;i++) //关闭所有从父进程继承来的文件
{
close(i);
}
while(1); //确保子进程一直在运行
return 0;
}
|
//关闭会话窗口,再打开一个新的窗口 ps -elf | grep daemon 查看 kill -9 21103 杀掉这个守护进程 |
创建一个守护进程,守护进程每隔5秒钟,向/tmp下的一个名字为log的文件内写入“I have run 5s",就是5秒钟写一次。zuoye.c | |
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
int main()
{
pid_t pid;
char *buf = "I have run 5s\n" ;
pid = fork() ; //创建子进程
if(pid < 0)
{
printf("fork error\n") ;
exit(1) ;
}
else if (pid > 0)
exit(0) ; //父进程退出
setsid() ; //创建新会话,成为新进程组的组长和会话领导,脱离控制终端
chdir("/") ; //改变当前工作目录为跟目录
umask(0) ; //设置文件权限掩码
int i;
for(i = 0; i < 3; ++i)
{
close(i) ; //关闭文件权限描述符,关闭所有从父进程继承来的文件
}
int fd ;
while(1) //守护进程每隔5s打日志
{
fd = open("/tmp/daemon.log", O_CREAT|O_WRONLY|O_APPEND, 0666);
if (fd < 0)
{
printf("open file error\n") ;
exit(1) ;
}
write(fd, buf, strlen(buf)) ;
close(fd) ;
sleep(5) ; //进程睡眠5s
}
return 0;
}
|