Linux系统编程之守护进程

本章节介绍Linux系统中特殊的进程-守护进程(Daemon),其类似于安卓中后台服务的存在,生存周期足够长,能够长期运行于后台,周期的执行某些任务或事件。其次,在介绍守护进程模型之前,我们还需要了解进程间几个重要的概念,例如控制终端、进程组和会话等。

守护进程

Daemon(精灵)进程,是Linux中的后台服务进程,生存期较长的进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。

它不需要用户输入就能运行而且提供某种服务,不是对整个系统就是对某个用户程序提供服务。Linux系统的大多数服务器就是通过守护进程实现的。常见的守护进程包括系统日志进程syslogd、 web服务器httpd、邮件服务器sendmail和数据库服务器mysqld等。

守护进程一般在系统启动时开始运行,除非强行终止,否则直到系统关机都保持运行。守护进程经常以超级用户(root)权限运行,因为它们要使用特殊的端口(1-1024)或访问某些特殊的资源。

守护进程的父进程是init进程,因为它真正的父进程在fork出子进程后就先于子进程exit退出了,所以它是一个由init继承的孤儿进程。守护进程是非交互式程序,没有控制终端,所以任何输出,无论是向标准输出设备stdout还是标准出错设备stderr的输出都需要特殊处理。

守护进程的名称通常以d结尾,比如sshd、xinetd、crond等。

对于守护进程的创建过程,需要了解进程中几个重要概念,控制终端、进程组和会话。

控制终端

在UNIX系统中,用户通过终端登录系统后得到一个Shell进程,这个终端成为Shell进程的控制终端(Controlling Terminal)。

控制终端是保存在PCB中的信息,而我们知道fork会复制PCB中的信息,因此由Shell进程启动的其它进程的控制终端也是这个终端。

默认情况下(没有重定向),每个进程的标准输入、标准输出和标准错误输出都指向控制终端,进程从标准输入读也就是读用户的键盘输入,进程往标准输出或标准错误输 出写也就是输出到显示器上。

文件与I/O中讲过,每个进程都可以通过一个特殊的设备文件/dev/tty访问它的控制终端。事实上每个终端设备都对应一个不同的设备文件,/dev/tty提供了一个通用的接口,一个进程要访问它的控制终端既可以通过/dev/tty也可以通过该终端设备所对应的设备文件来访问。ttyname函数可以由文件描述符查出对应的文件名,该文件描述符必须指向一个终端设备而不能是任意文件。

终端数据执行流程

数据流在终端中执行流程如下图:

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

网络终端

虚拟终端或串口终端的数目是有限的,虚拟终端(字符控制终端)一般就是/dev/tty1 ∼ /dev/tty6六个,串口终端的数目也不超过串口的数目。然而网络终端或图形终端窗口的数目却是不受限制的,这是通过伪终端(Pseudo TTY)实现的。一套伪终端由一个主设备(PTY Master)和一个从设备(PTY Slave)组成。主设备在概念上相当于键盘和显示器,只不过它不是真正的硬件而是一个内核模块,操作它的也不是用户而是另外一个进程。从设备和上面介绍的/dev/tty1这样的终端设备模块类似,只不过它的底层驱动程序不是访问硬件而是访问主设备。网络终端或图形终端窗口的Shell进程以及它启动的其它进程都会认为自己的控制终端是伪终端从设备,例如/dev/pts/0、/dev/pts/1等。下面以telnet为例说明网络登录和使用伪终端的过程。

如果telnet客户端和服务器之间的网络延迟较大,我们会观察到按下一个键之后要过几秒钟才能回显到屏幕上。这说明我们每按一个键telnet客户端都会立刻把该字符发送给服务器,然后这个字符经过伪终端主设备和从设备之后被Shell进程读取,同时回显到伪终端从设备,回显的字符再经过伪终端主设备、telnetd服务器和网络发回给telnet客户端,显示给用户看。也许你会觉得吃惊,但真的是这样:每按一个键都要在网络上走个来回!

其网络终端中数据流导向如下图所示:

进程组

进程组: 一个或多个进程的集合,进程组ID是一个正整数。

