1. 什么是守护进程

守护进程daemon,是指没有控制终端,运行在后台的进程,通常伴随着系统启动产生,系统关机结束。可以使用命令ps -axj查看系统的守护进程,输出如下所示:

 父ID   PID  组ID  会话ID 终端           状态    用户ID      命令

 PPID   PID  PGID   SID  TTY      TPGID STAT   UID   TIME COMMAND
0 1 1 1 ? -1 Ss 0 0:02 /usr/lib/systemd/systemd --switched-root --system --deserialize 21
0 2 0 0 ? -1 S 0 0:00 [kthreadd]
2 3 0 0 ? -1 S 0 0:00 [ksoftirqd/0] 说明:
1. 终端为 ? 表示没有控制终端,守护进程本身没有控制终端。
2. 状态为 s(小写) 表示此进程为Session Leader(即会话首领)。
3. 命令用 [ ] 括起来表示内核的守护进程,可以看到目前新版Linux系统使用Systemd,而不是init进程。

Linux系统中常见的守护进程有:

  • cron 进程定期执行crontab设置的定时任务。
  • kswap守护进程定期将物理脏页写回磁盘来回收页面。
  • rsyslogd记录日志信息。
  • 还有一些常见的服务器程序,例如Redis、Nginx、MySQL等等。

2. 会话、进程组、控制终端

说到守护进程,它的一个重要的特点是没有控制终端,于是引出了有关会话、进程组、控制终端等概念:

会话:又称为Session,我们正常登录shell之后整个shell程序可以看做一个会话。
进程组:一个会话可以有多个进程组,如果此会话有控制终端,则会有一个前台进程组和若干个后台进程组。
控制终端:一个会话最多只有一个控制终端或者没有,如果ssh登录或者通过命令gnome-terminal打开一个shell,则默认会打开一个终端与此shell对应,在Linux上,通常指的是虚拟终端,也就是/dev/pts/x,可以执行tty命令查看目前shell对应的终端。

通过ps命令,我们再次区分下这几个概念:

$ tty
/dev/pts/1 $ ps -o pid,ppid,pgid,sid,comm | less PID PPID PGID SID COMMAND
16838 14313 16838 16838 bash
28697 16838 28697 16838 ps
28698 16838 28697 16838 less 说明:
1. ps 和 less 都是 shell 的子进程。
2. 他们SID相同,属于同一个会话,且会话首领是 bash,会话对应的虚拟终端为/dev/pts/1。
3. ps 和 less 进程组ID是一样的,表明他们属于同一个进程组。

这三个进程之间的关系如下图所示:

3. 如何创建一个守护进程

创建一个守护进程通常来说有以下步骤:

  • fork(退出父进程,保证子进程不是当前会话的首领,这样才可以调用setsid重新创建新会话,因为会话首领不可以重新创建会话)
  • setsid(重新创建一个会话)
  • fork(再次fork退出父进程,保证子进程不是新会话的首领,则不具有重新获得控制终端的能力,因为会话首领有重新申请终端的能力)
  • chdir(更改守护进程运行的默认目录,防止目前目录被卸载)
  • umask(守护进程可能需要创建日志文件,因此将umask置为0,默认文件权限则为:666-000)
  • signal(忽略或者处理信号SIGHUP(进程和控制终端分离时收到SIGHUP)、 SIGTERM(系统关机之前收到SIGTERM))
  • close(关闭从父进程继承来的不会使用的文件描述符)

代码示例如下:

#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <signal.h>
#include <sys/resource.h> void daemon_two_fork()
{
int fd;
struct rlimit rl; if (fork() != 0) { // 第一次fork
exit(0);
} if (setsid() == -1) { // 创建新会话
perror("setsid");
} if (fork() != 0) { // 再次fork,保证子进程不是会话首领
exit(0);
} if (chdir("/tmp") == -1) { // 修改默认工作目录
perror("chdir");
} umask(0); // 设置umask signal(SIGHUP, SIG_IGN); // 信号处理
signal(SIGCHLD, SIG_IGN); if (getrlimit(RLIMIT_NOFILE, &rl) < 0) { // 通过getrlimit得到进程所能打开的最大文件描述符
perror("getrlimit");
} if (rl.rlim_max == RLIM_INFINITY) {
rl.rlim_max = 1024;
} /* 将除过(0,1,2)之外的文件描述符全部关闭,对一个没有打开的文件描述符
调用close会出现`Bad file descriptor`,这时候只能选择忽略 */
for (int i = 3; i < rl.rlim_max; ++i) {
close(i);
} /*将 0 1 2 输出导入 /dev/null */
if ((fd = open("/dev/null", O_RDWR, 0) != -1)) {
dup2(fd, STDIN_FILENO);
dup2(fd, STDOUT_FILENO);
dup2(fd, STDERR_FILENO);
if (fd > STDERR_FILENO) {
close(fd);
}
}
} int main(int argc, char *argv[])
{
int fd;
pid_t pid; daemon_two_fork(); fd = open("/tmp/log", O_RDWR | O_TRUNC);
if (fd == -1) {
perror("open");
} pid = fork(); if (pid > 0) {
for (int i = 0; i < 60; ++i) {
write(fd, "Hello World\n", 12);
sleep(1);
}
} close(fd);
return 0;
}

4. 第二次fork是必须的吗?

答案是:否

因为第二次fork的目的是:让子进程不再是会话首领,这样它就没能力再次申请终端,也避免了守护进程收到与终端相关信号的干扰,比如SIGHUP等,那么如果程序能确保后续不会有申请终端的操作,第二次fork也就没有什么意义了。

比如 Redis(一个内存数据库)实现守护进程的代码如下点这里

void daemonize(void) {
int fd; if (fork() != 0) exit(0); /* parent exits */
setsid(); /* create a new session */ /* Every output goes to /dev/null. If Redis is daemonized but
* the 'logfile' is set to 'stdout' in the configuration file
* it will not log at all. */
if ((fd = open("/dev/null", O_RDWR, 0)) != -1) {
dup2(fd, STDIN_FILENO);
dup2(fd, STDOUT_FILENO);
dup2(fd, STDERR_FILENO);
if (fd > STDERR_FILENO) close(fd);
}
}

可以看到除了只使用一次fork之外,umask,close等操作也都没有,这是因为作为一个服务器程序,如果用户指定了守护进程方式启动,通常在主进程还没有打开文件的时候就会完成daemon的操作,此时既没有信号,也没有打开的文件,不然等到主进程开始打开文件,监听套接字了再去daemon,岂不是很麻烦。

此外,除了Redis, Nginx实现的守护进程也只有一次fork,代码见 这里

5. 守护进程如何支持Systemd方式启动?

比较新的Linux发行版本开始使用 Systemd 来作为initupstart的替代,也就是它是新晋的1号进程,可以看到下面ps的结果PID为 1:

[yangbodong@centos-linux system]$ ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 191244 4356 ? Ss Nov27 0:05 /usr/lib/systemd/systemd --switched-root --system --deserialize 21

如果我们想让自己的程序(服务)被systemd管理,则编写守护进程则要遵循一些systemd下的规则了…

参考自[1]

  • 后台服务进程代码不需要执行两次fork来实现后台进程,只需要实现服务本身的主循环即可。(传统编写守护进程要至少调用一次fork,然后停止父进程)
  • 不要调用 setsid(),交给 systemd 处理
  • 不再需要维护 pid 文件。(传统编写守护进程,会自己在某个目录下,生成pid文件,一是记录本守护进程pid,另外一点是防止本守护进程被多次重启而导致出错或者导致多个实例在运行)
  • Systemd 提供了日志功能,服务进程只需要输出到 stderr 即可,无需使用 syslog。(传统编写守护进程我们要将标准输出、出错、输入关闭或者重定向,日志信息都是发往rsyslog)
  • 处理信号 SIGTERM,这个信号的唯一正确作用就是停止当前服务,不要做其他的事情。(传统守护进程一般SIGTERM也是用来做这种事情的)
  • SIGHUP 信号的作用是重启服务。(传统数据进程一般SIGHUP也是做这种事情的)
    需要套接字的服务,不要自己创建套接字,让 systemd 传入套接字。(这个承接systemd快速启动优点而设立的,可以实现这个特点,也可以不实现)
  • 使用 sd_notify()函数通知 systemd 服务自己的状态改变。一般地,当服务初始化结束,进入服务就绪状态时,可以调用它。(没用过)

