进程

1
int main(int argc, char *argv[]);

当内核执行c程序时(使用一个exec函数),在调用main前先调用一个特殊的启动例程。可执行文件将此启动例程指定为程序的起始地址。启动例程从内核取得命令行参数和环境变量的值。然后为调用main函数做好准备。

进程终止有八种方式,其中五种为正常终止:

  • 正常终止
  • 从main返回
  • 调用exit
  • 调用_exit或_Exit
  • 最后一个线程从其启动例程返回
  • 最后一个线程调用pthread_exit

异常终止:

  • 调用abort
  • 接到一个信号
  • 最后一个线程对取消请求作出响应
1
2
3
4
5
#include<stdlib.h>
void exit(int status);
void _Exit(int status);
#include<unsitd.h>
void _exit(int status);

这三个函数用于正常终止一个程序:_exit_Exit立即进入内核,exit先执行一些清理处理,然后返回内核。

exit函数总是执行一个标准I/O库的清理关闭操作(对所有打开流调用fclose函数)。通过atexit函数向exit登记终止处理程序exit调用这些函数的顺序与它们登记的顺序相反。

1
2
#include<stdlib.h>
int atexit(void (*func)(void));
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
static void my_exit1(void);
static void my_exit2(void);

int main(void)
{
     if (atexit(my_exit2) != 0)
         err_sys("can't register my_exit2");

     if (atexit(my_exit1) != 0)
         err_sys("can't register my_exit1");

     if (atexit(my_exit1) != 0)
         err_sys("can't register my_exit1");

     printf("main is done\n");
     return(0);
}

static void my_exit1(void)
{
   printf("first exit handler\n");
}

static void my_exit2(void)
{
   printf("second exit handler\n");
}
output:
    main is done
    first exit handler
    first exit handler
    second exit handler

内核使程序执行的唯一方法是调用execl函数,进程自愿终止的唯一方法是显示或隐式地(exit)调用_exit_Exit

命令行参数的argv[0]为程序名。

1
2
3
4
5
6
7
8
int main(int argc, char *argv[])
{
    int     i;

    for (i = 0; i < argc;  i++)     /* echo all command-line args */
        printf("argv[%d]: %s\n", i, argv[i]);
    exit(0);
}

每个程序都接收到一张环境表,环境表也是一个字符指针数组,每个指针包含一个以null结束的C字符串的地址。

全局变量environ包含了该指针数组的地址

1
extern char **environ ;

每个字符串的结尾处都显式有一个null字节

我们称environ为环境指针,指针数组为环境表,其中各指针指向的字符串为环境字符串

使用getenvputenv函数来访问特定的环境变量。

c程序内存布局

历史沿袭至今,c成员有以下几部分组成:

  • 正文段(txt)。正文段通常是可共享,只读
  • 初始化数据段。例如任何之外的函数声明。int maxcount = 99
  • 未初始化数据段(bss)。long sum[1000]
  • 栈(stack)。
  • 堆(heap)。

未初始化数据段(bss)的内容不存放在磁盘上,内核在程序开始运行前将它们设置为0,需要存放在磁盘程序文件中的段只有正文段和初始化数据段。

size命令可查看各段长度(以字节为单位)。

存储空间分配

1
2
3
4
5
6
7
#include <stdlib.h>
//成功返回非空指针,失败返回null
void *malloc(size_t size);  //分配指定字节数的存储空间,此存储空间中的初始值不确定。
void *calloc(size_t nobj, size_t size);     //为指定数量指定长度的对象分配存储空间,每个bit位初始化为0。
void *realloc(void *ptr, size_t newsize);   //增加或减少以前分配区的长度。

void free(void *ptr);

环境变量

1
2
3
4
5
6
#include <stdlib.h>

char *getenv(const char *name);
int putenv(char *str);
int setenv(const char *name, const char *value, int rewrite);
int unsetenv(const char *name);

c语言中的goto语句是不能跨越函数的,setjmplongjmp可以实现跨越函数的跳转。

