嵌入式Linux系统编程-》守护进程+面试题+源码【第5天:10679字】

在这里插入图片描述


【1】守护进程-》基本概念

1. 概述

在编写守护进程的过程中,会跟如下诸多概念打交道:进程组、前台进程组、后台进程组、会话、控制终端等,下图简略地反应了它们之间的关系:
在这里插入图片描述

系统调度的最小单位是进程(或称任务,task)若干进程可组成一个进程组,若干进程组可组成一个会话,可见这几个概念都只是进程的组织方式,之所以要构造进程组和会话,其根本目的是为了便于发送信号:我们可以通过给进程组或会话发送信号,便可使得其内所有的进程均可收到信号。


控制终端指的是跟会话相关联的登录窗口程序,或伪终端程序(比如gnome-terminal),这些程序一般都有关联的输入设备,一般是键盘。我们可以使用控制终端设备来产生输入数据或信号,进而影响会话中的前台进程组,也可通过挂断操作来影响会话中的后台进程组。


另外需要澄清的一点是,守护进程在英文中称为 daemon 进程,经常被翻译为:

  • 精灵进程(√)
  • 守护进程(√)
  • 后台进程

前两种翻译都没问题,最后一种翻译要注意,跟上述图中所示的后台进程组没有关系,当我们提到后台进程组的时候,通常指会话中除了前台进程组以外的其他进程组。


2. 进程组

2.1 基本逻辑

进程是系统中的活跃个体,是系统的调度单位,进程就像现实世界中的活动的人,社会为了便于管理,一般都会将人归总到某一个集体中,比如公司、学校、组织等,系统中的进程也一样,他们可以按照实际所需进入某个进程组。进程组的好处在于,可以给组内的所有进程统一发送信号。

设置/获取进程所属的进程组的函数接口是:

#include <sys/types.h>
#include <unistd.h>

// 功能:将进程pid的所在进程组设置为pgid
//       如果pid == 0,则设置本进程
//       如果pgid == 0,等价于pgid == pid
// 注意:若进程pid与进程组pgid不在同一会话内,设置将失败
int setpgid(pid_t pid, pid_t pgid);

// 功能:获取进程pid所在进程组ID
pid_t getpgid(pid_t pid);

以下代码显示了函数setpgid( )使用细节:

// 将指定进程123加入进程组7799中
// 注意,进程组7799必须存在且与进程123同处相同的会话
setpgid(123, 7799);

// 将本进程加入进程组7799中
// 注意,进程组7799必须存在且与本进程同处相同的会话
setpgid(0, 7799);

// 创建一个ID等于本进程PID的进程组
// 并将本进程置入其中,成为进程组组长
setpgid(0, 0);

2.2 前台进程组

一般而言,进程从终端启动时,会自动创建一个新进程组,并且该进程组内只包含这个创始进程,而其后代进程默认都将会被装载在该进程组内部,这个进程组被称为前台进程组。


前台进程组最大的特点是:可以接收控制终端发来的信号,所谓控制终端,一般就是指标准输入设备键盘


以下代码显示了一个子进程脱离父进程的前台进程组,从而脱离控制终端的过程:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

void showid()
{
    
    
    printf("本进程ID:%d\t", getpid());
    printf("父进程ID:%d\t", getppid());
    printf("进程组ID:%d\t", getpgid(0));
    printf("所在会话ID:%d\n", getsid(0));
}

int main()
{
    
    
    if(fork() == 0)
    {
    
    
        showid();
        
        // 将本进程的所属进程组,设置为等于本进程ID
        // 即:创建一个仅包含自身的进程组并将自身置入其中
        if(setpgid(0, 0) < 0)
            perror("setpgid() failed");
        else
            showid();
    }
    
    pause();
}

上述代码的效果是,使得子进程“自立门户”,不再与其父进程同处一个进程组,因此如果从控制终端按下 ctrl+c ,那么此快捷键触发的信号
SIGINT 将只会发送给前台进程组,子进程将不受影响。


