目录
1.守护进程
1.1基本概念
&:启动后台任务
jobs:查看后台任务
fg:将后台任务切换到前台
bg:将挂起的任务恢复到后台运行
Ctrl + z:挂起当前前台任务
1.2守护进程化步骤
setsid
/dev/null
1.3守护进程vs孤儿进程
deamon
6.4Xshell的登录
1.守护进程
守护进程(Daemon)是在后台运行、自成会话且脱离终端控制的特殊进程,通常用于执行系统任务(如日志记录、网络服务、定时任务等)
Linux中的守护进程通常以d结尾(如sshd),这是一种命名惯例,以标识其为后台服务进程
1.1基本概念
- 会话(Session):用户登录后创建一个会话,包含一个或多个进程组
场景 终端与会话的关系 典型示例 默认情况 一个终端严格对应一个会话 用户直接在终端中运行 bash使用 screen/tmux一个物理终端管理多个子会话(每个子会话关联独立伪终端) screen -S session1异常情况 终端被抢占或误配置(罕见) 系统错误导致终端关联混乱 - 进程组(Process Group):一组相关进程,组长为第一个进程
每个任务(前后台)对应一个进程组 - 在一个会话中,任何时刻只能有一个前台进程组,但可以有多个后台进程组
- 前台/后台任务:前台进程组拥有终端控制权,可读取键盘输入
后台进程组只能向终端输出,不能读输入 - 当多个任务(进程组)在同一个会话内启动时,它们的SID(Session ID)相同,且通常与创建该会话的Shell 进程(如
bash)的 PID相同 - 在 Linux 中关闭 Xshell 或退出 bash 时,默认后台进程会因 SIGHUP 被终止,但可通过守护进程化(如用 nohup、disown、setsid 或 systemd/screen 管理)使其脱离终端控制,避免受用户登录/退出影响,实现持久运行
&:启动后台任务
在命令末尾添加
&符号可以将任务放到后台执行此时会立即返回命令提示符,允许用户继续输入其他命令
jobs:查看后台任务
功能:列出当前 Shell 会话中的后台任务(包括暂停和运行中的任务)
用法:jobs [选项]
常用选项
- -l:显示任务的 PID(进程 ID)。
- -p:仅显示任务的 PID。
- -r:仅显示运行中的任务。
- -s:仅显示暂停的任务。
- [1]:任务编号(Job ID),用于 fg/bg 操作。
- +:默认操作的任务(如直接输入 fg 会操作该任务)。
- -:下一个默认操作的任务。
- Running:任务正在运行。
- Stopped:任务已挂起(如按 Ctrl+Z)
fg:将后台任务切换到前台
功能:将后台任务或挂起的任务恢复到前台运行
用法:fg [%任务编号]
如果不指定任务编号,默认操作
+标记的任务fg后面不加%也可以,但只能写数字,而且只在bash里比较稳妥
bg:将挂起的任务恢复到后台运行
功能:将暂停的任务(如
Ctrl+Z挂起的任务)恢复到后台继续运行用法:bg [%任务编号]
如果不指定任务编号,默认操作
+标记的任务bg后面不加%也可以,但只能写数字,而且只在bash里比较稳妥
Ctrl + z:挂起当前前台任务
功能:暂停当前正在前台运行的任务,并返回 Shell 交互界面
行为:任务被挂起(
Stopped状态),可以通过bg恢复到后台运行,或fg恢复到前台继续运行
| 命令/快捷键 | 作用 |
|---|---|
jobs | 查看后台任务列表 |
fg %n | 将任务n切换到前台 |
bg %n | 将挂起的任务n恢复到后台运行 |
Ctrl+Z | 挂起当前前台任务 |
kill %n | 终止任务n(通过 PID 也可用kill PID) |
1.2守护进程化步骤
- 忽略异常信号:如
SIGPIPE、SIGHUP等 - 创建独立会话:调用
setsid()创建新会话,成为新会话首进程和新进程组组长。注意:进程组组长不能调用setsid,需先fork子进程,让子进程调用setsid - 改变工作目录:通常改为根目录
/,避免卸载文件系统时产生冲突 - 重定向标准输入/输出/错误:重定向到
/dev/null,防止操作终端 - 可选:再次
fork并退出父进程,确保进程不是会话首进程(会话首进程可能通过open("/dev/tty")重新关联终端)
#pragma once #include <iostream> #include <unistd.h> #include <cstdlib> #include <signal.h> #include <string> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> const std::string nullfile = "/dev/null"; void Daemon(const std::string& cwd = "/") // 默认切换到根目录 { // 1. 忽略常见信号(避免僵尸进程和管道写入错误) signal(SIGCHLD, SIG_IGN); // 忽略子进程退出信号 signal(SIGPIPE, SIG_IGN); // 忽略管道破裂信号 signal(SIGHUP, SIG_IGN); // 忽略终端挂断信号(关键!) // 2. 第一次 fork:脱离终端控制 pid_t pid = fork(); if (pid < 0) { std::cerr << "First fork failed!" << std::endl; exit(EXIT_FAILURE); } if (pid > 0) exit(EXIT_SUCCESS); // 父进程退出,子进程成为孤儿进程(被 init 领养) // 3. 创建新会话(脱离原会话和终端) if (setsid() < 0) { std::cerr << "setsid() failed!" << std::endl; exit(EXIT_FAILURE); } // 4. 第二次 fork:确保进程不是会话首进程(防止重新获取终端) pid = fork(); if (pid < 0) { std::cerr << "Second fork failed!" << std::endl; exit(EXIT_FAILURE); } if (pid > 0) exit(EXIT_SUCCESS); // 父进程退出 // 5. 更改工作目录(避免占用可卸载的文件系统) if (!cwd.empty()) { if (chdir(cwd.c_str()) < 0) { std::cerr << "chdir() to " << cwd << " failed!" << std::endl; exit(EXIT_FAILURE); } } // 6. 重定向标准输入/输出/错误到 /dev/null int fd = open(nullfile.c_str(), O_RDWR); if (fd < 0) { std::cerr << "open(/dev/null) failed!" << std::endl; exit(EXIT_FAILURE); } dup2(fd, STDIN_FILENO); // 标准输入 dup2(fd, STDOUT_FILENO); // 标准输出 dup2(fd, STDERR_FILENO); // 标准错误 close(fd); // 7. 可选:重置文件权限掩码 umask(0); }进程脱离终端的核心方法是通过两次
fork()结合setsid()创建独立会话,并主动放弃控制终端关联权限:首次fork()后父进程退出,子进程调用setsid()成为新会话首进程(脱离原终端),随后二次fork()使孙进程继承会话但不再是会话首进程(彻底丧失重新关联终端的权限),最后关闭或重定向标准文件描述符(stdin/stdout/stderr)至/dev/null,确保进程完全独立于终端运行,避免因终端关闭或用户信号被意外终止
setsid
| 类别 | 详细说明 |
|---|---|
| 函数原型 | #include <unistd.h>pid_t setsid(void); |
| 所属系统 | Unix/Linux(POSIX 标准) |
| 返回值 | 成功返回新会话ID,调用进程成为新会话首进程 失败返回 -1,并设置错误码 EPERM:调用进程已是进程组组长(需先调用 setpgid() 调整); EACCES:进程缺乏权限(罕见,通常与终端权限相关) |
| 头文件依赖 | <unistd.h> |
| 功能 | 描述 |
|---|---|
| 创建新会话 | 调用进程成为新会话的会话首进程(Session Leader) |
| 脱离原终端 | 新会话没有控制终端(除非进程主动关联) |
| 重置进程组 | 调用进程成为新会话中的独立进程组组长(Process Group Leader) |
| 隔离信号 | 新会话中的进程不会收到原终端的信号(如SIGHUP) |
| 行为 | 条件与结果 |
|---|---|
| 调用进程资格 | 必须不是进程组组长(否则返回-1,错误码EPERM) |
| 典型调用场景 | 子进程通过fork()创建后调用(确保非进程组组长) |
| 对终端的影响 | 原终端不再控制新会话,但新会话可主动关联其他终端 |
setsid() 强制要求调用者必须是非进程组组长,否则会因同一进程同时持有新旧两组组长身份而破坏进程组唯一性,导致信号传递、会话管理等关键操作出现结构混乱(如原进程组与新会话的归属冲突);调用成功后,新进程会脱离原会话并成为新会话和全新进程组的唯一组长,从而严格保证每个进程组和会话的层级独立性
/dev/null
| 类别 | 详细说明 |
|---|---|
| 设备类型 | 虚拟设备文件(字符设备,主设备号1,次设备号3) |
| 文件路径 | /dev/null |
| 核心功能 | 丢弃所有写入的数据,读取时返回EOF(文件结束符) |
| 权限 | crw-rw-rw-(所有用户可读写) |
| 底层实现 | 内核中的空设备驱动,不存储任何数据,仅处理读写操作并立即返回 |
| 用途 | 示例场景 | 命令/代码示例 |
|---|---|---|
| 丢弃输出 | 隐藏命令的冗余输出(如日志、调试信息) | command > /dev/null(丢弃标准输出)command 2> /dev/null(丢弃错误输出) |
| 清空文件 | 快速清空大文件内容(比> file更高效) | cat /dev/null > file或> file(后者更常用) |
| 终止进程输入 | 强制结束依赖输入的进程(如阻塞的read调用) | command < /dev/null(进程会立即收到EOF并退出) |
| 重定向错误 | 分离标准输出和错误输出(仅保留错误) | command > /dev/null 2>&1(丢弃所有输出)command 2> /dev/null(仅丢弃错误) |
| 脚本中的占位符 | 作为无效输入源或输出目标,避免文件创建 | while read line < /dev/null; do ... done(循环立即结束) |
| 特性 | 说明 |
|---|---|
| 写入行为 | 数据被内核直接丢弃,不占用磁盘空间或内存 |
| 读取行为 | 立即返回EOF(文件结束符),无数据返回 |
| 性能影响 | 极高效率(无 I/O 操作),适合高频重定向场景 |
与/dev/zero区别 | /dev/zero返回空字符(\0),而/dev/null直接丢弃数据 |
1.3守护进程vs孤儿进程
| 特性 | 守护进程(Daemon) | 孤儿进程(Orphan Process) |
|---|---|---|
| 定义 | 脱离终端控制、在后台独立运行的进程,通常由系统初始化脚本或用户手动启动 | 父进程终止后,被init(或systemd)接管的子进程 |
| 创建方式 | 主动通过fork()+setsid()+ 脱离终端等操作实现 | 被动产生(父进程意外终止或主动退出) |
| 会话控制 | 主动调用setsid()创建新会话,成为会话首进程(Session Leader) | 通常仍属于原会话(除非显式调用setsid()) |
| 目的 | 提供长期后台服务(如 Web 服务器、日志服务等) | 由系统自动回收,避免僵尸进程(除非子进程本身有问题) |
| 终端关联 | 完全脱离终端,不会因终端关闭而终止 | 可能仍关联原终端(若未显式分离) |
| 进程组 | 通常成为独立进程组组长(通过setsid()) | 进程组可能不变(除非父进程在退出前调整) |
普通孤儿进程的生命周期由 init 管理,而守护进程主动控制自身行为(如重定向日志、监听端口等)
守护进程的独立性更强,而普通孤儿进程可能仍依赖原会话或进程组
deamon
| 项目 | 说明 |
|---|---|
| 函数原型 | int daemon(int nochdir, int noclose);(定义于<unistd.h>,部分系统可能位于其他头文件) |
| 功能 | 将进程转换为守护进程(Daemon),封装了标准的守护进程化步骤(如脱离终端、切换目录、重定向文件描述符等),简化开发流程。 |
| 参数 | -nochdir:若为非零值,则不切换工作目录到根目录(/);若为 0,则切换。- noclose:若为非零值,则不关闭/重定向标准文件描述符(stdin/stdout/stderr);若为 0,则将其重定向到/dev/null。 |
| 返回值 | 成功时返回0,失败时返回-1并设置errno(如权限不足、系统资源不足等)。 |
| 底层实现 | 内部通常调用以下系统调用和操作: 1. fork()→ 创建子进程,父进程退出。2. setsid()→ 脱离终端,创建新会话。3. chdir("/")(若nochdir=0)。4. 重定向 stdin/stdout/stderr到/dev/null(若noclose=0)。 |
| 使用场景 | - 需要快速实现守护进程化的场景。 - 避免手动编写重复的守护进程化代码。 - 适用于简单的后台服务程序。 |
| 注意事项 | 1.非标准函数:daemon()不是 POSIX 标准函数,部分系统(如某些嵌入式 Linux)可能不支持。2.可移植性:若需跨平台,建议手动实现守护进程化步骤或使用条件编译。 3.日志记录:若需记录日志,应在调用 daemon()前重定向文件描述符到日志文件,而非依赖默认的/dev/null。 |
| 特性 | daemon()函数 | 手动实现(系统调用组合) |
|---|---|---|
| 开发效率 | 高(一键调用,封装细节) | 低(需逐个调用系统调用并处理错误) |
| 灵活性 | 较低(参数有限,行为固定) | 高(可自定义目录切换、文件描述符处理等) |
| 可移植性 | 较低(非标准函数,依赖系统支持) | 较高(基于标准系统调用,跨平台兼容性强) |
| 适用场景 | 快速开发、简单守护进程 | 复杂需求、需要精细控制的场景 |
1.4Xshell的登录
当Xshell客户端通过SSH协议连接服务器时,sshd守护进程接受连接后创建子进程处理认证,认证成功后通过
fork()+exec()启动用户Shell(如bash),并将其标准输入/输出/错误绑定到Xshell的伪终端(/dev/pts/X),最终形成sshd子进程→bash进程的父子关系,用户即可通过终端与bash交互