非局部goto setjmplongjmp,非局部指的是这不是由普通的c语言goto语句在一个函数内实施的跳转,而是在栈上跳过若干调用帧,返回到当前函数调用路径上的某一个函数中去。

1
2
3
4
#include<setjmp.h>
int setjmp(jmp_buf env);
//直接调用返回0,若conglongjmp返回,则为非0
void longjmp(jmp_buf, int val);

每个进程都有一组资源限制,其中一些可以用getrlimit和setrlimt查询和修改。

1
2
3
4
5
6
7
#include <sys/resource.h>
 struct rlimit {
    rlim_t  rlim_cur;   /* soft limit: current limit */
    rlim_t  rlim_max;   /* hard limit: maximum value for rlim_cur */
};
int getrlimit(int resource, struct rlimit *rlptr);
int setrlimit(int resource, const struct rlimit *rlptr);

进程控制

进程标识

每个进程都有一个非负整型表示的唯一进程ID。进程id是可复用的,复用使用延迟复用算法,防止新进程是原来的进程。

ID为0的进程通常是调度进程,常被称为交换进程(swapper)。该进程是内核的一部分,它并不执行任何磁盘上的程序,因此也被称为系统进程。

进程ID为1的通常是init进程,在自举过程结束时由内核调用。此进程负责在自觉内核后启动一个unix系统。init通常读取与系统有关的初始化文件(/etc/rc*文件或/etc/inittab文件或在/etc/init.d中的文件),init进程不会终止,它是一个普通的用户进程,以超级用户特权运行。

某些unix的虚拟存储器实现中,进程ID2是页守护进程(page daemon),此进程负责支持虚拟存储器系统的分页操作。

1
2
3
4
5
6
7
8
#include <unistd.h>

pid_t getpid(void);     //返回调用进程的进程id
pid_t getppid(void);    //返回进程的父进程id
uid_t getuid(void);     //返回进程的实际用户id
uid_t geteuid(void);    //返回进程的有效用户id
gid_t getgid(void);     //返回进程的实际组id
gid_t getegid(void);    //返回进程的有效组id

fork函数

一个现有的进程可以调用fork函数创建一个新进程。创建的新进程为子进程,fork函数执行一次,但返回两次。子进程返回值为0,父进程返回子进程的进程ID。

1
2
#include<unistd.h>
pid_t fork(void);

fork后的子进程获得父进程数据空间,堆和栈的副本(不共享)。这些副本不是立即分配,而是采用了一种写时复制(Copy-On-Write, COW)技术,内核将父子进程共享的部分设置为只读,当有进程试图修改这些区域时,内核为要修改的区域制作一个副本,通常是虚拟存储系统中的一页。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include "apue.h"

int     glob = 6;       /* external variable in initialized data */
char    buf[] = "a write to stdout\n";

int main(void)
{
    int       var;      /* automatic variable on the stack */
    pid_t     pid;

    var = 88;
    if (write(STDOUT_FILENO, buf, sizeof(buf)-1) != sizeof(buf)-1)
        err_sys("write error");
    printf("before fork\n");    /* we don't flush stdout */

    if ((pid = fork()) < 0) {
        err_sys("fork error");
    } else if (pid == 0) {      /* child */
        glob++;                 /* modify variables */
        var++;
    } else {
        sleep(2);               /* parent */
    }

    printf("pid = %d, glob = %d, var = %d\n", getpid(), glob, var);
    exit(0);
}

fork一个子进程会将父进程的所有打开文件描述符都复制到子进程中。父进程与子进程每个相同的文件描述符共享一个文件表项。父子进程还共享同一个文件偏移量

exit函数

除打开文件外,子进程还继承了一些父进程的属性:

  • 实际用户id,实际组id,有效用户id,有效组id。
  • 附属组id。
  • 进程组id。
  • 会话id。
  • 控制终端。
  • 设置用户id标志和设置组id标志。
  • 当前工作目录。
  • 根目录。
  • 文件模式创建屏蔽字。
  • 信号屏蔽和安排。
  • 对任一打开文件描述符的执行时关闭标志。
  • 环境。
  • 链接的共享内存。
  • 存储映像。
  • 资源限制。

