您的位置:首页 > 大数据 > 人工智能

Mjpeg-streamer源码学习笔记-Main-守护进程Daemon(二)

2013-09-19 11:16 295 查看
目标文件:mjpg-stream/mjpg-stream.c + utils.c
这一篇的主要难点是main()中的一段守护进程daemon编程.
新手写,有不对的请大神指正,鼓励。

本人参考文章:
http://www.360doc.com/content/13/0913/13/13876325_314174121.shtml
本人参考书籍:
Apue第二版

一:Daemon引言
守护进程也叫精灵进程(Daemon),生存期长,系统自举时启动,仅在系统关闭时才终止。
最重要的特点没有控制终端,所以是在后台运行的

二:Daemon特征
先来查看一些常用的系统守护进程。
命令 ps -axj
-a 显示由其他用户所拥有的进程的状态
-x 显示没有控制终端的进程状态
-j 显示与作业有关的信息:会话ID,进程组ID,控制终端,终端进程组ID。



按照顺序,各列标题为:父进程ID,进程ID,进程组ID,会话ID,终端名称,终端进程组ID,状态,用户ID,命令字符串
父进程ID为0的进程通常是内核进程,通常作为系统自举过程的一部分而启动(init是例外,它是内核自举时启动的用户层命令)
进程1通常是init,它是一个系统守护进程,负责启动各运行层次特定的系统服务。

下面再介绍几个重要守护进程:
keventd:在内核中在运行计划执行的函数提供进程上下文。也称为页面调出守护进程(pageout daemon)
bdflushkupdataed:将高速缓存中的数据冲洗到键盘上
portmap:端口映射守护进程,提供RPC(Remote Procedure Call 远程过程调用)程序号映射为网络端口号的服务
inetd:(xinetd)它侦听系统网络接口,以便取得来自网络的对各种网络服务进程的请求
nfsd,lockdrpciod:提供对网络文件系统的支持
cron:在指定的日期和时间执行指定的命令
cupsd:是打印假脱机进程,它处理对系统提出的所有打印请求
注意:大部分守护进程都以超级用户(用户ID是0)特权运行。用户层守护进程缺少控制终端可能是守护进程调用了setsid的结果。所有用户层守护进程都是进程组的组长进程以及会话进程的首进程,而且是这些进程组合会话中的唯一进程。最后,应当引起注意的是大多数守护进程的父进程是init进程。

最后,守护进程的启动方式有其特殊之处------它可以在 Linux系统启动时从启动脚本/etc/rc.d中启动,

可以由作业规划进程crond启动,还可以由用户终端(通常是shell)执行。

总之,除开这些特殊性以外,守护进程与普通进程基本上没有什么区别,

因此,编写守护进程实际上是把一个普通进程按照上述的守护进程的特性改造成为守护进程。下面介绍编程规则。

三:编程规则
1.在后台运行。
调用fork,然后使父进程退出(exit).这样做实现了下面几点:
~1如果该守护进程是作为一条简单shell命令启动的,那么父进程终止使得shell认为这条命令已经执行完毕
~2子进程继承了父进程的进程组ID(PGID),但具有一个新的进程ID(PID),这就保证了子进程不是一个进程组的组长进程,这 对于下面就要做的setsid调用是必要的前提条件
if(pid=fork()) exit(0); //是父进程,结束父进程,子进程继续

2.脱离控制终端,登录会话和进程组
有必要先介绍一下Linux中的进程与控制终端,登录会话和进程组之间的关系:

进程属于 一个进程组,进程组号(GID)就是进程组长的进程号(PID)。

登录会话可以包含多个进程组。这些进程组共享一个控制终端。这个控制终端通常是创建进程的登录终端。
控制终端,登录会话和进程组通常是从父进程继承下来的。我们的目的就是要摆脱它们 ,使之不受它们的影响。

方法是在第1点的基础上,调用setsid()使进程成为会话组长:

setsid();

说明:当进程是会话组长时setsid()调用失败。但第2步已经保证进程不是会话组长。

setsid()调用成功后,进程成为新的会话组长和新的进程组长,并与原来的登录会话和进程组脱离。

由于会话过程对控制终端的独占性,进程同时与控制终端脱离。

总结起来,调用setsid以创建一个新会话。使调用进程进行三个操作:
~1 成为新会话的首进程
~2 成为一个新进程的组长进程
~3 脱离控制终端

3. 禁止进程重新打开控制终端

现在,进程已经成为无终端的会话组长。但它可以重新申请打开一个控制终端。

可以通过使进程不再成为会话组长来禁止进程重新打开控制终端:

if(pid=fork()) exit(0); //结束第一子进程,第二子进程继续(第二子进程不再是会话组长)

4. 关闭打开的文件描述符

进程从创建它的父进程那里继承了打开的文件描述符。

如不关闭,将会浪费系统资源, 造成进程所在的文件系统无法卸下以及引起无法预料的错误。

按如下方法关闭它们:
for(i=0;i < 关闭打开的文件描述符;i++)close(i);

或通过APUE的2-4程序open_max或7.11节getrlimit函数。来判定最高文件描述符值,并关闭直到该值的所有描述符.
---------------------------------------------------------
#include<sys/resource.h>
int getrlimit(int resource,struct rlimit *rlptr);
具体应用参数
RLIMIT_NPROC
---------------------------------------------------------

5. 改变当前工作目录更改为根目录
从父进程处继承过来的当前工作目录可能在一个装配文件系统中。因为守护进程通常在系统在引导之前是一直存在的,所以如果守护进程的当前工作目录在一个装配文件中,那么该文件系统就不能被拆卸,这与装配文件系统的原意不符。
所以进程活动时,其工作目录所在的文件系统不能卸下。一般需要将工作目录

改变到根目录 。对于需要转储核心,写运行日志的进程将工作目录改变到特定目录如/tmp

chdir("/")

6. 重设文件创建掩模
进程从创建它的父进程那里继承了文件创建掩模。它可能修改守护进程所创建的文件的存取位。

为防止这一点,将文件创建掩模清除:

umask(0);

介绍一下umask
-------------------------------------------------------------------------------
#inlcudeM<sys/stat.h>
mode_t umask(mode_t cmask);
-------------------------------------------------------------------------------

例如:
调用umask(S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
之后用creat创建的文件权限就是: -rw-------
在命令行中输入umask也会打印当前文件模式创建屏蔽字

7. 处理SIGCHLD信号
处理SIGCHLD信号并不是必须的。但对于某些进程,特别是服务器进程往往在请求到来时生成子进程处理请求。

如果父进程不等待子进程结束,子进程将成为僵尸进程(zombie )从而占用系统资源。

如果父进程等待子进程结束,将增加父进程的负担,影响服务器 进程的并发性能。

在Linux下可以简单地将SIGCHLD信号的操作设为SIG_IGN。

signal(SIGCHLD,SIG_IGN);
(在一个进程终止或者停止时,将SIGCHLD信号发送给其父进程)

这样,内核在子进程结束时不会产生僵尸进程。

这一点与BSD4不同,BSD4下必须显式等待子进程结束才能释放僵尸进程.

signal()介绍
--------------------------------------------------------------------------------------
#include<signal.h>
void (*signal(int signo,void(* func)(int)))(int)
参数signo:需要加载处理的信号的编号,如SIGKILL等
参数func:是一个函数的指针,这些函数通常是用户自己定义的
通常第二个参数有以下三种:
~1 SIG_IGN:表示忽略该信号
~2 SIG_DFL:表示使用默认的信号处理方式
~3 其他已定义的函数的指针
--------------------------------------------------------------------------------------

僵尸进程(zombie)
--------------------------------------------------------------------------------------
在UNIX 系统中,一个进程结束了,但是他的父进程没有等待(调用wait
/ waitpid)他, 那么他将变成一个僵尸进程。 但是如果该进程的父进程已经先结束了,那么该进程就不会变成僵尸进程, 因为每个进程结束的时候,系统都会扫描当前系统中所运行的所有进程, 看有没有哪个进程是刚刚结束的这个进程的子进程,如果是的话,就由Init 来接管他,成为他的父进程。

如果进程不调用wait / waitpid的话,那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程.
此即为僵尸进程的危害,应当避免。

僵尸进程的避免:
~1 父进程通过wait和waitpid等函数等待子进程结束,这会导致父进程挂起。
~2 如果父进程很忙,那么可以用signal函数为SIGCHLD安装handler,因为子进程结束后,
父进程会收到该信号,可以在handler中调用wait回收。
~3 如果父进程不关心子进程什么时候结束,那么可以用signal(SIGCHLD,SIG_IGN)
通知内核,自己对子进程的结束不感兴趣,那么子进程结束后,内核会回收,
并不再给父进程发送信号。
~4 还有一些技巧,就是fork两次,父进程fork一个子进程,然后继续工作,子进程fork一
个孙进程后退出,那么孙进程被init接 管孙进程结束后,init会回收。不过子进程的回收 还要自己做。
--------------------------------------------------------------------------------------

8.dev/null对守护进程的作用
关于/dev/null及用途
把/dev/null看作"黑洞". 它非常等价于一个只写文件.
所有写入它的内容都会永远丢失. 而尝试从它那儿读取内容则什么也读不到.
然而, /dev/null对命令行和脚本都非常的有用.
例如:
禁止标准输出
1 cat $filename >/dev/null
2 # 文件内容丢失,而不会输出到标准输出.
禁止标准错误
1 rm $badname 2>/dev/null
2 # 这样错误信息[标准错误]就被丢到太平洋去了.

至此,编程规则已介绍完。

四:分析APUE里的13-1程序,初始化一个守护进程

#include<syslog.h>
#include<fcntl.h>
#include<sys/recusore.h>

void daemonize(const char *cmd)
{
int i,fd0,fd1,fd2;
pid_t pid;
struct rlimit rl;
struct sigaction sa;

/* Clear file creation mask */ //把文件屏蔽字设置为0
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 TTY */ //防止进程重新打开终端
sa.sa_handle = 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);

/* Change the current working directory to the root */ //改变当前目录到根目录
if(chdir("/") < 0)
err_quit("%s : can't change directory to /",cmd);

/* 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 */ //打开dev/null使其具有文件描述符0,1,2
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);
}
}
以上就是全部代码。
运行程序
ps -axj
PPID PID PGID SID TTY TPGID UID COMMAND
1 3346 3345 3345 ? -1 501 a.out
ps -axj | grep 3345
1 3346 3345 3345 ? -1 501 a.out
会发现没有一个活动的ID是3345,这意味着,我们的守护进程在一个孤儿进程组中,它不是一个会话首进程,于是不会有机会分配打一个TTY,这是由于两次fork的原因。这个在我们说僵尸进程的避免时也提到了。由此可见,此守护进程已经被正确的初始化了。
然而所谓孤儿进程组:就是一个其父进程已终止的进程称为孤儿进程,这种进程由init进程“收养”。

