Skip to content

LV040-守护进程

本文主要是进程——进程的关系与守护进程的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

一、守护进程

1. 什么是守护进程?

守护进程( Daemon Process )是 Linux 三种进程类型之一,也称为精灵进程,是运行在后台的一种特殊进程,它独立于控制终端并且周期性地执行某种任务或等待处理某些事情的发生,主要有以下特点:

  • 长期运行。守护进程是一种生存期很长的一种进程,它们一般在系统启动时开始运行,除非强行终止,否则直到系统关机都会保持运行。与守护进程相比,普通进程都是在用户登录或运行程序时创建,在运行结束或用户注销时终止,但守护进程不受用户登录注销的影响,它们将会一直运行着、直到系统关机
  • 与控制终端脱离。在 Linux 中,系统与用户交互的界面称为终端,每一个从终端开始运行的进程都会依附于这个终端,也就是会话的控制终端。当控制终端被关闭的时候,该会话就会退出,由控制终端运行的所有进程都会被终止,这使得普通进程都是和运行该进程的终端相绑定的;但守护进程能突破这种限制,它脱离终端并且在后台运行脱离终端的目的是为了避免进程在运行的过程中的信息在终端显示并且进程也不会被任何终端所产生的信息所打断。
  • 周期性的执行某种任务或等待处理特定事件

Linux 中大多数服务器就是用守护进程实现的,例如, Internet 服务器 inetd 、 Web 服务器 httpd 等。同时,守护进程完成许多系统任务,譬如作业规划进程 crond 等。

守护进程 Daemon ,通常简称为 d ,一般进程名后面带有 d 就表示它是一个守护进程。守护进程与终端无任何关联,用户的登录与注销与守护进程无关、不受其影响,守护进程自成进程组、自成会话,即 pid = gid = sid 。在终端执行以下命令:

shell
ps -ajx

会有如下输入信息:

shell
   PPID     PID    PGID     SID TTY        TPGID STAT   UID   TIME COMMAND
      0       1       1       1 ?             -1 Ss       0   0:06 /sbin/init splash
      0       2       0       0 ?             -1 S        0   0:00 [kthreadd]
      2       3       0       0 ?             -1 I<       0   0:00 [rcu_gp]
      2       4       0       0 ?             -1 I<       0   0:00 [rcu_par_gp]
      2       6       0       0 ?             -1 I<       0   0:00 [kworker/0:0H-events_highpri]
      2       9       0       0 ?             -1 I<       0   0:00 [mm_percpu_wq]
      2      10       0       0 ?             -1 S        0   0:00 [rcu_tasks_rude_]
      2      11       0       0 ?             -1 S        0   0:00 [rcu_tasks_trace]
# 后边还有很多,这里省略 ...

TTY 一栏是问号 ? 表示该进程没有控制终端,也就是守护进程,其中 COMMAND 一栏使用中括号 [] 括起来的表示内核线程,这些线程是在内核里创建,没有用户空间代码,因此没有程序文件名和命令行,通常采用 k 开头的名字,表示 Kernel 。

2. 创建守护进程

2.1 创建步骤

  • (1)创建子进程,终止父进程

父进程调用 fork() 创建子进程,然后父进程使用 exit() 退出,然后子进程变成孤儿进程,被 init 进程(字符模式的话)收养,子进程在后台运行。

这样做,第一,如果该守护进程是作为一条简单地 shell 命令启动,那么父进程终止会让 shell 认为这条命令已经执行完毕。第二,虽然子进程继承了父进程的进程组 ID ,但它有自己独立的进程 ID ,这保证了子进程不是一个进程组的组长进程,这是后边要调用 setsid 函数的先决条件。

  • (2)子进程调用 setsid() 创建会话

在子进程中调用 setsid() 函数创建新的会话,由于之前子进程并不是进程组的组长进程,所以调用 setsid() 会使得子进程创建一个新的会话,子进程成为新会话的首领进程,同样也创建了新的进程组、子进程成为组长进程,此时创建的会话将没有控制终端。所以这里调用 setsid 有三个作用:让子进程摆脱原会话的控制、让子进程摆脱原进程组的控制和让子进程摆脱原控制终端的控制。 在调用 fork 函数时,子进程继承了父进程的会话、进程组、控制终端等,虽然父进程退出了,但原先的会话期、进程组、控制终端等并没有改变,因此,那还不是真正意义上使两者独立开来。 setsid 函数能够使子进程完全独立出来,从而脱离所有其他进程的控制。

  • (3)将工作目录更改为根目录

