# 7. 进程控制 ## 7.1 完整概念讲解 进程控制是 Linux 系统编程的核心内容,涉及进程身份标识获取、工作环境修改、权限管理以及资源限制等方面。在嵌入式 Linux 应用开发中,合理使用这些接口对系统安全和稳定至关重要。 **为什么需要进程控制?** 1. **身份识别**:系统需要知道"我是谁"(用户ID/组ID),以决定权限范围 2. **环境管理**:进程需要在正确的目录下运行,避免路径混乱 3. **权限控制**:嵌入式设备常需要切换用户身份(如从 root 降权),防止误操作 4. **资源管控**:防止某个进程耗尽系统资源(文件描述符、栈空间等) **进程身份标识体系** Linux 中每个进程都关联四个 ID: | ID 类型 | 含义 | 获取函数 | 说明 | | ------- | -------- | ----------- | -------------------- | | PID | 进程ID | `getpid()` | 每个进程唯一 | | PPID | 父进程ID | `getppid()` | 创建当前进程的进程 | | UID | 用户ID | `getuid()` | 实际用户(登录用户) | | GID | 组ID | `getgid()` | 实际用户组 | 与之对应的还有 **有效 ID**(EUID/EGID),用于权限检查。例如 `setuid()` 程序运行时 EUID 可能是 root,而实际 UID 是普通用户。 **umask 与文件创建** 每个进程都有一个 umask 值,它是一个权限掩码,用于在创建文件时屏蔽不需要的权限位。例如 umask 为 `0022` 时,创建的文件不会具有 group-write 和 other-write 权限。 **进程资源限制** Linux 通过 `rlimit` 结构体为每个进程设定资源上限,包括: - 栈大小(RLIMIT_STACK) - 打开文件数(RLIMIT_NOFILE) - 进程数限制(RLIMIT_NPROC) - CPU 时间(RLIMIT_CPU) - 虚拟内存(RLIMIT_AS) ## 7.2 核心API/语法 ### 进程/用户信息获取 ```c #include #include pid_t getpid(void); // 返回当前进程PID pid_t getppid(void); // 返回父进程PID uid_t getuid(void); // 返回实际用户ID(real user id) uid_t geteuid(void); // 返回有效用户ID(effective user id) gid_t getgid(void); // 返回实际组ID gid_t getegid(void); // 返回有效组ID ``` **返回值说明:** - `getpid()` / `getppid()`:返回 `pid_t` 类型(通常为 int),不会失败 - `getuid()` / `geteuid()` / `getgid()` / `getegid()`:返回对应的 ID 值,不会失败 ### 目录操作 ```c #include int chdir(const char *path); // 改变当前工作目录 int fchdir(int fd); // 通过文件描述符改变工作目录 #include int chroot(const char *path); // 更改根目录(仅 root 权限) ``` - `chdir()`:成功返回 0,失败返回 -1 - `chroot()`:需要 CAP_SYS_CHROOT 权限(通常为 root),将进程的根目录 `/` 重定向到指定路径 ### 权限掩码 ```c #include mode_t umask(mode_t mask); // 设置文件创建权限掩码 ``` - 参数 `mask`:新的 umask 值(八进制表示,如 0022) - 返回值:**之前的** umask 值 - umask 只影响 `open()`/`creat()`/`mkdir()` 等创建操作,不影响 `chmod()` 的显式设置 ### 用户/组ID切换 ```c #include int setuid(uid_t uid); // 设置有效用户ID int seteuid(uid_t euid); // 设置有效用户ID(推荐) int setgid(gid_t gid); // 设置有效组ID int setegid(gid_t egid); // 设置有效组ID(推荐) ``` | 函数 | 作用 | 特权要求 | 说明 | | ----------- | ----------------------------- | -------- | ------------------------ | | `setuid()` | 设置 real+saved+effective UID | root | 非 root 调用只能设置一次 | | `seteuid()` | 仅设置 effective UID | root | 可反复切换,更灵活 | | `setgid()` | 设置 real+saved+effective GID | root | 同 setuid | | `setegid()` | 仅设置 effective GID | root | 可反复切换 | **嵌入式推荐做法**:使用 `seteuid()`/`setegid()` 在 root 和普通用户之间切换,而非 `setuid()`。 ### 进程资源限制 ```c #include int getrlimit(int resource, struct rlimit *rlim); int setrlimit(int resource, const struct rlimit *rlim); struct rlimit { rlim_t rlim_cur; // 软限制(当前生效值) rlim_t rlim_max; // 硬限制(软限制的上限) }; ``` **常用 resource 常量:** | 常量 | 含义 | 典型值 | | ------------- | -------------------- | ------------ | | RLIMIT_STACK | 栈大小 | 默认 8MB | | RLIMIT_NOFILE | 最大打开文件数 | 默认 1024 | | RLIMIT_NPROC | 最大子进程数 | 默认系统决定 | | RLIMIT_CPU | CPU 时间(秒) | 无限 | | RLIMIT_AS | 最大虚拟内存(字节) | 无限 | | RLIMIT_DATA | 数据段大小(字节) | 无限 | | RLIMIT_FSIZE | 最大文件大小(字节) | 无限 | ## 7.3 代码示例 ### 示例1:获取进程与用户信息 ```c #include #include #include #include #include int main(void) { pid_t pid, ppid; uid_t uid, euid; gid_t gid, egid; struct passwd *pw; struct group *gr; pid = getpid(); ppid = getppid(); uid = getuid(); euid = geteuid(); gid = getgid(); egid = getegid(); printf("PID: %d\n", pid); printf("PPID: %d\n", ppid); pw = getpwuid(uid); if (pw) { printf("User: %s (UID=%d)\n", pw->pw_name, uid); } if (uid != euid) { pw = getpwuid(euid); if (pw) { printf("EUser: %s (EUID=%d)\n", pw->pw_name, euid); } } gr = getgrgid(gid); if (gr) { printf("Group: %s (GID=%d)\n", gr->gr_name, gid); } if (gid != egid) { gr = getgrgid(egid); if (gr) { printf("EGroup: %s (EGID=%d)\n", gr->gr_name, egid); } } return 0; } ``` **编译命令:** ```bash gcc -o proc_info proc_info.c ./proc_info ``` ### 示例2:chdir 与 umask ```c #include #include #include #include #include #include int main(void) { char buf[1024]; mode_t old_mask; /* 1. 获取并打印当前工作目录 */ if (getcwd(buf, sizeof(buf)) == NULL) { perror("getcwd"); return 1; } printf("Current dir: %s\n", buf); /* 2. 修改工作目录 */ if (chdir("/tmp") < 0) { perror("chdir"); return 1; } if (getcwd(buf, sizeof(buf)) == NULL) { perror("getcwd"); return 1; } printf("After chdir: %s\n", buf); /* 3. umask 示例 */ old_mask = umask(0000); /* 清除所有掩码 */ printf("Old umask: %04o\n", old_mask); /* 创建文件,权限为 0666 & ~0000 = 0666 */ int fd = open("test_perm.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666); if (fd >= 0) { close(fd); printf("Created test_perm.txt with umask 0000\n"); } umask(0022); /* 恢复默认掩码 */ fd = open("test_default.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666); if (fd >= 0) { close(fd); printf("Created test_default.txt with umask 0022\n"); } /* 清理 */ unlink("test_perm.txt"); unlink("test_default.txt"); return 0; } ``` **编译命令:** ```bash gcc -o dir_perm dir_perm.c ./dir_perm ``` ### 示例3:seteuid 权限切换 ```c #include #include #include #include #include int main(void) { uid_t real_uid, effective_uid; real_uid = getuid(); effective_uid = geteuid(); printf("Before seteuid:\n"); printf(" UID=%d, EUID=%d\n", real_uid, effective_uid); if (effective_uid == 0) { /* 当前是 root,降到普通用户 */ if (seteuid(real_uid) < 0) { perror("seteuid"); return 1; } printf("After seteuid (drop privileges):\n"); printf(" UID=%d, EUID=%d\n", getuid(), geteuid()); /* 尝试以普通用户身份访问 /etc/shadow */ FILE *fp = fopen("/etc/shadow", "r"); if (fp == NULL) { printf(" Cannot open /etc/shadow (expected, no root)\n"); } else { printf(" WARNING: opened /etc/shadow!\n"); fclose(fp); } /* 恢复 root 权限 */ if (seteuid(0) < 0) { perror("seteuid restore"); return 1; } printf("After restore root:\n"); printf(" UID=%d, EUID=%d\n", getuid(), geteuid()); } else { printf("Not running as root, skip privilege demo\n"); } return 0; } ``` **编译命令:** ```bash gcc -o priv_demo priv_demo.c sudo ./priv_demo # 需要 sudo 起始权限 ``` ### 示例4:getrlimit/setrlimit 资源限制 ```c #include #include #include #include #include static void print_rlimit(const char *name, int resource) { struct rlimit rl; if (getrlimit(resource, &rl) < 0) { perror("getrlimit"); return; } printf("%-20s soft=%-15ld hard=%-15ld\n", name, (long)rl.rlim_cur, (long)rl.rlim_max); } int main(void) { struct rlimit rl; printf("=== Current Resource Limits ===\n\n"); print_rlimit("RLIMIT_STACK", RLIMIT_STACK); print_rlimit("RLIMIT_NOFILE", RLIMIT_NOFILE); print_rlimit("RLIMIT_NPROC", RLIMIT_NPROC); print_rlimit("RLIMIT_CPU", RLIMIT_CPU); print_rlimit("RLIMIT_AS", RLIMIT_AS); print_rlimit("RLIMIT_DATA", RLIMIT_DATA); print_rlimit("RLIMIT_FSIZE", RLIMIT_FSIZE); /* 修改栈大小为 4MB */ printf("\n--- Set stack to 4MB ---\n"); if (getrlimit(RLIMIT_STACK, &rl) < 0) { perror("getrlimit"); return 1; } rl.rlim_cur = 4 * 1024 * 1024; if (setrlimit(RLIMIT_STACK, &rl) < 0) { perror("setrlimit"); return 1; } print_rlimit("RLIMIT_STACK", RLIMIT_STACK); /* 修改最大打开文件数为 512 */ printf("\n--- Set nofile to 512 ---\n"); if (getrlimit(RLIMIT_NOFILE, &rl) < 0) { perror("getrlimit"); return 1; } rl.rlim_cur = 512; if (setrlimit(RLIMIT_NOFILE, &rl) < 0) { perror("setrlimit"); return 1; } print_rlimit("RLIMIT_NOFILE", RLIMIT_NOFILE); return 0; } ``` **编译命令:** ```bash gcc -o rlimit_demo rlimit_demo.c ./rlimit_demo ``` ## 7.4 注意事项与易错点 1. **`getpid()` 不会失败**:返回值始终有效,无需检查错误 2. **`setuid()` vs `seteuid()` 区别**: - `setuid()` 同时修改 real、effective、saved UID,非 root 调用只能执行一次 - `seteuid()` 只修改 effective UID,可反复切换,嵌入式中推荐使用 3. **`chroot()` 需要 root 权限**:且被 chroot 的进程仍可通过 `chdir("..")` 逃逸(需额外防护) 4. **umask 影响范围有限**:只影响 `open()/creat()/mkdir()` 等创建类系统调用,`chmod()` 不受影响 5. **`setrlimit()` 只能降低不能提高**:非 root 进程只能将软限制降低到硬限制以下 6. **`RLIMIT_NPROC` 限制的是 real UID 的进程数**:有效 UID 的进程不受此限制 7. **`getcwd()` 返回的是绝对路径**:如果当前目录已被删除,行为依赖实现 8. **嵌入式注意事项**:资源受限环境下务必设置合理的 `RLIMIT_NOFILE` 和 `RLIMIT_STACK`,避免系统耗尽 ## 7.5 面试要点(3-5个核心Q&A) **Q1:`getuid()` 和 `geteuid()` 有什么区别?什么时候会不同?** A:`getuid()` 返回实际用户ID(登录用户),`geteuid()` 返回有效用户ID(用于权限检查)。当进程通过 `setuid` 程序(如 `passwd`、`sudo`)运行时,EUID 会被设置为文件所有者(通常是 root),此时两者不同。正常 fork 出的子进程两者相同。 **Q2:`setuid()` 和 `seteuid()` 有什么区别?嵌入式开发中如何选择?** A:`setuid()` 修改 real、effective、saved UID 三个值,非 root 调用只能执行一次(不可逆)。`seteuid()` 只修改 effective UID,可反复切换。嵌入式中推荐 `seteuid()`:程序以 root 启动后可降到普通用户执行不敏感操作,再在需要时恢复 root 权限。 **Q3:umask 的作用是什么?它影响 `chmod()` 吗?** A:umask 是文件创建权限掩码,在 `open(O_CREAT)` 时自动屏蔽对应权限位。例如 umask=022 创建的文件权限为 0644 而非 0666。umask **不影响** `chmod()`,因为 `chmod()` 是显式设置权限。 **Q4:`setrlimit()` 能将软限制提高到硬限制以上吗?** A:不能。非 root 进程只能将软限制设置为 ≤ 硬限制的值。要提高软限制,必须先提高硬限制(需要 root 权限),然后再降低软限制。root 进程可以将硬限制设置为任意值(包括 RLIM_INFINITY)。 **Q5:嵌入式系统中为什么需要限制进程的文件描述符数量?** A:嵌入式设备内存和内核资源有限,如果不设限制,bug 程序可能打开大量文件描述符导致系统无法分配新的 fd("Too many open files"),影响其他关键进程。通过 `setrlimit(RLIMIT_NOFILE, ...)` 可以为每个进程设定合理上限,实现资源隔离。