Linux编写守护进程获取进程信息
文章目录
目的
记录系统运行期间所有运行的进程,记录信息包括:进程PID,可执行程序名称,用户名,创建时间,撤销时间,开机能自启动
守护进程
进程
- 每个进程都有一个父进程
- 当子进程终止时,父进程会得到通知并能取得子进程的退出状态。
进程组
- 每个进程也属于一个进程组
- 每个进程主都有一个进程组号,该号等于该进程组组长的PID号
- 一个进程只能为它自己或子进程设置进程组ID号
会话期
- 对话期(session)是一个或多个进程组的集合。
- setsid()函数可以建立一个对话期:
- 如果,调用setsid的进程不是一个进程组的组长,此函数创建一个新的会话期。
(1)此进程变成该对话期的首进程
(2)此进程变成一个新进程组的组长进程。
(3)此进程没有控制终端,如果在调用setsid前,该进程有控制终端,那么与该终端的联系被解除。如果该进程是一个进程组的组长,此函数返回错误。
(4)为了保证这一点,我们先调用fork()然后exit(),此时只有子进程在运行,
子进程继承了父进程的进程组ID,但是进程PID却是新分配的,所以不可能是新会话的进程组的PID。从而保证了这一点。
if((pid=fork())>0) //parent
exit(0);
else if(pid==0){ //th1 child
setsid(); //th1是成为会话期组长
if(fork() ==0){ //th2不会是会话期组长(变成孤儿进程组)
...
}
}
守护进程及其特性
- 守护进程最重要的特性是后台运行。在这一点上DOS下的常驻内存程序TSR与之相似。
- 其次,守护进程必须与其运行前的环境隔离开来。这些环境包括未关闭的文件描述符,控制终端,会话和进程组,工作目录以及文件创建掩模等。这些环境通常是守护进程从执行它的父进程(特别是shell)中继承下来的。
- 最后,守护进程的启动方式有其特殊之处。它可以在Linux系统启动时从启动脚本/etc/rc.d中启动,
可以由作业规划进程crond启动,还可以由用户终端(通常是 shell)执行。 - 总之,除开这些特殊性以外,守护进程与普通进程基本上没有什么区别。
因此,编写守护进程实际上是把一个普通进程按照上述的守护进程的特性改造成为守护进程。
守护进程的编程要点 (来自UEAP)
前面讲过,不同Unix环境下守护进程的编程规则并不一致。所幸的是守护进程的编程原则其实都一样,区别在于具体的实现细节不同。这个原则就是要满足守护进程的特性。同时,Linux是基于Syetem V的SVR4并遵循Posix标准,实现起来与BSD4相比更方便。编程要点如下;
-
在后台运行。
为避免挂起控制终端将Daemon放入后台执行。方法是在进程中调用fork使父进程终止,让Daemon在子进程中后台执行。
if(pid=fork())
exit(0); //是父进程,结束父进程,子进程继续
-
脱离控制终端,登录会话和进程组
进程属于一个进程组,进程组号(GID)就是进程组长的进程号(PID)。登录会话可以包含多个进程组。这些进程组共享一个控制终端。这个控制终端通常是创建进程的登录终端。控制终端,登录会话和进程组通常是从父进程继承下来的。我们的目的就是要摆脱它们,使之不受它们的影响。方法是在第1点的基础上,调用setsid()使进程成为会话组长:
setsid();
说明:当进程是会话组长时setsid()
调用失败。但第一点已经保证进程不是会话组长 setsid()
调用成功后,进程成为新的会话组长和新的进程组长,并与原来的登录会话和进程组脱离。由于会话过程对控制终端的独占性,进程同时与控制终端脱离。
-
禁止进程重新打开控制终端
现在,进程已经成为无终端的会话组长。但它可以重新申请打开一个控制终端。
可以通过使进程不再成为会话组长来禁止进程重新打开控制终端:
if(pid=fork())
exit(0); //结束第一子进程,第二子进程继续(第二子进程不再是会话组长)
-
关闭打开的文件描述符
进程从创建它的父进程那里继承了打开的文件描述符。如不关闭,将会浪费系统资源,造成进程所在的文件系统无法卸下以及引起无法预料的错误。按如下方法关闭它们:
for(i = 0;i<NOFILE;i++) //关闭打开的文件描述符
close(i);
-
改变当前工作目录
进程活动时,其工作目录所在的文件系统不能卸下。一般需要将工作目录改变到根目录对于需要转储核心,写运行日志的进程将工作目录改变到特定目录如/tmp
。
chdir("/")
-
重设文件创建掩模
进程从创建它的父进程那里继承了文件创建掩模。它可能修改守护进程所创建的文件的存取位。为防止这一点,将文件创建掩模清除。
umask(0);
-
处理SIGCHLD信号
处理SIGCHLD
信号并不是必须的。 但对于某些进程,特别是服务器进程往往在请求到来时生成子进程处理请求。如果父进程不等待子进程结束,子进程将成为僵尸进程(zombie)从而占用系统资源。如果父进程等待子进程结束,将增加父进程的负担,影响服务器进程的并发性能。在Linux下可以简单地将SIGCHLD
信号的操作设为SIG_IGN
。
signal(SIGCHLD,SIG_IGN);
这样,内核在子进程结束时不会产生僵尸进程。 这一点与BSD4不同,BSD4下必须显式等待子进程结束才能释放僵尸进程。
实例
编写创建守护进程的函数
(博主将文件放在init.c
文件中
#include<unistd.h>
#include<signal.h>
#include<stdio.h>
#include<sys/param.h>
#include<sys/stat.h>
#include <stdlib.h>
void init_daemon()
{
int pid;
int i;
if (pid = fork())
exit(0); //结束父进程
else if (pid < 0)
exit(1); //fork失败,退出
//第一个子进程,后台继续执行
setsid(); //第一个子进程成为新的会话组长和进程组长并与控制终端分离
if(pid = fork())
exit(0); //结束第一个子进程
else if(pid < 0)
exit(1); //fork失败,退出
//第二个子进程,继续,此进程不再是回话组长
for(i = 0;i<NOFILE;i++) //关闭打开的文件描述符
close(i);
chdir("/"); //改变工作目录到/
umask(0); //重设文件创建掩模
return ;
}
通过处理ps
命令获取进程的信息
(此文件在pid.c
文件中
#define _XOPEN_SOURCE 700 //解决strptime函数不能用的问题
#include <stdlib.h>
#include<stdio.h>
#include <unistd.h>
#include <stdio.h>
#include <dirent.h>
#include <string.h>
#include <sys/stat.h>
#include <stdlib.h>
#include<time.h>
#include<sys/types.h>
#include<string.h>
#define BUFF_LEN 1024 //行缓冲区的长度
typedef struct{
pid_t pid; //进程的pid
char user[20]; //进程用户名
char name[256]; //进程名称
char stime[50]; //开始时间
char etime[50]; //撤销时间
}proc_info_st;
void init_daemon(); //守护进程初始化函数
void get_endTime(char *stime,char *etime,char *string); //时间转换
int main(int argc, char *argv[])
{
init_daemon(); //初始化为Daemon,守护进程的创建
while(1){
FILE *fo;
fo = fopen("/tmp/test.log","w"); //初始化输出文件
fclose(fo);
FILE *fp;
char buffer[BUFF_LEN]; //存储文件信息
fp = popen("ps -eo pid,user,comm,etime,lstart","r"); //读取信息
int i=0;
while(fgets(buffer,sizeof(buffer),fp)){
if(i == 0){ //第一行为标题,舍弃
i++;
continue;
}
proc_info_st info;
char e[50]; //存储进程运行时间
sscanf(buffer,"%d\t%s\t%s\t%s\t%[^\n]",
&info.pid,info.user,info.name,e,info.stime);
get_endTime(info.stime,e,info.etime); //将运行时间和开始时间相加,得到结束时间
if((fo = fopen("/tmp/test.log","a")) >= 0){ //输出到test.log文件中
fprintf(fo, "pid:%d\nuser:%s\nname:%s\nstart_time:%s\nend_time:%s\nrun_time:%s\n\n\n",
info.pid,info.user,info.name,info.stime,info.etime,e);
fclose(fo);
}
}
fclose(fp);
sleep(60); //每隔1min,保存一下信息
}
return 0;
}
void get_endTime(char *stime,char *etime,char *string)
{
struct tm s,e;
memset(&s, 0, sizeof(s));
memset(&e, 0, sizeof(e));
strptime(stime, "%a\t%b\t%d\t%H:%M:%S\t%Y", &s);
time_t ee = 0;
if(strlen(etime) > 6) {
strptime(etime, "%H:%M:%S", &e);
ee+=e.tm_hour * 3600 + e.tm_min * 60 + e.tm_sec;
}
else{
strptime(etime, "%M:%S", &e);
ee+=e.tm_min * 60 + e.tm_sec;
}
time_t ss = mktime(&s);
time_t all = ss + ee + 8 * 3600;
struct tm *a;
a = gmtime(&all);
strftime(string, 50, "%a\t%b\t%d\t%H:%M:%S\t%Y",a);
}
通过命令行编译文件
gcc -g -o pid init.c pid.c
./pid
查看/tmp
下,test.log
文件是否有输出
若成功,结果如下
设置开机自启动
Ubuntu18.04
不再使用initd
管理系统,改用systemd
.
然而systemd
很难用,改变太大,跟之前的完全不同。
使用systemd
设置开机启动
为了像以前一样,在/etc/rc.local
中设置开机启动程序,需要以下几步:
- 将
/lib/systemd/system/rc-local.service
链接到/etc/systemd/system/
目录下面来:
sudo ln -fs /lib/systemd/system/rc-local.service /etc/systemd/system/rc-local.service
- 创建
/etc/rc.local
文件
sudo touch /etc/rc.local
- 赋可执行权限
sudo chmod 755 /etc/rc.local
- 编辑
rc.local
,添加需要开机启动的任务
sudo gedit /etc/rc.local
(我将编译好的pid
文件放在了/usr
下面,根据你的目录文件设置即可
5. 重启,完成。