好的,我们来深入学习 setsid
系统调用,从 Linux 编程小白的角度出发。
1. 函数介绍 Link to heading
在 Linux 系统中,进程不是孤立存在的,它们之间有复杂的组织关系。理解几个关键概念很重要:
- 进程组 (Process Group): 一个或多个进程的集合。通常由一个“组长进程”(其 PID 等于进程组 ID, PGID)创建。向进程组发送信号(如
Ctrl+C
),会同时影响组内所有进程。 - 会话 (Session): 一个或多个进程组的集合。会话通常与一个控制终端 (Controlling Terminal) 相关联(例如你登录的 tty 终端)。会话的领导者(Session Leader)是创建该会话的进程。
- 控制终端 (Controlling Terminal): 与会话相关联的终端设备。用户可以通过终端向会话中的前台进程组发送信号(如
Ctrl+C
发送SIGINT
,Ctrl+Z
发送SIGTSTP
)。
setsid
(Set Session ID) 系统调用的作用是让调用进程执行以下三件事:
- 创建一个新的会话 (如果它不是进程组的领导者)。
- 成为新会话的领导者 (新会话的 SID 等于调用进程的 PID)。
- 创建一个新的进程组 (新进程组的 PGID 也等于调用进程的 PID)。
- 脱离控制终端 (如果它之前有一个)。
简单来说,setsid
就是让一个进程“自立门户”,成为一个新家庭(新会话)的“家长”(会话领导者),并且和原来的“家庭”(旧会话和控制终端)彻底断绝关系。
典型应用场景:
- 守护进程 (Daemon): 这是最常见的用途。守护进程是在后台运行的长期服务(如 web 服务器、数据库服务器)。它们需要与启动它们的终端和用户会话完全分离,以确保即使用户退出登录,守护进程也能继续运行。
setsid
是创建守护进程过程中的关键一步。 - 启动独立的子系统: 有时你可能想启动一组完全独立的进程,不希望它们受到父进程终端信号的影响。
2. 函数原型 Link to heading
#include <unistd.h> // 包含系统调用声明
pid_t setsid(void);
3. 功能 Link to heading
创建一个新的会话,调用进程成为新会话的领导者、新进程组的领导者,并与任何控制终端分离。
4. 参数 Link to heading
- 无参数。
5. 返回值 Link to heading
- 成功: 返回新创建的会话 ID (SID),其值等于调用进程的 PID。
- 失败: 返回 -1,并设置全局变量
errno
来指示具体的错误原因。
6. 错误码 (errno
)
Link to heading
EPERM
: 调用进程已经是一个进程组的领导者。根据 POSIX 标准,这种情况下setsid
不允许调用,以防止会话领导者意外地把自己从当前进程组中移出,留下一个没有领导者的进程组。这是最常见的失败原因。
7. 相似函数或关联函数 Link to heading
getsid
: 获取指定进程的会话 ID (SID)。getpid
: 获取调用进程的进程 ID (PID)。getppid
: 获取调用进程的父进程 ID (PPID)。getpgid
/getpgrp
: 获取调用进程的进程组 ID (PGID)。tcgetpgrp
/tcsetpgrp
: 获取/设置指定终端的前台进程组。fork
: 创建新进程。通常在调用setsid
之前会先fork
,并在子进程中调用setsid
,以避免EPERM
错误。
8. 示例代码 Link to heading
下面的示例将演示如何使用 setsid
来创建一个简单的守护进程。
#define _GNU_SOURCE // 启用 GNU 扩展
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h> // 包含 open, O_RDWR 等
#include <fcntl.h> // 包含 open, O_RDWR 等
#include <syslog.h> // 包含 syslog
#include <signal.h> // 包含 signal
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
// 全局标志,用于信号处理
volatile sig_atomic_t keep_running = 1;
// 信号处理函数
void signal_handler(int sig) {
if (sig == SIGTERM) {
syslog(LOG_NOTICE, "Received SIGTERM, initiating shutdown...");
keep_running = 0;
}
}
// 简单的守护进程主函数
void daemon_main() {
// 打开 syslog 进行日志记录
openlog("my_simple_daemon", LOG_PID, LOG_DAEMON);
syslog(LOG_INFO, "Daemon started");
// 设置信号处理
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = signal_handler;
sigemptyset(&sa.sa_mask);
// 重启被信号中断的系统调用
sa.sa_flags = SA_RESTART;
if (sigaction(SIGTERM, &sa, NULL) == -1) {
syslog(LOG_ERR, "Failed to set signal handler for SIGTERM: %s", strerror(errno));
closelog();
exit(EXIT_FAILURE);
}
// 守护进程的主循环
while (keep_running) {
syslog(LOG_INFO, "Daemon is running...");
// 这里可以执行守护进程的实际工作
// 例如:监听端口、处理文件、执行定时任务等
sleep(10); // 每10秒记录一次日志
}
syslog(LOG_NOTICE, "Daemon shutting down gracefully.");
closelog();
}
// 守护进程初始化函数
void daemonize() {
pid_t pid, sid;
// 1. Fork off the parent process
// 这是为了确保子进程不是进程组的领导者,从而可以成功调用 setsid
pid = fork();
if (pid < 0) {
// fork 失败
exit(EXIT_FAILURE);
}
if (pid > 0) {
// 父进程退出
// 这使得子进程成为孤儿进程,由 init 进程 (PID 1) 收养
exit(EXIT_SUCCESS);
}
// 2. Create a new SID for the child process
// 现在我们在子进程中
sid = setsid();
if (sid < 0) {
// setsid 失败
exit(EXIT_FAILURE);
}
syslog(LOG_INFO, "New session ID (SID) created: %d", sid);
// 3. (可选但推荐) 改变当前工作目录
// 守护进程通常将工作目录更改为根目录 (/),
// 以避免阻止卸载它可能驻留的文件系统。
if ((chdir("/")) < 0) {
syslog(LOG_ERR, "Failed to change directory to /: %s", strerror(errno));
exit(EXIT_FAILURE);
}
// 4. (可选但推荐) 设置文件权限掩码
// 设置一个明确的文件权限掩码,以确保守护进程创建的文件具有预期的权限。
umask(0);
// 5. (可选但推荐) 关闭并重定向标准文件描述符
// 守护进程不应该有标准输入、输出或错误。
// 我们通常将它们重定向到 /dev/null。
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
// 打开 /dev/null 三次,分别对应 STDIN_FILENO (0), STDOUT_FILENO (1), STDERR_FILENO (2)
// O_RDWR 可以同时用于读写
open("/dev/null", O_RDWR); // 通常分配为 0 (STDIN_FILENO)
dup(0); // 复制 STDIN_FILENO 到 1 (STDOUT_FILENO)
dup(0); // 复制 STDIN_FILENO 到 2 (STDERR_FILENO)
// 现在,标准输入、输出、错误都指向 /dev/null
// 6. 调用守护进程的主逻辑函数
daemon_main();
}
int main() {
printf("--- Starting Simple Daemon ---\n");
printf("PID before fork: %d\n", getpid());
// 调用守护进程初始化函数
daemonize();
// daemonize 函数中的 daemon_main 不会返回到这里
// 如果返回了,说明出错了
return EXIT_SUCCESS;
}
另一个更简单的示例,仅用于演示 setsid 的效果:
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
void print_process_info(const char* stage) {
printf("[%s] PID: %d, PPID: %d, PGID: %d, SID: %d\n",
stage, getpid(), getppid(), getpgid(0), getsid(0));
}
int main() {
pid_t pid, sid;
printf("--- Demonstrating setsid ---\n");
print_process_info("Before fork");
// 第一次 fork
pid = fork();
if (pid < 0) {
perror("First fork");
exit(EXIT_FAILURE);
}
if (pid > 0) {
// 父进程等待子进程结束并退出
wait(NULL);
printf("First parent (PID %d) exiting.\n", getpid());
exit(EXIT_SUCCESS);
}
// --- 第一个子进程 ---
print_process_info("After first fork (First child)");
printf("This process is the process group leader: %s\n",
(getpid() == getpgid(0)) ? "Yes" : "No");
// 尝试在这里调用 setsid 会失败,因为这个子进程是其进程组的领导者
sid = setsid();
if (sid == -1) {
printf("setsid failed in first child (as expected): %s\n", strerror(errno));
} else {
printf("setsid succeeded in first child (unexpected): SID = %d\n", sid);
}
// 第二次 fork
pid = fork();
if (pid < 0) {
perror("Second fork");
exit(EXIT_FAILURE);
}
if (pid > 0) {
// 第一个子进程(也是第二个父进程)退出
printf("Second parent (PID %d) exiting.\n", getpid());
exit(EXIT_SUCCESS);
}
// --- 第二个子进程 ---
print_process_info("After second fork (Second child)");
printf("This process is the process group leader: %s\n",
(getpid() == getpgid(0)) ? "Yes" : "No");
// 现在在这个子进程中调用 setsid,应该会成功
// 因为它不是其进程组的领导者 (它的 PGID 是其父进程的 PID)
sid = setsid();
if (sid == -1) {
perror("setsid in second child");
exit(EXIT_FAILURE);
} else {
printf("setsid succeeded in second child. New SID = %d\n", sid);
}
// 再次打印信息,确认变化
print_process_info("After setsid (Second child)");
printf("This process is now the session leader: %s\n",
(getpid() == getsid(0)) ? "Yes" : "No");
printf("This process is now the process group leader: %s\n",
(getpid() == getpgid(0)) ? "Yes" : "No");
printf("\n--- Summary ---\n");
printf("1. The first fork() ensures the child is not a process group leader.\n");
printf("2. setsid() in the second child creates a new session and detaches from the controlling terminal.\n");
printf("3. The second fork() is often done by daemons to ensure the final process is not a session leader,\n");
printf(" which prevents it from accidentally acquiring a controlling terminal again.\n");
// 模拟守护进程做一些事
printf("Second child process now running independently...\n");
sleep(30); // 睡眠 30 秒
printf("Second child process exiting.\n");
return 0;
}
9. 编译和运行 Link to heading
# 假设守护进程代码保存在 simple_daemon.c 中
gcc -o simple_daemon simple_daemon.c
# 假设 setsid 演示代码保存在 setsid_demo.c 中
gcc -o setsid_demo setsid_demo.c
# 运行 setsid 演示
./setsid_demo
# 运行守护进程 (需要 root 权限来写入 /var/log/messages 或类似文件,或配置 syslog)
# 否则,日志可能只输出到 stderr 或系统日志的用户部分
sudo ./simple_daemon
# 检查守护进程是否在运行
ps aux | grep simple_daemon
# 查看系统日志 (位置可能因发行版而异)
# tail -f /var/log/syslog | grep my_simple_daemon
# 或
# journalctl -f | grep my_simple_daemon
# 停止守护进程 (发送 SIGTERM 信号)
sudo kill -TERM <daemon_pid>
10. 预期输出 (setsid_demo
)
Link to heading
--- Demonstrating setsid ---
[Before fork] PID: 12345, PPID: 23456, PGID: 12345, SID: 12345
[After first fork (First child)] PID: 12346, PPID: 12345, PGID: 12346, SID: 12345
This process is the process group leader: Yes
setsid failed in first child (as expected): Operation not permitted
Second parent (PID 12346) exiting.
[After second fork (Second child)] PID: 12347, PPID: 12346, PGID: 12346, SID: 12345
This process is the process group leader: No
setsid succeeded in second child. New SID = 12347
[After setsid (Second child)] PID: 12347, PPID: 1, PGID: 12347, SID: 12347
This process is now the session leader: Yes
This process is now the process group leader: Yes
--- Summary ---
1. The first fork() ensures the child is not a process group leader.
2. setsid() in the second child creates a new session and detaches from the controlling terminal.
3. The second fork() is often done by daemons to ensure the final process is not a session leader,
which prevents it from accidentally acquiring a controlling terminal again.
Second child process now running independently...
Second child process exiting.
11. 总结 Link to heading
setsid
是 Linux 系统编程中一个重要的系统调用,尤其在编写守护进程时不可或缺。它的核心作用是让进程脱离原有的会话和控制终端,成为一个新的、独立会话的领导者。理解其工作原理和使用模式对于构建健壮的后台服务至关重要。通常的模式是“fork -> setsid -> fork”来创建一个完全独立的守护进程。