一、守护进程
脱离于终端并且在后台运行的进程,用于长期运行,守护自己的职责(如:监听端口、服务等)。
1、特点:
- 不受用户登录、注销影响。大多数Linux下的服务器都是利用守护进程实现的,如MySQL数据守护进程mysqld,HTTP服务器守护进程httpd;
- 父进程不是用户创建的进程。
- 以pid=1的进程为父进程的有:
int进程或者systemd(pid=1)以及用户人为启动的用户层进程。 - 以kthreadd为父进程的:
以kthreadd内核进程创建的守护进程。
- 守护进程一般为会话首进程、组长进程。
- 工作目录为根目录("/"),主要是为了防止占用磁盘导致无法卸载磁盘。
2、创建守护进程步骤:
- 创建子进程、父进程退出;
- 子进程调用系统函数 setsid 创建新的会话;
- 改变当前目录为根目录;
- 重设文件权限掩码;
- 关闭文件描述符;
详细见《后台开发核心技术与应用实践》p348
需掌握相关概念:
进程组、会话周期。
3、创建守护进程示例:
#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 0;
}
ret = fscanf(pidfile,"%d",&pid);
if (ret == EOF && ferror(pidfile) != 0){
syslog(LOG_INFO,"Error open pid file %s",PID_FILE);
}
fclose(pidfile);
// 检测进程是否存在
if ( kill(pid , 0 ) ){
syslog(LOG_INFO,"Remove a zombie pid file %s", PID_FILE);
unlink(PID_FILE);
return 0;
}
return pid;
}
pid_t sampled(){
pid_t pid;
struct rlimit rl;
int fd,i;
// 创建子进程,并退出当前父进程
if((pid = fork()) < 0){
syslog(LOG_INFO,"sampled : fork error");
return -1;
}
if ( pid != 0) {
// 父进程直接退出
exit(0);
}
// 新建会话,成功返回值是会话首进程id,进程组id ,首进程id
pid = setsid();
if ( pid < -1 ){
syslog(LOG_INFO,"sampled : setsid error");
return -1;
}
// 将工作目录切换到根目录
if ( chdir("/") < 0 ) {
syslog(LOG_INFO,"sampled : chidr error");
return -1;
}
// 关闭所有打开的句柄,如果确定父进程未打开过句柄,此步可以不做
if ( rl.rlim_max == RLIM_INFINITY ){
rl.rlim_max = 1024;
}
for(i = 0 ; i < rl.rlim_max; i ++) {
close(i);
}
// 重定向输入输出错误
fd = open("/dev/null",O_RDWR,0);
if(fd != -1){
dup2(fd,STDIN_FILENO);
dup2(fd,STDOUT_FILENO);
dup2(fd,STDERR_FILENO);
if (fd > 2){
close(fd);
}
}
// 消除文件掩码
umask(0);
return 0;
}
int pidfile_write(){
// 这里不用fopen直接打开文件是不想创建666权限的文件
FILE * pidfile = NULL;
int pidfilefd = creat(PID_FILE,S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
if(pidfilefd != -1){
pidfile = fdopen(pidfilefd,"w");
}
if (! pidfile){
syslog(LOG_INFO,"pidfile write : can't open pidfile:%s",PID_FILE);
return 0;
}
fprintf(pidfile,"%d",getpid());
fclose(pidfile);
return 1;
}
int main(){
int err,signo;
sigset_t mask;
if (sampled_running() > 0 ){
exit(0);
}
if ( sampled() != 0 ){
}
// 写记录锁文件
if (pidfile_write() <= 0) {
exit(0);
}
while(1) {
// 捕捉信号
err = sigwait(&mask,&signo);
if( err != 0 ){
syslog(LOG_INFO,"sigwait error : %d",err);
exit(1);
}
switch (signo){
default :
syslog(LOG_INFO,"unexpected signal %d \n",signo);
break;
case SIGTERM:
syslog(LOG_INFO,"got SIGTERM. exiting");
exit(0);
}
}
}
程序编译运行结果,可以看到:
pid 、进程组id、会话id是一样的,没有终端,并且直接由pid为1的进程接管。此时的进程已经成为一个守护进程。
二、sighup与nohup
sighup(挂断)信号:
在控制终端或者控制进程死亡时向关联会话中的进程发出,默认进程对SIGHUP信号的处理是终止程序,所以我们在shell下建立的程序,在登录退出连接断开之后,会一并退出。
网上搜索到的解释: 什么时候会发送 SIGHUP信号?当一个进程组成为孤儿进程组时,posix.1要求向孤儿进程组中处于停止状态的进程发送SIGHUP(挂断)信号,系统对于这种信号的默认处理是终止进程,然而如果无视这个信号或者另行处理的话,那么这个挂起进程仍可以继续执行。
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起到类似守护进程的效果,但是跟传统的守护进程还是有区别的
- nohup创建的进程工作目录是你执行命令时所在的目录
- 0 1 2 标准输入 标准输出 标准错误 指向nohup.out文件
- nohup创建的进程组中,除首长进程的父进程id变为1之外,其余进程依然保留原来的会话id、进程组id、父进程id,都保持不变
网上搜索到的解释: nohup命令可以将程序以忽略挂起信号的方式运行起来,被运行的程序的输出信息将不会显示到终端。
无论是否将 nohup 命令的输出重定向到终端,输出都将附加到当前目录的 nohup.out 文件中。如果当前目录的 nohup.out
文件不可写,输出重定向到$HOME/nohup.out文件中。如果没有文件能创建或打开以用于追加,那么 command
参数指定的命令不可调用。如果标准错误是一个终端,那么把指定的命令写给标准错误的所有输出作为标准输出重定向到相同的文件描述符。