一些区别:

  • fork的返回值不同。
  • 进程ID不变。
  • 子进程的tms_utime,tms_stime,tms_cutime和tms_ustime的值设置为0。
  • 子进程不继承父进程设置的文件锁。
  • 子进程的未处理闹钟被清除。
  • 子进程的未处理信号集设置为空集。

不管进程如何终止,最后都会执行内核中的同一段代码,为相应进程关闭所有打开描述符,释放它所使用的存储器等。

孤儿进程:如果父进程在子进程之前终止,它们的父进程到改变为init进程。

僵尸进程:如果子进程在父进程之前终止,但是其父进程尚未进行善后处理(终止子进程的相关信息,释放它占用的资源)就会成为(zombie)僵尸进程。

wait和waitpid函数

当一个进程正常或者异常终止时,内核就向其父进程发送SIGCHLD信号。

调用wait或waitpid会发生如下情况:

  • 如果其所有子进程都还在运行,则阻塞。
  • 如果一个子进程已经终止,正等待父进程获取其终止状态,则取得改子进程的终止状态后马上返回。
  • 如果没有任何子进程,则立即出错返回-1。
1
2
3
4
#include <sys/wait.h>
//成功返回进程ID,出错返回0或-1
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int *statloc, int options);

statloc是一个整形指针,如果statloc不是一个控指针,则终止进程的终止状态就存放在它所指向的单元内。如果不关心终止状态,则可以将参数指定未空指针。

这两个函数返回的整型状态字是由实现宏定义的。其终止状态可在如下宏来查看:

说明
WIFEXITED(status) 若为正常终止紫禁城返回的状态则为真
WIFSIGNALED(status) 若为异常终止紫禁城返回的状态则为真
WIFSTOPPED(status) 若为当前暂停子进程的返回状态则为真
WIFCONTINUED(status) 若在作业控制暂停后已经继续的子进程返回状态则为真
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#include "apue.h"
#include <sys/wait.h>

void pr_exit(int status)
{
    if (WIFEXITED(status))
        printf("normal termination, exit status = %d\n",
                    WEXITSTATUS(status));
    else if (WIFSIGNALED(status))
        printf("abnormal termination, signal number = %d%s\n",
                    WTERMSIG(status),
#ifdef  WCOREDUMP
                    WCOREDUMP(status) ? " (core file generated)" : "");
#else
                    "");
#endif
    else if (WIFSTOPPED(status))
        printf("child stopped, signal number = %d\n",WSTOPSIG(status));
}

waitpid函数提供了wait函数没有提供的3个功能:

  1. waitpid可等待一个特定的进程,而wait则返回任意终止子进程的状态。
  2. waitpid提供了一个wait的非阻塞版本。有时希望获取一个子进程的状态,但不想阻塞。
  3. waitpid通过WUNTRACED和WCONTINUED选项支持作业控制。(options参数提供)

pid参数的作用解释如下:

  • pid==-1 等待任意子进程。
  • pid>0 等待进程id和pid相等的子进程。
  • pid==0 等待组id等于调用组id的任一子进程。
  • pid<-1 等待组id等于pid绝对值的任一子进程。

其它wait类型函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include <sys/wait.h>
int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options) ;
        //成功返回0出错返回-1)
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/time.h>
#include <sys/resource.h>

pid_t wait3(int *statloc, int options, struct
 rusage *rusage);
 
pid_t wait4(pid_t pid, int *statloc, int options,
  struct rusage *rusage);

当多个进程企图对共享数据进行某种处理,而最后的结果取决于进程运行的顺序时,我们认为发生了竞争条件

exec函数

当使用fork创建新的子进程后,子进程往往需要调用一种exec函数以执行另一个程序。当进程调用一种exec函数的时候,该进程执行的程序完全替换未新程序,新程序从自己的main函数开始执行。因为调用exec并不创建新进程,所以前后的进程id并不改变。exec只是使用磁盘上的一个新程序替换了当前进程的正文段,数据段,堆段和栈段。

