守护进程介绍

在介绍守护进程之前,需要了解一些概念,比如终端、进程组和会话。

1.终端和控制终端

在Linux系统中,每一个系统与用户进行交互的界面称为终端。从该终端开始运行的进程都会依附于这个终端,这个终端称为这些进程的控制终端。当控制终端被关闭时,相应的进程都会被自动关闭。 (比如,用户通过终端登录系统之后得到一个shell进程,这个终端成为shell进程的控制终端(Controlling Terminal)。在进程中,控制终端的信息保存在PCB中,通过fork函数创建的子进程会复制PCB中的信息,因此由shell进程启动的其他进程的控制终端也是这个终端。)。

在默认情况下(没有进行重定向),每个进程的标准输入、标准输出和标准错误输出都指向控制终端,进程从标准输入读 也就是读用户的键盘输入,进程进行标准输出和标准错误输出 也就是输出到显示器上。(标准输入、标准输出和标准错误的文件描述符分别是0、1、2)。

在控制终端中输入一些特殊的控制键可以给前台进程发信号,例如Ctrl C 会产生SIGINT信号,Ctrl \会产生SIGQUIT信号。

2.进程组

每个进程除了有一个进程ID之外,还属于一个进程组,进程组是一个或多个进程的集合。进行组由一个或多个共享同一进程组标识符( PGID )的进程组成。一个进程组拥有一个进程组首进程,该进程是创建该组的进程,其进程 ID 为该进程组的 ID ,新进程会继承其父进程所属的组 ID 。

通常,它们与同一作业相关联,可以接收来自同一终端的各种信号。每个进程组有一个唯一的进程组ID。每个进程组都可以有一个组长进程。组长进程的进程组ID等于其进程ID。组长进程可以创建一个进程组,创建该组中的进程,然后终止。

需要注意的是,只要在某个进程组中有一个进程存在,则该进程组就存在,这与其组长进程是否终止无关。进程组拥有一个生命周期,其开始的时间为首进程(组长进程)创建组的时刻,结束时间为最后一个成员进程退出组的时刻。一个进程可能会因为终止而退出进程组,也可能会因为加入了另外一个进程组而退出进程组。进程组首进程无需是最后一个离开进程组的成员。

3.作业

在Linux中,作业是一个用户向计算机系统提交的任务或工作。它可以包括要执行的程序、要处理的数据以及执行的方式。当用户在终端中输入命令或启动应用程序时,他们实际上是在向系统提交一个作业。

作业与进程的概念密切相关,但也有区别。用户提交一个作业后,系统会建立进程来执行这个作业。通常一个作业对应一个进程,此时进程与作业可以看作是同一个事物。但有时一个作业可能对应多个进程,例如“ls -s | sort -nr | more”这个作业就同时启动了3个进程,分别执行ls、sort和more程序,它们协作完成作业规定的任务。此时的作业与进程就是不同的事物了,作业对应的是这些进程的整体。

总之,作业是用户的观点,是用户向系统提交工作的实体单位。而进程是系统的观点,是系统完成工作时执行的实体单位。作业描述用户和操作系统之间的工作委托关系,而进程描述操作系统执行任务的过程。

4.会话

会话(Session)是一个或者多个进程组的集合。是一组进程组的集合。会话首进程是创建该新会话的进程,其进程ID会成为会话ID。新进程会继承其父进程的会话ID。

一个会话中的所有进程共享单个控制终端。控制终端会在会话首进程首次打开一个终端设备时被建立。一个终端最多可能会成为一个会话的控制终端。

在任一时刻,会话中的其中一个进程组会成为终端的前台进程组,其他进程组会成为后台进程组。只有前台进程组中的进程才能从控制终端中读取输入。当用户在控制终端中输入终端字符生成信号后,该信号会被发送到前台进程组中的所有成员。

当控制终端的连接建立起来之后,会话首进程会成为该终端的控制进程。

一个会话可以有一个控制终端,这通常是登录到其上的终端设备(在终端登录情况下)或伪终端设备(在网络登录情况下)。建立与控制终端连接的会话首进程被称为控制进程。一个会话中的几个进程组可被分为一个前台进程组以及一个或多个后台进程组。所以一个会话中,应该包括控制进程(会话首进程),一个前台进程组和任意多个后台进程组。

参考链接:Linux任务管理与守护进程 - 知乎

