linux守护进程、SIGHUP与nohup详解

简介: 前端时间帮忙定位个问题。docker容器故障恢复后,其中的keepalived进程始终无法启动,也看不到Keepalived的日志。 strace 查看系统调用之后,发现了原因所在 1 socket(PF_LOCAL, SOCK_DGRAM|SOCK_CLOEXEC, 0) = 3 ...

前端时间帮忙定位个问题。docker容器故障恢复后,其中的keepalived进程始终无法启动,也看不到Keepalived的日志。

strace 查看系统调用之后,发现了原因所在

 1 socket(PF_LOCAL, SOCK_DGRAM|SOCK_CLOEXEC, 0) = 3
 2 connect(3, {sa_family=AF_LOCAL, sun_path="/dev/log"}, 110) = -1 ENOENT (No such file or directory)
 3 close(3)                                = 0
 4 open("/var/run/keepalived.pid", O_RDONLY) = 3
 5 fstat(3, {st_mode=S_IFREG|0644, st_size=1, ...}) = 0
 6 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fe85ab1b000
 7 read(3, "\n", 4096)                     = 1
 8 read(3, "", 4096)                       = 0
 9 close(3)                                = 0
10 munmap(0x7fe85ab1b000, 4096)            = 0
11 kill(0, SIG_0)                          = 0
12 socket(PF_LOCAL, SOCK_DGRAM|SOCK_CLOEXEC, 0) = 3
13 connect(3, {sa_family=AF_LOCAL, sun_path="/dev/log"}, 110) = -1 ENOENT (No such file or directory)
14 close(3)                                = 0
15 exit_group(0)                           = ?
16 +++ exited with 0 +++

这就是一个典型的linux单例守护进程启动做的事情:检测进程是否已经存在(判断记录文件是否存在以及对应pid进程是否还在执行),并通过syslog套接字文件向syslog服务端发送日志。

很显然,Keepalived无法正常启动是故障宕机时,相应的pid文件没有清理干净,如果仅仅如此,Keepalived应该可以启动,一般守护进程启动都会覆盖残留的锁文件,问题关键在read(3, "\n", 4096) : 锁文件Keepalived.pid是空的!! 而kil 向进程0 发送信号0,执行成功,则Keepalived认为已经有Keepalived进程正在运行。所以问题出在锁文件存在且内容为"\n",故依次清理 keepalived.pid vrrp.pid checkers.pid文件后,Keepalived正常启动。至于定位为何锁文件内容为"\n",那是后话了。

经此一事,笔者想写一写Linux 守护进程

守护进程特点与相关概念

并非运行时间长的程序即是守护进程,笔者并未找到守护进程最标准的定义,但 守护进程都有下面几个特点:
1、没有控制终端,终端名设置为?号:也就意味着没有 stdin 0 、stdout 1、stderr 2
2、父进程不是用户创建的进程,init进程或者systemd(pid=1)以及用户人为启动的用户层进程一般以pid=1的进程为父进程,而以kthreadd内核进程创建的守护进程以kthreadd为父进程
3、守护进程一般是会话首进程、组长进程。
4、工作目录为 \ (根),主要是为了防止占用磁盘导致无法卸载磁盘
 
这里涉及到一些概念,是unix为了更好管理进程间的关系提出的概念和方法,稍做说明下

控制终端

通过网络登录或者终端登录建立的会话,会分配唯一一个tty终端或者pts伪终端(网络登录),实际上它们都是虚拟的,以文件的形式建立在/dev目录,而并非实际的物理终端。

在终端中按下的特殊按键:中断键(ctrl+c)、退出键(ctrl+\)、终端挂起键(ctrl + z)会发送给当前终端连接的会话中的前台进程组中的所有进程

在网络登录程序中,登录认证守护程序 fork 一个进程处理连接,并以ptys_open 函数打开一个伪终端设备(文件)获得文件句柄,并将此句柄复制到子进程中作为标准输入、标准输出、标准错误,所以位于此控制终端进程下的所有子进程将可以持有终端

与控制终端相连的会话首进程也叫控制进程

进程组

进程组是一个或者多个进程的集合。一般由某个程序fork出一个家族来构成进程组,或者由管道命令建立作业构成进程组。

同一个进程组中的所有进程接收来自同一终端的信号。

进程组中的第一个进程作为进程组的首长,进程组id取首长进程的id。在各个进程中,通过函数getpgrp获取其所属进程组id

孤儿进程组

一个进程的父进程终止后,进程变成了孤儿进程,将被pid为1的进程(init进程或者systemd)收养。

而对孤儿进程组的定义是:进程组中每个进程的父进程要么在组中,也么不在该组所在会话中。