下面 :简单说一下程序
~1 首先两个结构体,rlimit 和sigaction
-----------------------------------------------------------------------------------------
rlimit 是在APUE进程环境一章节中 函数getrlinmit中提到的
struct rlimit
{
rlim_t rlim_cur; /* soft limit : current limint */
rlim_t rlim_max; /* hard limit:maximum value for rlim_cur */
}
在更改资源限制时,须遵循下列三条规则:
1)任何一个进程都可将一个软限制更改为小于或等于其硬限制值。
2)任何一个进程都可降低其硬限制值,但它必须大于或等于软限制值。这种降低对普通用户而言是不可逆的。
3)只有超级用户可以提高硬限制值
其中,常量RLIM_INFINITY指定了一个无限量的限制
-----------------------------------------------------------------------------------------

-----------------------------------------------------------------------------------------

sigaction是结构体也是一个函数,信号安装函数,结构体同名
sigaction(int signum,const struct sigaction *act,struct sigaction *oldact)

sa_sigaction的原型是一个带三个参数,类型分别为int,struct siginfo *,void *,返回类型为void的函数指针。第一个参数为信号值;第二个参数是一个指向struct
siginfo结构的指针

代码中: if(sigaction(SIGHUP,&sa,NULL)
< 0)

struct sigaction
{

void (*sa_handler)(int); /* func pointer */

void (*sa_sigaction)(int, siginfo_t *, void *); /*func pointer */

sigset_t sa_mask;

int sa_flags;

void (*sa_restorer)(void);

}

~sa_handler的原型是一个参数为int,返回类型为void的函数指针。参数即为信号值,所以信号不能传递除信号值之外的任何信息;

代码中:sa.sa_handle =
SIG_IGN;
后面会代码中的此函数。

~sa_mask指定在信号处理程序执行过程中,哪些信号应当被阻塞。默认当前信号本身被阻塞。
代码中:sigemptyset(&sa.sa_mask);

~sa_flags包含了许多标志位,比较重要的一个是SA_SIGINFO,当设定了该标志位时,表示信号附带的参数可以传递到信号处理函数中。即使sa_sigaction指定信号处理函数,如果不设置SA_SIGINFO,信号处理函数同样不能得到信号传递过来的数据,在信号处理函数中对这些信息的访问都将导致段错误。

代码中:sa.sa_flags
= 0;
~sa_restorer已过时,POSIX不支持它,不应再使用。

sigemptyset()函数

int
sigemptyset(sigset_t *set);

sigemptyset()用来将参数set信号集初始化并清空。

执行成功则返回0,如果有错误则返回-1。