进程基本控制原语:使用fork创建新进程,用exec执行新的程序,exit和wait函数处理终止和等待终止。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#include<unistd.h>
//使用完整路径做参数
int execl(const char * pathname, const char *arg0, ...);
int execv(const char *pathname, char *const argv[]) ;
int execle(const char *pathname, const char *arg0) ;
int execve(const char *pathname, char *const argv[],char *const envp[]) ;
//使用PATH环节变量中的文件(或fd)做参数
int execlp(const char *filename, char *arg0, ...) ;
int execvp(const char *filename, char *const argv[]);
int fexecve(int fd, char *const argv[], char *const envp[]) ;
//出错返回-1,成功无返回

当使用filenname做参数的时候,如果filename包含/,则将其视为路径名,否则就按PATH环境变量,在它所指定的目录中搜索可执行文件。

如果execlp或execvp使用路径前缀中的一个找到了一个可执行文件,但是该文件不是由连接编辑器产生的机器可执行文件,则就认为该文件是一个shell脚本,于是试着调用sh并以该filename作为输入。

在执行exec后,进程ID没有改变,但新程序从调用进程继承了下列属性:

  • 进程ID和父进程ID
  • 实际用户ID和实际组ID
  • 附属组ID
  • 进程组ID
  • 会话ID
  • 控制终端
  • 闹钟尚余留的时间
  • 当前工作目录
  • 根目录
  • 文件模式创建屏蔽字
  • 文件锁
  • 进程信号屏蔽
  • 未处理信号
  • 资源限制
  • nice值
  • tms_utime,tms_stime,tms_cutime以及tms_cstime值

对于打开文件的处理与每个描述符的执行时(close-on-exec)标志有关(FD_CLOEXEC)标志。进程中每个打开描述符都有一个执行时关闭的标志。若设置了此标志,则在执行exec时关闭该描述符;否则该描述符仍打开。除非特地用fcntl设置了该执行时关闭标志,否则系统的默认操作是exec后仍保持这种描述符打开。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include "apue.h"
#include <sys/wait.h>

char    *env_init[] = { "USER=unknown", "PATH=/tmp", NULL };

int main(void)
{
    pid_t   pid;
    
    if ((pid = fork()) < 0) {
        err_sys("fork error");
    } else if (pid == 0) {/* specify pathname, specify environment */
        if (execle("/home/sar/bin/echoall", "echoall", "myarg1",
                "MY ARG2", (char *)0, env_init) < 0)
                err_sys("execle error");
    }
    
    if (waitpid(pid, NULL, 0) < 0)
        err_sys("wait error");
    
    if ((pid = fork()) < 0) {
        err_sys("fork error");
    } else if (pid == 0) {  /* specify filename, inherit environment */
        if (execlp("echoall", "echoall", "only 1 arg", (char *)0) < 0)
            err_sys("execlp error");
    }

    exit(0);
}

更改用户ID和更改组ID

1
2
3
4
5
6
#include <unistd.h>
int setuid(uid_t uid);  //实际用户ID和有效用户ID
int setgid(gid_t gid);  //实际组ID和有效组ID
int seteuid(uid_t uid); //只更改有效用户ID
int setegid(gid_t uid); //只更改有效组ID
//成功0,失败-1
  • 若进程具有超级用户特权,则setuid函数将实际用户ID,有效用户ID以及保存的设置用户ID设置为uid。
  • 若进程没有超级用户特权,但是uid等于实际用户ID或保存的设置用户ID,则setuid只将有效用户ID设置为uid。不更改实际用户ID和保存的设置用户ID。
  • 如果上述条件都不满足,则errno设置为EPERM,并返回-1。

system函数

1
2
#include<stdlib.h>
int system(const char *cmdstring);
  • fork失败或者waitpid返回除EINTR之外的出错,则system返回-1,并且设置errno以指示错误类型。
  • 如果exec失败(表示不能执行shell),则其返回值如果shell执行了exit(127)一样。
  • 否则所有3个函数(fork,exec和waitpid)都成功,那么system的返回值是shell的终止状态。

