apue 第13章 守护进程

转载:http://www.cnblogs.com/mickole/p/3188321.html

1.守护进程概述

守护进程是生存期长的一种进程,在系统引导时启动,系统关闭时才终止。

Linux Daemon(守护进程)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。它不需要用户输入就能运行而且提供某种服务,不是对整个系统就是对某个用户程序提供服务。Linux系统的大多数服务器就是通过守护进程实现的。常见的守护进程包括系统日志进程syslogd、 web服务器httpd、邮件服务器sendmail和数据库服务器mysqld等。

守护进程一般在系统启动时开始运行,除非强行终止,否则直到系统关机都保持运行。守护进程经常以超级用户(root)权限运行,因为它们要使用特殊的端口(1-1024)或访问某些特殊的资源。

一个守护进程的父进程是init进程,因为它真正的父进程在fork出子进程后就先于子进程exit退出了,所以它是一个由init继承的孤儿进程。守护进程是非交互式程序,没有控制终端,所以任何输出,无论是向标准输出设备stdout还是标准出错设备stderr的输出都需要特殊处理。

守护进程的名称通常以d结尾,比如sshd、xinetd、crond等。

2、创建守护进程步骤

首先我们要了解一些基本概念:

进程组 :

  • 每个进程也属于一个进程组
  • 每个进程主都有一个进程组号,该号等于该进程组组长的PID号 .
  • 一个进程只能为它自己或子进程设置进程组ID号

会话期:

会话期(session)是一个或多个进程组的集合。
setsid()函数可以建立一个对话期:
setsid()说明:进程从它的双亲进程获得它的对话过程和进程组识别号。setsid()就是将进程和它当前的对话过程和进程组分离开,并且把它设置成一个新的对话过程的领头进程。如果,调用setsid的进程不是一个进程组的组长,此函数创建一个新的会话期。
(1)此进程变成该对话期的首进程
(2)此进程变成一个新进程组的组长进程。
(3)此进程没有控制终端,如果在调用setsid前,该进程有控制终端,那么与该终端的联系被解除。 如果该进程是一个进程组的组长,此函数返回错误。
(4)为了保证这一点,我们先调用fork()然后exit(),此时只有子进程在运行

编写守护进程的一般步骤步骤:

  1. 在父进程中执行fork并exit推出;
  2. 在子进程中调用setsid函数创建新的会话;
  3. 在子进程中调用chdir函数,让根目录 ”/” 成为子进程的工作目录;
  4. 在子进程中调用umask函数,设置进程的umask为0;
  5. 在子进程中关闭任何不需要的文件描述符

说明:
1. 在后台运行。
为避免挂起控制终端将Daemon放入后台执行。方法是在进程中调用fork使父进程终止,让Daemon在子进程中后台执行。

if(pid=fork()) 
exit(0);//是父进程,结束父进程,子进程继续 
  1. 脱离控制终端,登录会话和进程组
    有必要先介绍一下Linux中的进程与控制终端,登录会话和进程组之间的关系:进程属于一个进程组,进程组号(GID)就是进程组长的进程号(PID)。登录会话可以包含多个进程组。这些进程组共享一个控制终端。这个控制终端通常是创建进程的登录终端。
    控制终端,登录会话和进程组通常是从父进程继承下来的。我们的目的就是要摆脱它们,使之不受它们的影响。方法是在第1点的基础上,调用setsid()使进程成为会话组长: setsid(); 说明:当进程是会话组长时setsid()调用失败。但第一点已经保证进程不是会话组长。setsid()调用成功后,进程成为新的会话组长和新的进程组长,并与原来的登录会话和进程组脱离。由于会话过程对控制终端的独占性,进程同时与控制终端脱离。
  2. 禁止进程重新打开控制终端
    现在,进程已经成为无终端的会话组长。但它可以重新申请打开一个控制终端。可以通过使进程不再成为会话组长来禁止进程重新打开控制终端:
