时隔多年,每次碰到守护进程就想起当年,当年在大学学linux的时候,需要做个大作业,然后老师给了好多个题目,翻来翻去,发现就这个守护进程最简单,那就选守护进程吧。
选了守护进程的题目之后,发现还是不会做(哎,当年就没想过做linux相关的,真是人算不如天算)。不会做怎么办呢?那就找同学借鉴了(说是借鉴其实就是抄),然后就找到了叶某人的抄了过来,好像当时是完全抄过来的,因为当年确实对守护进程很懵逼。抄完了,那就交作业了。
交了作业,就开始答辩了,我们组是在前面答辩,答辩说了一些,也忘记具体说啥了,反正最后的评分,比抄叶某人的评分高了很多,抄的人分数反而更高,那时候嘲笑了叶某人好久,哈哈哈。
回忆总是美好的,现在该卷还是要卷,开始我们今天的守护进程之旅吧。
12.1 前后台进程
其实前后台进程,不应该放在这里讲的,不过都安排在这里了,就在这里吧,这样也好区别守护进程。
12.1.1 前台进程
先来看看前台进程,前台进程很简单就是运行在前台的,绑定了控制终端的,可以接受控制终端的信号。
我们来写一个前台进程:
#include <stdio.h>
int main()
{
printf("test\n");
while(1);
return 0;
}
当然真正的代码不能像我这样写,这是一个测试代码,我们接着来编译运行:
root@ubuntu:~/c_test/12
root@ubuntu:~/c_test/12
test
ls
ls
^C
root@ubuntu:~/c_test/12
这种就是前台进程,我们再来看看属性:
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
1415 1513 1513 1415 pts/0 1513 R+ 0 0:09 ./test
R+:表示正在运行的进程,+是前台进程
TTY:就是控制终端,pts就是网络终端
TPGID:进程连接到的tty(终端)所在的前台进程组的ID
12.1.2 后台进程
接着我们来看看后台进程,后台进程其实也比较简单,启动的时候加一个&,就可以了
root@ubuntu:~/c_test/12
[1] 1571
test
root@ubuntu:~/c_test/12
接着我们来看看状态:
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
1415 1571 1571 1415 pts/0 1415 R 0 0:19 ./test
STAT:后面没有+,说明不是前台进程。
TTY:但是终端还是pts/0
但是我们通过终端去发送中断信号,后台进程是接收不到的,只能是关闭终端,终端都关闭了,终端下的进程自然都会关闭。
这个实验,只能自己看了。
12.1.3 nohup命令
在上家公司工作的时候,就发现了这个命令来启动后端程序,现在我们来看看:
root@ubuntu:~/c_test/12
[1] 1673
root@ubuntu:~/c_test/12
root@ubuntu:~/c_test/12
nohup.out test test.c
root@ubuntu:~/c_test/12
好像有那么点像模像样,我们查看一下状态:
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
1653 1673 1673 1653 pts/0 1653 R 0 0:13 ./test
好像还是有控制终端,我们把控制终端关闭了测试一下。
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
1 1673 1673 1653 ? -1 R 0 5:42 ./test
好神奇哦,关闭了终端,TTY也跟着改了,这样看着就是真正的守护进程了。
nohup命令做了一下事情:
- 阻止
SIGHUP 信号发到这个进程。 - 关闭标准输入。该进程不再能够接收任何输入,即使运行在前台。
- 重定向标准输出和标准错误到文件
nohup.out 。
这一篇文章不错:[Linux 守护进程的启动方法]
12.2 守护进程
上面吹了这么多水,终于来到了今天的重点,守护进程的实现,我们在代码中实现守护进程,启动的的时候不用带nohup和&,直接./就可以了。
12.2.1 守护进程步骤
-
创建子进程,父进程退出(必须的) 有如下原因: 第一:父进程有可能是进程组组长(在命令行启动下是肯定的),从而不能够执行后面的setsid函数,子进程继承了父进程的进程组ID,所以子进程一定不是进程组组长,所以子进程一定可以执行setsid。 第二:父进程的退出,shell会以为这条命令执行结束了,从而让子进程在后台执行,也就是变成孤儿进程。 -
在子进程创建新会话(必须) 这一步是调用setsid函数,也是关键的一步,这一步是脱离了终端,因此终端发送的信号,都不会影响到子进程。子进程调用这个函数之后,会成为新会话的首进程,成为一个新进程组的组长进程,没有控制终端 -
修改当前目录为根目录(不是必须) 只有根目录是一定存在的,如果是其他目录,有可能存在卸载等问题,所以可以修改成根目录。chdir("/") -
重设文件权限掩码(不是必须) 文件权限掩码是继承父进程的,有可能父进程的权限有点低,所以为了增加守护进程的灵活性,需要重新设置文件掩码。umask(0)。 -
再次fork,父进程退出(不是必须) daemon可能会打开一个终端设备,这个打开终端设备可能会成为daemon进程的控制终端。既然如此,为了确保万无一失,只有确保daemon不是会话首进程,所以需要再次fork。 -
关闭文件描述符(不是必须) 文件描述符也是继承自父进程的,在守护进程中是不需要这些的,所以都需要关闭。标准输入,标准输出,标准错误这些都不需要,统一关闭。
12.2.2 守护进程的简单实现
先按照上面的步骤写一波守护进程的实现
#include <unistd.h>
#include <stdio.h>
#include <sys/resource.h>
#include <fcntl.h>
void daemonize()
{
pid_t pid = -1;
struct rlimit rl;
if(getrlimit(RLIMIT_NOFILE, &rl) < 0)
printf("getrlimit err\n");
pid = fork();
if(pid < 0)
{
printf("fork err\n");
return 0;
} else if(pid > 0)
{
_exit(0);
}
setsid();
chdir("/");
umask(0);
if((pid = fork()) < 0)
{
printf("fork err\n");
return 0;
} else if(pid > 0)
{
_exit(0);
}
if(rl.rlim_max == RLIM_INFINITY)
rl.rlim_max = 1024;
for(int i=0; i<rl.rlim_max; i++)
close(i);
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
}
就这样吧。
12.2.3 守护进程的进化版
有些守护进程需要做单实例,可以使用文件和记录锁来做单实例,第一次启动守护进程,就会创建文件,并且加一把写锁,之后如果再有守护进程创建,再去写这个文件,就会失败,从而做到了单实例。
还有守护进程有以下的惯例:
- 若守护进程使用锁文件,那么该文件通常存储在/var/run目录中,需要超级用户权限才能在此目录下创建文件,锁文件名字通常是name.pid
- 若守护进程支持配置选项,那么配置文件通常存放在/etc目录中,配置文件名字为name.conf
- 守护进程可用命令行启动,但是也可以在系统初始化脚本启动。(/etc/rc*或/etc/init.d/*)
- 守护进程会捕捉SIGHUP信号,然后重新读取配置文件。
写了一个简单的例子,不是完全的守护进程的例子。等之后做项目的时间再写个完整的。
#include <unistd.h>
#include <stdio.h>
#include <sys/resource.h>
#include <fcntl.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
void daemonize()
{
pid_t pid = -1;
struct rlimit rl;
if(getrlimit(RLIMIT_NOFILE, &rl) < 0)
printf("getrlimit err\n");
pid = fork();
if(pid < 0)
{
printf("fork err\n");
return ;
} else if(pid > 0)
{
_exit(0);
}
setsid();
chdir("/");
umask(0);
if((pid = fork()) < 0)
{
printf("fork err\n");
return ;
} else if(pid > 0)
{
_exit(0);
}
if(rl.rlim_max == RLIM_INFINITY)
rl.rlim_max = 1024;
for(int i=0; i<rl.rlim_max; i++)
close(i);
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
}
#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)
{
printf("fd open\n");
exit(1);
}
if(lockfile(fd) < 0)
{
if(errno == EACCES || errno == EAGAIN) {
close(fd);
return 1;
}
printf("lockfile\n");
exit(1);
}
ftruncate(fd, 0);
sprintf(buf, "%ld", (long)getpid());
write(fd, buf, strlen(buf)+1);
return 0;
}
int main()
{
daemonize();
while(1);
return 0;
}
12.2.4 glibc中的daemon
其实在glibc中也封装了一个daemon函数,从而帮我们将程序转化成daemon进程。
int
daemon (int nochdir, int noclose)
{
int fd;
switch (__fork()) {
case -1:
return (-1);
case 0:
break;
default:
_exit(0);
}
if (__setsid() == -1)
return (-1);
if (!nochdir)
(void)__chdir("/");
if (!noclose) {
struct stat64 st;
if ((fd = __open_nocancel(_PATH_DEVNULL, O_RDWR, 0)) != -1
&& (__builtin_expect (__fxstat64 (_STAT_VER, fd, &st), 0)
== 0)) {
if (__builtin_expect (S_ISCHR (st.st_mode), 1) != 0
#if defined DEV_NULL_MAJOR && defined DEV_NULL_MINOR
&& (st.st_rdev
== makedev (DEV_NULL_MAJOR, DEV_NULL_MINOR))
#endif
) {
(void)__dup2(fd, STDIN_FILENO);
(void)__dup2(fd, STDOUT_FILENO);
(void)__dup2(fd, STDERR_FILENO);
if (fd > 2)
(void)__close (fd);
} else {
__close_nocancel_nostatus (fd);
__set_errno (ENODEV);
return -1;
}
} else {
__close_nocancel_nostatus (fd);
return -1;
}
}
return (0);
}
其实看这个源码跟我们上面讲的也差不多,只不多这是有两个参数。
nochdir:用来控制是否将当前目录切换到根目录。(看代码也理解了)
noclose :用来控制是否将标准输入,标准输出,标准错误重定向到/dev/null。(这个看代码也理解)
12.3 总结
虽然这篇守护进程的代码没有写全,但是原理也都介绍了,具体到项目的时候,在把守护进程写全,因为服务器的代码基本都是守护进程的方式存在的,不急,未来的路还很长。
|