linux守护进程、SIGHUP与nohup详解
前端时间帮忙定位个问题。docker容器故障恢复后,其中的keepalived进程始终无法启动,也看不到Keepalived的日志。
strace 查看系统调用之后,发现了原因所在
socket(PF_LOCAL, SOCK_DGRAM|SOCK_CLOEXEC, ) =
connect(, {sa_family=AF_LOCAL, sun_path="/dev/log"}, ) = - ENOENT (No such file or directory)
close() =
open("/var/run/keepalived.pid", O_RDONLY) =
fstat(, {st_mode=S_IFREG|, st_size=, ...}) =
mmap(NULL, , PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -, ) = 0x7fe85ab1b000
read(, "\n", ) =
read(, "", ) =
close() =
munmap(0x7fe85ab1b000, ) =
kill(, SIG_0) =
socket(PF_LOCAL, SOCK_DGRAM|SOCK_CLOEXEC, ) =
connect(, {sa_family=AF_LOCAL, sun_path="/dev/log"}, ) = - ENOENT (No such file or directory)
close() =
exit_group() = ?
+++ exited with +++
这就是一个典型的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 守护进程
守护进程特点与相关概念
控制终端
通过网络登录或者终端登录建立的会话,会分配唯一一个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的进程设置为一个新进程组的首长进程。
- 断开已连接的控制终端
这三步是创建守护进程的重要步骤。
下图结合了笔者对这些概念的理解,做出的判断