在一个会话中,前台进程组只有一个,而后台进组可以有多个,如下图所示,因此如果一个进程需要接受键盘的输入,那么必须要处于前台进程组中。
在这里插入图片描述

2.3 后台进程组

可以在终端中加个 ‘&’ 来使得进程进入后台进程组:

# 使 a.out 在后台进程组中运行
gec@ubuntu:~$ ./a.out &

让一个进程在后台进程组中运行,通常是为了使其让出控制终端,不接受控制终端的输入和信号,但这并不意味着它不受控制终端控制,控制终端依然可向其发送挂断信号。

上述后台进组进程 a.out 与即将要介绍的后台进程(或称精灵进程、守护进程)无关。


3. 会话

会话(session)原本指的是一个登录过程中所产生的的所有进程组的总和,可以理解为一个登录窗口就是一个会话。但在伪终端中,每打开一个窗口实际上也是创建了一个会话,例如:

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

伪终端所产生的会话 如上所述,会话可以理解为就是进程组的进程组,会话的作用可总结为:
[1]可关联一个控制终端(比如键盘)设备,并从控制终端获得输入信息
[2]可对会话内的所有进程组统一发送信号
[3]可将控制终端的关闭动作,转换为触发挂断信号并发送给所有进程

如下图所示:
在这里插入图片描述

当我们打开一个伪终端,或者打开一个远程登录工具输入账户密码的过程中,默认都调用了如下函数接口去创建一个新的会话。

#include <sys/types.h>
#include <unistd.h>

pid_t setsid(void);

注意:

进程组组长不能调用该函数。 新创建的会话没有关联控制终端,因此其内进程不受控制终端影响。
创建会话的进程,称为该会话的创始进程,创始进程有权捕获一个控制终端(在编写守护进程时通常需要避免),会话的其余成员进程无权获得控制终端。

4. 控制终端

控制终端通常会关联一个输入设备,可以给前台进程组发送数据或信号,平常使用的许多信号快捷键,就是通过控制终端发送给前台进程组内的进程的:

快捷键	对应信号
ctrl + c	SIGINT
ctrl + \	SIGQUIT
ctrl + z	SIGSTOP

控制终端不仅可以向前台进程组发送数据,也能向整个会话发送挂断信号(即SIGHUP),默认情况下收到SIGHUP的进程会被终止,因此,为了避免被控制终端“误杀”,常驻内存的守护进程的必备步骤须包括忽略掉SIGHUP、脱离控制终端、避免再次获取控制终端等操作。以下是示例代码:

// 忽略信号SIGHUP
signal(SIGHUP, SIG_IGN);

// 脱离控制终端(新建一个会话)
setsid();

// 避免会话再次关联控制终端(退出会话创始进程)
if(fork() > 0)
    exit(0);

【2】守护进程-》代码编写

1. 概述

1.1 守护进程是什么?

守护进程(Daemon)也被翻译为精灵进程、后台进程,是一种旨在运行于相对干净环境、不受终端影响的、常驻内存的进程,就像神话中的精灵拥有不死的特性,长期稳定提供某种功能或服务。


在Unix/Linux系统中,使用 ps 命令可以看到许多以 -d 结尾的进程,它们大多都是守护进程,例如:

gec@ubuntu:/mnt/hgfs/codes/rockChip/dataStructure$ ps -ef | grep '.*d$'
root        407      1  0 6月09 ?       00:00:29 /lib/systemd/systemd-journald
root        436      1  0 6月09 ?       00:00:10 /lib/systemd/systemd-udevd
systemd+   1049      1  0 6月09 ?       00:00:06 /lib/systemd/systemd-timesyncd
root       1150      1  0 6月09 ?       00:00:00 /usr/lib/bluetooth/bluetoothd
root       1151      1  0 6月09 ?       00:00:42 /usr/sbin/irqbalance --foreground
root       1192      1  0 6月09 ?       00:00:00 /usr/sbin/acpid
root       1193      1  0 6月09 ?       00:00:02 /usr/lib/udisks2/udisksd
root       1201      1  0 6月09 ?       00:00:03 /lib/systemd/systemd-logind
root       1583      1  0 6月09 ?       00:14:52 /usr/sbin/vmtoolsd
root       1832      1  0 6月09 ?       00:00:01 /usr/lib/upower/upowerd
gec        1860   1427  0 6月09 ?       00:00:00 /usr/lib/gvfs/gvfsd
root       1932      1  0 6月09 ?       00:00:00 /usr/lib/x86_64-linux-gnu/boltd
root       1970      1  0 6月09 ?       00:01:55 /usr/lib/packagekit/packagekitd
colord     2069      1  0 6月09 ?       00:00:02 /usr/lib/colord/colord
systemd+  25748      1  0 6月15 ?       00:00:00 /lib/systemd/systemd-networkd
root      26322      1  0 00:08 ?        00:00:00 /usr/sbin/cups-browsed
root      26489      1  0 00:46 ?        00:00:13 /usr/lib/snapd/snapd
systemd+  27264      1  0 18:58 ?        00:00:00 /lib/systemd/systemd-resolved

有许多程序或服务理应成为这种“不死”的守护进程,比如提供系统网络服务的核心程序systemd-networkd,只要系统需要基于TCP/IP协议栈进行网络通信,它就应该一直常驻内存,永不退出。


1.2 怎样成为守护进程?

成为守护进程的关键在于使进程运行在一个相对独立、干净的环境,尽量不受各种事件的影响(除非断电、关机),以下就是成为这种进程的详细步骤。


2. 守护进程编写步骤

2.1 忽略SIGHUP

由于终端的关闭会触发SIGHUP并发送给终端所关联的会话的所有进程,而一开始进程尚未脱离原会话,因此应尽早忽略该信号,避免被挂断信号误杀。

实现代码如下:

// 1,忽略挂断信号SIGHUP,防止被终端误杀
signal(SIGHUP, SIG_IGN);

2.2 产生子进程

从终端(不管是远程登录窗口还是本地伪终端)启动的进程所在的会话都关联了控制终端,而控制终端会有各种数据或信号的输入干扰,为了避开这些干扰,需要脱离控制终端,而脱离控制终端的简单做法就是新建一个新的、没有控制终端的会话,但创建一个新会话的进程必须是非进程组组长,但Linux系统中,从终端启动的进程默认就是其所在进程组的组长,因此摆脱这一困境的简单做法就是让其产生一个子进程,退出原进程(即父进程)并让子进程继续下面的步骤即可。

实现代码如下:

// 2,退出父进程(原进程组组长),为能成功创建新会话做准备
if(fork() > 0)
    exit(0);

2.3 创建新会话

创建新会话,脱离原会话,脱离控制终端。

实现代码如下:

// 3,创建新会话,脱离原会话,脱离控制终端。
setsid();

2.4 产生孙子进程

此时的进程是其所在的会话的创始进程,而创始进程拥有可以再次关联的控制终端的权限,为避免此种情况的发生,最简单的做法就是退出当前创始进程,改由其子进程(非创始进程)继续完成成为守护进程的使命。

实现代码如下:

// 4,断绝重新关联控制终端的可能性
if(fork() > 0)
    exit(0);

2.5 进入新进程组

虽然此时进程的父进程、祖父进程已经退出,但进程组是一直都在的,且处于新会话中的孙子进程一直都在其祖父进程的进程组之中,而进程组是可以传递信号的,因此为了与任何方面脱离关系,应“自立门户”创建新进程组,并将自身置入其中。

实现代码如下:

// 5,脱离原进程组,创建并进入只包含自身的进程组
setgpid(0, 0);

2.6 关闭文件资源

