2-怎么编写一个标准的 daemon 进程_Adv

20 篇文章 0 订阅

一、概述

Daemon(守护进程)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。它不需要用户输入就能运行而且提供某种服务,不是对整个系统就是对某个用户程序提供服务。Linux系统的大多数服务器就是通过守护进程实现的。常见的守护进程包括系统日志进程syslogd、 web服务器httpd、邮件服务器sendmail和数据库服务器mysqld等。

守护进程一般在系统启动时开始运行,除非强行终止,否则直到系统关机都保持运行。守护进程经常以超级用户(root)权限运行,因为它们要使用特殊的端口(1-1024)或访问某些特殊的资源。

守护进程的父进程是init进程,因为它真正的父进程在fork出子进程后就先于子进程exit退出了,所以它是一个由init继承的孤儿进程。守护进程是非交互式程序,没有控制终端,所以任何输出,无论是向标准输出设备stdout还是标准出错设备stderr的输出都需要特殊处理。

守护进程的名称通常以d结尾,比如sshd、xinetd、crond等。

二、守护进程的创建

首先我们需要理解一些基本概念:

  • 进程组(process group): 一个或多个进程的集合,每个进程都有一个进程组ID,这个ID就是进程组长的进程ID
  • 会话期(session): 一个或多个进程组的集合,每个会话有唯一一个会话首进程(session leader),会话ID为会话首进程ID
  • 控制终端(controlling terminal) :每一个会话可以有一个单独的控制终端,与控制终端连接的会话首进程就是控制进程(controlling process)。
    这时候,与当前终端交互的就是前台进程组,其他的都是后台进程组。

参考阅读:进程组和组长进程—很好的解释了一个session中怎么出现多个进程组的,是通过setpgid创建进程组的

创建守护进程的过程中会用到一个关键函数:setsid(),这个函数用于创建一个新的会话期。

给出 setsid() 的 Linux 描述

#include <unistd.h>
 
pid_t setsid(void);
 
DESCRIPTION 
       setsid()  creates a new session if the calling process is not a process 
       group leader.  The calling process is the leader of  the  new  session, 
       the  process group leader of the new process group, and has no control- 
       ling tty.  The process group ID and session ID of the  calling  process 
       are set to the PID of the calling process.  The calling process will be 
       the only process in this new process group and in this new session.
RETURN VALUE 
       On success, the (new) session ID of the calling  process  is  returned. 
       On  error,  (pid_t) -1  is  returned,  and errno is set to indicate the 
       error.

进程调用 setsid()函数会:

首先请注意: 只有当该进程不是一个进程组长时,才会成功创建一个新的会话期

(1)摆脱原会话的控制。该进程变成新会话期的首进程

(2)摆脱原进程组。成为一个新进程组的组长

(3)摆脱终端控制。如果在调用 setsid() 前,该进程有控制终端,那么与该终端的联系被解除。 如果该进程是一个进程组的组长,此函数返回错误(想想也是一个进程不能同时是两个进程组的组长)

创建守护进程的的一般步骤:

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

这是创建守护进程的第一步。由于守护进程是脱离控制终端的,因此,完成第一步后就会在Shell终端里造成程序已经运行完毕的假象。之后的所有工作都在子进程中完成,而用户在Shell终端里则可以执行其他命令(但是如果子进程中后面有向标准输出输出信息,也是会在命令行输出的,给人冷不丁的感觉),从而在形式上做到了与控制终端的脱离,在后台工作。

2、在子进程中调用 setsid() 函数创建新的会话

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

3、再次 fork() 一个子进程并让父进程退出(有意思哈)

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

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

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

5、在子进程中调用 umask() 函数,设置进程的文件权限掩码为0

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

6、在子进程中关闭任何不需要的文件描述符

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

7. 处理SIGCHLD信号 (不是必须的)

