守护进程
一 了解守护进程
- daemon(也叫agent,精灵进程)是运行于后台常驻内存的一种特殊进程,守护进程(daemon)是指在后台运行的,没有控制终端与之相连的进程。他独立于控制终端,通常周期性的执行某种任务
- 在Linux系统中,我们会发现在系统启动的时候有很多的进程就已经开始运行了,也称为服务,这就是我们说的守护进程。Linux的大多数服务器就是用守护进程的方式实现的
- 常常在系统引导装入时启动,仅在系统关闭时才终止
- 一般以字母"d"结尾的进程。例如:
syslogd
就是指管理系统日志的守护进程,web服务器httpd
,邮件服务器sendmail
和数据库服务器mysqld
- 守护进程常以超级用户(root)权限执行,因为他们要使用特殊的端口(1-1024)或访问某些特殊的资源
- 一个守护进程的父进程是init进程,因为它真正的父进程在fork出子进程后就先于子进程exit退出了,所以他是一个由init继承的孤儿进程。
- 守护进程是非交互式程序,没有控制终端,所以任何输出,无论是向标准输出设备stdout还是标准出错设备stderr的输出都需要特殊处理
1.1 守护进程和普通进程的区别
区别是指:将后台程序变成一种服务
eg:用命令行输入启动程序,如果不是守护进程的话,一旦命令行窗口关闭,程序就终止了;如果是守护进程,退出命令行窗口之后,服务一直处于运行状态
1.2 为什么要脱离终端
避免与作业控制,终端会话管理,终端产生信号等发生任何不期望的交互,也可以避免在后台运行的守护进程非预期地输出到终端
二 守护进程的启动方式
-
在系统启动阶段,许多守护进程由***系统初始化脚本***启动。这些脚本通常位于/etc目录或以/etc/rc开头的某个目录中。由***这些脚本启动的守护进程一开始拥有超级用户特权***。
- 有若干个网络服务器通常从这些脚本启动:inetd超级服务器,Web服务器,邮件服务器(sendmail),syslogd守护进程通常也由某个系统初始化脚本启动
-
许多网络服务器由inetd超级服务器启动。inetd自身由上一条中的某个脚本启动。inetd监听网络请求(Telnet,FTP等),每当有一个请求到达时,启动相应的实际服务器(Telnet服务器,FTP服务器等)
-
cron守护进程按照规则定期执行一些程序,而由它启动执行的程序同样作为守护进程运行。cron自身由第一条启动方法中的某个脚本启动
-
at命令用于指定将来某个时刻的程序执行。这些程序的执行时刻到来时,通常由cron守护进程启动执行他们,因此这些程序同样作为守护进程运行
-
可以从用户终端或在前台或在后台启动。为了测试守护程序或重启因某种原因而终止了的某个守护进程
三 创建守护进程的步骤
3.1 一些基本概念
3.1.1 进程组
- 每个进程除了有一个进程ID之外,还属于一个进程组
- 进程组是一个或多个进程的集合,同一进程组中的各进程接受来自同一终端的各种信号
- 每个进程组有一个组长进程,组长进程的进程组ID等于其进程ID
3.1.2 会话期
会话期(session)是一个或多个进程组的集合,进程调用setsid()函数(pid_t setsid(void))可以建立一个对话。
进程调用setsid函数建立一个新会话,如果调用此函数的进程不是一个进程组的组长,则此函数创建一个新会话。具体会发生以下事情:
- 该进程变成新会话的会话首进程(session leader,会话首进程是创建该会话的进程)。此时,该进程是新会话的唯一进程
- 该进程成为一个新进程组的组长进程。新进程组ID是该调用进程的进程ID
- 该进程没有控制终端。如果调用setsid之前该进程有一个控制终端,那么这种联系也被切断
如果该调用进程已经是一个进程组的组长,则此函数返回出错。为了保证不处于这种情况,通常先调用fork,然后使其父进程终止,而子进程则继续。因为子进程继承了父进程的进程组ID,而其进程ID是重新分配的,两者不可能相等,这就保证了子进程不是一个进程组的组长。
3.2 过程
- 调用fork创建子进程。
- 父进程终止,让子进程在后台继续执行
- 调用setsid创建一个新会话期.
- 控制终端,登录会话和进程组通常是从父进程继承下来的,守护进程要摆脱它们,不受它们的影响,其方法是调用setsid使进程成为一个会话组长,进程成为新的会话组长和进程组长,并与原来的登录会话和进程组脱离,由于会话过程对控制端的独占性,进程同时与控制终端脱离
- 禁止进程重新打开控制端。
- 经过上面的步骤,进程已经成为一个无终端的会话组长,但是它可以重新申请打开一个终端。为了避免这种情况的发生,可以通过使进程不再是会话组长来实现。再一次通过fork创建新的子进程,使调用fork的进程退出
- 关闭不再需要的文件描述符
- 创建的子进程从父进程继承打开的文件描述符。如不关闭,将会浪费系统资源,造成进程所在的文件系统无法卸下以及引起无法预料的错误。先得到最高文件描述符值,然后用一个循环程序,关闭0到最高文件描述符值的所有文件描述符
- 将当前目录更改为根目录。
- 当守护进程当前工作目录在一个装配文件系统中时,该文件系统不能被拆卸。一般需要将工作目录改为根目录
- 将文件创建时使用的屏蔽字设置为0。
- 进程从创建它的父进程那里继承的文件创建屏蔽字可能会拒绝某些许可权,为了防止这一点,使用umask(0)将屏蔽字清零
- 处理SIGCHLD信号。
- 不是必须的。但对于某些进程,特别是服务器进程往往在请求到来时生成子进程处理请求。如果父进程不等待子进程结束,子进程将成为僵尸进程,从而占用系统资源。如果父进程等待子进程结束,将会增加父进程的负担,影响服务器进程的并发性能。在Linux下可以简单地将SIGCHLD信号的操作设为SIG_ING。这样,子进程结束时不会产生僵尸进程
3.3 一定要fork两次?
在glibc源码中daemon函数的实现
//只fork了一次
int
daemon(nochdir, noclose)
int nochdir, noclose;
{
int fd;
switch (fork()) {
case -1:
return (-1);
case 0:
break;
default:
_exit(0);
}
if (__setsid() == -1)
return (-1);
if (!nochdir)
(void)__chdir("/");
if (!noclose && (fd = __open(_PATH_DEVNULL, O_RDWR, 0)) != -1) {
(void)__dup2(fd, STDIN_FILENO);
(void)__dup2(fd, STDOUT_FILENO);
(void)__dup2(fd, STDERR_FILENO);
if (fd > 2)
(void)__close (fd);
}
return (0);
}
nginx中的代码
/*
* Copyright (C) Igor Sysoev
* Copyright (C) Nginx, Inc.
*/
//只fork了一次
#include <ngx_config.h>
#include <ngx_core.h>
ngx_int_t
ngx_daemon(ngx_log_t *log)
{
int fd;
switch (fork()) {
case -1:
ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "fork() failed");
return NGX_ERROR;
case 0:
break;
default:
exit(0);
}
ngx_pid = ngx_getpid();
if (setsid() == -1) {
ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "setsid() failed");
return NGX_ERROR;
}
umask(0);
fd = open("/dev/null", O_RDWR);
if (fd == -1) {
ngx_log_error(NGX_LOG_EMERG, log, ngx_errno,
"open(\"/dev/null\") failed");
return NGX_ERROR;
}
if (dup2(fd, STDIN_FILENO) == -1) {
ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "dup2(STDIN) failed");
return NGX_ERROR;
}
if (dup2(fd, STDOUT_FILENO) == -1) {
ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "dup2(STDOUT) failed");
return NGX_ERROR;
}
#if 0
if (dup2(fd, STDERR_FILENO) == -1) {
ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "dup2(STDERR) failed");
return NGX_ERROR;
}
#endif
if (fd > STDERR_FILENO) {
if (close(fd) == -1) {
ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "close() failed");
return NGX_ERROR;
}
}
return NGX_OK;
}
原来并不是所有的创建都需要fork两下,那为什么?fork两下的意义在哪?
在Unix高级环境编程中第13章是这样解释的:
- 在基于System V的系统中,有些人推荐再fork一次,这些fork产生的进程就不再是session leader了,避免打开控制终端。还有一种可选的方法:打开终端设备的时候指定O_NOCTTY来避免打开控制终端
我自己的想法:
首先fork第一次后,让父进程退出,子进程继续。
- 如果直接调用setsid创建一个新对话期,则会失败,所以不能由会话组长创建。所以要让子进程调用setsid,使它变成会话组长,并且摆脱了终端!
会话组长还可能会打开终端的(因为打开一个终端的前台条件是该进程必须是会话组长),所以你不想让他某个时刻打开了终端,你就fork一下,让进程不再是会话组长。
- 接下来没有调用setsid!!!!