子进程是继承了父进程的当前工作目录,由于在进程运行中,当前目录所在的文件系统是不能卸载的,这对以后使用会造成很多的麻烦。因此通常的做法是让 / (根目录)作为守护进程的当前目录,当然也可以指定其它目录来作为守护进程的工作目录。

  • (4)重设文件权限掩码 umask

文件权限掩码 umask 用于对新建文件的权限位进行屏蔽。由于使用 fork 函数新建的子进程继承了父进程的文件权限掩码,这就给子进程使用文件带来了诸多的麻烦。因此,把文件权限掩码设置为 0 ,确保子进程有最大操作权限、这样可以大大增强该守护进程的灵活性。设置文件权限掩码的函数是 umask ,通常的使用方法为 umask(0) 。

  • (5)关闭不再需要的文件描述符

子进程继承了父进程的所有文件描述符,这些被打开的文件可能永远不会被守护进程(此时守护进程指的就是子进程,父进程退出、子进程成为守护进程)读或写,但它们一样消耗系统资源,可能导致所在的文件系统无法卸载,所以必须关闭这些文件,这使得守护进程不再持有从其父进程继承过来的任何文件描述符。

  • (6)将文件描述符号为 0 、 1 、 2 定位到 /dev/null 或者直接关闭也可以

将守护进程的标准输入、标准输出以及标准错误重定向到 /dev/null ,这使得守护进程的输出无处显示、也无处从交互式用户那里接收输入。

  • (7)其它操作:忽略 SIGCHLD 信号

处理 SIGCHLD 信号不是必须的,但对于某些进程,特别是并发服务器进程往往是特别重要的,服务器进程在接收到客户端请求时会创建子进程去处理该请求,如果子进程结束之后,父进程没有去 wait 回收子进程,则子进程将成为僵尸进程;如果父进程 wait 等待子进程退出,将又会增加父进程的负担、也就是增加服务器的负担,影响服务器进程的并发性能,在 Linux 下,可以将 SIGCHLD 信号的处理方式设置为 SIG_IGN ,也就是忽略该信号,可让内核将僵尸进程转交给 init 进程去处理,这样既不会产生僵尸进程、又省去了服务器进程回收子进程所占用的时间。

2.2 相关函数

2.2.1 getcwd()
2.2.1.1 函数说明

在 linux 下可以使用 man getcwd 命令查看该函数的帮助手册。

c
#include <unistd.h>
char *getcwd(char *buf, size_t size);

函数说明】获取当前工作目录。

函数参数

  • buf : char * 类型,参数 buf 为保存当前工作目录绝对路径的字符型指针。
  • size : size_t 类型,为 buf 的长度。

返回值】返回值为 char * 类型,成功返回当前工作目录绝对路径存储区域的首地址,失败则返回 NULL 、并设置 errno 。

使用格式】一般情况下基本使用格式如下:

c
/* 需要包含的头文件 */
#include <unistd.h>

/* 至少应该有的语句 */
char path[20];
getcwd(path, 20);

注意事项】 none

2.2.1.2 使用实例
c
#include <stdio.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
    char path[32];
	printf("path=%s\n", getcwd(path, 32));
	return 0;
}

在终端执行以下命令:

shell
gcc test.c -Wall # 生成可执行文件 a.out
./a.out # 执行程序

程序执行后,终端将会显示以下信息:

shell
path=/mnt/hgfs/Sharedfiles
2.2.2 chdir()
2.2.2.1 函数说明

在 linux 下可以使用 man chdir 命令查看该函数的帮助手册。

c
#include <unistd.h>
int chdir(const char *path);

函数说明】更改当前工作目录到 path 所指向的目录。

函数参数

  • path : const char * 类型,需要修改到的目标目录。

返回值】返回值为 int 类型,成功返回 0 ,失败则返回 -1 、并设置 errno 。

使用格式】一般情况下基本使用格式如下:

c
/* 需要包含的头文件 */
#include <unistd.h>

/* 至少应该有的语句 */
chdir("/home");

注意事项】 none

2.2.2.2 使用实例
c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
	int ret = 0;
	char path[32];
	printf("path=%s\n", getcwd(path, 32));
	ret = chdir("/home");
	if(ret < 0)
	{
		perror("chdir error");
		exit(-1);
	}
	else
	{
		printf("path=%s\n", getcwd(path, 32));
	}
	return 0;
}

在终端执行以下命令:

shell
gcc test.c -Wall # 生成可执行文件 a.out
./a.out # 执行程序

程序执行后,终端将会显示以下信息:

shell
path=/mnt/hgfs/Sharedfiles
path=/home
2.2.3 sysconf()
2.2.3.1 函数说明

在 linux 下可以使用 man sysconf 命令查看该函数的帮助手册。

