嵌入式开发之linux---程序稳健性守护进程daemon 仅登录用户可见
(1)、为什么需要daemon守护进程
(2)、什么是守护进程daemon
(3)、守护进程daemon和一般进程有什么区别
(4)、linux下的daemon 实现
(5)、fork、wait,nohup
(6)、killall app 和kill -9 123 的区别
(7)、守护进程的实现方案
----------author:midu
-----------------dateTime:20210612
----------------------qq:1327706646
(1)、为什么需要daemon守护进程
总的来说就是维护进程的稳定性,提高程序的鲁棒性。
(2)、什么是守护进程daemon
Daemon程序是一直运行的服务端程序,又称为 守护进程。通常在系统后台运行,没有控制终端不与前台交互,Daemon程序一般作为 系统服务使用。Daemon是长时间运行的进程,通常在系统启动后就运行,在系统关闭时才结束。一般说Daemon程序在后台运行,是因为它没有控制终端,无法和前台的用户交互。Daemon程序一般都作为服务程序使用,等待客户端程序与它通信。我们也把运行的Daemon程序称作 守护进程。
(3)、守护进程daemon和一般进程有什么区别
1.因为daemon进程独立于终端,故使用ps axj命令查看进程时,其终端名(TTY)显示为?,终端前台进程组ID(TPGID)显示为-1
2.守护进程与用&结尾的后台运行程序有什么区别呢?
最大的区别有几点:
1)守护进程已经完全脱离终端控制台了,而后台程序并未完全脱离终端,在终端未关闭前还是会往终端输出结果
2)守护进程在关闭终端控制台时不会受影响,而后台程序会随用户退出而停止,需要在以nohug xxx & 格式运行才能避免影响
3)守护进程的会话组和当前目录,文件描述符都是独立的。后台运行只是终端进行了一次fork,让程序在后台执行,这些都没改变。
3.daemon函数存在的原因是因为控制终端由于某些原因(如断开终端链接)会发送一些信号的原因。而接收进程处理这些信号缺省动作会让进程退出。这些信号会由于终端上敲一些特殊按键而产生。
(4)、linux下的daemon 实现
编写Daemon程序有一些基本的规则,以避免不必要的麻烦。
1、fork父进程退出,首先是程序运行后调用fork,并让 父进程退出。子进程获得一个新的进程ID,但继承了 父进程的 进程组ID。
2、stsid跟终端脱离,调用setsid创建一个新的session,使自己成为新session和新 进程组的leader,并使进程没有控制终端(tty)。
3、umask关掉0,1,2文件描述符,改变当前工作目录至根目录,以免影响可加载文件系统。或者也可以改变到某些特定的目录。
4、chdir,设置文件创建mask为0,避免创建文件时权限的影响。
5、信号处理,关闭不需要的打开 文件描述符。因为Daemon程序在后台执行,不需要于 终端交互,通常就关闭STDIN、STDOUT和STDERR。其它根据实际情况处理。
这里只有1和 2是必须的。3一般都会做;5大部分不做。
注意:另一个问题是Daemon程序不能和终端交互,也就无法使用printf方法输出信息了。我们可以使用syslog机制来实现信息的输出,方便程序的调试。在使用syslog前需要首先启动syslogd程序,关于syslogd程序的使用请参考它的man page,或相关文档,我们就不在这里讨论了。
一种不带信号检测的:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
bool start_daemon()
{
int fd;
switch (fork()) {
case -1:
printf("fork() failed\n");
return false;
case 0:
break;
default:
exit(0);
}
/*
pid_t setsid(void);
进程调用setsid()可建立一个新对话期间。
如果调用此函数的进程不是一个进程组的组长,则此函数创建一个新对话期,结果为:
1、此进程变成该新对话期的对话期首进程(session leader,对话期首进程是创建该对话期的进程)。
此进程是该新对话期中的唯一进程。
2、此进程成为一个新进程组的组长进程。新进程组ID就是调用进程的进程ID。
3、此进程没有控制终端。如果在调用setsid之前次进程有一个控制终端,那么这种联系也被解除。
如果调用进程已经是一个进程组的组长,则此函数返回错误。为了保证不处于这种情况,通常先调用fork(),
然后使其父进程终止,而子进程继续执行。因为子进程继承了父进程的进程组ID,而子进程的进程ID则是新
分配的,两者不可能相等,所以这就保证了子进程不是一个进程组的组长。
*/
if (setsid() == -1) {
printf("setsid() failed\n");
return false;
}
switch (fork()) {
case -1:
printf("fork() failed\n");
return false;
case 0:
break;
default:
exit(0);
}
umask(0);
chdir("/");
long maxfd;
if ((maxfd = sysconf(_SC_OPEN_MAX)) != -1)
{
for (fd = 0; fd < maxfd; fd++)
{
close(fd);
}
}
fd = open("/dev/null", O_RDWR);
if (fd == -1) {
printf("open(\"/dev/null\") failed\n");
return false;
}
/*
// Standard file descriptors.
#define STDIN_FILENO 0 // Standard input.
#define STDOUT_FILENO 1 // Standard output.
#define STDERR_FILENO 2 // Standard error output.
*/
/*
int dup2(int oldfd, int newfd);
dup2()用来复制参数oldfd所指的文件描述符,并将它拷贝至参数newfd后一块返回。
如果newfd已经打开,则先将其关闭。
如果oldfd为非法描述符,dup2()返回错误,并且newfd不会被关闭。
如果oldfd为合法描述符,并且newfd与oldfd相等,则dup2()不做任何事,直接返回newfd。
*/
if (dup2(fd, STDIN_FILENO) == -1) {
printf("dup2(STDIN) failed\n");
return false;
}
if (dup2(fd, STDOUT_FILENO) == -1) {
printf("dup2(STDOUT) failed\n");
return false;
}
if (dup2(fd, STDERR_FILENO) == -1) {
printf("dup2(STDERR) failed\n");
return false;
}
if (fd > STDERR_FILENO) {
if (close(fd) == -1) {
printf("close() failed\n");
return false;
}
}
return true;
}
int main(int argc, char** argv)
{
start_daemon();
while (true)
{
sleep(100);
}
return 0;
}
# 可以看到 example_daemon 进程在后台运行,并且其父进程ID为1
$ ps -ef | grep example_daemon
https://segmentfault.com/a/1190000022770900 Linux守护进程(Daemon)介绍与C++实现
一种带信号检测的:
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <time.h>
#include <stdio.h>
static bool flag = true;
void create_daemon();
void handler(int);
int main()
{
time_t t;
int fd;
create_daemon();
struct sigaction act;
act.sa_handler = handler;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
if(sigaction(SIGQUIT, &act, NULL))
{
printf("sigaction error.\n");
exit(0);
}
while(flag)
{
fd = open("/home/mick/daemon.log",
O_WRONLY | O_CREAT | O_APPEND, 0644);
if(fd == -1)
{
printf("open error\n");
}
t = time(0);
char *buf = asctime(localtime(&t));
write(fd, buf, strlen(buf));
close(fd);
sleep(60);
}
return 0;
}
void handler(int sig)
{
printf("I got a signal %d\nI'm quitting.\n", sig);
flag = false;
}
void create_daemon()
{
pid_t pid;
//忽略I/O信号,STOP信号
signal(SIGTTOU,SIG_IGN);
signal(SIGTTIN,SIG_IGN);
signal(SIGTTSTP,SIG_IGN);
signal(SIGTTUP,SIG_IGN);
pid = fork();
if(pid == -1)
{
printf("fork error\n");
exit(1);
}
else if(pid)
{
exit(0);//结束父进程,是的子进程成为后台进程
}
if(-1 == setsid())//建立一个新的进程组,并是的该子进程成为该组的首进程,脱离所有终端
{
printf("setsid error\n");
exit(1);
}
//为什么要两次fork
pid = fork();//再次新建一个子进程,退出父进程,保证该进程不是进程组长,同事让该进程无法再打开一个新的终端
if(pid == -1)
{
printf("fork error\n");
exit(1);
}
else if(pid)
{
exit(0);
}
chdir("/");//改变工作目录,使得进程不与任何文件系统联系
int i;
for(i = 0; i < 3; ++i)//关闭所有从父进程继承的不再需要的文件描述符
{
close(i);
}
umask(0);//将文件当时创建屏蔽字设置为0
signal(SIGCHLD,SIG_IGN);
return;
}
https://www.zhangshengrong.com/p/q0arAP4L1x/ C++编写LINUX守护进程的实现代码
(5)、(fork、wait),nohup,(fork、execl),(sigemptyset,sigaction,struct sigaction act),getpid,getppid
捕获sigchld信号,判断子进程是否为僵尸信号还是释放后的僵尸信号,避免守护进程waitpid阻塞死等,可以让守护进程不断运行。
struct sigaction act;
sigemptyset,sigaction;
sigemptyset 函数初始化信号集合set,将set 设置为空.
sigaction函数用来查询和设置信号处理方式,它是用来替换早期的signal函数。sigaction函数原型及说明如下
sigfillset 也初始化信号集合,只是将信号集合设置为所有信号的集合.
sigaddset 将信号signo 加入到信号集合之中,
sigdelset 将信号从信号集合中删除.
sigismember 查询信号是否在信号集合之中.s
igprocmask 是最为关键的一个函数.在使用之前要先设置好信号集合set.
https://blog.csdn.net/isunbin/article/details/84032708 SIGCHLD信号
pid:
从参数的名字pid和类型pid_t中就可以看出,这里需要的是一个进程ID。但当pid取不同的值时,在这里有不同的意义。
pid>0时,只等待进程ID等于pid的子进程,不管其它已经有多少子进程运行结束退出了,只要指定的子进程还没有结束,waitpid就会一直等下去。
pid=-1时,等待任何一个子进程退出,没有任何限制,此时waitpid和wait的作用一模一样。
pid=0时,等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid不会对它做任何理睬。
pid<-1时,等待一个指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值。
(6)、killall app 和kill -9 123 的区别
1.如果用户使用killall 命令杀掉守护进程程序,那么监控守护进程和被监控应用进程都会被杀死。这种情况需要隔离守护进程和应用进程程序,不能通过守护进程fork子进程应用程序。
2.所有父进程为init的子进程(包括守护进程)均不能被kill -9 杀掉。
3. 除了wait信号检测外,代码检测还有如下方式:
while( 1 )
{
if( kill(pid_to_be_check, 0) < 0 ) system("重启命令");
sleep(10);
}
其中的pid_to_be_check 就是要检测的进程的PID
(7)、守护进程的实现方案
总的来说有两种:
1. 一种是通过shell脚本来检测pid进程号判断是否存在,不存在就调用执行路径命令启动程序,通过脚本,不断的ps -aux | grep "aaaa",然后启动进程
2. 另一种是通过程序内部监控应用程序,然后拉起程序,即开启另外一个监控进程,通过IPC来进程进程间keepalive,一旦发现被监控进程挂了,则重启之。监控进,这里又分为两种:
a. 守护进程直接fork子进程。
b. 守护进程通过心跳或是消息和应用程序进行通信,保持运行状态检测,一旦发现应用程序崩掉异常,守护进程命令fork子进程。
使用fork+execl+domian socket 配合进行父进程和子进程的创建以及程序运行状态监控;
守护进程程序中使用fork函数、fork会按照当前进程的资源复制一个子进程,通过execl函数调用想要运行程序来覆盖子进程;
fork函数会在两个进程中分别返回两个值;父进程中返回大于0值,子进程中返回==0值;错误返回-1
通过判断fork()==0判断当前线程是否为子进程,成立就使用execl函数调用想运行的程序;
通过判断fork()>0判断是否为父进程,成立就运行domain socket和子进程建立socket通信,父进程可以通过捕获sigchld信号+wait函数来是否子进程死后的僵尸进程和释放僵尸进程或者waitpid(x,x,WNOHANG)就不会阻塞死等了,可以让守护进程不断运行;
b方案的心跳通信方式,可以采用内存共享、消息队列等:
两个进程,ipc共享内存,内容为0,一个对内存内容 每秒加1,一个对内存每秒减1,这样基本上维持为1 0 -1,如果发现大于1小于-1,就说明一个程序死了,另一个程序就启动该程序,并把内存清0.这只是一种想法.还有别的方法.这个只供借鉴
a. 方案代码实现:
1). 首先使用fork系统调用,创建子进程
2). 在子进程中使用execv函数,执行需要自动重启的程序
3). 在父进程中执行wait函数等待子进程的结束,然后重新创建一个新的子进程
supervisor.c
/**
*
* supervisor
*
* date: 2016-08-10
*
*/
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <time.h>
#define LOG_FILE "/var/log/supervisor.log"
void s_log(char *text) {
time_t t;
struct tm *tm;
char *log_file;
FILE *fp_log;
char date[128];
log_file = LOG_FILE;
fp_log = fopen(log_file, "a+");
if (NULL == fp_log) {
fprintf(stderr, "Could not open logfile '%s' for writing\n", log_file);
}
time(&t);
tm = localtime(&t);
strftime(date, 127, "%Y-%m-%d %H:%M:%S", tm);
/* write the message to stdout and/or logfile */
fprintf(fp_log, "[%s] %s\n", date, text);
fflush(fp_log);
fclose(fp_log);
}
int main(int argc, char **argv) {
int ret, i, status;
char *child_argv[100] = {0};
pid_t pid;
if (argc < 2) {
fprintf(stderr, "Usage:%s <exe_path> <args...>", argv[0]);
return -1;
}
for (i = 1; i < argc; ++i) {
child_argv[i-1] = (char *)malloc(strlen(argv[i])+1);
strncpy(child_argv[i-1], argv[i], strlen(argv[i]));
//child_argv[i-1][strlen(argv[i])] = '0';
}
while(1) {
pid = fork();
if (pid == -1) {
fprintf(stderr, "fork() error.errno:%d error:%s", errno, strerror(errno));
break;
}
if (pid == 0) {
s_log(child_argv[0]);
ret = execv