进程调度

进程可以通过调整nice值选择以更低优先级运行。只有特权进程允许提高调度权限。nice值越低,优先级越高。进程只能影响自己的nice值,不能影响任何其它进程的nice值。

1
2
#include<unistd.h>
int nice(int incr);

incr参数被增加到调用进程的nice值上。如果incr太大,系统直接把它降到最大合法值,不给提示。

getpriority函数可以像nice函数那样用于获取进程的nice值,但是getpriority还可以获取一组相关进程的nice值。成功返回nice值,失败返回-1。

1
2
#include<sys/resource.h>
int getpriority(int which, id_t who);

参数which取值:

  • PRIO_PROCESS表示进程
  • PRIO_PGRP表示进程组
  • PRIO_USER表示用户ID

getprioritywhich取值一样,设置nice值。

1
2
#include<sys/resource.h>
int setpriority(int which, id_t who, int value);
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#include<sys/times.h>

struct tms{
    clock_t tms_utime;  /* user cpu time */
    clock_t tms_stime;  /* system cpu time */
    clock_t tms_cutime; /* user cpu time, terminated children */
    clock_t tms_cstime; /* system cpu time,terminated children */
}

clock_t times(struct tms *buf);

进程关系

登录

终端登录

网络登录

进程组

进程组是一个或多个进程的集合。通常它们是在一个作业中被组合起来的。

同一个进程组中的各进程接收来自同一个终端的各种信号。

每个进程组有一个唯一的进程组ID(组长进程), 组长进程的进程组id等于其进程id。

1
2
3
#include<unistd.h>
pid_t getpgrp(void);
pid_t getpgid(pid_t pid);

getpgrp返回调用进程的进程组id,getpgid返回参数pid进程的进程组id。当pid是0时,返回调用进程的进程组id。getpgid(0) == getpgrp()

进程组组长可以创建一个进程组,创建该组中的进程,然后终止。只要在某个进程中有一个进程存在,则该进程组就存在,这与其组长进程是否终止无关。

进程调用setpgid函数可以加入一个现有的进程组或创建一个新进程组。setpgid函数将pid进程的进程组id设置为pgid。如果两个参数相等,则由pid指定的进程变成进程组组长。如果pid是0,则使用调用者的进程id。如果pgid是0,则由pid指定的进程id用作进程组id。

1
2
#include<unistd.h>
int setpgid(pid_t pid, pid_t pgid);

一个进程只能为它自或它的子进程设置组id,在他的子进程调用了exec后,他就不在更改该子进程的进程组id

会话

会话(session)是一个多个或多个进程组的集合,通常是由shell管道讲几个进程编程一组的。

进程调用setsid(void)创建一个新会话。

1
2
3
#include<unistd.h>
pid_t setsid(void);
pid_t getsid(pid_t pid);

如果调用此函数不是一个进程组的组长,则此函数创建一个新会话。

  • 该进程变成新会话的会话首进程(创建该会话的进程),此时,该进程是新会话中的唯一进程
  • 该进程成为一个新进程的组长进程,新进程组id是该调用进程的进程ID
  • 该进程没有控制终端

控制终端

  • 一个会话可以有一个控制终端(controlling terminal)
  • 建立与控制终端连接的会话首进程被称为控制进程(controlling process)
  • 一个会话中的几个进程组可被分成一个前台进程组和一个后台进程组
  • 无论何时键入终端的中断键(DELETE或Ctrl+C),都会将中断信号发送至前台进程组的所有进程
  • 无论何时键入终端的退出键(Ctrl+),都会将退出信号发送至前台进程组的所有进程
  • 如果终端接口检测到终端断开链接,则将挂断信号发送至终端控制进程

守护进程