接下来实现一个进程交给Systemd去管理,继续以前面守护进程打开/tmp/log并且写入Hello World为例:

操作系统:Centos 7

$ vim daemonize.c

// 内容如下,我们只用实现守护进程要做的事情就行了

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h> int main(int argc, char *argv[])
{
int fd; fd = open("/tmp/log", O_RDWR | O_TRUNC); for (int i = 0; i < 60; ++i) {
write(fd, "Hello World\n", 13);
sleep(1);
} close(fd);
return 0;
}

接下来编译程序,将可执行文件拷贝至/usr/local/bin/(可选操作,为了规范):

$ gcc daemonize.c -o daemonize
$ sudo cp daemonize /usr/local/bin/

然后需要编写相应的Systemd管理的.service文件 :

$ sudo touch  /usr/lib/systemd/system/mydaemon.service    // 创建自己的service为mydaemon
$ sudo vim /usr/lib/systemd/system/mydaemon.service [Unit]
Description=mydaemon test // 描述自己的service
After=network.target [Service]
ExecStart=/usr/local/bin/daemonize // 启动命令,更多参数选项见参考[2]
ExecStop=pkill daemonize // 结束命令 [Install]
WantedBy=multi-user.target

然后我们为/etc/systemd/system/创建软链接指向实际编写的service文件,并且刷新Systemd的缓存:

$ sudo ln -s /usr/lib/systemd/system/mydaemon.service /etc/systemd/system/multi-user.target.wants/mydaemon.service

$ sudo systemctl daemon-reload

