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. PHPCMS V9轻松完成WAP手机网站搭建全教程

    ---恢复内容开始--- 应用PHPCMS V9轻松完成WAP手机网站搭建全教程 用PHPCMS最新发布的V9搭建了PHPCMS研究中心网站(http://www.17huiyi.net)完成后,有用 ...

  2. 启动Jenkins后无法访问,如何排错

    做IT工作,使用各种工具的时候,遇到错误都是一堆英文,对于英语不好的人,看到报错可能就会心烦,我刚开始就是这种状态.后来,遇到问题,首先复制报错信息到百度上搜索,没有人请教的时候,你不能坐等问题自己解 ...

  3. 配置 放上传文件的目录 apache(httpd)

    1. 确认服务器 开放8088端口 https://www.apachefriends.org/download.html 下载XAMPP for Windows,安装 2. 修改apache主配置文 ...

  4. Linux文件(夹)属性与权限

    文件属性与权限,文件权限设置 参考资料:鸟哥的Linux私房菜 用户与用户组 Linux系统是一个多用户多任务的分时操作系统,任何一个要使用系统资源的用户,都必须首先向系统管理员申请一个账号,然后以这 ...

  5. django 自定义auth中user登陆认证以及自写认证

    第一种: 重写自定义auth中user登陆认证模块, 引入MobelBackend from django.contrib.auth.backends import ModelBackend 重写验证 ...

  6. 基于go语言学习工厂模式

    工厂模式 简单工厂模式(Simple Factory) 定义 优点 缺点 适用范围 代码实现 工厂方法模式(Factory Method) 定义 优点 缺点 适用范围 代码实现 抽象工厂模式(Abst ...

  7. The Data Way Vol.5|这里有一场资本与开源的 battle

    关于「The Data Way」 「The Data Way」是由 SphereEx 公司出品的一档播客节目.这里有开源.数据.技术的故事,同时我们关注开发者的工作日常,也讨论开发者的生活日常:我们聚 ...

  8. 【集成学习】:Stacking原理以及Python代码实现

    Stacking集成学习在各类机器学习竞赛当中得到了广泛的应用,尤其是在结构化的机器学习竞赛当中表现非常好.今天我们就来介绍下stacking这个在机器学习模型融合当中的大杀器的原理.并在博文的后面附 ...

  9. linux版火狐浏览器部署详解

    Firefox下载地址 Firefox全历史版本下载: http://ftp.mozilla.org/pub/firefox/releases/ Firefox驱动问题下载  https://gith ...

  10. 初学python-day3 列表