一、调度程序的基本概念
后台服务程序往往需要长期运行,运行中的进程可能出现意外终止等情况,调度程序的功能就是要检测后台服务程序是否意外终止,若服务程序终止,需要负责再次启动服务程序。还有一类程序可能需要周期性启动,比如每个60秒运行一次数据生成程序来生成测试数据,此类需要周期性启动的程序也需要服务程序定期将其启动。
二、调度模块的实现
分别实现用于长期运行程序的调度程序,和用于周期性启动程序的调度程序代码。
1. 用于保持服务程序长期运行的调度程序
对于需要长期运行的程序,调度程序可以通过fork()函数创建一个子进程,子进程中运行实际的服务程序,父进程作为监控进程通过wait(pid)判断子进程是否退出,若退出再重新启动子进程继续运行服务程序。
在上述调度程序中,子进程需要调用其他的代码文件(即服务程序代码),如何实现在当前代码文件中调用另一份代码文件是关键。这里采用execl函数实现该功能,部分函数的详细说明放在服务程序整体代码实现之后。
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
int main(int argc, char* argv[]){
if(argc < 2){
//程序输入参数有误,提示正确输入:被调度程序名 被调度程序的参数
printf("Using:./program argvs\n"); return -1;
}
//关闭所有信号
for(int i = 0; i < 64; ++i) signal(i, SIG_IGN);
//生成被调度程序的参数数组,用于传递给execv函数
char * arg[argc];
for(int i = 1; i < argc; ++i){
arg[i - 1] = argv[i];
}
arg[argc - 1] = NULL;
while(true){
pid_t pid = fork();
if(pid == 0){ //子进程
execv(argv[1], arg);
exit(0);
}else if(pid < 0){
//fork函数创建失败,将错误信息写入日志或在控制台输出错误信息,该程序是简单案例采用在控制台输出错误的方式
printf("fork failed!\n"); return -1;
}else{ //父进程
int status = 0;
wait(&status);
printf("program crash!\n");
sleep(2);
}
}
}
相关函数
- wait函数:用来判断当前进程的子进程是否已经退出。调用该函数后父进程阻塞并等待子进程结束。
pit_t wait(int* status);
status为传出参数,用于记录子进程退出状态,一般不关心。函数执行成功,返回退出的子进程的PID,函数执行失败返回-1。
- execl函数:该函数的作用是加载一个新进程以替换当前进程的内容,函数执行成功后,该进程原本的后续代码均不会被执行。
int execv(const char* path, const char* arg[]);
path为要启动程序的路径(可以是绝对路径也可以是相对路径),arg表示启动程序的参数数组(第一个参数为程序名./program),数组中最后一个位置必须为NULL。上述程序中需要将传入调度程序中的参数进行提取,提取出替换程序的参数形成数组,并加入NULL传入execv函数中。该函数执行成功则不会执行原本的后续代码,如果执行失败则会执行原本的后续代码。
其他注意事项:
- 在子进程部分,execv函数后添加exit(0)语句的原因:如果不加exit(0),execv函数执行失败会立刻进入到下一次循环,继续执行失败,进入死循环。正确的方法是添加eixt(0),若execv函数执行失败则会转而执行原本的后续代码即exit(0),程序退出。
- 父进程中在子进程终止后执行sleep(2)的原因:为了防止马上fork下一个进程,而部分资源还在上一个进程中没来得及释放。
- 调度程序通常需要关闭所有信号
2.用于周期性启动服务程序的调度程序
周期性启动的程序的被调度程序执行时间较短,调度程序需要做的是周期性启动该程序执行。具体上述思路类似,需要修改函数传入参数,增加输入一个程序执行周期参数即可,并将父进程中sleep函数的参数用给定周期参数替换即可。代码如下:
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
int main(int argc, char* argv[]){
if(argc < 3){
//程序输入参数有误,提示正确输入:被调度程序名 执行周期(s) 被调度程序的参数
printf("Using:./program timetvl argvs\n"); return -1;
}
//生成被调度程序的参数数组,用于传递给execv函数
char * arg[argc];
for(int i = 1; i < argc; ++i){
arg[i - 1] = argv[i];
}
arg[argc - 1] = NULL;
while(true){
pid_t pid = fork();
if(pid == 0){ //子进程
execv(arg[0], arg);
exit(0);
}else if(pid < 0){
//fork函数创建失败,将错误信息写入日志或在控制台输出错误信息,该程序是简单案例采用在控制台输出错误的方式
printf("fork failed!\n"); return -1;
}else{ //父进程
int status = 0;
wait(&status);
printf("program crash!\n");
sleep(stoi(argv[2])); //周期参数作为sleep函数的参数传入
}
}
}
三、测试程序
1.长期运行服务程序的调度程序测试
被调度程序是一个需要长期运行的程序,当它意外退出的时候调度程序应该发现并再次启动该程序。本例中被调度程序是一个连续计数的函数。
/*程序名为:continous_test.cpp
编译指令:g++ -g -o continous_test continous_test.cpp
编译后执行该程序的命令为:./continous_test "Current number is"*/
#include <stdio.h>
#include <unistd.h>
int main(int argc, char* argv[]){
int i = 0;
while(true){
printf("%s, %d\n", argv[1], i); ++i;
sleep(1);
}
return 0;
}
单独运行后该程序的预期结果如下:
若不向进程发送终止信号或意外终止,程序会一直计数并打印结果。接下来用调度程序运行该计数程序,输入ps -ef命令查看所有进程,找到该调度程序和被调度程序进程:
进程963672为被调度进程,其父进程963671为调度程序进程。接下来执行kill -9 963672强行杀死被调度进程,再查看系统中所有进程:
被调度程序被再次启动,验证成功!