换言之,如果一个进程组中进程的父进程如果是组中成员,或者是init、systemd进程的话,这个进程组就一定是孤儿进程组。这样的进程组是很常见的,下图就是一个简单且典型的孤儿进程组

很显然,只有一个进程的进程组,并且是孤儿进程的话,进程组将变成孤儿进程组(哪怕它只有一个进程)。

典型的例子是一个父进程fork子进程之后,父进程立即退出,这样子进程所在的进程组将变为孤儿进程组。这样的孤儿进程组中的每个停止(Stopped)状态的每个进程都将收到挂断信号(SIGHUP),然后又立即收到继续信号(SIGCONT)。所以fork子进程之后,退出父进程,如果子进程还需要继续运行,则需要处理挂断信号,否则进程对挂断信号的默认处理将是退出。

此时的孤儿进程组并没有变为后台进程,一些博客将后台进程说成是孤儿进程组的一个特点,笔者认为是不正确的,在他们的示例中,孤儿进程组变为后台进程的原因是:父进程退出后,子进程在运行时向自身发送了SIGTSTP信号,这就像在终端按下终端挂起键(ctrl+z)一样,暂时断开了进程与控制终端的连接,自然变成了后台进程。

所以这是将进程转到后台运行的一个手段,但并不能创建守护进程,后面会将怎么创建守护进程。

会话

表示一个或多个进程组的集合,在有控制终端的会话中,可以被分为一个前台进程组和多个后台进程组。

取首进程id为会话id。

函数getsid用来获取会话id,而函数setsid用来新建一个会话,只有非首长进程(非进程组的组长)才能调用setsid新建会话。实际上setsid做了三件事

  • 设置当前进程的会话id为该进程id,此进程成为会话首进程。
  • 将调用setsid的进程设置为一个新进程组的首长进程。
  • 断开已连接的控制终端

这三步是创建守护进程的重要步骤。

下图结合了笔者对这些概念的理解,做出的判断

 

守护进程的创建

创建守护进程有标准的步骤:
  1. 如果是单例守护进程,结合锁文件和kill函数检测是否有进程已经运行
  2. umask取消进程本身的文件掩码设置,也就是设置Linux文件权限,一般设置为000,这是为了防止子进程创建创建一个不能访问的文件(没有正确分配权限)。此过程并非必须,如果守护进程不会创建文件,也可以不修改
  3. fork出子进程,父进程退出。这样子进程一定不是组长进程(进程id不等于进程组id)
  4. 子进程调用setsid新建会话(使子进程变为会话首进程、组长进程,并断开终端)
  5. 如果是单例守护进程,将pid写入到记录锁文件,一般为/var/run/xxx.pid
  6. 切换工作目录到根目录,这是为了防止占用磁盘造成磁盘不能卸载。所以也可以改到别的目录,只要保证目录所在磁盘不会中途卸载
  7. 重定向输入输入错误文件句柄,将其指向/dev/null。

前面提到,守护进程一般借助记录锁文件来(文件存在并且文件内记录的pid对应的进程依然活跃)判断是否已经有进程存在。

多数守护进程并不自己维护日志文件,而是统一将日志输出给遵循syslog协议的日志进程(如:rsyslogd)处理,统一将日志输出至 /var/log/messages,当然这些日志进程也是可以配置的。