关于find和wc为什么父进程是bash(400),但是进程组ID确是658,而不是复制父进程的组ID。其实fork函数产生的子进程才是复制父进程的组ID。那通过这种命令产生的子进程,组ID是怎么确立的?在《Linux/UNIX系统编程手册》2.13 中提到,“shell执行的每个程序都会在一个新进程内发起”,这句话解释了为什么find、wc、sort、uniq这四个都是一个单独的进程。“除了Bourne shell以外,几乎所有主流shell都提供了一种交互式特性,名为任务控制。该特性允许用户同时执行并操纵多条命令或管道。在支持任务控制的shell中会将管道内所有进程置于一个新进程组或任务中。如果情况很简单,shell命令行只包含一条命令,那么就会创建一个只包含单个进程的新进程组。进程组中每个进程都具有相同的进程组标识符,其实就是进程组组长的ID”,这段话可以解释为什么两条命令产生了两个新的进程组,并且不同于bach进程组。这种shell命令创建子进程一定要和fork函数区分开来。

进程组、会话操作函数

pid_t getpgrp(void);  //获取当前进程的进程组ID

pid_t getpgid(pid_t pid);  //获取进程号为pid的进程组ID

int setpgid(pid_t pid, pid_t pgid);  //设置进程号为pid的进程组ID为pgid

pid_t getsid(pid_t pid);  //获取进程号为pid的会话ID

pid_t setsid(void);   //设置当前进程的会话ID

5.守护进程

5.1基本概念

守护进程也称精灵进程(Daemon),是运行在后台的一种特殊的后台服务进程,它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。

守护进程具备下列特征:

  • 生命周期很长,守护进程会在系统启动的时候被创建并一直运行直至系统被关闭。
  • 它在后台运行并且不拥有控制终端。没有控制终端确保了内核永远不会为守护进程自动生成任何控制信号以及终端相关的信号(如 SIGINT 、 SIGQUIT )。

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

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

我们可以用ps axj命令查看系统中的进程:

  • 参数a表示不仅列出当前用户的进程,也列出所有其他用户的进程。
  • 参数x表示不仅列出有控制终端的进程,也列出所有无控制终端的进程。
  • 参数j表示列出与作业控制相关的信息。

凡是TPGID一栏写着-1的都是没有控制终端的进程,也就是守护进程。

除此之外,在COMMAND一列用[ ]括起来的名字表示内核线程,这些线程在内核里创建,没有用户空间代码,因此没有程序文件名和命令行,通常采用以k开头的名字,表示Kernel。

个别说明:

  • udevd负责维护/dev目录下的设备文件。
  • acpid负责电源管理。
  • syslogd负责维护/var/log下的日志文件。

可以看出,守护进程通常采用以d结尾的名字,表示Daemon

5.2 守护进程的创建

  • 1:执行一个 fork(),之后父进程退出,子进程继续执行。
  • 2:子进程调用 setsid() 开启一个新会话。
  • 3:清除进程的 umask 以确保当守护进程创建文件和目录时拥有所需的权限。
  • 4:修改进程的当前工作目录,通常会改为根目录(/)。
  • 5:关闭守护进程从其父进程继承而来的所有打开着的文件描述符。在关闭了文件描述符 0 、 1 、 2 之后,守护进程通常会打开 /dev/null 并使用 dup2()使所有这些描述符指向这个设备。
  • 6:核心业务逻辑

说明:

fork()之后,为什么父进程要退出,而子进程要继续执行?

是为了能够调用setsid()开启一个正确的新会话。setsid()是怎么样执行的?

调用setsid创建新会话时,要求调用进程不能是进程组组长,但是当我们在命令行上启动多个进程协同完成某种任务时,其中第一个被创建出来的进程就是组长进程,因此我们需要fork创建子进程,让子进程调用setsid创建新会话并继续执行后续代码,而父进程我们直接让其退出即可。

假设一个进程它的pid为100,它是一个进程组的组长进程,那么pgid为100,如果用它来创建会话的话,那么新会话中的进程的pid为100,pgid为100,会话id也为100。这样就存在两个pid一样的进程。如果这个pid为100的进程有一个子进程,它的pid为101,用子进程调用setsid(),那么创建出来的新会话中,进程pid为101,pgid为101,会话id也为101。

【通过父进程(pid=100)调用setid的话,最终的结果是两个不同的会话里面有相同的进程组ID,还是说是两个相同的会话(100)里面有相同的进程组ID(100)?新的会话以创建会话的进程ID为会话ID,那新的会话ID也是100,和原来的会话ID100冲突了!】

创建出来的新会话,如果没有与它建立连接,那它就没有控制终端。

清除进程的 umask 以确保当守护进程创建文件和目录时拥有所需的权限

在Linux系统中,umask是一个函数用于设置进程的默认文件权限。当一个进程创建新文件或目录时,它将使用umask的当前值来确定新文件或目录的权限。

umask函数的作用是反向设置文件权限。例如,umask(022)意味着创建一个新文件时,文件的权限将被设置为默认权限减去022。

