创建守护进程的步骤

什么是守护进程?

答:守护进程是后台运行的、系统启动是就存在的、不予任何终端关联的,用于处理一些系统级别任务的特殊进程。

实现思路:

实现一个守护进程,其实就是将普通进程按照上述特性改造为守护进程的过程。
需要注意的一点是,不同版本的 Unix 系统其实现机制不同,BSD 和 Linux 下的实现细节就不同。

根据上述的特性,我们便可以创建一个简单的守护进程,这里以 Linux 系统下从终端 Shell 来启动为例。

在此有必要说一下两个概念:会话和进程组。

参考 https://www.cnblogs.com/zengyiwen/p/5755191.html

进程都有父进程,父进程也有父进程,这就形成了一个以init进程为根的家族树。除此以外,进程还有其他层次关系:进程、进程组和会话。进程组和会话在进程之间形成了两级的层次:进程组是一组相关进程的集合,会话是一组相关进程组的集合。

这样说来,一个进程会有如下ID:

·PID:进程的唯一标识。对于多线程的进程而言,所有线程调用getpid函数会返回相同的值。

·PGID:进程组ID。每个进程都会有进程组ID,表示该进程所属的进程组。默认情况下新创建的进程会继承父进程的进程组ID。

·SID:会话ID。每个进程也都有会话ID。默认情况下,新创建的进程会继承父进程的会话ID。

前面提到过,新进程默认继承父进程的进程组ID和会话ID,如果都是默认情况的话,那么追根溯源可知,所有的进程应该有共同的进程组ID和会话ID。但是调用ps axjf可以看到,实际情况并非如此,系统中存在很多不同的会话,每个会话下也有不同的进程组。

为何会如此呢?

就像家族企业一样,如果从创业之初,所有家族成员都墨守成规,循规蹈矩,默认情况下,就只会有一个公司、一个部门。但是也有些“叛逆”的子弟,愿意为家族公司开疆拓土,愿意成立新的部门。这些新的部门就是新创建的进程组。如果有子弟“离经叛道”,甚至不愿意呆在家族公司里,他别开天地,另创了一个公司,那这个新公司就是新创建的会话组。由此可见,系统必须要有改变和设置进程组ID和会话ID的函数接口,否则,系统中只会存在一个会话、一个进程组。

进程组和会话是为了支持shell作业控制而引入的概念。

当有新的用户登录Linux时,登录进程会为这个用户创建一个会话。用户的登录shell就是会话的首进程。会话的首进程ID会作为整个会话的ID。会话是一个或多个进程组的集合,囊括了登录用户的所有活动。在登录shell时,用户可能会使用管道,让多个进程互相配合完成一项工作,这一组进程属于同一个进程组。

当用户通过SSH客户端工具(putty、xshell等)连入Linux时,与上述登录的情景是类似的。

通常,会话开始于用户登录,终止于用户退出,期间的所有进程都属于这个会话。一个会话一般包含一个会话首进程、一个前台进程组和一个后台进程组,控制终端可有可无;此外,前台进程组只有一个,后台进程组可以有多个,这些进程组共享一个控制终端。

  • 前台进程组:
    该进程组中的进程可以向终端设备进行读、写操作(属于该组的进程可以从终端获得输入)。该进程组的 ID 等于控制终端进程组 ID,通常据此来判断前台进程组。

  • 后台进程组:
    会话中除了会话首进程和前台进程组以外的所有进程,都属于后台进程组。该进程组中的进程只能向终端设备进行写操作。

下图为会话、进程组、进程和控制终端之间的关系(登录 shell 进程本身属于一个单独的进程组)。

Linux 守护进程的实现

想了解更多关于会话 Sessions 内容,可以认真读一下 APUE 这本书。

如果调用进程非组长进程,那么就能创建一个新会话:

  • 该进程变成新会话的首进程
  • 该进程成为一个新进程组的组长进程
  • 该进程没有控制终端,如果之前有,则会被中断(会话过程对控制终端的独占性)

也就是说:组长进程不能成为新会话首进程,新会话首进程必定成为组长进程。

 

1、fork()创建子进程,父进程exit()退出;

这是创建守护进程的第一步。由于守护进程是脱离控制终端的,完成这一步后就会在Shell终端里造成程序已经运行完毕的假象。之后的所有工作都在子进程中完成,而用户在Shell终端里则可以执行其他命令,从而在形式上做到了与控制终端的脱离,在后台工作。

由于父进程先于子进程退出,子进程就变为孤儿进程,并由 init 进程作为其父进程收养。

2、在子进程调用setsid()创建新会话;

在调用了 fork() 函数后,子进程全盘拷贝了父进程的会话期、进程组、控制终端等,虽然父进程退出了,但会话期、进程组、控制终端等并没有改变。这还不是真正意义上的独立开来,而 setsid() 函数能够使进程完全独立出来

 setsid()创建一个新会话,调用进程担任新会话的首进程,其作用有:

  • 使当前进程脱离原会话的控制
  • 使当前进程脱离原进程组的控制
  • 使当前进程脱离原控制终端的控制

这样,当前进程才能实现真正意义上完全独立出来,摆脱其他进程的控制。

3、再次 fork() 一个子进程,父进程exit退出;

现在,进程已经成为无终端的会话组长,但它可以重新申请打开一个控制终端,可以通过 fork() 一个子进程,该子进程不是会话首进程,该进程将不能重新打开控制终端。退出父进程。

也就是说通过再次创建子进程结束当前进程,使进程不再是会话首进程来禁止进程重新打开控制终端

4、在子进程中调用chdir()让根目录“/”成为子进程的工作目录;

这一步也是必要的步骤。使用fork创建的子进程继承了父进程的当前工作目录。由于在进程运行中,当前目录所在的文件系统(如“/mnt/usb”)是不能卸载的,这对以后的使用会造成诸多的麻烦(比如系统由于某种原因要进入单用户模式)。因此,通常的做法是让"/"作为守护进程的当前工作目录,这样就可以避免上述的问题,当然,如有特殊需要,也可以把当前工作目录换成其他的路径,如/tmp。改变工作目录的常见函数是chdir。(避免原父进程当前目录带来的一些麻烦)

5、在子进程中调用umask()重设文件权限掩码为0;

文件权限掩码是指屏蔽掉文件权限中的对应位。比如,有个文件权限掩码是050,它就屏蔽了文件组拥有者的可读与可执行权限(就是说可读可执行权限均变为7)。由于使用fork函数新建的子进程继承了父进程的文件权限掩码,这就给该子进程使用文件带来了诸多的麻烦。因此把文件权限掩码重设为0即清除掩码(权限为777),这样可以大大增强该守护进程的灵活性。通常的使用方法为umask(0)。(相当于把权限开发)

6、在子进程中close()不需要的文件描述符;

同文件权限码一样,用fork函数新建的子进程会从父进程那里继承一些已经打开了的文件。这些被打开的文件可能永远不会被守护进程读写,但它们一样消耗系统资源,而且可能导致所在的文件系统无法卸下。其实在上面的第二步之后,守护进程已经与所属的控制终端失去了联系。因此从终端输入的字符不可能达到守护进程,守护进程中用常规方法(如printf)输出的字符也不可能在终端上显示出来。所以,文件描述符为0、1和2 的3个文件(常说的输入、输出和报错)已经失去了存在的价值,也应被关闭。(关闭失去价值的输入、输出、报错等对应的文件描述符

for (i=0; i < MAXFILE; i++)
    close(i); 

7、守护进程退出处理

当用户需要外部停止守护进程运行时,往往会使用 kill 命令停止该守护进程。所以,守护进程中需要编码来实现 kill 发出的signal信号处理,达到进程的正常退出。

 

一张简单的图可以完美诠释之前几个步骤:

 

至此为止,一个简单的守护进程就建立起来了。

注意守护进程一般需要在 root 权限下运行。

通过

ps -ef | grep 'daemon'

对比执行前后确实可以看到多了一个进程:

并且产生了 daemon.log,里面是这样的时间标签

  1. Thu Dec 8 14:35:11 2016

  2. Thu Dec 8 14:36:11 2016

  3. Thu Dec 8 14:37:11 2016

最后我们想退出守护进程,只需给守护进程发送 SIGQUIT 信号即可

sudo kill -3 26454 

再次使用 ps 会发现进程已经退出。

程序如下:

#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <time.h>
#include <stdio.h>
 
static bool flag = true;
void create_daemon();
void handler(int);
 
int main()
{
	time_t t;
	int fd;
	create_daemon();
	struct sigaction act;
	act.sa_handler = handler;
	sigemptyset(&act.sa_mask);
	act.sa_flags = 0;
	if(sigaction(SIGQUIT, &act, NULL))
	{
		printf("sigaction error.\n");
		exit(0);
	}
	while(flag)
	{
		fd = open("/home/mick/daemon.log", O_WRONLY | O_CREAT | O_APPEND, 0644);
		if(fd == -1)
		{
			printf("open error\n");
		}
		t = time(0);
		char *buf = asctime(localtime(&t));
		write(fd, buf, strlen(buf));
		close(fd);
		sleep(60);
	}
	return 0;
}
void handler(int sig)
{
	printf("I got a signal %d\nI'm quitting.\n", sig);
	flag = false;
}
void create_daemon()
{
	pid_t pid;
	/*(1)-----创建一个进程来用作守护进程-----*/
	pid = fork();
	
	if(pid == -1)
	{
		printf("fork error\n");
		exit(1);
	}
	/*(1.1)-----------原父进程退出-------------*/
	else if(pid)
	{
		exit(0);
	}
 	/*(2)---setsid使子进程独立。摆脱会话控制、摆脱原进程组控制、摆脱终端控制----*/
	if(-1 == setsid())
	{
		printf("setsid error\n");
		exit(1);
	}
  	/*(3)---通过再次创建子进程结束当前进程,使进程不再是会话首进程来禁止进程重新打开控制终端----*/
	pid = fork();
	if(pid == -1)
	{
		printf("fork error\n");
		exit(1);
	}
	else if(pid)
	{
		exit(0);
	}
  	/*(4)---子进程中调用chdir()让根目录成为子进程工作目录----*/
	chdir("/");
	int i;
	/*(6)---关闭文件描述符(常说的输入,输出,报错3个文件)----*/
	for(i = 0; i < 3; ++i)
	{
		close(i);
	}
	/*(5)---重设文件掩码为0(将权限全部开放)----*/
	umask(0);
	return;
}

 

以下为其他的两种实现,大同小异:

 C++实现 :

#include<stdio.h>
#include<fcntl.h>
#include<sys/stat.h>
#include<string.h>
#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>

int main(){
    pid_t pid;
    char buf[]="this is a dameon\n";
    pid=fork();//创建一个进程用来做守护进程
    if(pid>0){ //父进程退出,此时 子进程变为孤儿进程。
        exit(0);
    }
    setsid();   //使子进程独立1.摆脱原会话控制 2.摆脱原进程组的控制 3.摆脱控制终端的控制
    chdir("/"); //改变当前工作目录,这也是为了摆脱父进程的影响
    umask(0);   //重设文件权限掩码
    int i;
    for(i=0;i<1024;i++){    //关闭文件描述符(常说的输入,输出,报错3个文件),
        close(i);            //因为守护进程要失去了对所属的控制终端的联系,这三个文件要关闭
    }

    int fd=open("dameon.txt",O_CREAT|O_WRONLY|O_APPEND,0777);
    if(fd<0){
        perror("open");
        return -1;
    }
    while(1){
        write(fd,buf,strlen(buf)+1);
		printf("This is a deamon!!\n");
        sleep(5);
    }

    close(fd);
}

 

 

 

 

C语言实现(有时间再研究):

test.c文件

#include <unistd.h>
#include <stdio.h> 
#include <time.h>
#include <stdlib.h>

void init_daemon(void);//守护进程初始化函数
main()
{
	FILE *fp;
	time_t t;
	init_daemon();//初始化为Daemon
	while (1)//每隔一分钟向test.log报告运行状态 
	{
		sleep(10);//睡眠一分钟 
		if ((fp = fopen("test.log", "a")) >= 0)
		{
			t = time(0);
			fprintf(fp, "I'm here at %sn",asctime(localtime(&t)) );
			fclose(fp);
		}
	}
}

init.c文件

#include<unistd.h>
#include <signal.h> 
#include <sys/param.h> 
#include <sys/types.h> 
#include <sys/stat.h>
#include <stdlib.h>
void init_daemon(void)
{
	int pid;
	int i;

	if (pid = fork())
		exit(0);//是父进程,结束父进程 
	else if (pid< 0)
		exit(1);//fork失败,退出 
				//是第一子进程,后台继续执行

	setsid();//第一子进程成为新的会话组长和进程组长 
			 //并与控制终端分离 
	if (pid = fork())
		exit(0);//是第一子进程,结束第一子进程 
	else if (pid< 0)
		exit(1);//fork失败,退出 
				//是第二子进程,继续 
				//第二子进程不再是会话组长

	for (i = 0; i< NOFILE; ++i)//关闭打开的文件描述符 
		close(i);
	chdir("/tmp");//改变工作目录到/tmp 
	umask(0);//重设文件创建掩模 
	return;
}

编译:gcc –g –o test init.c test.c 
执行:./test 
查看进程:ps –ef 
从输出可以发现test守护进程的各种特性满足上面的要求。

展开阅读全文

没有更多推荐了,返回首页