c
#include <unistd.h>
long sysconf(int name);

函数说明】获取系统执行的配置信息。

函数参数

  • name : int 类型,配置项的宏名称。常用配置项宏名称如下:
_SC_PAGESIZE查看缓存内存页面的大小
_SC_PHYS_PAGES查看内存的总页数
_SC_AVPHYS_PAGES查看可以利用的总页数
_SC_NPROCESSORS_CONF查看cpu的个数
_SC_NPROCESSORS_ONLN查看在使用的cpu个数
_SC_LOGIN_NAME_MAX查看最大登录名长度
_SC_HOST_NAME_MAX查看最大主机长度
_SC_OPEN_MAX每个进程运行时打开的最大文件数目
_SC_CLK_TCK查看每秒中跑过的运算速率
【**返回值**】返回值为 long 类型,返回值有以下情况:
  • 如果 name 对应于一个最大或最小限制,并且该限制是不确定的,则返回 -1 ,并且不更改 errno 。
  • 如果发生错误,则返回 -1 ,并设置 errno 来指示错误的原因。
  • 如果 name 对应一个选项,则支持该选项时返回正值,不支持该选项时返回 -1 。
  • 否则,返回选项或限制的当前值。

使用格式】一般情况下基本使用格式如下:

c
/* 需要包含的头文件 */
#include <unistd.h>

/* 至少应该有的语句 */
sysconf(name);

注意事项】 none

2.2.3.2 使用实例
c
#include <stdio.h>
#include <unistd.h>

#define ONE_MB (1024 * 1024)

int main()
{
	printf("The number of processors configured is :%ld\n",
	       sysconf(_SC_NPROCESSORS_CONF));
	printf("The number of processors currently online (available) is :%ld\n",
	       sysconf(_SC_NPROCESSORS_ONLN));
	printf ("The pagesize: %ld\n", sysconf(_SC_PAGESIZE));
	printf ("The number of pages: %ld\n", sysconf(_SC_PHYS_PAGES));
	printf ("The number of available pages: %ld\n", sysconf(_SC_AVPHYS_PAGES));
	printf ("The memory size: %lld MB\n",
	        (long long)sysconf(_SC_PAGESIZE) * (long long)sysconf(_SC_PHYS_PAGES) / ONE_MB );
	printf ("The number of files max opened:: %ld\n", sysconf(_SC_OPEN_MAX));
	printf("The number of ticks per second: %ld\n", sysconf(_SC_CLK_TCK));
	printf ("The max length of host name: %ld\n", sysconf(_SC_HOST_NAME_MAX));
	printf ("The max number of simultaneous processes per user: %ld\n", sysconf(_SC_CHILD_MAX));
	return 0;

}

在终端执行以下命令:

shell
gcc test.c -Wall # 生成可执行文件 a.out
./a.out # 执行程序

程序执行后,终端将会显示以下信息:

shell
The number of processors configured is :2
The number of processors currently online (available) is :2
The pagesize: 4096
The number of pages: 997003
The number of available pages: 196547
The memory size: 3894 MB
The number of files max opened:: 1024
The number of ticks per second: 100
The max length of host name: 64
The max number of simultaneous processes per user: 15308

3. 创建守护进程实例

3.1 创建实例1

c
#include <stdio.h>
#include <unistd.h>  /* fork setsid getpid getpgid chdir */
#include <stdlib.h>  /* exit */
#include <sys/stat.h>/* umask */

int main()
{
	pid_t pid;

	printf("before father fork!\n-----------------\n");
	/* 1. 父进程创建子进程,并退出父进程 */
	pid = fork();
	/* 下边的代码父进程执行一次,子进程也执行一次 */
	if(pid > 0)/* 父进程 */
	{
		printf("father process printf<pid:%d,child pid:%d,father return:%d>\n", getpid(), pid, pid);
		/* 退出父进程 */
		exit(2);
	}
	else if(pid < 0)
	{
		perror("fork");
		return 0;
	}
	/* 2. 父进程退出后,子进程变成孤儿进程,被init进程收养*/
	else if(pid == 0)/* 子进程 */
	{
		printf("I am a deamon\n");
		printf("child process printf<pid:%d,father pid:%d,child return:%d>\n", getpid(), getppid(), pid);

		/* 3. 新建会话 */
		printf("sid=%d,pid=%d,pgid=%d\n", getsid(getpid()), getpid(), getpgid(getpid()));
		if(setsid() < 0)
		{
			perror("setsid");
			exit(0);
		}
		printf("sid=%d,pid=%d,pgid=%d\n", getsid(getpid()), getpid(), getpgid(getpid()));
		/* 4. 修改当前工作目录 */
		chdir("/");
		/* 5. 重设文件权限掩码 */
		if(umask(0) < 0)
		{
			perror("unmask");
			exit(0);
		}
		/* 6. 关闭所有的文件描述符 */
		for (i = 0; i < sysconf(_SC_OPEN_MAX); i++) 
			close(i);
		printf("after close \n");

		sleep(100);
	}
	return 0;

}

