一、 守护进程,也就是通常所说的 Daemon 进程,是 Linux 中的后台服务进程。它是一个生存期较长的进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。守护进程常常在系统引导载入时启动,在系统关闭时终止守护进程还能完成许多系统任务,例如,作业规划进程 crond、打印进程 lqd 等(这里的结尾字母 d 就是 Daemon 的意思)。由于在 Linux 中,每一个系统与用户进行交流的界面称为终端,每一个从此终端开始运行的进程都会依附于这个终端,这个终端就称为这些进程的控制终端,当控制终端被关闭时,相应的进程都会自动关闭。但是守护进程却能够突破这种限制,它从被执行开始运转,直到整个系统关闭时才会退出。如果想让某个进程不因为用户、终端或者其他的变化而受到影响,那么就必须把这个进程变成一个守护进程。
守护进程创建步骤:
1、创建子进程,父进程退出
守护进程是脱离控制终端的,完成第一步后就会在shell终端里造成一种程序已经运行完毕的假象。之后所有的工作都在子进程中完成。而用户在shell终端里则可以执行其他的命令,从而在形式商做到了与控制终端的脱离。父进程先于子进程退出,会造成子进程没有父进程,从而变成了一个孤儿进程,在linux中,每当系统发现一个孤儿进程,就会自动由1号(也就是init进程)进程收养它,这样原先的子进程就会变成了init进程的子进程了。
关键代码:
pid = fork();
if (pid > 0)
{
exit(0); /*父进程退出*/
}
2、在子进程中创建新回话setsid()
使用系统函数setsid()函数,头文件:#include <sys/types.h> #include <unistd.h>, 函数原型:pid_t setsid(void),返回值:成功:该进程组 ID,出错:−1。
进程组:
进程组是一个或多个进程的集合。进程组由进程组 ID 来惟一标识。除了进程号(PID)之外,进程组 ID 也是一个进程的必备属性。每个进程组都有一个组长进程,其组长进程的进程号等于进程组 ID。且该进程ID 不会因组长进程的退出而受到影响。
会话期:会话组是一个或多个进程组的集合。通常,一个会话开始于用户登录,终止于用户退出,在此期间该用户运行的所有进程都属于这个会话期。
setsid()函数用于创建一个新的会话,并担任该会话组的组长。调用 setsid()有下面的 3 个作用:
让进程摆脱原会话的控制。
让进程摆脱原进程组的控制。
让进程摆脱原控制终端的控制。
创建守护进程的第一步,在那里调用了 fork()函数来创建子进程再令父进程退出。由于在调用 fork()函数时,子进程全盘复制了父进程的会话期、进程组和控制终端等,虽然父进程退出了,但原先的会话期、进程组和控制终端等并没有改变,因此,还不是真正意义上的独立,而 setsid()函数能够使进程完全独立出来,从而脱离所有其他进程的控制。
3、改变当前的目录为根目录chdir()
这一步也是必要的步骤。 使用 fork()创建的子进程继承了父进程的当前工作目录。由于在进程运行过程中,当前目录所在的文件系统(比如“/mnt/usb”等)是不能卸载的,这对以后的使用会造成诸多的麻烦(比如系统由于某种原因要进入单用户模式)。因此,通常的做法是让“/”作为守护进程的当前工作目录,这样就可以避免上述的问题,当然,如有特殊需要,也可以把当前工作目录换成其他的路径,如/tmp。改变工作目录的常见函数是 chdir()。
4、重新设置文件权限掩码umask(0)
文件权限掩码是指屏蔽掉文件权限中的对应位。比如,有一个文件权限掩码是050,它就屏蔽了文件组拥有者的可读与可执行权限。由于使用 fork()函数新建的子进程继承了父进程的文件权限掩码, 这就给该子进程使用文件带来了诸多的麻烦。 因此,把文件权限掩码设置为 0,可以大大增强该守护进程的灵活性。设置文件权限掩码的函数是 umask()。在这里,通常的使用方法为 umask(0)。
文件权限:
sfe1012@sfe1012-ThinkPad-Edge-E540:~$ ls -lh
总用量 35M
drwxrwxr-x 5 sfe1012 sfe1012 4.0K 1月 5 17:03 BeagleBone Black
-rw-r--r-- 1 sfe1012 sfe1012 8.8K 7月 7 2014 examples.desktop
-rw-rw-r-- 1 sfe1012 sfe1012 39 9月 7 21:22 fuck~
drwx------ 3 sfe1012 sfe1012 4.0K 12月 22 14:19 GPS Latest
drwx------ 3 sfe1012 sfe1012 4.0K 12月 19 14:52 HogCamShift
当执行ls -l 或 ls -al 命令后显示的结果中,最前面的第2~10个字符是用来表示权限。第一个字符一般用来区分文件和目录:
d:表示是一个目录,事实上在ext2fs中,目录是一个特殊的文件。
-:表示这是一个普通的文件。
l: 表示这是一个符号链接文件,实际上它指向另一个文件。
b、c:分别表示区块设备和其他的外围设备,是特殊类型的文件。
s、p:这些文件关系到系统的数据结构和管道,通常很少见到。
第2~10个字符当中的每3个为一组,左边三个字符表示所有者权限,中间3个字符表示与所有者同一组的用户的权限,右边3个字符是其他用户的权限。这三个一组共9个字符,代表的意义如下:
r(Read,读取):对文件而言,具有读取文件内容的权限;对目录来说,具有浏览目录的权
w(Write,写入):对文件而言,具有新增、修改文件内容的权限;对目录来说,具有删除、移动目录内文件的权限。
x(eXecute,执行):对文件而言,具有执行文件的权限;对目录了来说该用户具有进入目录的权限。
-:表示不具有该项权限。
-rwx------: 文件所有者对文件具有读取、写入和执行的权限。
-rwxr―r--: 文件所有者具有读、写与执行的权限,其他用户则具有读取的权限。
-rw-rw-r-x: 文件所有者与同组用户对文件具有读写的权限,而其他用户仅具有读取和执行的权限。
drwx--x--x: 目录所有者具有读写与进入目录的权限,其他用户近能进入该目录,却无法读取任何数据。
Drwx------: 除了目录所有者具有完整的权限之外,其他用户对该目录完全没有任何权限。
5、关闭文件描述符 close(i);
同文件权限掩码一样,用 fork()函数新建的子进程会从父进程那里继承一些已经打开了的文件。这些被打开的文件可能永远不会被守护进程读或写,但它们一样消耗系统资源,而且可能导致所在的文件系统无法被卸载。上面的第二步之后,守护进程已经与所属的控制终端失去了联系。因此从终端输入的字符不可能达到守护进程,守护进程中用常规方法(如 printf())输出的字符也不可能在终端上显示出来。所以,文件描述符为 0、1 和 2 的 3 个文件(常说的输入、输出和报错这
3 个文件)已经失去了存在的价值,也应被关闭。通常按如下方式关闭文件描述符:
for(i = 0; i < MAXFILE; i++)
{
close(i);
}
6、守护进程的流程图
开始---》fork()创建子进程,exit()使父亲进程退出---》setsid()创建新回话---》chdir(“/”)设置工作目录---》umask(),重新设置文件权限---》close()关闭文件描述符---》结束。
<pre name="code" class="cpp">#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<fcntl.h>
#include<sys/types.h>
#include<unistd.h>
#include<sys/wait.h>
//建立了一个守护进程,然后让该守护进程每
//隔 10s 向日志文件/tmp/daemon.log 写入一句话
//tail -f /tmp/daemon.log
int main()
{
pid_t pid;
int
i, fd;
char *buf = "This is a Daemon\n";
pid = fork(); /* 第一步 */
if (pid < 0)
{
printf("Error fork\n");
exit(1);
}
else if (pid > 0)
{
exit(0); /* 父进程推出 */
}
setsid(); /*第二步*/
chdir("/"); /*第三步*/
umask(0); /*第四步*/
for(i = 0; i < getdtablesize(); i++) /*第五步*/
{
close(i);
}
/*这时创建完守护进程,以下开始正式进入守护进程工作*/
int k =10;
while(k>0)
{
if ((fd = open("/tmp/daemon.log",O_CREAT|O_WRONLY|O_APPEND, 0600)) < 0)
{
printf("Open file error\n");
exit(1);
}
write(fd, buf, strlen(buf) + 1);
close(fd);
sleep(10);
k--;;
}
exit(0);
}
tail -f /tmp/daemon.log 查看守护进程运行状态sfe1012@sfe1012-ThinkPad-Edge-E540:~$ tail -f /tmp/daemon.log This is a DaemonThis is a DaemonThis is a DaemonThis is a DaemonThis is a DaemonThis is a DaemonThis is a DaemonThis is a DaemonThis is a Daemon
杀死进程
$ps -aux
$kill -s 9 XXXX
二、守护进程的出错处理
守护进程的具体调试过程中会发现,由于守护进程完全脱离了控制终端,因此,不能像其他普通进程一样将错误信息输出到控制终端来通知程序员,即使使用 gdb 也无法正常调试。那么,守护进程的进程要如何调试呢?一种通用的办法 是 使 用 syslog 服 务 , 将 程 序 中 的 出 错 信 息 输 入 到 系 统 日 志 文 件 中 ( 例 如 :“/var/log/messages”),从而可以直观地看到程序的问题所在。“/var/log/message”系统日志文件只能由拥有 root 权限的超级用户查看。在不注意同 Linux 发行版本中,系统日志文件路径全名可能有所不同,例如可能是”/var/log/syslog”syslog 是 Linux 中的系统日志管理服务,通过守护进程 syslogd 来维护。 该守护进程在启动时会读一个配置文件“/etc/syslog.conf”。该文件决定了不同种类的消息会发送向何处。例如,紧急消息可被送向系统管理员并在控制台上显示,而警告消息则可被记录到一个文件中。该机制提供了 3 个 syslog 相关函数,分别为 openlog()、syslog()和 closelog()。
/* syslog_daemon.c利用syslog服务的守护进程实例 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include <syslog.h>
int main()
{
pid_t pid, sid;
int i, fd;
char *buf = "This is a Dameon\n";
pid = fork(); /* 第一步 */
if (pid < 0)
{
printf("Error fork\n");
exit(1);
}
else if (pid > 0)
{
exit(0); /* 父进程推出 */
}
openlog("daemon_syslog", LOG_PID, LOG_DAEMON); /* 打开系统日志服务,openlog */
if ((sid = setsid()) < 0) /*第二步*/
{
syslog(LOG_ERR, "%s\n", "setsid");
exit(1);
}
if ((sid = chdir("/")) < 0) /*第三步*/
{
syslog(LOG_ERR, "%s\n", "chdir");
exit(1);
}
umask(0); /*第四步*/
for(i = 0; i < getdtablesize(); i++) /*第五步*/
{
close(i);
}
/*这时创建完守护进程,以下开始正式进入守护进程工作*/
while(1)
{
if ((fd = open("/tmp/daemon.log", O_CREAT|O_WRONLY|O_APPEND, 0600)) < 0)
{
syslog(LOG_ERR, "open");
exit(1);
}
write(fd, buf, strlen(buf) + 1);
close(fd);
sleep(10);
}
closelog();
exit(0);
}
读者可以尝试用普通用户的身份执行此程序,由于这里的 open()函数必须具有root 权限,因此,syslog 就会将错误信息写入到系统日志文件(例如“/var/log/messages”)中,如下所示:Jan
30 18:20:08 localhost daemon_syslog[612]: open
//TODO 实验中未找到 daemon_syslog系统日志文件 ,/var/log/messages这个也没有啊!!!!!
通过查阅资料可以知道,ubuntu系统可以在系统的 “搜索应用程序” 中输入“log” 就会出现“系统日志”syslog”了
守护进程实验 守护进程编写和调试的方法
首先建立起一个守护进程,然后在该守护进程中新建一个子进程,该子进程暂停 10s,然后自动退出,并由守护进程收集子进程退出的消息。在这里,子进程和守护进程的退出消息都在系统日志文件(日志文件的全路径名因版本的不同可能会有所不同,ubuntu可以在搜索中输入log 然后,打开syslog日志文件)中输出。子进程退出后,守护进 fork创建子进程2程循环暂停,其间隔时间为 10s。
/* daemon_proc.c */
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include <syslog.h>
int main(void)
{
pid_t child1,child2;
int i;
/*创建子进程1*/
child1 = fork();
if (child1 == 1)
{
perror("child1 fork");
exit(1);
}
else if (child1 > 0)
{
exit(0);
}
/*打开日志服务*/
openlog("daemon_proc_info", LOG_PID, LOG_DAEMON);
/*以下几步是编写守护进程的常规步骤*/
setsid();
chdir("/");
umask(0);
for(i = 0; i < getdtablesize(); i++)
{
close(i);
}
/*创建子进程2*/
child2 = fork();
if (child2 == 1)
{
perror("child2 fork");
exit(1);
}
else if (child2 == 0)
{
/*在日志中写入字符串*/
syslog(LOG_INFO, " child2 will sleep for 10s ");
sleep(10);
syslog(LOG_INFO, " child2 is going to exit! ");
exit(0);
}
else
{
waitpid(child2, NULL, 0);
syslog(LOG_INFO, " child1 noticed that child2 has exited ");
/*关闭日志服务*/
closelog();
while(1)
{
sleep(10);
}
}
}
查看系统日志文件syslog文件
Jan 14 16:53:06 sfe1012-ThinkPad-Edge-E540 daemon_proc_info[6742]: child2 will sleep for 10s
Jan 14 16:53:16 sfe1012-ThinkPad-Edge-E540 daemon_proc_info[6742]: child2 is going to exit!
Jan 14 16:53:16 sfe1012-ThinkPad-Edge-E540 daemon_proc_info[6741]: child1 noticed that child2 has exited
时间戳里清楚地看到 child2 确实暂停了 10s。
sfe1012@sfe1012-ThinkPad-Edge-E540:~$ ps -ef|grep daemon_proc
sfe1012 6741 1 0 16:53 ? 00:00:00 ./daemon_proc
sfe1012 8262 7304 0 16:56 ? 00:00:00 gedit /home/sfe1012/嵌入式linux应用程序开发标准教程/source/process/7-4-2/daemon_proc.c
sfe1012 8486 8364 0 16:58 pts/5 00:00:00 grep --color=auto daemon_proc
daemon_proc 确实一直在运行