获取进程组id

用来获得当前进程进程组ID的函数

pid_t getpgid(pid_t pid)
pid_t getpgrp(void)
获取进程组实例

获取父子进程的进程组

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>

int main(void){

    pid_t pid;
    if((pid = fork()) < 0){
        perror("fork");
        exit(1);
    }else if(pid == 0){

        printf("child process PID is %d\n", getpid());
        printf("Group ID is %d\n", getpgrp());
        printf("Group ID is %d\n", getpgid(0));
        printf("Group ID is %d\n", getpgid(getpid()));
        exit(0);

    }

    sleep(3);
    printf("parent process PID is %d\n",getpid());
    printf("Group ID is %d\n", getpgrp());

    return 0;
}

注意:

  1. 组长进程标识:其进程组ID == 其进程ID
  2. 组长进程可以创建一个进程组,创建该进程组中的进程,然后终止,只要进程组中有一个进程存在,进程组就存在,与组长进程是否终止无关
  3. 进程组生存期:进程组创建到最后一个进程离开(终止或转移到另一个进程组)
  4. 一个进程可以为自己或子进程设置进程组ID
创建或设置进程组

setpgid()加入一个现有的进程组或创建一个新进程组,如改变父子进程为新的组

int setpgit(pid_t pid, pid_t pgid)
如改变子进程为新的组,应在fork后,exec之前
非root进程只能改变自己创建的子进程,或者有权限操作的进程
设置进程组实例
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>

int main(void){

    pid_t pid;
    if((pid = fork()) < 0){
        perror("fork");
        exit(1);
    }else if(pid == 0){

        printf("child process PID is %d\n", getpid());
        // 返回当前的组id
        printf("Group ID is %d\n", getpgid(0));

        sleep(5);
        // 返回改变之后的组id
        printf("Group ID of child is changed to %d\n", getpgid(0)); 
        exit(0);
    }

    sleep(1);
    printf("parent process PID is %d\n",getpid());
    printf("Group ID is %d\n", getpgrp());

    setpgid(pid, pid);
    sleep(5);

    printf("parent process PID is %d\n",getpid());
    printf("parent of parent process PID is %d\n",getppid());
    printf("Group ID of parent is %d\n", getpgid(0));

    // 改变父进程的组id为父进程的父进程
    setpgid(getpid(), getpgid());
    printf("Group ID of parent is changed to %d\n", getpgid(0));

    return 0;
}

会话session

其实叫做会话期(session),它包括了期间所有的进程组,一般一个会话期开始于用户login,一般login的是shell终端,所以shell终端又是此次会话期的首进程,会话一般结束于logout。对于非进程组长,它可以调用setsid()创建一个新的会话。

pid_t setsid(void)

注意:

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

获取会话id

pid_t getsid(pid_t pid)

pid 为 0 表示察看当前进程session ID

ps ajx命令查看系统中的进程。

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

组长进程不能成为新会话首进程,新会话首进程必定会成为组长进程。

会话实例
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>

int main(void){

    pid_t pid;
    if((pid = fork()) < 0){
        perror("fork");
        exit(1);
    }else if(pid == 0){

        printf("child process PID is %d\n", getpid());
        printf("Group ID is %d\n", getpgid(0));
        printf("Session ID of child is %d\n", getsgid(0));

        sleep(10);
        // 子进程非组长进程,故其成为新会话首进程,且为组长进程。
        // 该进程组id极为会话进程。
        setsid();

        printf("Changed: \n");
        printf("parent process PID is %d\n",getpid());
        printf("Group ID is %d\n", getpgrp());

        printf("Session ID of child is %d\n",getsid(0));
        sleep(20);

        exit(0);
    }
    return 0;
}

守护进程模型

守护进程的创建有一定的规律,如下总结:

  1. 创建子进程,父进程退出
    任务工作于子进程中,形式上脱离控制终端

  2. 子进程创建新会话,独立出来
    setsid()

  3. 改变当前工作目录为根目录(也可以其他路径),防止占用可卸载的文件系统
    chdir()

  4. 重设文件权限掩码值,防止继承的文件创建屏蔽字拒绝某些权限,增减守护进程的灵活性
    umask()

  5. 关闭文件描述符,减少系统资源浪费
    由于子进程继承父进程的文件描述符,关闭他们

  6. 开始执行守护进程的核心工作
    while()死循环处理

  7. 守护进程退出处理(非必要)
    释放必要的资源

