7. 进程控制.md 13 KB

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/语法

进程/用户信息获取

#include <unistd.h>
#include <sys/types.h>

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 值,不会失败

目录操作

#include <unistd.h>

int chdir(const char *path);   // 改变当前工作目录
int fchdir(int fd);            // 通过文件描述符改变工作目录

#include <unistd.h>
int chroot(const char *path);  // 更改根目录(仅 root 权限)
  • chdir():成功返回 0,失败返回 -1
  • chroot():需要 CAP_SYS_CHROOT 权限(通常为 root),将进程的根目录 / 重定向到指定路径

权限掩码

#include <sys/stat.h>

mode_t umask(mode_t mask);     // 设置文件创建权限掩码
  • 参数 mask:新的 umask 值(八进制表示,如 0022)
  • 返回值:之前的 umask 值
  • umask 只影响 open()/creat()/mkdir() 等创建操作,不影响 chmod() 的显式设置

用户/组ID切换

#include <unistd.h>

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()

进程资源限制

#include <sys/resource.h>

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:获取进程与用户信息

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <pwd.h>
#include <grp.h>

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;
}

编译命令:

gcc -o proc_info proc_info.c
./proc_info

示例2:chdir 与 umask

#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>

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;
}

编译命令:

gcc -o dir_perm dir_perm.c
./dir_perm

示例3:seteuid 权限切换

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <errno.h>
#include <string.h>

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;
}

编译命令:

gcc -o priv_demo priv_demo.c
sudo ./priv_demo    # 需要 sudo 起始权限

示例4:getrlimit/setrlimit 资源限制

#include <stdio.h>
#include <stdlib.h>
#include <sys/resource.h>
#include <errno.h>
#include <string.h>

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;
}

编译命令:

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_NOFILERLIMIT_STACK,避免系统耗尽

7.5 面试要点(3-5个核心Q&A)

Q1:getuid()geteuid() 有什么区别?什么时候会不同?

A:getuid() 返回实际用户ID(登录用户),geteuid() 返回有效用户ID(用于权限检查)。当进程通过 setuid 程序(如 passwdsudo)运行时,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, ...) 可以为每个进程设定合理上限,实现资源隔离。