if(pid=fork()) 
exit(0);//结束第一子进程,第二子进程继续(第二子进程不再是会话组长) 
  1. 关闭打开的文件描述符
    进程从创建它的父进程那里继承了打开的文件描述符。如不关闭,将会浪费系统资源,造成进程所在的文件系统无法卸下以及引起无法预料的错误。按如下方法关闭它们: for(i=0;i 关闭打开的文件描述符close(i);
  2. 改变当前工作目录
    进程活动时,其工作目录所在的文件系统不能卸下。一般需要将工作目录改变到根目录。对于需要转储核心,写运行日志的进程将工作目录改变到特定目录如/tmp,chdir("/")
  3. 重设文件创建掩模
    进程从创建它的父进程那里继承了文件创建掩模。它可能修改守护进程所创建的文件的存取位。为防止这一点,将文件创建掩模清除:umask(0);
  4. 处理SIGCHLD信号
    处理SIGCHLD信号并不是必须的。但对于某些进程,特别是服务器进程往往在请求到来时生成子进程处理请求。如果父进程不等待子进程结束,子进程将成为僵尸进程(zombie)从而占用系统资源。如果父进程等待子进程结束,将增加父进程的负担,影响服务器进程的并发性能。在Linux下可以简单地将SIGCHLD信号的操作设为SIG_IGN。
    signal(SIGCHLD,SIG_IGN);
    这样,内核在子进程结束时不会产生僵尸进程。这一点与BSD4不同,BSD4下必须显式等待子进程结束才能释放僵尸进程。

3、创建守护进程

setsid的使用:

#include <unistd.h>
pid_t setsid(void);

以下程序是创建一个守护进程,然后利用这个守护进程每个一分钟向daemon.log文件中写入当前时间:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <time.h>
#include <fcntl.h>
#include <string.h>
#include <sys/stat.h>

#define ERR_EXIT(m) \
do \
{\
  perror(m);\
  exit(EXIT_FAILURE);\
}\
while(0);\

void create_deamon(void);

int main()
{
    time_t t;
    int fd;
    create_deamon();
    while(1)
    {
        fd = open("deamon.log", O_WRONLY | O_CREAT | O_APPEND, 0644);
        if (fd == -1)
        {
            ERR_EXIT("open error!");
        }
        t = time(0);
        char* buf = asctime(localtime(&t));
        write(fd, buf, strlen(buf));
        close(fd);
        sleep(60);
    }

    return 0;
}

void create_deamon()
{
    pid_t pid;
    pid = fork();
    if (pid == -1)
        ERR_EXIT("fork error!");
    if (pid > 0)
        exit(EXIT_SUCCESS);
    if (setsid() == -1)
        ERR_EXIT("setsid() error");
    chdir("/");

    for (int i = 0; i < 3; ++i)
    {
        close(i);
        open("dev/null", O_RDWR);
        dup(0);
        dup(0);
    }
    umask(0);
    return;

}
ubuntu@VM-188-113-ubuntu:~/Code/apue/ch13Deamon$ vim CreateDeamon.c
ubuntu@VM-188-113-ubuntu:~/Code/apue/ch13Deamon$ gcc CreateDeamon.c 
ubuntu@VM-188-113-ubuntu:~/Code/apue/ch13Deamon$ ./a.out 
ubuntu@VM-188-113-ubuntu:~/Code/apue/ch13Deamon$ ps -ef|grep a.out
ubuntu    7792  6797  0 09:38 pts/0    00:00:00 grep --color=auto a.out
ubuntu@VM-188-113-ubuntu:~/Code/apue/ch13Deamon$ su root
Password: 
root@VM-188-113-ubuntu:/home/ubuntu/Code/apue/ch13Deamon# ./a.out 
root@VM-188-113-ubuntu:/home/ubuntu/Code/apue/ch13Deamon# ps -ef|grep a.out 
root      8082     1  0 09:43 ?        00:00:00 ./a.out
root      8086  8067  0 09:43 pts/0    00:00:00 grep --color=auto a.out
root@VM-188-113-ubuntu:/home/ubuntu/Code/apue/ch13Deamon# cd /
root@VM-188-113-ubuntu:/# ls -l deamon.log 
-rw-r--r-- 1 root root 25 Oct 29 09:43 deamon.log
root@VM-188-113-ubuntu:/# cat deamon.log 
Sat Oct 29 09:43:06 2016
root@VM-188-113-ubuntu:/# cat deamon.log 
Sat Oct 29 09:43:06 2016
Sat Oct 29 09:44:06 2016

结果显示:当我一普通用户执行a.out时,进程表中并没有出现新创建的守护进程,但当我以root用户执行时,成功了,并在/目录下创建了daemon.log文件,cat查看后确实每个一分钟写入一次。为什么只能root执行,那是因为当我们创建守护进程时,已经将当前目录切换我/目录,所以当我之后创建daemon.log文件是其实是在/目录下,那肯定不行,因为普通用户没有权限,或许你会问那为啥没报错呢?其实是有出错,只不过我们在创建守护进程时已经将标准输入关闭并重定向到/dev/null,所以看不到错误信息。

4、利用库函数daemon()创建守护进程

#include <unistd.h>
int daemon(int nochdir, int noclose);
  • 功能:创建一个守护进程
  • nochdir:=0将当前目录更改至“/” noclose:=0将标准输入、标准输出、标准错误重定向至“/dev/null”
  • 成功返回0,失败返回1.
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <time.h>
#include <fcntl.h>
#include <string.h>
#include <sys/stat.h>

#define ERR_EXIT(m) \
do \
{\
  perror(m);\
  exit(EXIT_FAILURE);\
}\
while(0);\


int main()
{
    time_t t;
    int fd;
    if (daemon(0, 0) == -1)
        ERR_EXIT("daemon error!");
    while(1)
    {
        fd = open("deamon.log", O_WRONLY | O_CREAT | O_APPEND, 0644);
        if (fd == -1)
        {
            ERR_EXIT("open error!");
        }
        t = time(0);
        char* buf = asctime(localtime(&t));
        write(fd, buf, strlen(buf));
        close(fd);
        sleep(60);
    }

    return 0;
}

出错处理

守护进程存在的一个问题是如何处理出错消息。因为它本就不应该有控制终端,所以不能只是简单的写到标准错误上。我们不希望所有守护进程都写到控制台设备上,因为在很多的工作站上,控制台设备都运行着一个窗口系统。我们也不希望每个守护进程将它自己的出错信息写到一个单独的文件中。对任何一个系统管理人员而言,如果关系哪一个守护进程写到哪一个记录文件中并定期地检查这些文件,那么一定会使他感到头痛。故需要一个集中的守护进程出错记录设施。

三种产生日志消息的方法:

  • 内核例程可以调用log函数。任何一个用户进程通过打开(open)然后读(read)/dev/klog设备就可以读取这些消息。
  • 大多数用户进程(守护进程)调用syslog函数以产生日志消息。
  • 在此主机上的一个用户进程,或通过TCP/IP网络连接到此主机的其他主机上的一个用户进程可将日志消息发向UDP端口514。

使用syslogd守护进程可以读取所有3种格式的日志文件

#include <syslog.h>
void openlog(const char *ident, int option, int facility);
void syslog(int priority, const char *format, ...);
void closelog(void);
  • ident一般是程序的名称
  • option参数是指定许多选项的位屏蔽
  • priority参数是facility和level的组合
  • 调用openlog是可选择的。如果不调用openlog,则在第一次调用syslog时,自动调用openlog。
  • 调用closelog也是也选择的——它只是关闭曾被用于与syslog守护进程通信的描述符。
  • 调用openlog使我们可以指定一个ident,将它加至每则日志消息中。ident一般是程序的名称(例如,cron、inetd等)。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <syslog.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/resource.h>
#include <sys/stat.h>

#define ERR_EXIT(m)\
{\
    perror(m);\
    exit(EXIT_FAILURE);\
}\
while(0);\

int daemonize()
{
    int fd0, fd1, fd2;
    pid_t pid;
    struct rlimit rl;

    umask(0);

    if ((pid = fork()) <0)
    {
        ERR_EXIT("fork error!");
    }
    else if (pid != 0)
        exit(0);

    setsid();

    if (chdir("/") < 0)
        ERR_EXIT("can't change director to /");
    if (getrlimit(RLIMIT_NOFILE, &rl) < 0)
        ERR_EXIT("can't get file limit.");
    if (rl.rlim_max == RLIM_INFINITY)
        rl.rlim_max = 1024;
    for (unsigned i = 0; i < rl.rlim_max; ++i)
        close(i);

    fd0 = open("/dev/null", O_RDWR);
    fd1 = dup(0);
    fd2 = dup(0);

    return 0;
}

int main()
{
    daemonize();
    openlog("daemotest", LOG_PID, LOG_USER);
    syslog(LOG_INFO, "program started.");
    while(1)
    {
        sleep(1);
    }

    return 0;
}


ubuntu@VM-188-113-ubuntu:~/Code/apue/ch13DaemonProcesses$ gcc UseSyslog.c -o daemotestubuntu@VM-188-113-ubuntu:~/Code/apue/ch13DaemonProcesses$ ./daemotest
ubuntu@VM-188-113-ubuntu:~/Code/apue/ch13DaemonProcesses$ ps axj | grep daemotest
1 20370 20370 20370 ? -1 Ss 500 0:00 ./daemotest
19411 20382 20381 19411 pts/2 20381 S+ 500 0:00 grep --color=auto daemotest

可以看出daemotest进程的父进程是1,没有终端控制(TTY选项为“?”)。其记录在/var/log/messages文件中,可以在该文件中找到”program started”字样的记录。

你可能感兴趣的:(apue 第13章 守护进程)