守护进程,顾名思义就是在操作系统后台运行Skynet
守护进程相关知识:搞懂进程组、会话、控制终端关系,才能明白守护进程如何创建 - 知乎 (zhihu.com)
Skynet中的守护进程
在文件skynet_start.c文件,第266行中,有守护进程的代码:
if (config->daemon) {
if (daemon_init(config->daemon)) {
exit(1);
}
}
// daemon()守护进程是否开启是可以在config->daemon中配置的
// config->daemon 其实就是一个文件路径 + 文件名,例如:./path/skynet.pid
让我们看看 daemon_init(config->daemon) 做了什么操作:
int
daemon_init(const char *pidfile) {
// 检查skynet的pid
// 具体实现在 代码段1
int pid = check_pid(pidfile);
// 如果返回的不是0,标识这个文件已经存在,则代表着skynet进程已经启动了
if (pid) {
fprintf(stderr, "Skynet is already running, pid = %d.\n", pid);
return 1; // skynet进程已启动,直接退出
}
#ifdef __APPLE__ // 检测代码是否在苹果(Apple)操作系统上运行
fprintf(stderr, "'daemon' is deprecated: first deprecated in OS X 10.5 , use launchd instead.\n");
#else
// 启动守护进程
if (daemon(1,1)) {
fprintf(stderr, "Can't daemonize.\n");
return 1;
}
#endif
// 读取skynet进程的pid,把pid写入文件中
// 具体实现在 代码段2
pid = write_pid(pidfile);
if (pid == 0) {
return 1;
}
// 重定向标准输入,标准输出,标准错误输出
// 即把上面的3项输出重定向到"/dev/null"
// 全部输出信息都不打印
if (redirect_fds()) {
return 1;
}
return 0;
}
关于daemon()函数
// daemon()用于创建一个守护进程
// daemon(1, 1)之后,新开了一个子进程,这个子进程就是守护进程
// 函数原型如下:
int daemon(int nochdir, int noclose)
// nochdir用于指定是否改变进程的当前工作目录
// 如果nochdir为非零值,守护进程将不会改变当前工作目录,否则它将切换到根目录
//
// noclose用于指定是否关闭标准输入、输出和错误输出
// 如果noclose为非零值,守护进程将保持标准输入、输出和错误输出打开,否则它们将被关闭
代码段1:看看check_pid()函数的实现
static int
check_pid(const char *pidfile) {
int pid = 0;
FILE *f = fopen(pidfile, "r"); // 打开 config->daemon 配置的文件
if (f == NULL) // 第一次启动进程,文件肯定是不存在的,直接返回0
return 0;
// 第二次启动进程,文件肯定存在,但存储的数据是上次进程的pid
// fscanf()是 C 语言标准库中的一个函数,用于从文件中按照指定的格式读取数据
// 可以通过 fscanf()!=1 来判断是否读取成功,如果返回值不等于1,表示读取失败
int n = fscanf(f, "%d", &pid); // 读取文件中的值,存储在pid中
fclose(f);
// getpid() 函数可以返回调用它的进程的进程ID
// n !=1表示:文件存在,但是读取数据失败,例如:把skynet.pid的数据清空了,留下空文件
// pid == 0表示:根本没有读取到之前的数据
// pid == getpid()表示:本次进程pid跟上次进程pid一样,说明没变动
if (n !=1 || pid == 0 || pid == getpid()) {
return 0;
}
// kill(pid, 0) 是一个系统调用函数,用于向指定的进程发送信号
// 在这个特定的用法中,我们将信号参数设置为 0,它的作用是检查进程是否存在
// kill(pid, 0) 的返回值有以下几种情况:
// 如果成功发送信号(进程存在),返回值为 0
// 如果目标进程不存在或者当前进程没有权限发送信号,返回值为 -1,并且设置 errno 为适当的错误代码
//
// 当我们使用 kill(pid, 0) 时,我们可以通过检查返回值和 errno 来判断目标进程是否存在
// errno 是一个全局的错误代码变量,它会在系统调用或库函数发生错误时被设置
// ESRCH 是一个表示进程不存在的错误代码
// kill(pid, 0) && errno == ESRCH 的判断可以用来检查进程是否存在
// 如果判断为真,表示目标进程不存在
// 尝试把上次进程杀掉
// errno == ESRCH表示:上次进程不存在
if (kill(pid, 0) && errno == ESRCH)
return 0;
return pid;
}
代码段2:看看write_pid()函数的实现
write_pid(const char *pidfile) {
FILE *f;
int pid = 0;
int fd = open(pidfile, O_RDWR|O_CREAT, 0644);
if (fd == -1) {
fprintf(stderr, "Can't create pidfile [%s].\n", pidfile);
return 0;
}
f = fdopen(fd, "w+");
if (f == NULL) {
fprintf(stderr, "Can't open pidfile [%s].\n", pidfile);
return 0;
}
if (flock(fd, LOCK_EX|LOCK_NB) == -1) {
int n = fscanf(f, "%d", &pid);
fclose(f);
if (n != 1) {
fprintf(stderr, "Can't lock and read pidfile.\n");
} else {
fprintf(stderr, "Can't lock pidfile, lock is held by pid %d.\n", pid);
}
return 0;
}
pid = getpid();
if (!fprintf(f,"%d\n", pid)) {
fprintf(stderr, "Can't write pid.\n");
close(fd);
return 0;
}
fflush(f);
return pid;
}
关于输出重定向
static int
redirect_fds() {
// open("/dev/null", O_RDWR) 的作用是打开一个特殊的设备文件 "/dev/null",并以读写模式打开
// "/dev/null" 是一个特殊的设备文件,它可以被视为一个黑洞
// 任何写入该文件的数据都会被丢弃,而任何从该文件读取的操作将立即返回文件结束符
// 因此将文件描述符指向 "/dev/null" 可以用于丢弃不需要的输出或将输出重定向到空
//
// 在创建守护进程时,常常需要关闭标准输入、标准输出和标准错误输出,以避免守护进程与终端相关联
// 一种常见的做法是将这些标准文件描述符重定向到 "/dev/null",这样所有输出都会被丢弃
int nfd = open("/dev/null", O_RDWR);
if (nfd == -1) {
perror("Unable to open /dev/null: ");
return -1;
}
// dup2(nfd, 0) 是一个系统调用函数,它将文件描述符 nfd 复制到标准输入文件描述符(文件描述符为 0)
// 通过调用 dup2(nfd, 0),我们可以将 nfd 复制到标准输入文件描述符,从而实现将输入重定向到指定的文件描述符
// 这意味着从标准输入读取数据时,实际上是从 nfd 所指向的文件或设备中读取数据
// 例如:
// 可以通过打开一个文件,并将其文件描述符传递给 dup2() 函数,从而将标准输入重定向到该文件
// 这样,守护进程就可以从该文件中读取输入数据
//
// 而下面 dup2(nfd, 0),dup2(nfd, 1),dup2(nfd, 2)的调用
// 都是为了把标准输入(stdin==0),标准输出(stdout==1),标准错误输出(stderr==2)重定向到 nfd
// 而 nfd 又是个 “黑洞”,即不会输出任何信息
if (dup2(nfd, 0) < 0) {
perror("Unable to dup2 stdin(0): ");
return -1;
}
if (dup2(nfd, 1) < 0) {
perror("Unable to dup2 stdout(1): ");
return -1;
}
if (dup2(nfd, 2) < 0) {
perror("Unable to dup2 stderr(2): ");
return -1;
}
// 上面这部分的代码非常明显:把所有的输出都放入到/dev/null
// 此后所有的输出都将会看不到,例如:
// fprintf(stdout, "something in here \n"); // 所有stdout、stdin、stderr都失效
//
// 因此在《Skynet源码之:service_logger》中我们可以看到,日志的输出如下
// 指定日志文件:inst->handle = fopen(parm,"a");
// 把输出写入文件:fprintf(inst->handle, "something in here \n");
// 即把输出标记为我们指定的日志文件:inst->handle
//
// 而 service_logger 服务也是根据 parm,也就是 config->logger 来判定是否把日志写入文件
close(nfd);
return 0;
}
总结一下:
在配置config中填写了daemon字段后,skynet在启动的时候就会开启守护进程
不仅开启了守护进程,还会把标准输入,标准输出,标准错误输出都重定向到"/dev/null"
然后把进程的pid写入daemon配置的文件目录和文件名
最后Skynet的终端关闭(会给Skynet发送一个SIGHUP信号,详细看《Skynet源码之:信号》)
这里特别注意:
此时所有的标准输入,标准输出,标准错误输出都重定向到"/dev/null"
那么这个时候是终端屏幕是看不到任何信息的了
而Skynet中的logger服务,会在service_logger.c文件中,第78行,logger_init()中
打开一个配置的日志文件,随后根据 fprintf(inst->handle, “something in here \n”)
把标准输入,标准输出,标准错误输出重定向到指定的日志文件
在config里面配置daemon和在shell里面加 & 让进程后台运行是有区别的:
关于skynet后台运行与lua print · cloudwu/skynet · Discussion #1758 (github.com)
Skynet 的 daemon 参数不仅仅是把进程放在后台,同时还会改为 log 直接写文件