操作系统中的精灵——“守护进程”

守护进程也称精灵进程(Daemon),是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任

或等待处理某些发生的事件。守护进程是一种很有用的进程。

Linux的大多数服务器就是用守护进程实现的。

比如,Internet服务器inetd,Web服务器httpd等。

同时,守护进程完成许多系统任务。比如,作业规划进程crond等。

在了解守护进程之前我们需要先知道下面的一些概念知识:

1、【进程组】、【作业】、【会话】基本概念

进程组

每个进程除了有一个进程ID之外,还属于一个进程组。进程组是一个或多个进程的集合。
通常,它们与同一作业相关联,可以接收来自同一终端的各种信号。每个进程组有一个唯一的进程组ID。
每个进程组都可以有一个组长进程。组长进程的标识是,其进程组ID等于其进程ID。
组长进程可以创建⼀一个进程组,创建该组中的进程,然后终止。只要在某个进程组中一个
进程存在,则该进程组就存在,这与其组长进程是否终止无关。

作业

Shell分前后台来控制的不是进程而是作业(Job)或者进程组(Process Group)。一个前台作业可以由多个进程组成,一个后台也可以由多个进程组成,
Shell可以运行一个前台作业和任意多个后台作业,这称为作业控制。
进程组与作业之间可以大致理解成是一样的,之后我们也不区分它们,但是他们是有区别的!!!下面我们会提到他们的区别。
会话
简单来说的话,会话是一个或者是多个进程组的集合。
一个会话可以有一个控制终端。这通常是登陆到其上的终端设备(在终端登陆情况下)或伪终端设备(在网络登陆情况下)。
建立与控制终端连接的会话首进程被称为控制进程。
一个会话中的几个进程组可被分为一个前台进程组以及一个或多个后台进程组。所以一个会话中,应该包括控制进程(会话首进程),一个前台进程组和任意后台进程组。
1 $ proc1 | proc2 &
2 $ proc3 | proc4 | proc5
其中proc1与proc2属于同一个后台作业,proc3,proc4和proc5属于同一个前台作业,
bash本身属于一个单独的作业。这些作业的控制终端相同,它们同属于一个会话,当用户在控制终端输入特殊的控制键(如Ctrl+C,产生SIGINT,Ctrk+\,产生IGQUIT,Ctrl+Z,产生SIGTSTP),内核发送相应的信号给前台作业中的所有进程。

2、【进程组】、【作业】的区别

作业与进程组的区别:
如果作业中的某个进程又创建了子进程,则子进程不属于作业。但是它属于进程组
一旦作业运行结束,Shell就把bash提到前台,如果原来的前台进程还存在(如果这个子进程还没终止),它自动变为后台进程组。
我们都知道 shell管理的是作业,我们现在写一份代码,要是当前的作业中的进程创建子进程,但是父进程退出,我们来观察子进程  
#include<stdio.h>
#include<unistd.h>

//configure  procgroup and  jobs
int main()
{
	if(fork()  >0 )
	{
		printf("parent :%d \n",getpid());
		sleep(1);
		exit(0);//父进程sleep 1秒之后  退出 
	}
	else
	{
		while(1)//子进程无限循环
		{
			printf("child :%d \n",getpid());
			sleep(2);
		}
	}
	return  0;
}
现在我们来看看这个运行结果吧:
现在我们应该能够知道进程组 、与作业之间又有什么样的区别 了吧!!
要是之后带大家也遇到了这种情况的话,在这里我来向大家介绍一个命令,可以来防止后台进程向前台打消息。。。
stty tostop
使用了这种情况之后,再也不会有这种情况发生了。。。。

3、终端

终端的基本概念