守护进程(daemon)是生存期长的一种进程。

  • 首先要做的是调用umask将文件模式创建屏蔽字设置为一个已知值(通常是0)。继承而来的文件模式创建屏蔽字可能会被设置为拒绝某些权限,如果守护进程要创建文件,那么可能要设置特点的权限。
  • 调用fork,然后使父进程exit
  • 调用setsid创建一个新会话。
  • 将当前工作目录更改为根目录。
  • 关闭不再需要的文件描述符。
  • 某些守护进程打开/dev/null使其具有文件描述符0,1,2。这样任何一个试图读标准输入,写标准输出或标准错误的库例程都不会产生任何效果。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#include "apue.h"
#include <syslog.h>
#include <fcntl.h>
#include <sys/resource.h>

void daemonize(const char *cmd)
{
    int                 i, fd0, fd1, fd2;
    pid_t               pid;
    struct rlimit       rl;
    struct sigaction    sa;
    /*
     * Clear file creation mask.
     */
    umask(0);

    /*
     * 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 lose controlling TTY.
     */
    if ((pid = fork()) < 0)
        err_quit("%s: can't fork", cmd);
    else if (pid != 0) /* parent */
        exit(0);
    setsid();

    /*
     * 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");
    if ((pid = fork()) < 0)
        err_quit("%s: can't fork", cmd);
    else if (pid != 0) /* parent */
        exit(0);

    /*
     * Change the current working directory to the root so
     * we won't prevent file systems from being unmounted.
     */
    if (chdir("/") < 0)
        err_quit("%s: can't change directory to /");

    /*
     * 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);

    /*
     * Attach file descriptors 0, 1, and 2 to /dev/null.
     */
    fd0 = open("/dev/null", O_RDWR);
    fd1 = dup(0);
    fd2 = dup(0);

    /*
     * Initialize the log file.
     */
    openlog(cmd, LOG_CONS, LOG_DAEMON);
    if (fd0 != 0 || fd1 != 1 || fd2 != 2) {
        syslog(LOG_ERR, "unexpected file descriptors %d %d %d",
          fd0, fd1, fd2);
        exit(1);
    }
}

syslog出错记录

syslog组织架构

syslogd守护进程读取3种格式的日志消息:

  • 内核例程可以调用log函数
  • 大多数用户进程(守护进程)调用syslog函数产生日志消息。
  • 日志消息发向UDP514端口。

syslogd守护进程在启动时读取/etc/syslog.conf配置文件。

1
2
3
4
5
#include <syslog.h>
void openlog(const char *ident, int option, int facility);
void syslog(int priority, const char *format, ...);
void closelog(void);
int setlogmask(int maskpri);

openlog函数是可选的,如果不调用openlog,则在第一次调用syslog时,自动调用openlogcloselog函数也是可选的。

ident参数可以被加载至每条日志消息中。一般是程序的名称。

setlogmask函数用于设置进程的记录优先级屏蔽字。

level 说明
LOG_EMERG 紧急(系统不可使用)(最高优先级)
LOG_ALERT 必须立即修复的情况
LOG_CRIT 严重情况
LOG_ERR 出错情况
LOG_WARNING 警告情况
LOG_NOTICE 正常但重要的情况
LOG_INFO 信息性消息
LOG_DEBUG 调试消息(最低优先级)

单实例守护进程

某些守护进程在任一时刻只运行该守护进程的一个副本。文件和记录锁机制为一种方便的互斥机制,该方法保证了一个守护进程只有一个副本在运行。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <syslog.h>
#include <string.h>
#include <errno.h>
#include <stdio.h>
#include <sys/stat.h>

#define LOCKFILE "/var/run/daemon.pid"
#define LOCKMODE (S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)

extern int lockfile(int);

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", 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", LOCKFILE, strerror(errno));
        exit(1);
    }
    ftruncate(fd, 0);  //之前的守护进程实例的进程ID可能长于当前的进程ID
    sprintf(buf, "%ld", (long)getpid());
    write(fd, buf, strlen(buf)+1);
    return(0);
}
  • 若守护进程使用锁文件,那么该文件通常存储在/var/run目录中。
  • 若守护进程支持配置选项,那么配置文件通常存放在/etc目录中。
  • 守护进程可用命令行启动,但通常它们是由系统初始化脚本之一。/etc/rc*或/etc/init.d/*
  • 某些守护进程捕捉SIGHUP信号,当接收到该信号时,重新读配置文件。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
#include "apue.h"
#include <pthread.h>
#include <syslog.h>

sigset_t    mask;

extern int already_running(void);

void reread(void)
{
    /* ... */
}