处理SIGCHLD信号并不是必须的。但对于某些进程,特别是服务器进程往往在请求到来时生成子进程处理请求。如果父进程不等待子进程结束,子进程将成为僵尸进程(zombie)从而占用系统资源。如果父进程等待子进程结束,将增加父进程的负担,影响服务器进程的并发性能。在Linux下可以简单地将SIGCHLD信号的操作设为SIG_IGN。

signal(SIGCHLD,SIG_IGN);

这样,内核在子进程结束时不会产生僵尸进程。这一点与BSD4不同,BSD4下必须显式等待子进程结束才能释放僵尸进程。

8、守护进程退出处理

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

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

在这里插入图片描述

以下程序是创建一个守护进程,然后利用这个守护进程每隔一分钟向daemon.log文件中写入当前时间,当守护进程收到 SIGQUIT 信号后退出。

#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;
	pid = fork();
	
	if(pid == -1)
	{
		printf("fork error\n");
		exit(1);
	}
	else if(pid)
	{
		exit(0);
	}
 
	if(-1 == setsid())
	{
		printf("setsid error\n");
		exit(1);
	}
 
	pid = fork();
	if(pid == -1)
	{
		printf("fork error\n");
		exit(1);
	}
	else if(pid)
	{
		exit(0);
	}
 
	chdir("/");
	int i;
	for(i = 0; i < 3; ++i)
	{
		close(i);
	}
	umask(0);
	return;
}

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

通过

ps -ef | grep 'daemon'

可以看到:

root     26454  2025  0 14:20 ?        00:00:00 ./daemon

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

Thu Dec  8 14:35:11 2016
Thu Dec  8 14:36:11 2016
Thu Dec  8 14:37:11 2016

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

sudo kill -3 26454 

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

三、利用库函数 daemon()创建守护进程

其实我们完全可以利用 daemon() 函数创建守护进程,其函数原型:

#include <unistd.h>
 
int daemon(int nochdir, int noclose);
 
DESCRIPTION         
       The daemon() function is for programs wishing to detach themselves
       from the controlling terminal and run in the background as system
       daemons.
       If nochdir is zero, daemon() changes the process's current working
       directory to the root directory ("/"); otherwise, the current working
       directory is left unchanged.
 
 
       If noclose is zero, daemon() redirects standard input, standard
       output and standard error to /dev/null; otherwise, no changes are
       made to these file descriptors.
 
RETURN VALUE         
       (This function forks, and if the fork(2) succeeds, the parent calls
       _exit(2), so that further errors are seen by the child only.)  On
       success daemon() returns zero.  If an error occurs, daemon() returns
       -1 and sets errno to any of the errors specified for the fork(2) and
       setsid(2).

现在让我们使用 daemon() 函数来再次创建一次守护进程,其实就是用 daemon() 替换掉我们自己的 create_daemon():

#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 handler(int);
 
int main()
{
	time_t t;
	int fd;
	if(-1 == daemon(0, 0))
	{
		printf("daemon error\n");
		exit(1);
	}
	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;
}

没问题,和之前一样。

谢谢分享参考:

  1. 【Linux编程】守护进程(daemon)详解与创建
  2. Linux系统编程之进程(八):守护进程详解及创建,daemon()使用

扩展1:在自己用 Python 写的 daemon 进程的时候,搜索 os.setsid() 时发现:
https://stackoverflow.com/questions/45911705/why-use-os-setsid-in-python
Point 1:

A double fork approach is generally recommended. At each fork the parent exits and the child continues. Actually nothing changes except the PID, but that’s exactly what is needed here.

First fork before the setsid makes sure the process is not a process group leader. That is required for a succesfull setsid.

The second fork after the setsid makes sure that a new association with a controlling terminal won’t be started merely by opening a terminal device.

Point2:

Well, double fork to daemonize is a good example. However, It’s better to understand what is process group and session.

Session ID (SID)

This is just the PID of the session leader. If PID == SID, then this process is a session leader.
Sessions and process groups are just ways to treat a number of related processes as a unit. All the members of a process group always belong to the same session, but a session may have multiple process groups.

