在Linux下,守护进程一般是随着系统启动而启动,直到系统关闭才关闭,没有控制终端,在后台运行。经常用作服务器进程。

编写一个守护进程一般有如下几步:

  1. 调用umask将文件模式创建屏蔽字设置为0.由继承得到的文件模式屏蔽字可能会拒绝设置某些权限。
  2. 调用fork,然后父进程退出(exit)。
  3. 调用setsid以创建一个新会话。
  4. 将当前工作目录更改为更目录。
  5. 关闭不需要的文件描述符。
  6. 某些守护进程打开/dev/null使其具有文件描述符0,1,2,这样,任何一个试图读标准输入,写标准输出或标准出错的库例程都不会产生任何效果。
    大致代码如下:
    void daemonize(const char cmd)
    {
    int i, fd0, fd1, fd2///fd0,fd1,fd2分表表示打开的文件描述符,正确的话分别是0,1,2
    pid_t pid//子进程idstruct rlimit rl;//getrlimit所需要的结构,查看文件描述符的最大值
    struct sigaction sa//siaaction结构
    / clear file creation mask. /
    umask(0);//设置umask值
    /
    Get maximum number of file descriptors.
    /
    if(getrlimit(RLIMIT_NOFILE, rl) < 0)//得到文件描述符的最大值
    err_quit(“%s: can’t get file limit”, cmd);/
    Become a session leader to loss controlling TTY.
    */
    if((pid = fork()) < 0)//创建一个子进程
    err_quit(“%s: can’t fork”, cmd);
    else if(pid != 0) //parent
    exit(0);//父进程退出
    setsid();//创建一个新会话,上面调用fork,父进程退出,所以保证流setsid的正确执行

printf(“setsid success\n);
/
Ensure future opens won’t allocate controlling TTYs.
*/
sa.sa_handler = SIG_IGN
sigemptyset(sa.sa_mask);
sa.sa_flags = 0
if(sigaction(SIGHUP, sa, NULL) < 0)//忽略控制终端的信号
err_quit(“%s: can’t ignore SIGHUP”, cmd);
if((pid = fork()) < 0)
err_quit(“%s: can’t fork”, cmd);
else if(pid != 0) //parent
exit(0);

printf(“second child\n);
/
change the current working directory to the root so
we won’t prevent file systems from being unmounted.
/
if(chdir(“/tmp”) < 0)//设置工作目录
err_quit(“%s: can’t change directory to /“, cmd);
else
printf(“chdir successed\n);
/
Close all open file descriptors.
/
if(rl.rlim_max == RLIM_INFINITY)
rl.rlim_max = 1024
for(i=0 i<rl.rlim_max; ++i)
close(i);//关闭所有的文件描述符
//从这里开始,所有的文件描述符都已经关闭流,因此printf的所有输出都不会有任何效果
printf(“rlimit_max:%d\n,rl.rlim_max);
/
Attach file descriptors 0, 1 and 2 to /dev/null
/
fd0 = open(“/dev/null”, O_RDWR);//这个会返回0
fd1 = dup(0);//返回1
fd2 = dup(0);//返回2

/
Initialize to log file.
*/
#if 1
printf(“before openlog\n);//这些已经没有效果了
openlog(cmd, LOG_CONS, LOG_DAEMON);//openlog是记录出错信息用的
printf(“after openlog\n);
if(fd0 != 0 || fd1 != 1 || fd2 != 2)//如果出错
{
syslog(LOG_ERR, “unexpected file descriptors %d %d %d”, fd0, fd1, fd2);
printf(“fd error\n);
exit(1);
}
#endif
}


这样我们就得到了一个守护进程了,可以在main函数里面调用,然后用ps查看结果。至于为什么需要利用两次fork。原因是第一次fork得到的子进程(first child)是它所在的session的组长,但是每个session的组长是可以控制一个终端的,只调用一次fork的话,那么就可能达不到我们的要求(守护进程不能有控制终端),第二次fork(first child会直接退出)得到的进程(second child)不是所在session的组长,不能控制终端,这样就确保了守护进程没有控制终端。

由于守护进程木有控制终端,所以出错记录需要另外记录,且要方便记录查看。这里就可以使用syslog来产生错误消息。上面代码中的openlog和syslog就是用来干这个事的,openlog是配置log文件的一些信息,syslog用来输出出错记录。

有时我们需要只运行守护进程的一个副本[如果有多个实例运行的话,可能导致任务运行多次,而产生错误],这样的话我们就需要用到单例守护进程。我们可以使用文件和记录锁机制来实现这个效果。即第一次运行的时候,给文件上锁,以后需要运行的时候,先查看文件是否上锁,如果上锁就退出,否则继续运行。代码如下

#define LOCKFILE “/var/run/daemon.pid”
#define LOCKMODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)int lockfile(int fd)//给文件上锁
{
struct flock fl
fl.l_type = F_WRLCK
fl.l_start = 0
fl.l_whence = SEEK_SET
fl.l_len = 0
return (fcntl(fd, F_SETLK, fl));
}
int already_running(void)
{
int fd
char buf[16];

fd = open(LOCKFILE, O_RDWR | O_CREAT, LOCKMODE);
if(fd < 0)
{
syslog(LOG_ERR, “can’t open %s: %s\n, LOCKFILE, strerror(errno));
exit(1);
}

if(lockfile(fd) < 0)
{
if(errno == EACCES || errno == EAGAIN)
{
close(fd);
return (1);
}
syslog(LOG_ERR, “can’t lock %s: %s\n, LOCKFILE, strerror(errno));
exit(1);
}

ftruncate(fd, 0);
sprintf(buf, “%ld”, (long)getpid());
write(fd, buf, strlen(buf) + 1);
return 0
}


这样的话,任何时候都只可能有一个进程副本在运行。

如果在守护进程中需要打开文件,而且调用openlog前先调用了chroot,那么怎么确保能够正确的打开文件呢?这个只需要用文件描述符就行了,文件描述符在chroot之后是不会更改的。

这篇文章大部分内容来自APUE第13章,更详细的内容请移步APUE。

Comments