void *thr_fn(void *arg)
{
    int err, signo;

    for (;;) {
        err = sigwait(&mask, &signo);
        if (err != 0) {
            syslog(LOG_ERR, "sigwait failed");
            exit(1);
        }

        switch (signo) {
        case SIGHUP:
            syslog(LOG_INFO, "Re-reading configuration file");
            reread();
            break;

        case SIGTERM:
            syslog(LOG_INFO, "got SIGTERM; exiting");
            exit(0);

        default:
            syslog(LOG_INFO, "unexpected signal %d\n", signo);
        }
    }
    return(0);
}

int main(int argc, char *argv[])
{
    int                 err;
    pthread_t           tid;
    char                *cmd;
    struct sigaction    sa;

    if ((cmd = strrchr(argv[0], '/')) == NULL)
        cmd = argv[0];
    else
        cmd++;

    /*
     * Become a daemon.
     */
    daemonize(cmd);

    /*
     * Make sure only one copy of the daemon is running.
     */
    if (already_running()) {
        syslog(LOG_ERR, "daemon already running");
        exit(1);
    }

    /*
     * Restore SIGHUP default and block all signals.
     */
    sa.sa_handler = SIG_DFL;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    if (sigaction(SIGHUP, &sa, NULL) < 0)
        err_quit("%s: can't restore SIGHUP default");
    sigfillset(&mask);
    if ((err = pthread_sigmask(SIG_BLOCK, &mask, NULL)) != 0)
        err_exit(err, "SIG_BLOCK error");

    /*
     * Create a thread to handle SIGHUP and SIGTERM.
     */
    err = pthread_create(&tid, NULL, thr_fn, 0);
    if (err != 0)
        err_exit(err, "can't create thread");
    /*
     * Proceed with the rest of the daemon.
     */
    /* ... */
    exit(0);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#include "apue.h"
#include <syslog.h>
#include <errno.h>

extern int lockfile(int);
extern int already_running(void);

void reread(void)
{
    /* ... */
}

void sigterm(int signo)
{
    syslog(LOG_INFO, "got SIGTERM; exiting");
    exit(0);
}

void sighup(int signo)
{
    syslog(LOG_INFO, "Re-reading configuration file");
    reread();
}
int
main(int argc, char *argv[])
{
    char                *cmd;
    struct sigaction    sa;
    if ((cmd = strrchr(argv[0], '/')) == NULL)
        cmd = argv[0];
    else
        cmd++;

    /*
     * Become a daemon.
     */
    daemonize(cmd);

    /*
     * Make sure only one copy of the daemon is running.
     */
    if (already_running()) {
        syslog(LOG_ERR, "daemon already running");
        exit(1);
    }

    /*
     * Handle signals of interest.
     */
    sa.sa_handler = sigterm;
    sigemptyset(&sa.sa_mask);
    sigaddset(&sa.sa_mask, SIGHUP);
    sa.sa_flags = 0;
    if (sigaction(SIGTERM, &sa, NULL) < 0) {
        syslog(LOG_ERR, "can't catch SIGTERM: %s", strerror(errno));
        exit(1);
    }
    sa.sa_handler = sighup;
    sigemptyset(&sa.sa_mask);
    sigaddset(&sa.sa_mask, SIGTERM);
    sa.sa_flags = 0;
    if (sigaction(SIGHUP, &sa, NULL) < 0) {
        syslog(LOG_ERR, "can't catch SIGHUP: %s", strerror(errno));
        exit(1);
    }

    /*
     * Proceed with the rest of the daemon.
     */
    /* ... */
    exit(0);
}