守护进程
- linux中的守护进程
- 进程组和会话
- 编程流程
- 程序代码
- 单实例守护进程
守护进程(daemon)
是一类在后台运行的特殊进程,用于执行特定的系统任务。很多守护进程在系统引导的时候启动,并且一直运行直到系统关闭。另一些只在需要的时候才启动,完成任务后就自动结束(百度百科)
linux中的守护进程
当我们启动一个linux
系统后,会有那些守护进程呢
ps ,process status
ps -efj
- e,查看所有的进程
- f,输出格式化
- j,显示与作业有关的信息
- id为0的进程,通常是调度进程,也叫做
交换进程
。他是内核的一部分,但并不执行任何磁盘上的程序,因此也被称为系统进程 - id为1的进程通常为
init
进程。通常用来读取与系统有关的初始化文件(/etc/init.d/
,、etc/rc*
),并将系统引导到一个状态(多用户),他是以超级用户特权运行的普通用户进程,他是不会终止的 - id为2的进程是
页守护进程
,负责支持虚拟存储器系统的分页操作
在我们ps
命令的打印结果中,守护进程的名字都存在于方括号中
-
kswapd
守护进程,也称为内存换页守护进程。可以让虚拟内存子系统在一段时间后将脏页面慢慢的写回磁盘来进行回收
-
jdb
守护进程,帮助实现了ext4
文件系统中的日志功能
-
flush
守护进程,在内存达到设置的最小阈值时将脏页面冲洗到磁盘。也是定期将脏页面冲洗回磁盘来减少系统出现故障时发生的数据丢失 ( 那个比较便宜的云服务器里好像没有这个守护进程
4. sync_supers
守护进程,定期将文件系统元数据冲洗至磁盘
守护进程的特点:
- 大多数都是以
root
用户权限运行 - 守护进程都没有控制终端,他们的终端名称
TTY
,显示为?
内核守护进程以无终端的方式启动
用户层守护进程缺少控制终端,可能是因为调用了setsid
函数导致
- 大多数用户层的守护进程都是进程组的组长进程以及会话的手进程,而且是这些进程组合会话中的唯一进程(
rsyslogd
例外 - 用户层守护进程的父进程是
init
进程
进程组和会话
进程组
就是我们的每一个进程,除了有一个进程ID之外,还属于一个进程组。
一个进程组中可能有多个进程,他们接收来自同一终端的各种信号,每一个进程都有一个唯一的进程组ID,表明他是哪一个进程组的进程
- 获取进程组id的函数
#include <unistd.h>
pid_t getpgrp(void); /* POSIX.1 version */
pid_t getpgrp(pid_t pid); /* BSD version */
每一个进程都有一个组长进程,组长进程的进程组ID
组长进程可以创建一个进程组,并且可以在该进程组中创建进程,然后终止。而一个进程组中,只要有进程存在,那么该进程组都会存在,和组长进程是否存在无关
进程组的生命周期
,从创建一个进程组开始,到该进程组中最后一个进程退出。最后一个进程可以退出终止,也可以转移到其他的进程组
- 进程组设置函数
#include <unistd.h>
int setpgrp(void); /* System V version */
int setpgrp(pid_t pid, pid_t pgid); /* BSD version */
将pid
进程的进程组,设置为pgid
。成功返回0,失败返回-1
注意:
一个进程只能为他自己,或者他的子进程设置进程组ID。等到他得子进程调用exec
后,就不能给这个子进程设置了
会话
会话,一个或多个进程组的集合
一般可以由shell
管道,将几个进程变为一个进程组
proc4 | proc5 &
proc1 | proc2 | proc3
- 建立一个会话的函数
#include <unistd.h>
pid_t setsid(void);
在调用时,会分为两种情况:
调用此函数的进程不是一个进程组的组长。此函数创建一个新的会话
- 该进程变为新会话的首进程,此时他是这个会话的唯一进程
- 该进程成为新进程组的组长,进程组ID为自己的进程ID
- 该进程没有控制终端
而如果他是一个进程组的组长,则函数会出错
会话和进程组的特性
- 一个会话可以由一个控制终端。
- 建立与控制终端连接的会话首进程为控制进程
- 一个会话中的进程组可以分为一个前台进程组以及一个或者多个后台进程组
- 如果一个会话有一个控制终端,则他有一个前台进程组,其他进程组为后台进程组
- 无论什么时候,只有出现中断请求,中断信号都会发送给前台进程组的所有进程
编程流程
- 调用
umask
将文件模式创建屏蔽字设置为一个已知值,一般为0
防止这样一种情况,我们的守护进程想要创建一个文件,因为他是fork
后的一个子进程,他的文件模式创建屏蔽字是和当时的父进程息息相关的,而如果当时的父进程对于文件的权限可能有他自己的定义,然后子进程直接创建之后,可能会有一些权限无法执行
比如说,父进程屏蔽了创建组可读,可写
权限,然后子进程想要让创建组可读可写
,那么直接进行创建就会发生冲突
而文件模式创建屏蔽字在使用的时候,是用定义的权限 & ~umask
。
所以设置为0的话,就可以防止权限的问题被影响到
- 调用
fork
命令,使父进程exit
,子进程进行剩余的操作
这样做有以下的几个好处:
如果守护进程是作为一条简单的shell
命令启动的,那么父进程的终止会让shell
认为这条命令以经执行完毕。
虽然子进程继承了父进程的进程组ID,但获得了一个新的进程ID,保证了子进程不是一个进程组的组长进程
- 调用
setsid
创建一个新的会话。
然后让该进程变成新会话的首进程,成为一个新进程组的组长进程,没有控制终端
这里其实还可以进行一步操作,就是让该守护进程不是该会话首进程。就是在这里,再次调用fork
,让父进程终止,继续使用子进程中的守护进程,可以防止他取得控制终端
- 更改当前工作目录为根目录,或者其他指定目录
因为如果不更改工作目录的话,如果我们想要删除当前工作目录,就会因为当前目录中存在一个正在运行的进程,那么当前工作的文件是不能被删除的
解决方法就是,把他放到其他地方
- 关闭不需要的文件描述符
守护进程是父进程fork
调用之后过来的,同时也复制了父进程的文件描述符,所以需要关闭
- 某些守护进程,会打开
/dev/null
使其具有0,1,2
文件描述符,任何一个试图读标准输入,写标准输出或者标准错误的库例程都不会产生任何效果
就是防止一种现象,如果其他用户在同一终端设备中登录,并且在运行过程中突然显示了守护进程的输出,会不会很滑稽
程序代码
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <fcntl.h>
#include <sys/resource.h>
#include <sys/types.h>
#include <sys/stat.h>
void daemonize() {
umask(0); // 修改文件模式创建屏蔽字
// fork
pid_t fd = fork();
if(fd < 0) {
perror("fork");
exit(0);
} else if(fd != 0) {
// 父进程,退出
exit(0);
}
// 子进程
// 创建一个新的会话
setsid();
// 二次调用 fork ,脱离会话的组长
fd = fork();
if(fd < 0) {
perror("fork 2");
exit(0);
} else if(fd != 0) {
// 父进程
exit(0);
}
// 子进程
// 更改工作目录
if(chdir("/") < 0) {
perror("chdir");
exit(0);
}
// 关闭所有文件描述符
struct rlimit rl;
getrlimit(RLIMIT_NOFILE,&rl);
for(rlim_t i = 0; i < rl.rlim_max; i ++) {
close(i);
}
// 附加文件描述符
int fd0 = open("/dev/null",O_RDWR);
int fd1 = dup(0);
int fd2 = dup(0);
// 判断是否重定向成功
if(fd0 != 0 || fd1 != 1 || fd2 != 2) {
printf("dup error\n");
exit(0);
}
// 守护进程创建成功
}
但是,这样有一个问题,就是如果我们将该程序运行两次,那么会产生两个守护进程,并且他们的作用是一样的
单实例守护进程
为了避免,在同一时刻有两个守护进程的副本同时运行的情况。
比如,对于cron
守护进程来说,如果同时有多个实例运行,那么每个副本都可能试图执行某一个预定的操作,势必造成该操作重复执行,就很有可能出错
怎么解决呢?
文件和记录锁机制。
如果我们每创建一个守护进程,就让他打开一个固定名字的文件,在这个文件上加一个写锁。这就保证了唯一性,如果打开的这个文件占有写锁,那么就说明当前已经有守护进程运行,自己必须退出,不能影响当前守护进程
锁机制的函数
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
没错,又是这个函数。
对于记录锁的话,cmd
是F_GETLK
,F_SETLK
,F_SETLKW
。然后第三个参数指向flock
类型的一个指针
-
F_GETLK
。判断由flockptr
所描述的锁是否被另外一个锁排斥
如果存在,则组织当前由flockptr
指向的锁,现有锁的信息将重写flockptr
指向的信息
如果不存在,会将l_type
设置为f_UNLCK
-
F_SETLK
。设置由flockptr
描述的锁。
如果出现不兼容性,导致加锁失败,此时errno
会被设置为EACCES
或者EAGAIN
-
F_SETLKW
。是F_SETLK
的阻塞版本
但是,F_GETLK
判断是否可以加锁,到进行加锁的过程中,这一部分并不是原子操作的,所以必须对F_SETLK
的返回值进行处理
字段 | 含义 | 所选类型 |
l_type | 所希望的锁的类型 |
|
l_whence,l_start | 要加锁或者解锁一个区域的偏移量 |
|
l_len | 区域的字节长度 | 若为0,表示锁的范围可以扩展到最大可能的偏移量 |
l_pid | 进程的Id,持有的锁可以阻塞当前进程 | 仅由 |
关于锁的兼容性,其实都是差不多的
读锁 | 写锁 | |
无锁 | 允许 | 允许 |
一把或者多个读锁 | 允许 | 拒绝 |
一把写锁 | 拒绝 | 拒绝 |
/ 确保守护进程唯一
#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() {
int fd = open(LOCKFILE, O_RDWR | O_CREAT, LOCKMODE);
if(fd < 0) {
perror("open");
exit(-1);
}
if(lockfile(fd) < 0) {
if(errno == EACCES | errno == EAGAIN) {
close(fd);
return 1;
}
perror("fcntl");
exit(1);
}
// 写入当前守护进程 pid
ftruncate(fd,0);
char buf[16] = {'\0'};
sprintf(buf,"%ld",(long)getpid());
write(fd,buf,strlen(buf) + 1);
return 0;
}
注意:守护进程因为他的一些工作目录比较特殊,需要使用
root
用户进行启动