而且守护进程因为是没有终端的后台进程,所以系统不会发送一些跟终端相关的信号给守护进程,程序可以通过捕捉这些只有可能人为发送的信号,来处理一些事情,比如处理SIGHUP来动态更新程序配置就是典型例子。下面的代码演示了如何创建一个守护进程。

  1 #include <stdio.h>
  2 #include <syslog.h>
  3 #include <errno.h>
  4 #include <unistd.h>
  5 #include <stdlib.h>
  6 #include <fcntl.h>
  7 #include <signal.h>
  8 #include <sys/types.h>
  9 #include <sys/stat.h>
 10 #include <sys/resource.h>
 11 
 12 #define PID_FILE "/var/run/sampled.pid"
 13 
 14 int sampled_running(){
 15     FILE * pidfile = fopen(PID_FILE,"r");
 16     pid_t pid;
 17     int ret ;
 18 
 19     if (! pidfile) {
 20         return 0;
 21     }
 22 
 23     ret = fscanf(pidfile,"%d",&pid);
 24     if (ret == EOF && ferror(pidfile) != 0){
 25         syslog(LOG_INFO,"Error open pid file %s",PID_FILE);
 26     }
 27 
 28     fclose(pidfile);
 29     
 30     // 检测进程是否存在
 31     if ( kill(pid , 0 ) ){
 32         syslog(LOG_INFO,"Remove a zombie pid file %s", PID_FILE);
 33         unlink(PID_FILE);
 34         return 0;
 35     }
 36 
 37     return pid;
 38 }
 39 
 40 pid_t sampled(){
 41     pid_t pid;
 42     struct rlimit rl;
 43     int fd,i;
 44 
 45     // 创建子进程,并退出当前父进程
 46     if((pid = fork()) < 0){
 47         syslog(LOG_INFO,"sampled : fork error");
 48         return -1;
 49     }
 50     if ( pid != 0) {
 51         //  父进程直接退出
 52         exit(0);
 53     }
 54 
 55     // 新建会话,成功返回值是会话首进程id,进程组id ,首进程id
 56     pid = setsid();
 57 
 58     if ( pid < -1 ){
 59         syslog(LOG_INFO,"sampled : setsid error");
 60         return -1;
 61     }
 62 
 63     // 将工作目录切换到根目录
 64     if ( chdir("/") < 0 ) {
 65         syslog(LOG_INFO,"sampled : chidr error");
 66         return -1;
 67     }
 68 
 69     // 关闭所有打开的句柄,如果确定父进程未打开过句柄,此步可以不做
 70     if ( rl.rlim_max == RLIM_INFINITY ){
 71         rl.rlim_max = 1024;
 72     }
 73     for(i = 0 ; i < rl.rlim_max; i ++) {
 74         close(i);
 75     }
 76 
 77     // 重定向输入输出错误
 78     fd = open("/dev/null",O_RDWR,0);
 79     if(fd != -1){
 80         dup2(fd,STDIN_FILENO);
 81         dup2(fd,STDOUT_FILENO);
 82         dup2(fd,STDERR_FILENO);
 83         if (fd > 2){
 84             close(fd);
 85         }
 86     }
 87     
 88     // 消除文件掩码
 89     umask(0);
 90     return 0;
 91 }
 92 
 93 int pidfile_write(){
 94     // 这里不用fopen直接打开文件是不想创建666权限的文件
 95     FILE * pidfile = NULL;
 96     int pidfilefd = creat(PID_FILE,S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
 97     if(pidfilefd != -1){
 98         pidfile = fdopen(pidfilefd,"w");
 99     }
100 
101     if (! pidfile){
102         syslog(LOG_INFO,"pidfile write : can't open pidfile:%s",PID_FILE);
103         return 0;
104     }
105     fprintf(pidfile,"%d",getpid());
106     fclose(pidfile);
107     return 1;
108 }
109 
110 int main(){
111     int err,signo;
112     sigset_t mask;
113 
114     if (sampled_running() > 0 ){
115          exit(0);
116     }
117 
118     if ( sampled() != 0 ){
119 
120     }
121     // 写记录锁文件  
122     if (pidfile_write() <= 0) {
123         exit(0);
124     }
125 
126     while(1) {
127         // 捕捉信号
128         err = sigwait(&mask,&signo);
129         if( err != 0  ){
130             syslog(LOG_INFO,"sigwait error : %d",err);
131             exit(1);
132         }
133         switch (signo){
134             default :
135                 syslog(LOG_INFO,"unexpected signal %d \n",signo);
136                 break;
137             case SIGTERM:
138                 syslog(LOG_INFO,"got SIGTERM. exiting");
139                 exit(0);
140         }
141 
142     }
143 
144 }

程序编译运行结果,可以看到pid  、进程组id、会话id是一样的,没有终端,并且直接由pid为1的进程接管。此时的进程已经成为一个守护进程。

 

sighup与nohup

sighup(挂断)信号在控制终端或者控制进程死亡时向关联会话中的进程发出,默认进程对SIGHUP信号的处理时终止程序,所以我们在shell下建立的程序,在登录退出连接断开之后,会一并退出。

nohup,故名思议就是忽略SIGHUP信号,一般搭配& 一起使用,&表示将此程序提交为后台作业或者说后台进程组。执行下面的命令

nohup bash -c "tail -f /var/log/messages | grep sys" &

nohup与&启动的程序, 在终端还未关闭时,完全不像传统的守护进程,因为其不是会话首进程且持有终端,只是其忽略了SIGHUP信号

从nohup源码就可以看到,其实nohup只做了3件事情

  1. dofile函数将输出重定向到nohup.out文件
  2. signal函数设置SIGHUP信号处理函数为SIG_IGN宏(指向sigignore函数),以此忽略SIG_HUP信号
  3. execvp函数用新的程序替换当前进程的代码段、数据段、堆段和栈段。

execvp 函数执行后,新程序(并没有fork进程)会继承一些调用进程属性,比如:进程id、会话id,控制终端等

登录连接断开之后

在终端关闭后,nohup起到类似守护进程的效果,但是跟传统的守护进程还是有区别的

1、nohup创建的进程工作目录是你执行命令时所在的目录
2、0 1 2 标准输入 标准输出 标准错误  指向nohup.out文件
3、nohup创建的进程组中,除首长进程的父进程id变为1之外,其余进程依然保留原来的会话id、进程组id、父进程id,都保持不变
 
 
相关实践学习
【涂鸦即艺术】基于云应用开发平台CAP部署AI实时生图绘板
【涂鸦即艺术】基于云应用开发平台CAP部署AI实时生图绘板
目录
相关文章
|
10月前
|
存储 Linux API
【Linux进程概念】—— 操作系统中的“生命体”,计算机里的“多线程”
在计算机系统的底层架构中,操作系统肩负着资源管理与任务调度的重任。当我们启动各类应用程序时,其背后复杂的运作机制便悄然展开。程序,作为静态的指令集合,如何在系统中实现动态执行?本文带你一探究竟!
【Linux进程概念】—— 操作系统中的“生命体”,计算机里的“多线程”
|
算法 Linux 调度
深入理解Linux操作系统的进程管理
本文旨在探讨Linux操作系统中的进程管理机制,包括进程的创建、执行、调度和终止等环节。通过对Linux内核中相关模块的分析,揭示其高效的进程管理策略,为开发者提供优化程序性能和资源利用率的参考。
364 32
|
8月前
|
并行计算 Linux
Linux内核中的线程和进程实现详解
了解进程和线程如何工作,可以帮助我们更好地编写程序,充分利用多核CPU,实现并行计算,提高系统的响应速度和计算效能。记住,适当平衡进程和线程的使用,既要拥有独立空间的'兄弟',也需要在'家庭'中分享和并行的成员。对于这个世界,现在,你应该有一个全新的认识。
308 67
|
7月前
|
Web App开发 Linux 程序员
获取和理解Linux进程以及其PID的基础知识。
总的来说,理解Linux进程及其PID需要我们明白,进程就如同汽车,负责执行任务,而PID则是独特的车牌号,为我们提供了管理的便利。知道这个,我们就可以更好地理解和操作Linux系统,甚至通过对进程的有效管理,让系统运行得更加顺畅。
225 16
|
7月前
|
Unix Linux
对于Linux的进程概念以及进程状态的理解和解析
现在,我们已经了解了Linux进程的基础知识和进程状态的理解了。这就像我们理解了城市中行人的行走和行为模式!希望这个形象的例子能帮助我们更好地理解这个重要的概念,并在实际应用中发挥作用。
144 20
|
6月前
|
监控 Shell Linux
Linux进程控制(详细讲解)
进程等待是系统通过调用特定的接口(如waitwaitpid)来实现的。来进行对子进程状态检测与回收的功能。
131 0
|
6月前
|
存储 负载均衡 算法
Linux2.6内核进程调度队列
本篇文章是Linux进程系列中的最后一篇文章,本来是想放在上一篇文章的结尾的,但是想了想还是单独写一篇文章吧,虽然说这部分内容是比较难的,所有一般来说是简单的提及带过的,但是为了让大家对进程有更深的理解与认识,还是看了一些别人的文章,然后学习了学习,然后对此做了总结,尽可能详细的介绍明白。最后推荐一篇文章Linux的进程优先级 NI 和 PR - 简书。
205 0
|
6月前
|
存储 Linux Shell
Linux进程概念-详细版(二)
在Linux进程概念-详细版(一)中我们解释了什么是进程,以及进程的各种状态,已经对进程有了一定的认识,那么这篇文章将会继续补全上篇文章剩余没有说到的,进程优先级,环境变量,程序地址空间,进程地址空间,以及调度队列。
133 0
|
6月前
|
Linux 调度 C语言
Linux进程概念-详细版(一)
子进程与父进程代码共享,其子进程直接用父进程的代码,其自己本身无代码,所以子进程无法改动代码,平时所说的修改是修改的数据。为什么要创建子进程:为了让其父子进程执行不同的代码块。子进程的数据相对于父进程是会进行写时拷贝(COW)。
185 0
|
10月前
|
存储 网络协议 Linux
【Linux】进程IO|系统调用|open|write|文件描述符fd|封装|理解一切皆文件
本文详细介绍了Linux中的进程IO与系统调用,包括 `open`、`write`、`read`和 `close`函数及其用法,解释了文件描述符(fd)的概念,并深入探讨了Linux中的“一切皆文件”思想。这种设计极大地简化了系统编程,使得处理不同类型的IO设备变得更加一致和简单。通过本文的学习,您应该能够更好地理解和应用Linux中的进程IO操作,提高系统编程的效率和能力。
457 34

热门文章

最新文章