守护进程代码模型

通过以上模型介绍,我们可以代码实现Daemon守护进程的模型。

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>

/*
Daemon 精灵 守护进程编程实现步骤
*/
void daemonize(void){
    pid_t pid;
    //1. 创建子进程,父进程退出
    if((pid = fork()) < 0){
        perror("fork error!");
        exit(1);
    }
    else if(pid !=0){
        exit(0);
    }
    // 子进程,创建新会话,独立终端,父进程成为init
    setsid();
    // 更改当期那路径为根路径,或者其他路径,避免占用可卸载文件系统
    if(chdir("/") < 0){
        perror("chdir error!");
        exit(1);
    }
    // 设置umask,防止继承文件系统创建屏蔽字拒绝某些权限
    umask(0);
    // 关闭文件描述符,较少系统资源浪费
    close(0);
    open("/dev/null", O_RDWR);
    dup2(0,1);
    dup2(0,2);

}

int main(void){

    daemonize();
    while(1){
        /* do something here!!!*/

    }
}

运行这个程序,它变成一个守护进程,不再和当前终端关联。用ps命令看不到,必须运行带x参数的ps命令才能看到。

另外还可以看到,用户关闭终端窗口或注销也不会影响守护进程的运行。

守护进程参考实例

创建一个守护进程,定时的记录时间到文件 /tmp/daemon.log 中。

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


#define FILEPATH "/tmp/daemon.log"
#define NEWLINE "\n"

/*
Daemon 精灵 守护进程编程实现步骤
*/
void daemonize(void){

    pid_t pid;
    //1. 创建子进程,父进程退出
    if((pid = fork()) < 0){
        perror("fork error!");
        exit(1);
    }else if(pid !=0){
        exit(0);
    }

    // 子进程,创建新会话,独立终端,父进程成为init
    setsid();

    // 更改当期那路径为根路径,或者其他路径,避免占用可卸载文件系统
    if(chdir("/") < 0){
        perror("chdir error!");
        exit(1);
    }

    // 设置umask,防止继承文件系统创建屏蔽字拒绝某些权限
    umask(0);

    // 关闭文件描述符,较少系统资源浪费
    close(0);
    open("/dev/null", O_RDWR);
    dup2(0,1);
    dup2(0,2);

}

int testFile(){
    return access(FILEPATH,F_OK);
}

int createFile(){
    int fd;
    if((fd = open(FILEPATH, O_CREAT, 0644)) < 0){
        perror("open file error!");
        exit(1);
    }
    close(fd);
    return 0;
}

int getTime(char *buf){

    time_t t;

    if(time(&t) != -1)
        return 0;
    else 
        retrun -1;
}

int saveTime2file(char *buf){
    int fd;
    if((fd = open(FILEPATH,O_RDWR | O_APPEND)) > 0){
        // strcat(buf,NEWLINE);
        write(fd,buf,strlen(buf));
        close(fd);
        printf("write time to file %s\n",buf);
    }
}

int main(void){

    char buf[1024] = {0};

    // 文件不存在,创建
    if(testFile() == -1){
        createFile();
    }

    daemonize();
    while(1){
        /* do something here!!!*/

        // 每10s记录一次
        sleep(10);

        if(getTime(buf)==0){
            saveTime2file(buf);
            memset(buf,0,1024);
        }
    }

}

总结

本博文主要介绍了Linux下进程组、会话以及守护进程方面的内容,对于守护进程,其有一套约定俗成的编程模版,不必死记,只需要了解起工作原理,脱离终端,孤儿进程,新会话,长期运行等特性,其特殊性也就决定了他和其他进程的不同。在以后用到的时候,回过头来看一下模版,即可迅速的实现Daemon守护进程的。

参考链接

邢文鹏Linux教学资料

坚持原创技术分享,您的支持将鼓励我继续创作!