Normally, a shell will be a session leader(好落地的一句话, 通过 ps -xj 查看进程 id 父进程 id, session id 等), and every pipeline executed by that shell will be a process group. This is to make it easy to kill the children of a shell when it exits. (See exit(3) for the gory details.)

Basically, if you log into a machine, your shell starts a session(好落地的一句话). If you want to keep your process running even when you log out, you should start a new session for the child.

The difference with double forked process is that you can still attach a control terminal to that process since it’s a session leader, whereas the daemon process created by double fork can not be attached to the terminal anymore.

Point 3:
在命令行通过 ps - xj 真真实实的展示, 进程 ID 、父进程 ID 、sessoin ID 直接的关系

3-1: 为了验证 if you log into a machine, your shell starts a session 同时你执行的系统命令或者 python + 文件名,实际上都是 shell 进程的子进程
以进程 python executor.py 2356 为例,进程 2356 和 2068 都是组长进程, 都属于 session 2068, 同时进程 2068 也是 session 2068 里的第一个进程
参考阅读:进程组和组长进程—很好的解释了一个session中怎么出现多个进程组的,是通过setpgid创建进程组的

root@robert-Ubuntu:/tmp# ps -xj
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
	0     1     1     1 ?           -1 Ss       0   0:02 /sbin/init splash
	0     2     0     0 ?           -1 S        0   0:00 [kthreadd]
	.......
	1   869   869   869 ?           -1 Ss       0   0:00 /usr/sbin/sshd -D
	1   922   922   922 ?           -1 Ss       0   0:00 nginx: master process /usr/sbin/nginx -g daemon 
  688   943   943   688 ?           -1 S        0   0:00 /sbin/dhclient -d -q -sf /usr/lib/NetworkManager
	1   949   949   949 ?           -1 Ssl      0   0:00 /usr/sbin/lightdm
  688   954   954   688 ?           -1 S        0   0:00 /sbin/dhclient -d -q -sf /usr/lib/NetworkManager
  688   956   956   688 ?           -1 S        0   0:00 /sbin/dhclient -d -q -sf /usr/lib/NetworkManager
	1   987   985   985 ?           -1 Sl       0   0:03 /usr/sbin/VBoxService --pidfile /var/run/vboxadd
  949  1003  1003  1003 tty7      1003 Ssl+     0   0:00 /usr/lib/xorg/Xorg -core :0 -seat seat0 -auth /v
	1  1447  1447  1447 tty1      1447 Ss+      0   0:00 /sbin/agetty --noclear tty1 linux
  949  1456   949   949 ?           -1 Sl       0   0:00 lightdm --session-child 16 19
  949  1508   949   949 ?           -1 S        0   0:00 lightdm --session-child 12 19
	1  1577  1577  1577 ?           -1 Ssl      0   0:00 /usr/lib/upower/upowerd
	2  1884     0     0 ?           -1 I        0   0:00 [kworker/u2:2]
  869  1993  1993  1993 ?           -1 Ss       0   0:00 sshd: root@pts/8,pts/10
	1  1995  1995  1995 ?           -1 Ss       0   0:00 /lib/systemd/systemd --user
 1995  2006  1995  1995 ?           -1 S        0   0:00 (sd-pam)
 .......
 1993  2068  2068  2068 pts/8     2356 Ss       0   0:00 -bash
 1993  2225  2225  2225 pts/10    2357 Ss       0   0:00 -bash
	2  2298     0     0 ?           -1 I        0   0:00 [kworker/0:2]
	2  2317     0     0 ?           -1 I        0   0:00 [kworker/u2:0]
	2  2328     0     0 ?           -1 I        0   0:00 [kworker/0:0]
	2  2352     0     0 ?           -1 I        0   0:00 [kworker/0:1]
 2068  2356  2356  2068 pts/8     2356 S+       0   0:00 python executor.py    executor.py 主体就是 sleep(10) ,前后各一句打印
 2225  2357  2357  2225 pts/10    2357 R+       0   0:00 ps -xj
