浅析Linux守护进程、守护进程的创建步骤

守护进程(daemon)是一类在后台运行的特殊进程,其生存期较长,独立于控制终端、执行特定的系统任务。很多守护进程在系统引导的时候启动,并且一直运行直到系统关闭。另一些只在需要的时候才启动,完成任务后就自动结束。
我们先来学习几个新概念:会话、会话首进程、进程组、组长进程。

进程组 / 组长进程 / 会话 / 会话首进程
进程组
进程组:每个进程都属于一个进程组,进程组中可以包含一个或多个进程。进程组中有一个组长进程(第一个进程),组长的进程 ID 是进程组 ID(PGID)。


当父进程,创建子进程的时候,默认子进程与父进程属于同一进程组。


组长进程可以创建一个进程组,创建该进程组中的进程,然后终止。只要进程组中有一个进程存在,进程组就存在,与组长进程是否终止无关。


进程组生存期:进程组创建到最后一个进程离开(终止或转移到另一个进程组)。

操作函数:
1、getpgrp函数:获取当前进程的进程组ID
pid_t getpgrp(void);  // 总是返回调用者的进程组ID
1
2、getpgid函数:获取指定进程的进程组ID
pid_t getpgid(pid_t pid);  // 成功:0;失败:-1,设置errno
1
如果pid = 0,那么该函数作用和getpgrp一样。
3、setpgid函数:改变进程默认所属的进程组。通常可用来加入一个现有的进程组或创建一个新进程组。
/*
** 将参数1对应的进程,加入参数2对应的进程组中。
** 成功:0;失败:-1,设置errno
**/
int setpgid(pid_t pid, pid_t pgid); 
12345
注意:

如改变子进程为新的组,应在fork后,在exec前。
权级问题。非root进程只能改变自己创建的子进程,或有权限操作的进程。


会话
会话:多个进程组构成一个会话,建立会话的进程是会话的领导进程,即会话首进程,该进程 ID 为会话的 SID。
会话中的每个进程组称为一个作业。会话可以有一个进程组称为会话的前台作业,其它进程组为后台作业。
一个会话可以有一个控制终端,当控制终端有输入和输出时都会传递给前台进程组,比如Ctrl + Z。会话的意义在于能将多个作业通过一个终端控制,一个前台操作,其它后台运行。

创建会话的注意事项:

调用进程不能是进程组组长,该进程变成新会话首进程(session header)
该进程成为一个新进程组的组长进程。
需有root权限(ubuntu不需要)
新会话丢弃原有的控制终端,该会话没有控制终端
若调用进程是组长进程,则出错返回
建立新会话时,先调用fork,父进程终止,子进程调用setsid

扫描二维码关注公众号,回复: 10855866 查看本文章


操作函数:
1、getsid函数:获取进程所属的会话ID
//成功:返回调用进程的会话ID;失败:-1,设置errno
pid_t getsid(pid_t pid); 
12
pid为0表示查看当前进程session ID

如果调用进程非组长进程,那么就能创建一个新会话:

该进程变成新会话的首进程
该进程成为一个新进程组的组长进程
该进程没有控制终端,如果之前有,则会被中断(会话过程对控制终端的独占性)

也就是说:组长进程不能成为新会话首进程,新会话首进程必定成为组长进程。

ps ajx命令查看系统中的进程。参数a表示不仅列当前用户的进程,也列出所有其他用户的进程,参数x表示不仅列有控制终端的进程,也列出所有无控制终端的进程,参数j表示列出与作业控制相关的信息。

2、setsid函数:创建一个会话,并以自己的ID设置进程组ID,同时也是新会话的ID。
pid_t setsid(void); //成功:返回调用进程的会话ID;失败:-1,设置errno
1
调用了setsid函数的进程,既是新会话首进程,也是新的组长进程。


简单总结如下:
进程组和会话在进程之间形成了两级的层次:进程组是一组相关进程的集合,会话是一组相关进程组的集合。


程序示例
我们接下来通过上面介绍的函数来查看一下一个进程及其子进程的相关信息
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
    printf("pid = %d, ppid = %d, pgid = %d, sid = %d \n",
     getpid(), getppid(), getpgrp(), getsid(0));
}
123456789
程序运行结果如下:

我们可以看到,当启动一个新进程时,是由其父进程(终端bash)创建,并且该新进程所属的会话ID就是bash的PID,因为它属于当前bash的创建的会话,那么bash为该会话的会话首进程。
另外我们看到,新进程的pgid即它的进程组ID就为它自身的pid,即当只启动一个进程时,进程组只有一个成员,就是它自身,因此该新进程也是新创建进程组的进程组长。
那么接下来我们在程序中fork()一个子进程,看一下结果:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
    fork();
    printf("pid = %d, ppid = %d, pgid = %d, sid = %d \n",
     getpid(), getppid(), getpgrp(), getsid(0));
}
12345678910

我们可以看到,子进程的ppid就是创建它的父进程的id,子进程所属的进程组id就为组长进程的pid,即它父进程的pid。它们的sid都为当前终端的pid。
那么还有一种情况如下图所示:

我们可以看到子进程的ppid为1,即说明了父进程已结束退出,那么有pid为1的init进程接管,但是我们看到其进程组id仍为其父进程(进程组组长)的pid,这也说明了我们前面所讲的:
只要进程组中有一个进程存在,进程组就存在,与组长进程是否终止无关。

守护进程的创建步骤
1、fork()创建子进程,父进程exit()退出
进程 fork 后,父进程退出。这么做的原因有以下两点:

如果守护进程是通过 Shell 启动,父进程退出,Shell 就会认为任务执行完毕,之后的所有工作都在子进程中完成,而用户在Shell终端里则可以执行其他命令,从而在形式上做到了与控制终端的脱离,在后台工作。这时子进程由 init 收养
子进程继承父进程的进程组 ID,保证了子进程不是进程组组长,因为下面将调用setsid(),它要求必须不是进程组长。


2、在子进程调用setsid()创建新会话
在调用了 fork() 函数后,子进程全盘拷贝了父进程的会话期、进程组、控制终端等,虽然父进程退出了,但会话期、进程组、控制终端等并没有改变。这还不是真正意义上的独立开来,而 setsid()函数,使子进程完全独立出来,脱离控制。
setsid()创建一个新会话,调用进程担任新会话的首进程,其作用有:

使当前进程脱离原会话的控制
使当前进程脱离原进程组的控制
使当前进程脱离原控制终端的控制

这样,当前进程才能实现真正意义上完全独立出来,摆脱其他进程的控制。

3、再次 fork() 一个子进程,父进程exit()退出
现在,进程已经成为无终端的会话组长(会话首进程),但它可以重新申请打开一个控制终端,可以通过 fork() 一个子进程,该子进程不是会话首进程,该进程将不能重新打开控制终端。退出父进程。
也就是说通过再次创建子进程结束当前进程,使进程不再是会话首进程来禁止进程重新打开控制终端。

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

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

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

7、守护进程退出处理
当用户需要外部停止守护进程运行时,往往会使用 kill 命令停止该守护进程。所以,守护进程中需要编码来实现 kill 发出的signal信号处理,达到进程的正常退出
创建守护进程程序示例
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <unistd.h>
#include <time.h>
#include <sys/stat.h>

int main()
{
    /* 1、创建子进程,父进程退出 */
    if (fork() != 0)
    {
        exit(0);
    }

    /* 2、setsid()创建会话 */
    setsid();
    
    /* 3、再次fork,父进程退出,即使新进程不再是会话首进程 */
    if (fork() != 0)
    {
        exit(0);
    }
    
    /* 4、让根目录成为子进程的工作目录 */
    chdir("/");

    /* 5、清空掩码,大大增强该守护进程的灵活性 */
    umask(0);

    /* 6、清空所有文件描述符,让其不占用系统资源 */
    int maxfd = getdtablesize();
    int i = 0;
    for (; i < maxfd; ++i)
    {
        close(i);
    }

    /* 每隔5s将当前时间写入日志文件 */
    while (1)
    {
        FILE* fp = fopen("/home/zy/Learn/a.log", "a+");
        if (fp == NULL)
        {
            break;
        }

        time_t tv;
        time(&tv);
        fprintf(fp, "Time is %s", asctime(localtime(&tv)));
        fclose(fp);
        sleep(5);
    }

    exit(0);
}
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556
运行上述程序,在终端中不会有任何输出,因为它已经变为了守护进程长期在系统中存在,但我们可以通过ps -ef命令查看到。

那么我们打开 a.log 日志文件,通过tail -f命令值实时刷新显示末尾数据。
我们可以看到,每隔5s文件中就多了一行时间信息。证明我们所创建的守护进程一直在后台工作。

我们可以通过kill命令结束守护进程。

发布了9 篇原创文章 · 获赞 5 · 访问量 2206

猜你喜欢

转载自blog.csdn.net/CSDN_liu_sir/article/details/103464744