在UNIX系统中,用户通过终端登录系统后得到一个Shell进程,这个终端成为Shell进程的控制终端 (Controlling Terminal),控制终端是保存在PCB中的信息,而我们知 道fork会复制PCB中的息,因此由Shell进程启动的其它进程的控制终端也是这个终端。默认情况 下(没有重定向),每个进程的标准输入、标准输出和标准错误输出都指向控制终端,进程从标准 输入读也就是读用户的键盘输入,进程往标准输出或标准错误输出写也就是输出到显示器上。此外 在控制终端输入一些特殊的控制键可以给前台进程发信号,例如Ctrl-C表 示SIGINT,Ctrl-\表示SIGQUIT。
每个进程都可以通过一个特殊的设备文件/dev/tty访问它的控制终 端。事实上每个终端设备都对应一个不同的设备文件,/dev/tty提供了一个通用的接口,一个进程 要访问它的控制终端既可以通过/dev/tty也可以通过该终端设备所对应的设备文件来访 问。ttyname函数可以由文件描述符查出对应的文件名,该文件描述符必须指向一个终端设备而不 能是任意文件。
下面我们通过实验看一下各种不同的终端所对应的设备文件名。


在Linux下,有着一切皆文件的说法,所以只要打开一个终端,在路径/dev/pts之下就会创建这么一个字符型文件。。。
另外要想实现两个终端之间的交流我们只需要将输出重定向到对应的终端文件中就可以实现终端之间的通信了。。。

使用echo输出一段话,并输出重定向到2号终端。对应的2号终端就会受到输出的字符

终端的登录过程

现在我们来看终端登录的过程:
1、系统启动时, init进程根据配置文件/etc/inittab确定需要打开哪些终端。例如配置文件中有这样一行:
1:2345:respawn:/sbin/getty 9600 tty1和/etc/passwd类似,每个字段用:号隔开。
开头的1是这一行配置的id,通常要和tty的后缀一致,配置tty2的那一行id就应该是2。
第二个字段2345表示运行级别2~5都执行这个配置。
最后一个字段/sbin/getty 9600 tty1是init进程要fork/exec的命令,打开终端/dev/tty1,波特率 是9600(波特率只对串口和Modem终端有意义),然后提示用户输入帐号。
中间的respawn字,表示输入错误,我们从终端退出登录后会再次提示输入帐号。
有些新的Linux发行版已经不⽤用/etc/inittab这个配置⽂文件了,例如Ubuntu用/etc/event.d⽬目录下的配置文件来配置init。
2、getty根据命令行参数打开终端设备作为它的控制终端,把文件描述符0、1、2都指向控制终 端,然后提示用户输入帐号。用户输入帐号之后,getty的任务就完成了,它再执行login程序:
execle("/bin/login", "login", "-p", username, NULL, envp);
3、login程序提示用户输入密码(输入密码期间关闭终端的回显),然后验证帐号密码的正确性。 如果密码不正确,login进程终止,init会重新fork/exec一个getty进程。如果密码正确,login程 序设置一些环境变量,设置当前工作目录为该⽤用户的主目录,然后执行Shell:
execl("/bin/bash", "-bash", NULL);
注意argv[0]参数的程序名前面加了一个-,这样bash就知道自己是作为登录Shell启动的,执行登录Shell的启动脚本。
从getty开始exec到login,再exec到bash,其实都是同一个进程,因此控制终 端没变,文件描述符0、1、2也仍然指向控制终端。由于fork会复制PCB信息,所以由Shell启动的 其它进程也都是如此。

线路规程 

内核中处理终端设备的模块包括硬件驱动程序和线路规程(Line Discipline)。
硬件驱动程序负责读写实际的硬件设备,比如从键盘读入字符和把字符输出到显示器,线路规程像 一个过滤器,对于某些特殊字符并不是让它直接通过,而是做特殊处理,比如在键盘上按下Ctrl- Z,Ctr -C对应的字符并不会被用户程序的read读到,而是被线路规程截获,解释成SIGTSTP以及SIGINT信号发给前台进程,通常会使该进程停止。线路规程应该过滤哪些字符和做哪些特殊处理是可以配置的。
用户从键盘输入字符的过程图如下图所示 :
                                                              

4、守护进程