root@robert-Ubuntu:/tmp# 



#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import time
import signal
def main():
	log.info("In the main")
	time.sleep(5)

3-2: 为了验证:父子进程是一个进程组,但是子进程不是进程组组长

root@robert-Ubuntu:/tmp# ps -xj
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
	0     1     1     1 ?           -1 Ss       0   0:02 /sbin/init splash
	0     2     0     0 ?           -1 S        0   0:00 [kthreadd]
	.....
	1   869   869   869 ?           -1 Ss       0   0:00 /usr/sbin/sshd -D
	1   922   922   922 ?           -1 Ss       0   0:00 nginx: master process /usr/sbin/nginx -g daemon 
  688   943   943   688 ?           -1 S        0   0:00 /sbin/dhclient -d -q -sf /usr/lib/NetworkManager
	1   949   949   949 ?           -1 Ssl      0   0:00 /usr/sbin/lightdm
  688   954   954   688 ?           -1 S        0   0:00 /sbin/dhclient -d -q -sf /usr/lib/NetworkManager
  688   956   956   688 ?           -1 S        0   0:00 /sbin/dhclient -d -q -sf /usr/lib/NetworkManager
	1   987   985   985 ?           -1 Sl       0   0:02 /usr/sbin/VBoxService --pidfile /var/run/vboxadd
  949  1003  1003  1003 tty7      1003 Ssl+     0   0:00 /usr/lib/xorg/Xorg -core :0 -seat seat0 -auth /v
	1  1447  1447  1447 tty1      1447 Ss+      0   0:00 /sbin/agetty --noclear tty1 linux
  949  1456   949   949 ?           -1 Sl       0   0:00 lightdm --session-child 16 19
  949  1508   949   949 ?           -1 S        0   0:00 lightdm --session-child 12 19
	1  1577  1577  1577 ?           -1 Ssl      0   0:00 /usr/lib/upower/upowerd
	2  1884     0     0 ?           -1 I        0   0:00 [kworker/u2:2]
  869  1993  1993  1993 ?           -1 Ss       0   0:00 sshd: root@pts/8,pts/10
	1  1995  1995  1995 ?           -1 Ss       0   0:00 /lib/systemd/systemd --user
 1995  2006  1995  1995 ?           -1 S        0   0:00 (sd-pam)
	.......
 1993  2068  2068  2068 pts/8     2339 Ss       0   0:00 -bash
 1993  2225  2225  2225 pts/10    2341 Ss       0   0:00 -bash
	2  2298     0     0 ?           -1 I        0   0:00 [kworker/0:2]
	2  2317     0     0 ?           -1 I        0   0:00 [kworker/u2:0]
	2  2328     0     0 ?           -1 I        0   0:00 [kworker/0:0]
 2068  2339  2339  2068 pts/8     2339 S+       0   0:00 python executor.py
 2339  2340  2339  2068 pts/8     2339 S+       0   0:00 python executor.py   子进程不是进程组长
 2225  2341  2341  2225 pts/10    2341 R+       0   0:00 ps -xj
root@robert-Ubuntu:/tmp# 


#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import time
import signal

def main():
	log.info("In the main")
	pid = os.fork()
	if pid == 0:
		log.info("The first sub process: pid = {0}".format(os.getpid()))
		time.sleep(3)
	else:
		log.info("The parent process: pid = {0}".format(os.getpid()))
		time.sleep(3)
		exit(0)

*3-3: 为了验证 os.setsid(); executor.py 主体就是一个真正的 daemon 进程

root@robert-Ubuntu:/tmp# ps -xj
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
	0     1     1     1 ?           -1 Ss       0   0:02 /sbin/init splash
	0     2     0     0 ?           -1 S        0   0:00 [kthreadd]
	.......
	1   869   869   869 ?           -1 Ss       0   0:00 /usr/sbin/sshd -D
	1   922   922   922 ?           -1 Ss       0   0:00 nginx: master process /usr/sbin/nginx -g daemon 
  688   943   943   688 ?           -1 S        0   0:00 /sbin/dhclient -d -q -sf /usr/lib/NetworkManager
	1   949   949   949 ?           -1 Ssl      0   0:00 /usr/sbin/lightdm
  688   954   954   688 ?           -1 S        0   0:00 /sbin/dhclient -d -q -sf /usr/lib/NetworkManager
  688   956   956   688 ?           -1 S        0   0:00 /sbin/dhclient -d -q -sf /usr/lib/NetworkManager
	1   987   985   985 ?           -1 Sl       0   0:02 /usr/sbin/VBoxService --pidfile /var/run/vboxadd
  949  1003  1003  1003 tty7      1003 Ssl+     0   0:00 /usr/lib/xorg/Xorg -core :0 -seat seat0 -auth /v
	1  1447  1447  1447 tty1      1447 Ss+      0   0:00 /sbin/agetty --noclear tty1 linux
  949  1456   949   949 ?           -1 Sl       0   0:00 lightdm --session-child 16 19
  949  1508   949   949 ?           -1 S        0   0:00 lightdm --session-child 12 19
	1  1577  1577  1577 ?           -1 Ssl      0   0:00 /usr/lib/upower/upowerd
	2  1884     0     0 ?           -1 I        0   0:00 [kworker/u2:2]
  869  1993  1993  1993 ?           -1 Ss       0   0:00 sshd: root@pts/8,pts/10
	1  1995  1995  1995 ?           -1 Ss       0   0:00 /lib/systemd/systemd --user
 1995  2006  1995  1995 ?           -1 S        0   0:00 (sd-pam)
	.......
 1993  2068  2068  2068 pts/8     2312 Ss       0   0:00 -bash
	2  2210     0     0 ?           -1 I        0   0:00 [kworker/0:0]
 1993  2225  2225  2225 pts/10    2315 Ss       0   0:00 -bash
	2  2252     0     0 ?           -1 I        0   0:00 [kworker/0:1]
	2  2277     0     0 ?           -1 I        0   0:00 [kworker/u2:1]
	2  2298     0     0 ?           -1 I        0   0:00 [kworker/0:2]
	1  2307  2307  2307 ?           -1 Ssl      0   0:00 /usr/lib/NetworkManager/nm-dispatcher
 2068  2312  2312  2068 pts/8     2312 S+       0   0:00 python executor.py
 2312  2313  2313  2313 ?           -1 Ss       0   0:00 python executor.py  (第一个子进程创建了一个新 session,然后确实变成了进程组长和 session leader)
 2313  2314  2313  2313 ?           -1 S        0   0:00 python executor.py
 2225  2315  2315  2225 pts/10    2315 R+       0   0:00 ps -xj
root@robert-Ubuntu:/tmp# 


#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import time
import signal

def main():
	log.info("In the main")
	pid = os.fork()
	if pid == 0:
		log.info("The first sub process: pid = {0}".format(os.getpid()))
		os.setsid()
		pid = os.fork()
		if pid == 0:
			log.info("The second sub process: pid = {0}".format(os.getpid()))
			'''
			1. Change the work dir of the second subprocess
			2. Change the umask of the second subprocess
			3. Add the signal handler for SIGCHLD
			4. Close the opened files
			'''
			os.chdir('/')
			os.umask(0)
			signal.signal(signal.SIGCHLD, signal.SIG_IGN)
			open_file_list = [0, 1, 2]
			for file in open_file_list:
			    os.close(file)
			poll_db_task()
		else:
			log.info("The parent of second sub process: pid = {0}".format(os.getpid()))
			time.sleep(3)
			exit(0)
	else:
		log.info("The parent process: pid = {0}".format(os.getpid()))
		time.sleep(3)
		exit(0)
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值