具体来说,umask的参数是一个八进制数字,表示要屏蔽的权限。例如,022表示屏蔽所有用户的写权限和组用户的写权限。

因此,当创建一个守护进程时,通常会调用umask函数来设置适当的默认文件权限。例如,如果守护进程需要写入其配置文件或日志文件,则可以调用umask(000)来确保所有用户都可以写入这些文件。如果守护进程不需要写入任何文件,则可以调用umask(077)来确保没有任何用户可以写入这些文件。

需要注意的是,umask的默认值通常为0077,这意味着新创建的文件或目录的权限将受到限制,只有文件的所有者可以访问它们。因此,在创建守护进程时,通常需要调用umask函数来覆盖默认值,以确保守护进程可以访问必要的文件和目录。

修改进程的当前工作目录,通常会改为根目录(/)

我们一般会将守护进程的工作目录设置为根目录,便于让守护进程以绝对路径的形式访问某种资源。(该操作不是必须的)

关闭守护进程从其父进程继承而来的所有打开着的文件描述符。在关闭了文件描述符 0 、 1 、 2 之后,守护进程通常会打开 /dev/null 并使用 dup2()使所有这些描述符指向这个设备。

守护进程不能直接和用户交互,也就是说守护进程已经与终端去关联了,因此一般我们会将守护进程的标准输入、标准输出以及标准错误都重定向到/dev/null,/dev/null是一个字符文件(设备),通常用于屏蔽/丢弃输入输出信息。(该操作不是必须的)

6.测试案例

写一个守护进程,每隔2秒获取一下系统时间,将这个时间写入到磁盘文件中。

/*
    写一个守护进程,每隔2s获取一下系统时间,将这个时间写入到磁盘文件中。
*/

#include <stdio.h>

#include <sys/stat.h>

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

#include <fcntl.h>  //open

#include <sys/time.h>  //setitimer

#include <signal.h>  //sigaction
#include <time.h> //time

#include <stdlib.h>  //exit
#include <string.h>

void work(int num) {
    // 捕捉到信号之后,获取系统时间,写入磁盘文件
    time_t tm = time(NULL);  //NULL返回的是秒数,从1970-01-01 000000
    struct tm * loc = localtime(&tm); //转换tm man 3 localtime
    // char buf[1024];

    // sprintf(buf, "%d-%d-%d %d:%d:%d\n",loc->tm_year,loc->tm_mon
    // ,loc->tm_mday, loc->tm_hour, loc->tm_min, loc->tm_sec);

    // printf("%s\n", buf);  //守护进程已经脱离 控制终端了;没有关闭、重定向文件描述符时,Ctrl c 结束不了,用kill -9 ./daemon
    // 关闭之后就再终端输出不了了。

    char * str = asctime(loc);
    int fd = open("time.txt", O_RDWR | O_CREAT | O_APPEND, 0664); //追加数据
    write(fd ,str, strlen(str));
    close(fd);
}

int main() {

    // 1.创建子进程,退出父进程
    pid_t pid = fork();

    if(pid > 0) {
        exit(0);  //父进程退出,子进程继续向下执行,
    }

    // 2.将子进程重新创建一个会话,返回值为pid_t类型,是会话ID
    // 新的会话会脱离原来的控制终端,这样就不能通过在控制终端杀死这个进程
    setsid();

    // 3.设置掩码
    umask(0);

    // 4.更改工作目录(“/”)
    chdir("/home/liang/");

    // 5. 关闭、重定向文件描述符,重定向到"/dev/null"
    int fd = open("/dev/null", O_RDWR);  
    dup2(fd, STDIN_FILENO);
    dup2(fd, STDOUT_FILENO);
    dup2(fd, STDERR_FILENO);

    // 6.业务逻辑

    // 捕捉定时信号SIGALRM
    struct sigaction act;
    act.sa_flags = 0;
    act.sa_handler = work;
    sigemptyset(&act.sa_mask);
    sigaction(SIGALRM, &act, NULL);

    struct itimerval val;
    val.it_value.tv_sec = 2;
    val.it_value.tv_usec = 0;
    val.it_interval.tv_sec = 2;
    val.it_interval.tv_usec = 0;

    // 创建定时器
    setitimer(ITIMER_REAL, &val, NULL);

    // 不让进程结束
    while(1) {
        sleep(10);
    }

    return 0;
}

编译运行这个代码之后,并不会在终端输入任何信息。这个时候会在第4步更改的工作目录下生成一个time.txt文件,通过vim打开,并且在其中输入 :e 可以刷新这个文件,可以看到写入的内容。可以看到是每隔两秒写入一次。

最后,通过ps aux命令查看 ./daemonpid,通过kill -9 pid将它结束,否则他会一直向文件中写入。

在这里可能会有一个疑问:不是说守护进程不能被控制终端发送信号吗?怎么还能通过kill结束啊?

kill是发送信号的shell命令,可以给指定进程发送指定信号。守护进程不能被控制终端的Ctrl+C或者Ctrl+\结束,因为它没有自己的控制终端,是新的会话没有绑定控制终端,但它还是一个进程,可以接收信号。

信号其实都是内核发送的,守护进程只是说它没有控制终端,但是通过kill会让内核直接发送一个终止信号,它是可以收到这个信号的,例子中的定时器信号不也是正常收到了。

其他:

守护进程可以算作特殊的孤儿进程。守护进程脱离终端会话的影响运行在后台,它从被执行开始运转直到整个系统关闭时才退出。虽然守护进程脱离了终端,但它并不会像孤儿进程那样成为没有父进程的进程。相反,守护进程通常由系统初始化进程(如init进程)收养,成为其子进程。因此,守护进程虽然脱离了终端,但并不算是真正的孤儿进程。

以下内容是抄的Linux任务管理与守护进程 - 知乎

为了方便复习。

会话(Session)是一个或多个进程组的集合。

一个会话可以有一个控制终端,这通常是登录到其上的终端设备(在终端登录情况下)或伪终端设备(在网络登录情况下)。建立与控制终端连接的会话首进程被称为控制进程。一个会话中的几个进程组可被分为一个前台进程组以及一个或多个后台进程组。所以一个会话中,应该包括控制进程(会话首进程),一个前台进程组和任意多个后台进程组。

例如,下面我们用同一个死循环代码生成了5个可执行程序。

我们将mytest1和mytest2放到后台运行,将mytest3、mytest4和mytest5放到前台运行。

其中mytest1与mytest2属于同一个后台进程组,mytest3、mytest4和mytest5属于同一个前台进程组,而Shell本身属于一个单独的进程组。

这些进程组的控制终端相同,它们同属于一个会话,当用户在控制终端输入特殊的控制键(如Ctrl+C产生SIGINT,Ctrl+\产生SIGQUIT,Ctrl+Z产生SIGTSTP),内核就会发送相应的信号给前台进程组中的所有进程。

前台进程&后台进程

直接运行某一可执行程序,例如./可执行程序,此时默认将程序放到前台运行,在前台运行的进程的状态后有一个+号,例如R+。

运行可执行程序时在后面加上&,可以指定将程序放到后台运行,例如./可执行程序 &,在后台运行的进程的状态后没有+号。

我们将程序放到后台运行时会发现多了一行提示信息,例如上述的:

[1] 16437

其中[1]是作业的编号,如果同时运行多个作业可以用这个编号进行区分,16437是该作业中某个进程的id(一个作业可以由多个进程组成)。

我们可以用该可执行程序同时创建四个进程放到后台运行:

此时我们就可以将它们分别叫做当前终端下的1号作业、2号作业、3号作业和4号作业。

jobs、fg、bg

使用jobs命令,可以查看当前会话当中有哪些作业。

使用fg命令(foreground),可以将某个作业提至前台运行,如果该作业正在后台运行则直接提至前台运行,如果该作业处于停止状态,则给进程组的每个进程发SIGCONT信号使它继续运行并提至前台。

例如,使用fg 1命令将1号作业提到前台运行。

由于1号作业被提至前台运行,所以其运行状态也由R变成了R+。

需要注意的是,前台进程只能有一个,当一个进程变成前台进程后,bash会自动变为后台进程,此时bash就无法进行命令行解释了。

例如,我们将1号作业提至前台运行后,bash进程的状态后面的+号就没有了,也就意味着bash自动由前台进程变为了后台进程。

将一个前台进程放到后台运行可以使用Ctrl+Z,但使用Ctrl+Z后该进程就会处于停止状态(Stopped)。

使用bg命令,可以让某个停止的作业在后台继续运行(Running),本质就是给该作业的进程组的每个进程发SIGCONT信号。

例如,使用bg 1命令让1号作业在后台继续运行。

ps命令查看指定的选项

使用ps命令时携带-o选项,可以查看指定的信息。

当我们用Xshell或是终端登录时,本质都是先创建一个bash进程,整体称之为一个会话(所有的命令行的进程都是bash的子进程),所有的命令行启动的任务都是在对应的会话内运行的。

实际我们每一次登录的过程都是新建会话的过程,同一个会话中的所有进程的SESS是相同的。

说明一下:ps命令是一个系统级的命令,该命令能查看所有进程的信息,例如ps axj,只不过-o选项只查看当前会话的进程信息。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值