在终端执行以下命令:

shell
gcc test.c -Wall # 生成可执行文件 a.out
./a.out # 执行程序

程序执行后,终端将会显示以下信息:

shell
before father fork!
-----------------
father process printf<pid:18394,child pid:18395,father return:18395>
I am a deamon
child process printf<pid:18395,father pid:935,child return:0>
sid=11992,pid=18395,pgid=18394
sid=18395,pid=18395,pgid=18395

然后我们会发现程序正常结束了,接下来我们输入以下命令查看 a.out 进程情况:

shell
ps -ajx|grep a.out

然后会看到如下信息:

shell
    935   18395   18395   18395 ?             -1 Ss    1000   0:00 ./a.out
  11992   18406   18405   11992 pts/1      18405 S+    1000   0:00 grep --color=auto a.out

发现 a.out 进程的 TTY 已经变成了 ? ,这就表示它已经是一个守护进程了。

3.2 创建实例2

创建守护进程,每隔1秒将系统时间写入文件 time.log 。

c
#include <stdio.h>
#include <unistd.h>  /* fork setsid getpid getpgid chdir */
#include <stdlib.h>  /* exit */
#include <sys/stat.h>/* umask */
#include <time.h>

int main()
{
	int i = 0;
	pid_t pid;
	FILE *fp;/* 定义一个文件结构体指针变量 */
	time_t t;

	printf("before father fork!\n-----------------\n");
	/* 1. 父进程创建子进程,并退出父进程 */
	pid = fork();
	/* 下边的代码父进程执行一次,子进程也执行一次 */
	if(pid > 0)/* 父进程 */
	{
		printf("father process printf<pid:%d,child pid:%d,father return:%d>\n", getpid(), pid, pid);
		/* 退出父进程 */
		exit(2);
	}
	else if(pid < 0)
	{
		perror("fork");
		return 0;
	}
	/* 2. 父进程退出后,子进程变成孤儿进程,被init进程收养*/
	else if(pid == 0)/* 子进程 */
	{
		printf("I am a deamon\n");
		printf("child process printf<pid:%d,father pid:%d,child return:%d>\n", getpid(), getppid(), pid);

		/* 3. 新建会话 */
		printf("sid=%d,pid=%d,pgid=%d\n", getsid(getpid()), getpid(), getpgid(getpid()));
		if(setsid() < 0)
		{
			perror("setsid");
			exit(0);
		}
		printf("sid=%d,pid=%d,pgid=%d\n", getsid(getpid()), getpid(), getpgid(getpid()));
		/* 4. 修改当前工作目录 */
		chdir("/home/hk");
		/* 5. 重设文件权限掩码 */
		if(umask(0) < 0)
		{
			perror("unmask");
			exit(0);
		}
		/* 6. 关闭所有的文件描述符 */
		for (i = 0; i < sysconf(_SC_OPEN_MAX); i++)
			close(i);
		printf("after close \n");
		/* 7. 打开文件 */
		if ((fp = fopen("/home/hk/time.log", "a")) == NULL)
		{
			perror("fopen");
			exit(-1);
		}
		/* 8. 写入时间 */
		while(1)
		{
			time(&t);
			fprintf(fp, "%s", (char *)ctime(&t));
			fflush(fp);
			sleep(1);
		}

	}
	return 0;
}

在终端执行以下命令:

shell
gcc test.c -Wall # 生成可执行文件 a.out
./a.out # 执行程序

程序执行后,终端将会显示以下信息:

shell
before father fork!
-----------------
father process printf<pid:18699,child pid:18700,father return:18700>
I am a deamon
child process printf<pid:18700,father pid:935,child return:0>
sid=18649,pid=18700,pgid=18699
sid=18700,pid=18700,pgid=18700

然后我们会发现程序正常结束了,接下来我们输入以下命令查看 time.log 文件内容:

shell
vim ~/time.log

然后会看到如下信息:

shell
Mon May 23 19:00:59 2022
Mon May 23 19:01:00 2022
Mon May 23 19:01:01 2022
Mon May 23 19:01:02 2022
Mon May 23 19:01:03 2022

会发现,守护进程的存在系统会每隔 1s 向该文件写入一次时间。