好了,这下就可以开心的使用`Systemd启动我们的mydaemon服务了:

$ sudo systemctl start mydaemon.service
$ sudo systemctl status mydaemon.service
● mydaemon.service - mydaemon test
Loaded: loaded (/usr/lib/systemd/system/mydaemon.service; enabled; vendor preset: disabled)
Active: active (running) since Tue 2017-11-28 00:54:00 CST; 1s ago
Main PID: 28930 (daemonize)
CGroup: /system.slice/mydaemon.service
└─28930 /usr/local/bin/daemonize Nov 28 00:54:00 centos-linux.shared systemd[1]: Started mydaemon test.
Nov 28 00:54:00 centos-linux.shared systemd[1]: Starting mydaemon test...
[yangbodong@centos-linux system]$

可以看到它的状态是active (running),用ps命令看一下:

$ ps aux | grep daemonize
root 29281 0.0 0.0 4156 340 ? Ss 00:55 0:00 /usr/local/bin/daemonize
yangbod+ 29300 0.0 0.0 112652 960 pts/1 R+ 00:55 0:00 grep --color=auto daemonize

可以看到它的TTY为?,表明没有终端,而且s(小写)表示它是一个会话首领,再看下/tmp/log,发现Hello World被正确写入:

参考资料:
[1] Systemd
[2] Systemd 入门教程:实战篇

[完]

Linux 守护进程原理及实例(Redis、Nginx)的更多相关文章

  1. Linux守护进程简单介绍和实例具体解释

    Linux守护进程简单介绍和实例具体解释 简单介绍 守护进程(Daemon)是执行在后台的一种特殊进程.它独立于控制终端而且周期性地执行某种任务或等待处理某些发生的事件.守护进程是一种非常实用的进程. ...

  2. Linux守护进程详解(init.d和xinetd) [转]

    一 Linux守护进程 Linux 服务器在启动时需要启动很多系统服务,它们向本地和网络用户提供了Linux的系统功能接口,直接面向应用程序和用户.提供这些服务的程序是由运行在后台 的守护进程来执行的 ...

  3. Linux守护进程的编程实现

    Linux 守护进程的编程方法 守护进程(Daemon)是执行在后台的一种特殊进程.它独立于控制终端而且周期性地执行某种任务或等待处理某些发生的事件.守护进程是一种非常实用的进程.Linux的大多数s ...

  4. Linux守护进程详解(init.d和xinetd)

    一 Linux守护进程 Linux 服务器在启动时需要启动很多系统服务,它们向本地和网络用户提供了Linux的系统功能接口,直接面向应用程序和用户.提供这些服务的程序是由运行在后台的守护进程来执行的. ...

  5. 笔记整理--Linux守护进程

    Linux多进程开发(三)进程创建之守护进程的学习 - _Liang_Happy_Life__Dream - 51CTO技术博客 - Google Chrome (2013/10/11 16:48:2 ...

  6. CentOS 7.4 初次手记:第一章 Linux守护进程(daemon)

    第一节 init & sysvinit 6 I sysvinit 运行顺序... 6 II Sysvinit和系统关闭... 7 III Sysvinit 的小结... 7 IV 运行级别.. ...

  7. [Linux] 守护进程和守护线程

    对于JAVA而言,一般一个应用程序只有一个进程——JVM.除非在代码里面另外派生或者开启了新进程. 而线程,当然是由进程开启的.当开启该线程的进程离开时,线程也就不复存在了. 所以,对于JAVA而言, ...

  8. C#与Linux守护进程

    用C#编写Linux守护进程   如果要在Red Hat Enterprise Linux上将.NET Core进程作为后台进程运行,则可以创建自定义systemd单元.今天我将为.NET Core编 ...

  9. Linux守护进程编写指南

    Linux守护进程编写指南 守护进程(Daemon)是运行在后台的一种特殊进程.它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件.守护进程是一种很有用的进 程.Linux的大多数服务器 ...

随机推荐

  1. deepin(debian)中双网卡上内外网的设置方法(通过NetworkManager运行脚本)

    国产良心操作系统deepin,界面好看,反应速度快,开箱即用,深度商店里有非常多好用的linux.windows软件,其windows软件通过crossover进行运行,还可以运行一些安卓的apk程序 ...

  2. vivo全球商城-营销价格监控方案的探索

    一.背景 现在日常官网商城的运营中有一定概率出现以下两个问题: 1)优惠信息未对齐 官网商城促销优惠的类型越来越多,能影响最终用户实付价的优惠就有抢购.满减.优惠券.代金券等.实际业务操作中存在不同促 ...

  3. Spring Security 学习+实践

    Spring Security是Spring为解决应用安全所提供的一个全面的安全性解决方案.基于Spring AOP和Servlet过滤器,启动时在Spring上下文中注入了一组安全应用的Bean,并 ...

  4. 15种Python片段去优化你的数据科学管道

    来源:15 Python Snippets to Optimize your Data Science Pipeline 翻译:RankFan 15种Python片段去优化你的数据科学管道 为什么片段 ...

  5. Ubuntu系统的开机全流程介绍及grub美化

    目录 前言 Ubuntu开机经历的步骤 BIOS Boot Loader Kernel 配置 Grub 的个性化主题 /usr/share/grub/default/grub /etc/default ...

  6. HTML(思维导图)

  7. 数值计算:四阶龙格-库塔法 for 二阶微分方程

    引言 考虑存在以下二阶偏微分方程 \[\begin{align} f_2 \cdot \ddot{X(t)}+f_1 \cdot \dot{X(t)} +f_0 \cdot {X(t)} =F(t) ...

  8. Linux下Electron loadURL报错 ERR_FAILED(-2) Not allowed to load local resource

    Linux下Electron loadURL报错 ERR_FAILED(-2) Not allowed to load local resource 背景 使用electron-vue的时候,窗体创建 ...

  9. 10 月 30 日 北京 LiveVideoStack 阿里云视频云专场限量赠票 100 张

    10 月 30 日 | 北京 LiveVideoStack 将携手阿里云共邀 4 位技术大咖,一同探讨从上云到创新,视频云的新技术与新场景.阿里云视频云依托阿里云服务数百万开发者的卓越服务能力与实践, ...

  10. 前段---css

    css主要是用来做如何显示html元素的 当浏览器读到一个样式表,它就会按照这个样式表来对文档做渲染 注意:每一个css样式表都是由两个部分组成的, 1,选择器 2,声明 声明又包括属性值和属性,每个 ...