Linux系统启动时会启动很多系统服务进程,这些系统服 务进程没有控制终端,不能直接和用户交互。其它进程都是在用户登录或运行程序时创建,在运行结束或用户注销时终止,但系统服务进程不受用户登录注销的影响,它们一直在运行着。这种进程有一个名称叫守护进程(Daemon)。

守护进程的特点

(1)、集成24小时运行 ;
(2)、不受用户登录、注销的影响;
(3)、一般守护进程是以d结尾;
(4)、自成一个会话;
(5)、自称进程组;
(6)、话首进程是孤儿进程 ;

守护进程的创建过程 

1、设置umask值为0(有的守护进程一般会创建日志文件)
2、调用fork函数,父进程退出;
3、子进程调用setsid函数;
4、更改进程的工作目录是 根目录(防止不小心删除当前文件夹,导致守护进程访问文件时出现错误)
5、关闭文件描述符;
6、忽略SIGCHLD信号 。

setsid函数、chdir函数

大家都看到了上面的守护进程的创建过程,都看到了上面的第三部调用setsid函数,但这个函数有什么用呢??、调了它有什么样的结果呢??
创建守护进程最关键的⼀一步是调用setsid函数创建⼀一个新的Session,并成为Session Leader。

 调用setsid函数成功的结果:

1、当前进程成为话首进程;
2、当前进程成为进程组的组长;
3、与当前的终端脱关联。
注意,调用这个函数之前,当前进程不允许是进程组的Leader,否则该函数返回-1。要保证当前进程不是进 程组的Leader也很容易,只要先fork再调用setsid就行了。fork创建的子进程和父进程在同一个进 程组中,进程组的Leader必然是该组的第一个进程,所以子进程不可能是该组的第一个进程,在子 进程中调用setsid就不会有问题了。

chdir函数

       #include <unistd.h>
	//修改当前进程的工作目录
       int chdir(const char *path);//path表示要设置的路径

怎么自己实现一个守护进程

实现代码:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<signal.h>
void mydaemon()
{
	//修改umask值
	umask(0);
	pid_t  pid;
	//fork创建子进程
	if(pid = fork()> 0)
	{
		//父进程
		exit(0);
	}
	//子进程调用函数 setsid
	setsid();
	//修改用户工作目录为根目录 
	chdir("/");
	//关闭文件描述符
	close(0);
	close(1);
	close(2);
	//忽略SIGCHLD信号
	signal(SIGCHLD,SIG_IGN);
}
int main()
{
	mydaemon();
	while(1);
	return  0;
}

实现结果:

系统调用接口daemon

上面是我们自己实现的一个守护进程,但是操作系统为我们提供了这么一个进口可以直接创建一个守护进程
       #include <unistd.h>

       int daemon(int nochdir, int noclose);//两个参数表示的是否要修改进程的工作目录,以及是否关闭文件描述符   0表示修改、关闭

函数测试:

测试结果:


测试成功,要是想不跟进程的工作目录,不关闭文件描述符
可以调用函数 
daemon(1,1);

为什么创建守护进程时有人fork两次? 

(1)调用一次fork的作用:
    第一次fork的作用是让shell认为这条命令已经终止,不用挂在终端输入上,还有就是为了后面的setsid服务,因为调用setsid函数的进程不能是进程组组长,如果不fork出子进程,则此时的父进程是进程组组长,就无法调用setsid。当子进程调用完setsid函数之后,子进程是会话组长也是进程组组长,并且脱离了控制终端,此时,不管控制终端如何操作,新的进程都不会收到一些信号使得进程退出。
(2)第二次fork的作用:
虽然当前关闭了和终端的联系,但是后期可能会误操作打开了终端。
只有会话首进程能打开终端设备,也就是再fork一次,再把父进程退出,再次fork的子进程作为守护进程继续运行,保证了该精灵进程不是对话期的首进程,
第二次不是必须的,是可选的,市面上有些开源项目也是fork一次





  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值