文件资源是可以在父子进程之间代际相传的,这其中也包括了标准输入输出文件,而作为守护进程,是一种在后台运行的程序,运行过程中一般无需交互,若有消息需要输出一般会以系统日志的方式输出到指定日志文件中。因此,为了节约系统资源,也为了避免不必要的逻辑谬误,守护进程一般都需要将所有从父辈进程继承下来的文件全部关闭。

实现代码如下:

// 6,关闭父辈继承下来的所有文件
for(int i=0; i<sysconf(_SC_OPEN_MAX); i++)
		close(i);

2.7 关闭文件权限掩码

在Linux系统中创建一个新文件时,可以通过相关的函数参数指定文件的权限,比如:

// 试图在file.txt不存在的情况下,创建一个权限为0777的文件
int fd = open("file.txt", O_CREAT|O_RDWR, 0777);

但其实被创建出来的文件的权限并非代码中指定的权限,该权限与系统当前的文件权限掩码做位与操作之后的值才是文件真正的权限,我们可以通过命令
umask 来查看当前系统默认的文件权限掩码的值:

gec@ubuntu:~$ umask
0022
gec@ubuntu:~$ 

因此,上述创建的文件的权限最后不是0777,而是0755:

gec@ubuntu:~$ ls -l
-rwxr-xr-x  1 gec gec     0 617 00:18  file.txt
gec@ubuntu:~$ 

为了让守护进程在后续工作过程创建文件时指定的权限不受系统文件权限掩码干扰,可以将umask设置为0。

实现代码如下:

// 7,避开系统文件权限掩码的干扰
umask(0);

2.8 切换进程工作路径

任何一个进程都有一个当前工作路径,从终端启动的进程的工作路径就是启动时终端所在的系统路径。以下代码可以输出进程当前所在路径:

int main(void)
{
    
    
	printf("%s\n", getcwd(NULL, 0));
}

注意:

当一个进程的工作路径被卸载时,进程也会随时消亡。守护进程为了避免此种情况发生,最简单的做法就是将自身的工作路径切换到一个无法被卸载的路径下,比如根目录。

实现代码如下:

// 8,避免所在路径被卸载
chdir("/");

【3】守护进程-》源码

#include "daemon.h"

int main(void)
{
    
    
	pid_t a;
	int max_fd, i;

	/*********************************************
	1. ignore the signal SIGHUP, prevent the
	   process from being killed by the shutdown
	   of the present controlling termination
	**********************************************/
	signal(SIGHUP, SIG_IGN);

	/***************************************
	2. generate a child process, to ensure
	   successfully calling setsid()
	****************************************/
	a = fork();
	if(a > 0)
		exit(0);

	/******************************************************
	3. call setsid(), let the first child process running
	   in a new session without a controlling termination
	*******************************************************/
	setsid();

	/*************************************************
	4. generate the second child process, to ensure
	   that the daemon cannot open a terminal file
	   to become its controlling termination
	**************************************************/
	a = fork();
	if(a > 0)
		exit(0);

	/*********************************************************
	5. detach the daemon from its original process group, to
	   prevent any signal sent to it from being delivered
	**********************************************************/
	setpgrp();

	/*************************************************
	6. close any file descriptor to release resource
	**************************************************/
	max_fd = sysconf(_SC_OPEN_MAX);
	for(i=0; i<max_fd; i++)
		close(i);

	/******************************************
	7. clear the file permission mask to zero
	*******************************************/
	umask(0);

	/****************************************
	8. change the process's work directory,
	   to ensure it won't be uninstalled
	*****************************************/
	chdir("/");


	// Congratulations! Now, this process is a DAEMON!
	pause();
	return 0;
}


【4】守护进程-》面试题

问:老师,有时候我们也会用&来让一个程序在后台运行,请问这跟守护进程有什么关系?

答:使用&可以使一个程序进入后台进程组,不再占用前台终端,但这跟deamon进程没有关系,deamon进程的关键是脱离会话、脱离进程组、屏蔽信号、打开系统日志甚至还要阻止重新打开终端,而前者仅仅是脱离控制终端。之所以会引起误会,完全是因为翻译的问题,因此建议将deamon进程直译为精灵进程,或守护进程。


回答如下问题:

什么是前台进程组?什么是后台进程组? 如何让进程进入后台进程组?有什么用? 后台进程是什么?

解答:

在一个会话中,与控制终端直接关联的进程组称为前台进程组。终端中启动的进程默认都进入前台进程组,除非启动时添加了&,就进入了后台进程组,后台进程组是相对前台进程组而言的,进入后台进程组的进程无法从终端获得输入、信号。
在启动程序时在后面加一个&可让进程进入后台,这样可以避免占用终端。
后台进程有时候指守护进程,有时候指后台进程组中的进程,这两个含义截然不同,需要在上下文中加以区分,这些含糊措词的来源是中文的翻译问题,后台进程组是所谓的background进程,是相对前台进程而言的,而守护进程是daemon进程。

(具名管道FIFO、守护进程)

【1】编程实现两个程序:一个服务器server,一个客户机client。 要求:

服务器采用守护进程模式在后台运行,常驻内存。 服务器创建并监视有名管道FIFO,一旦发现有数据则将其保存到一个指定的地方。
客户机每隔一段时间产生一个子进程。 客户机的这些子进程将当前系统时间和自身的PID写入有名管道FIFO就退出。

解答:

本题与管道相关章节的课后实验题是一样的,唯一的要求是要将服务程序设置为守护进程,常驻内存运行。

这只要将守护进程的编写步骤封装成一个函数,让服务器在启动时调用该封装函数即可。示例代码如下:点击下载

// 头文件 head.h
#ifndef __HEAD_H
#define __HEAD_H

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

#define FIFONAME "/tmp/logfifo"
#define LOGFILE  "/tmp/logfile"

void daemon_init(void);

#endif

// 守护进程封装接口
#include "head.h"
void daemon_init(void)
{
    
    
	pid_t pid;
	int fd0, fd1, fd2, max_fd,i;	

	// 1
	if((pid = fork()) < 0)
	{
    
    
		perror("产生子进程失败");
		exit(0);
	}
	else if(pid > 0)
		exit(0);

	// 2
	signal(SIGHUP, SIG_IGN);

	// 3
	if(setsid() < 0)
	{
    
    
		perror("设置会话失败");
		exit(0);
	}

	// 4
	if((pid = fork()) < 0)
	{
    
    
		perror("产生孙子进程失败");
		exit(0);
	}
	else if(pid > 0)
		exit(0);

	// 5
	if(setpgid(0, 0) < 0)
	{
    
    
		perror("设置进程组失败");
		exit(0);
	}

	// 6
	max_fd = sysconf(_SC_OPEN_MAX);
	for (i = max_fd; i>=0; i--)
		close(i);

	// 7
	umask(0);

	// 8
	chdir("/");
}
#include "head.h"

// 服务器程序
int main(int argc, char **argv)
{
    
    
	// 初始化为守护进程
	daemon_init();	

	mkfifo(FIFONAME, 0777);
	int fifofd = open(FIFONAME, O_RDWR);
	int logfd  = open(LOGFILE, O_WRONLY|O_CREAT|O_APPEND, 0777);

	char buf[1024];
	while(1)
	{
    
    
		bzero(buf, 1024);
		read(fifofd, buf, 1024);

		write(logfd, buf, strlen(buf));
	}

	return 0;
}
// 客户端程序
#include "head.h"

int main(int argc, char **argv)
{
    
    
	mkfifo(FIFONAME, 0777);
	int fd = open(FIFONAME, O_WRONLY);

	char buf[1024];
	time_t t;
	while(1)
	{
    
    
		bzero(buf, 1024);

		time(&t);
		snprintf(buf, 1024, "[%-6d] %s",
			getpid(), ctime(&t));

		write(fd, buf, strlen(buf));
		sleep(1);
	}

	return 0;
}

#端午趣味征文赛–用代码过端午#
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/m0_45463480/article/details/125209824