守护进程的创建
- 如果是单例守护进程,结合锁文件和kill函数检测是否有进程已经运行
- umask取消进程本身的文件掩码设置,也就是设置Linux文件权限,一般设置为000,这是为了防止子进程创建创建一个不能访问的文件(没有正确分配权限)。此过程并非必须,如果守护进程不会创建文件,也可以不修改
- fork出子进程,父进程退出。这样子进程一定不是组长进程(进程id不等于进程组id)
- 子进程调用setsid新建会话(使子进程变为会话首进程、组长进程,并断开终端)
- 如果是单例守护进程,将pid写入到记录锁文件,一般为/var/run/xxx.pid
- 切换工作目录到根目录,这是为了防止占用磁盘造成磁盘不能卸载。所以也可以改到别的目录,只要保证目录所在磁盘不会中途卸载
- 重定向输入输入错误文件句柄,将其指向/dev/null。
前面提到,守护进程一般借助记录锁文件来(文件存在并且文件内记录的pid对应的进程依然活跃)判断是否已经有进程存在。
多数守护进程并不自己维护日志文件,而是统一将日志输出给遵循syslog协议的日志进程(如:rsyslogd)处理,统一将日志输出至 /var/log/messages,当然这些日志进程也是可以配置的。
而且守护进程因为是没有终端的后台进程,所以系统不会发送一些跟终端相关的信号给守护进程,程序可以通过捕捉这些只有可能人为发送的信号,来处理一些事情,比如处理SIGHUP来动态更新程序配置就是典型例子。下面的代码演示了如何创建一个守护进程。
#include <stdio.h>
#include <syslog.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/resource.h> #define PID_FILE "/var/run/sampled.pid" int sampled_running(){
FILE * pidfile = fopen(PID_FILE,"r");
pid_t pid;
int ret ; if (! pidfile) {
return ;
} ret = fscanf(pidfile,"%d",&pid);
if (ret == EOF && ferror(pidfile) != ){
syslog(LOG_INFO,"Error open pid file %s",PID_FILE);
} fclose(pidfile); // 检测进程是否存在
if ( kill(pid , ) ){
syslog(LOG_INFO,"Remove a zombie pid file %s", PID_FILE);
unlink(PID_FILE);
return ;
} return pid;
} pid_t sampled(){
pid_t pid;
struct rlimit rl;
int fd,i; // 创建子进程,并退出当前父进程
if((pid = fork()) < ){
syslog(LOG_INFO,"sampled : fork error");
return -;
}
if ( pid != ) {
// 父进程直接退出
exit();
} // 新建会话,成功返回值是会话首进程id,进程组id ,首进程id
pid = setsid(); if ( pid < - ){
syslog(LOG_INFO,"sampled : setsid error");
return -;
} // 将工作目录切换到根目录
if ( chdir("/") < ) {
syslog(LOG_INFO,"sampled : chidr error");
return -;
} // 关闭所有打开的句柄,如果确定父进程未打开过句柄,此步可以不做
if ( rl.rlim_max == RLIM_INFINITY ){
rl.rlim_max = ;
}
for(i = ; i < rl.rlim_max; i ++) {
close(i);
} // 重定向输入输出错误
fd = open("/dev/null",O_RDWR,);
if(fd != -){
dup2(fd,STDIN_FILENO);
dup2(fd,STDOUT_FILENO);
dup2(fd,STDERR_FILENO);
if (fd > ){
close(fd);
}
} // 消除文件掩码
umask();
return ;
} int pidfile_write(){
// 这里不用fopen直接打开文件是不想创建666权限的文件
FILE * pidfile = NULL;
int pidfilefd = creat(PID_FILE,S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
if(pidfilefd != -){
pidfile = fdopen(pidfilefd,"w");
} if (! pidfile){
syslog(LOG_INFO,"pidfile write : can't open pidfile:%s",PID_FILE);
return ;
}
fprintf(pidfile,"%d",getpid());
fclose(pidfile);
return ;
} int main(){
int err,signo;
sigset_t mask; if (sampled_running() > ){
exit();
} if ( sampled() != ){ }
// 写记录锁文件
if (pidfile_write() <= ) {
exit();
} while() {
// 捕捉信号
err = sigwait(&mask,&signo);
if( err != ){
syslog(LOG_INFO,"sigwait error : %d",err);
exit();
}
switch (signo){
default :
syslog(LOG_INFO,"unexpected signal %d \n",signo);
break;
case SIGTERM:
syslog(LOG_INFO,"got SIGTERM. exiting");
exit();
} } }
程序编译运行结果,可以看到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件事情
- dofile函数将输出重定向到nohup.out文件
- signal函数设置SIGHUP信号处理函数为SIG_IGN宏(指向sigignore函数),以此忽略SIG_HUP信号
- execvp函数用新的程序替换当前进程的代码段、数据段、堆段和栈段。
execvp 函数执行后,新程序(并没有fork进程)会继承一些调用进程属性,比如:进程id、会话id,控制终端等
登录连接断开之后

在终端关闭后,nohup起到类似守护进程的效果,但是跟传统的守护进程还是有区别的
linux守护进程、SIGHUP与nohup详解的更多相关文章
- Linux系统编程之--守护进程的创建和详解【转】
本文转载自:http://www.cnblogs.com/mickole/p/3188321.html 一,守护进程概述 Linux Daemon(守护进程)是运行在后台的一种特殊进程.它独立于控制终 ...
- linux运行进程实时监控pidstat详解
- Linux守护进程详解(init.d和xinetd) [转]
一 Linux守护进程 Linux 服务器在启动时需要启动很多系统服务,它们向本地和网络用户提供了Linux的系统功能接口,直接面向应用程序和用户.提供这些服务的程序是由运行在后台 的守护进程来执行的 ...
- Linux守护进程详解(init.d和xinetd)
一 Linux守护进程 Linux 服务器在启动时需要启动很多系统服务,它们向本地和网络用户提供了Linux的系统功能接口,直接面向应用程序和用户.提供这些服务的程序是由运行在后台的守护进程来执行的. ...
- Linux 命令详解(六)Linux 守护进程的启动方法
Linux 守护进程的启动方法 http://www.ruanyifeng.com/blog/2016/02/linux-daemon.html
- 云计算:Linux运维核心管理命令详解
云计算:Linux运维核心管理命令详解 想做好运维工作,人先要学会勤快: 居安而思危,勤记而补拙,方可不断提高: 别人资料不论你用着再如何爽那也是别人的: 自己总结东西是你自身特有的一种思想与理念的展 ...
- 笔记整理--Linux守护进程
Linux多进程开发(三)进程创建之守护进程的学习 - _Liang_Happy_Life__Dream - 51CTO技术博客 - Google Chrome (2013/10/11 16:48:2 ...
- Linux Shell编程与编辑器使用详解
<Linux Shell编程与编辑器使用详解> 基本信息 作者: 刘丽霞 杨宇 出版社:电子工业出版社 ISBN:9787121207174 上架时间:2013-7-22 出版日期:201 ...
- [转]❲阮一峰❳Linux 守护进程的启动方法
❲阮一峰❳Linux 守护进程的启动方法 "守护进程"(daemon)就是一直在后台运行的进程(daemon). 本文介绍如何将一个 Web 应用,启动为守护进程. 一.问题的由来 ...
随机推荐
- c#面向对象-类(类及其构成)
学习c#已经快一个学期,在这一段时间里,通过自己的努力和老师指导,自己感觉收获颇丰,所以我想把自己学到东西整理一下,供大家点评!若有错误或不当之处,敬请指出. 今天,我先从类及其构成说起! 1. ...
- 第一章:pip 安装 和 卸载 django
1. 在dos命令行中输入 pip 如下命令进行安装: 安装最新的版本的 Django 命令如下: pip install django 安装 指定版本的 Django 命令如下: pip insta ...
- ORA-01157,记一次Oracle故障恢复过程
生产环境中有两台部署PowerCenter的ETL业务机,近期发现无法通过客户端连接到ETL服务. 初步怀疑是PowerCenter挂掉了,或者资料库出现了故障. 登陆设备后发现PowerCenter ...
- hdu2410(水)
题意 如果两个数字除了带问号的位以外都相同,我们称这两个数可以相互匹配 给你两个数,其中第一个数字里有一些问号,问有多少个大于第二个数的数字可以和第一个数字匹配 一开始懒得读题,到网上搜题意,结果居然 ...
- spring的applicationContext.xml配置SessionFactory抛异常
<bean id="sessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFa ...
- Java 数据库编程 ResultSet 的 使用方法
结果集(ResultSet)是数据中查询结果返回的一种对象,可以说结果集是一个存储查询结果的对象,但是结果集并不仅仅具有存储的功能,他同时还具有操纵数据的功能,可能完成对数据的更新等. 结果集读取数据 ...
- A start job is running for xxx to stop
CentOS7开机时,遇到这样的问题已经好多回了,查阅了许多这样的问题,总是没能找到自己想要的答案. 今天本来启动顺利,但是设置mysql.httpd服务开机启动之后,再次开机时又遇到这样的问题. 这 ...
- cocos 射线检测 3D物体 (Sprite3D点击)
看了很多朋友问怎么用一个3D物体做一个按钮,而且网上好像还真比较难找到答案, 今天翻了一下cocos源码发现Ray 已经封装了intersects函数,那么剩下的工作其实很简单了, 从屏幕的一个poi ...
- Android - 自定义控件之圆形控件
自定义控件 - 圈圈 Android L: Android Studio 效果:能够自定义圆圈半径和位置:设定点击效果:改变背景颜色 下面是demo图 点击前: 点击后: 自定义控件一般要继承View ...
- RxSwift 系列(五) -- Filtering and Conditional Operators
前言 本篇文章将要学习RxSwift中过滤和条件操作符,在RxSwift中包括了: filter distinctUntilChanged elementAt single take takeLast ...