代码中:sigemptyset(&sa.sa_mask);
-----------------------------------------------------------------------------------------

~2 然后简单说一下dup,其他的代码在编程规则中都已讲清
-----------------------------------------------------------------------------------------
#include<unistd.h>
int dup(int filedes);
成功返回新的描述符,出错返回-1
注意:若由dup返回的新文件描述符一定是当前可用文件描述符中的最小值
-----------------------------------------------------------------------------------------

至此,程序13-1已经分析完了。继续看源码中的守护进程代码

五:源码中Daemon分析
继文章一中的代码继续往下贴。
/* fork to the background */

if(daemon) { /* 当命令后面设置了b命令时,daemon就会被置为1,即Switch中的case8,9 */

LOG("enabling daemon mode");

daemon_mode();

}

即分析一下该程序是否需要成为守护进程。

在源码utils.c中,贴上daemon_mode函数源码
void daemon_mode(void)

{

int fr = 0;

fr = fork();

if(fr < 0) { /* fork失败 */

fprintf(stderr, "fork() failed\n");

exit(1);

}

if(fr > 0) { /* 结束父进程,子进程继续 */

exit(0);

}

if(setsid() < 0) { /* 创建新的会话组,子进程成为组长,并与控制终端分离 */

fprintf(stderr, "setsid() failed\n");

exit(1);

}

fr = fork(); /* 防止子进程(组长)获取控制终端 */

if(fr < 0) {

fprintf(stderr, "fork() failed\n");

exit(1);

}

if(fr > 0) { /* 父进程,退出 */

fprintf(stderr, "forked to background (%d)\n", fr);

exit(0); /* 第二子进程继续执行 , 第二子进程不再是会会话组组长*/

}

/* 设置文件屏蔽字为0 */

umask(0);

/* 改变当前目录到根目录 */

fr = chdir("/");

if(fr != 0) {

fprintf(stderr, "chdir(/) failed\n");

exit(0);

}

/*为设置/dev/null 做准备 */

close(0);

close(1);

close(2);

/*设置/dev/null */

open("/dev/null", O_RDWR);

fr = dup(0);

fr = dup(0);

}

讲了13-1,这个就很熟悉了。其中有几步顺序打乱了,但不影响。
仔细分析后,源码更容易看懂,没13-1那么复杂。

下面,回到APUE,脱离源码,继续了解Daemon

六:Daemon的出错记录
与守护进程有关的一个问题是如何处理出错消息,因为它没有控制终端,所以不能只是简单地写到标准出错上。
解决这个问题的办法就是应用BSD syslog设施



大多数用户进程(守护进程)调用syslog(3)函数以产生日志消息。
该设施的接口是syslog函数,在文章一中已经介绍过了。
这一部分重要的就是syslog的参数option参数表,openlog的facility参数表,syslog中的level表.
其中还有2个函数:
~1 void closelog( void )
closelog:调用它是可选的--它只是关闭曾被用于与syslog守护进程通信的描述符(openlog也是可选的,如果不调用,那么第一次调用syslog的时候,自动调用openlog)

~2 int setlogmask(int maskpri)
setlogmask:用于设置进程的记录优先级屏蔽字,类似umask。注意,设置该屏蔽字为0并不产生任何作用。
然后回到代码。

七:关于SIGNAL部分(信号处理)
源码:
/* ignore SIGPIPE (send by OS if transmitting to closed TCP sockets) */

signal(SIGPIPE, SIG_IGN); /* 忽略SIGPIPE信号(当关闭TCP sockets时,OS会发送该信号) */

/* register signal handler for <CTRL>+C in order to clean up */

if(signal(SIGINT, signal_handler) == SIG_ERR) { 注册<CTRL>+C信号处理函数,来结束该程序

LOG("could not register signal handler\n"); /* 它的功能很直观:记录一些程序运行时信息,多数情况是用来辅助debug的
*/

closelog(); /* 前面提到的可选系统日志函数 */

exit(EXIT_FAILURE); /* 就是exit(1) */

}

/*

* messages like the following will only be visible on your terminal

* if not running in daemon mode

*/ 像下面的消息只会是可见的在你的终端,如果不是运行在Daemon模式下

#ifdef SVN_REV

LOG("MJPG Streamer Version: svn rev: %s\n", SVN_REV);

#else

LOG("MJPG Streamer Version.: %s\n", SOURCE_VERSION);

#endif

至此,守护进程部分的代码已经介绍完了。
在下一篇中讲继续讲源码的条件变量,全局变量_globals引出的插件,以及动态链接库问题。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: