# 进程控制

# 孤儿进程

在操作系统中,如果父进程在子进程退出之前终止,子进程的状态会发生变化,成为所谓的 “孤儿进程”。在这种情况下,操作系统会将孤儿进程自动分配给 PID 为 1 的特殊进程,即 init 进程,作为其新的父进程。

孤儿进程的处理:

  • 收养机制:操作系统确保所有孤儿进程都有一个父进程。当父进程退出时,操作系统会介入,将孤儿进程的父进程设置为 init 进程(PID 为 1)。
  • 资源清理:当孤儿进程退出时,它的资源清理工作将由其新的父进程 ——init 进程来完成。这确保了系统资源得到适当的释放和回收。

init 进程的角色:init 进程:在类 UNIX 系统中,init 进程是第一个启动的进程,其 PID 固定为 1。它负责管理系统中的所有其他进程,包括收养孤儿进程并处理它们的资源清理。

孤儿进程的生命周期:孤儿进程在父进程退出后继续执行,直到它完成自己的任务并退出。在整个生命周期中,孤儿进程由 init 进程代为管理。

当子进程执行完毕后,如果父进程未能及时调用 wait() 或 waitpid() 函数来获取子进程的退出状态并进行清理,子进程将不会立即从系统中消失。相反,它将转变为所谓的 “僵尸进程”(zombie process)。僵尸进程保留了一个进程控制块(PCB),但不再占用其他系统资源。

# 僵尸进程

僵尸进程的产生:

  • 子进程退出:子进程执行完毕后,会向其父进程发送 SIGCHLD 信号,通知父进程其已经终止。
  • 父进程的责任:父进程应当捕捉此信号,并调用 wait() 或 waitpid() 函数来处理子进程的退出状态。如果父进程未能这么做,子进程将变成僵尸进程。

僵尸进程的影响:

  • 资源占用:僵尸进程虽然不再执行,但它的 PCB 继续占用内核态空间,这可能导致内核资源的浪费。
  • 系统性能:如果系统中存在大量的僵尸进程,它们可能会占用过多的内核空间,从而影响系统性能。

僵尸进程的处理:

  • 父进程的角色:父进程应当定期检查子进程的状态,并在子进程退出后及时调用 wait() 或 waitpid() 函数进行清理。
  • 系统管理员的职责:系统管理员应当监控系统中的僵尸进程数量,并采取措施确保父进程能够及时处理这些子进程。

# 守护进程

# 进程的用户 ID 和组 ID

在操作系统中,进程在运行时必须与特定的用户身份关联,这使得内核能够实施有效的权限控制。程序在执行时默认继承启动它的用户的身份。

用户身份的继承:例如,如果用户 A 登录系统并启动了一个程序,无论该程序是否由用户 A 创建,程序在运行时将采用用户 A 的身份。这意味着程序的进程将拥有用户 A 的用户 ID(UID)和组 ID(GID),这些分别被称为进程的 真实用户 ID 和 真实组 ID。

获取用户 ID 和组 ID:进程可以通过调用 getuid() 和 getgid() 函数来获取其 真实用户 ID 和 真实组 ID。这些函数是 UNIX 和类 UNIX 系统中的标准 API,用于获取当前进程的用户和组身份信息。

以下是一个简单的 C 语言示例,展示如何使用 getuid() 和 getgid() 函数:

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
int main() {
    uid_t uid = getuid(); // 获取真实用户 ID
    gid_t gid = getgid(); // 获取真实组 ID
    printf("Real User ID: %d\n", uid);
    printf("Real Group ID: %d\n", gid);
    return 0;
}

在这个示例中,程序将打印出启动它的用户的真实用户 ID 和真实组 ID。

# 进程的有效用户 ID 和组 ID

除了进程的 真实用户 ID(Real User ID)和 真实组 ID(Real Group ID),操作系统还定义了 有效用户 ID(Effective User ID)和 有效组 ID(Effective Group ID)的概念。这些附加的身份标识符对权限控制和安全策略至关重要。

系统内核在对进程执行访问权限检查时,依据的是进程的 有效用户 ID 和 有效组 ID,而不是它们的 真实用户 ID 和 真实组 ID。这意味着进程的实际权限可能不同于启动它的用户的身份。

在默认情况下,进程的 有效用户 ID 通常与其 真实用户 ID 相同,有效组 ID 也与其 真实组 ID 相同。这种设置简化了权限管理,但在需要时,系统允许进行更复杂的权限设置。

进程可以通过调用 geteuid() 和 getegid() 函数来获取其 有效用户 ID 和 有效组 ID。这些函数提供了对进程当前权限状态的洞察,允许程序根据实际的权限执行相应的操作。

sudo 命令的作用: sudo 是一种在 UNIX 和类 UNIX 系统中广泛使用的命令行工具,它允许授权的用户以另一个用户的身份,通常是超级用户(root),来执行命令。 sudo 的主要作用之一是通过提升执行命令的有效用户 ID 来临时提升执行权限。这使得普通用户能够执行一些需要更高权限的操作,而无需直接使用 root 账户,从而增强了系统的安全性。

有效组 ID 的修改:与有效用户 ID 类似,有效组 ID 也可以通过系统调用来修改。然而,这种修改可能会引入安全隐患。如果一个进程的有效组 ID 被更改为具有更高权限的组,那么该进程可能会访问到它原本不应该访问的资源。因此,在修改有效组 ID 时需要格外小心,并确保这种权限提升是出于合法和安全的操作需求。

# 文件权限

Linux 文件系统提供了一套丰富的权限控制机制,除了基本的读(r)、写(w)、执行(x)权限外,还包含几个特殊的权限位,这些特殊权限位对文件和目录的访问控制提供了额外的灵活性。

# SetUID(Set User ID upon execution)

SetUID 权限允许一个程序以文件所有者的权限来运行,而不是以执行该程序的用户权限运行。在文件权限的表示中,SetUID 通过在所有者的执行权限位旁添加一个's' 字符来标识。例如,如果一个可执行文件的权限设置为 -rwsr-xr-x ,这意味着无论哪个用户执行该文件,都将以文件所有者的权限来运行,同时该程序的有效用户 ID 也将设置为文件所有者的 ID。

# SetGID(Set Group ID upon execution)

SetGID 权限与 SetUID 类似,但它应用于文件的组权限。当 SetGID 应用于一个目录时,新创建的文件或目录将继承该目录的组 ID,而不是默认的创建者所属组。在文件权限表示中,SetGID 通过在组的执行权限位旁添加一个's' 字符来标识,例如 -rwxr-sr-x 。

# Sticky 位

Sticky 位 通常应用于目录,它允许只有文件所有者和超级用户可以删除或重命名目录中的文件。Sticky 位在文件权限表示中通过在其他权限位旁添加一个 't' 字符来标识。

# 修改文件权限和所有权

使用 chown 命令可以更改文件或目录的所有者和所属组,例如:

sudo chown root:root 文件

这将把指定文件的所有者和组更改为 root。

使用 chmod 命令可以修改文件或目录的权限,增加 SetUID 权限的示例命令为:

sudo chmod u+s 文件

这将为指定文件添加 SetUID 权限。

# 进程组

进程组 是操作系统中的一种进程集合机制,它将一个或多个进程组织在一起进行管理。

进程组的定义和特性

  • 在操作系统中,每个进程不仅拥有唯一的进程 ID(PID),还属于一个特定的进程组,并拥有一个 进程组 ID(process group ID)。
  • 进程组的组长是该组中第一个创建的进程,其 PID 与进程组 ID 相同。

进程组的创建和管理

  • 当通过 shell 启动一个新进程时,该进程不仅被创建,而且会创建一个新的进程组,并成为该进程组的组长。
  • 在使用 fork() 系统调用创建子进程时,子进程默认继承父进程的进程组 ID,即子进程和父进程处于同一个进程组。

进程组的存在条件:一个进程组在至少包含一个进程的情况下才存在。即使该进程不是组长,进程组依然有效。

获取进程组 ID: getpgrp() 函数用于获取调用进程的进程组 ID。

以下是一个简单的 C 语言示例,展示如何使用 getpgrp() 函数:

#include <unistd.h>
#include <stdio.h>
int main() {
    pid_t pgrp = getpgrp(); // 获取当前进程的进程组 ID
    printf("Process Group ID: %d\n", pgrp);
    return 0;
}

在这个示例中,程序将打印出当前进程所属的进程组 ID。

# 终端

在 Linux 操作系统中,终端是用户与系统交互的界面,它可以是物理的本地终端,也可以是通过网络连接的远程终端。操作系统启动过程中, init 进程扮演着关键角色,负责初始化系统环境并启动必要的服务。

终端的启动和管理

  • 当 Linux 操作系统启动时, init 进程会创建子进程,并使用 exec 函数替换当前进程的映像,以执行 getty 程序。 getty 程序负责管理虚拟控制台,设置终端类型,并打开终端设备。
  • 对于远程登录, getty 程序也会等待来自网络的连接请求。一旦连接建立, getty 程序同样会使用 exec 函数调用 login 程序。
  • login 程序是用户身份验证的关键环节,它要求用户输入用户名和密码,并验证这些凭证以确认用户的身份。

进程的创建和替换: exec 系列函数(如 execl , execv , execle , execve 等)用于替换当前进程的映像,启动新的程序。在 init 进程启动 getty 和 login 程序的过程中, exec 函数确保了当前进程资源的高效利用。

用户身份验证:一旦用户成功登录, login 程序将为用户创建一个新的会话,并启动用户的 shell 程序。这个过程涉及到多个系统调用,包括但不限于 fork , exec , setuid , 和 setgid 。

# 会话

# 会话特点

会话 是指用户与操作系统之间进行的一系列交互操作或通信过程。每当用户打开一个新的终端窗口或启动一个新的终端会话时,系统就会创建一个新的会话实例。

会话的定义和特性:会话允许用户完成从登录到注销的整个过程。在这个过程中,用户与系统的所有交互,包括打开文件、运行程序等,都是会话的一部分。

会话与进程组的关系:在一个会话中,可以存在多个进程组。会话本身可以看作是一个或多个进程组的集合。

控制进程的角色:控制终端建立连接的会话首进程被称为 控制进程。它负责管理与终端相关的信号和输入输出操作。

前台进程组与后台进程组:一个会话包含 最多一个前台进程组 和 多个后台进程组。前台进程组接收用户直接输入的信号,而后台进程组则在没有用户直接交互的情况下运行。

信号的发送:当用户从终端输入中断信号时,这些信号会被发送到前台进程组中的所有进程。

终端断开连接的影响:当终端与会话的连接断开时,挂断信号(如 SIGHUP)会被发送给控制进程。控制进程的结束通常会导致会话中的前台进程组和后台进程组也随之结束。

# 会话 ID

在 Linux 操作系统中,每个会话都拥有一个唯一的会话 ID,该 ID 通常与最初创建该会话的进程的进程 ID(PID)相同。会话的概念是作业控制的核心部分,它允许用户和进程进行有效的管理和通信。

会话 ID 的获取与会话的创建:

  • getsid() 函数用于获取调用进程所在会话的会话 ID。这个系统调用是检查和管理系统会话的重要工具。
  • setsid() 系统调用用于创建一个新的会话。调用此函数的进程将成为新会话的领导进程,即会话首进程。

创建会话的条件:调用 setsid() 的进程不能是任何进程组的组长。这是为了确保新会话的独立性和新会话首进程的控制权。

会话的持续性:一旦创建了新会话,即使创建它的原始 shell 或终端会话被关闭,新会话及其子进程也会继续存在,不会受到影响。

# 守护进程

守护进程(Daemon)是 Linux 操作系统中一种特殊的后台进程,它们用于执行系统或应用程序的特定任务。与需要用户交互的进程不同,守护进程独立于终端运行,通常长时间持续运行,直到系统关闭,而不是与用户会话绑定。守护进程的例子包括日志记录、系统监控、调度任务、邮件服务和服务器程序等。

守护进程的创建流程:

  1. 创建子进程:父进程首先创建一个子进程,然后父进程终止。这是为了确保守护进程在系统启动时能够独立于任何用户会话运行。
  2. 创建新会话:子进程使用 setsid() 调用创建一个新的会话,并成为该会话的会话首进程。
  3. 更改工作目录:子进程将当前工作目录更改为根目录( / )。这样做是为了防止守护进程对当前目录的依赖,因为如果当前目录所在的文件系统被卸载,守护进程可能会受到影响。
  4. 重设文件权限掩码:子进程通过调用 umask(0) 将文件权限掩码设置为 0,以确保创建的文件具有最大的权限,避免权限受限。
  5. 关闭文件描述符:子进程关闭所有不必要的文件描述符,特别是标准输入(0)、标准输出(1)和标准错误(2)。这通常通过重定向这些描述符到 /dev/null 来实现,从而避免守护进程与终端的交互。

以下是一个简化的 C 语言示例,展示如何创建一个守护进程:

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
int main() {
    // 创建子进程并终止父进程
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork failed");
        return 1;
    }
    if (pid > 0) {
        // 父进程终止
        _exit(0);
    }
    // 创建新的会话
    if (setsid() < 0) {
        perror("setsid failed");
        return 1;
    }
    // 更改工作目录
    if (chdir("/") < 0) {
        perror("chdir failed");
        return 1;
    }
    // 重设文件权限掩码
    umask(0);
    // 关闭标准文件描述符
    close(STDIN_FILENO);
    close(STDOUT_FILENO);
    close(STDERR_FILENO);
    // 守护进程的代码逻辑
    // ...
    return 0;
}

在这个示例中,程序展示了守护进程创建的基本步骤,包括创建子进程、创建新会话、更改工作目录、重设文件权限掩码和关闭文件描述符。

# Linux shell 指令

# ps 指令

在 Linux 系统中, ps 命令是一个强大的工具,用于显示系统中所有进程的信息。虽然 ps 命令提供了多种选项和复杂的功能,但在日常工作中,最常用的两种选项组合是 ps -elf 和 ps aux 。

# ps -elf (UNIX System V 风格)

这个选项组合以长格式显示所有进程的信息,包括:

  • F(Flags):显示进程的标志,这些标志反映了内核处理进程的不同属性。
  • S(Status):显示进程的状态,如运行中(R)、睡眠中(S)、不可中断的睡眠状态(D)、僵尸进程(Z)、停止或被跟踪(T)和空闲进程(I)。
  • UID:显示启动进程的用户的用户 ID。
  • PID:显示进程的 ID。
  • PPID:显示创建当前进程的父进程的 ID。
  • C(CPU Usage):显示进程占用的 CPU 使用率的近似值。
  • PRI(Priority):显示进程的优先级。
  • NI(Nice Value):显示进程的 nice 值,这是一个影响进程调度优先级的值。
  • ADDR:显示进程的内存地址。
  • SZ(Size):显示进程使用的内存大小。
  • WCHAN:如果进程处于睡眠状态,这里显示它正在等待的资源。
  • STIME:显示进程开始的时间。
  • TTY:显示进程绑定的终端设备。
  • TIME(CPU Time):显示进程使用的 CPU 时间总计。
  • CMD(Command):显示启动进程的命令名或命令行。

# ps aux (UNIX BSD 风格)

这个选项组合以用户为中心的格式显示所有进程的信息,包括:

  • USER:进程的所有者。
  • PID:进程 ID。
  • %CPU:进程占用的 CPU 百分比。
  • %MEM:进程占用的内存百分比。
  • VSZ(Virtual Size):进程使用的虚拟内存量(以 KB 为单位)。
  • RSS(Resident Set Size):进程占用的固定内存量(以 KB 为单位)。
  • TTY:进程的终端类型。
  • STAT(Status):显示进程的状态,可能包括附加值,如高优先级进程(<)、会话领导进程(s)、多线程(l)、位于后台的进程组(+)等。
  • START:进程的启动时间。
  • TIME:进程占用 CPU 的时间。
  • COMMAND:启动进程的命令。

# free 指令

free 命令是 Linux 系统中一个用于查看内存占用情况的重要工具。它提供了关于系统内存使用情况的即时快照,包括物理内存和交换空间的详细信息。

# 物理内存(Mem)

  • Total:显示系统安装的总内存量。
  • Used:显示已被系统使用的内存量,包括被应用程序和系统进程占用的内存。
  • Free:显示当前未被使用的内存量。
  • Shared:显示被多个进程共享的内存量,通常用于存储临时文件。
  • Buff/Cache:显示用于缓存的内存量,这部分内存用于存储文件系统的数据,以提高数据访问速度。
  • Available:提供一个粗略估计,表示系统在不影响现有应用程序运行的情况下还能使用多少内存。

# 交换空间(Swap)

交换空间是当物理内存不足时,系统用于存储不活跃或暂时不需要的数据的磁盘空间。 free 命令也显示了交换空间的使用情况:

  • Total:显示系统配置的总交换空间大小。
  • Used:显示当前已使用的交换空间大小。
  • Free:显示当前未使用的交换空间大小。

交换空间主要用于存储因内存不足而被移出物理内存的数据,而新的数据加载通常直接从磁盘加载到内存中,而不是首先加载到交换空间。

# top 指令

top 命令是一个动态的系统监控工具,用于实时显示系统中运行进程的信息。以下是 top 命令的主要输出内容:

# 系统情况

  • 当前时间:显示当前的系统时间。
  • 系统运行时间:自系统启动以来的运行时间。
  • 当前登录用户数:当前登录到系统的用户数量。
  • 系统负载:显示过去 1 分钟、5 分钟和 15 分钟的平均负载值。对于单个 CPU,负载在 0 到 1.00 之间通常表示正常;超过 1.00 可能表明系统过载。在多核 CPU 系统中,平均负载不应超过 CPU 核心的总数。

# 任务情况(Tasks)

  • 显示系统中的进程总数、运行中的进程数、睡眠中的进程数、停止的进程数和僵尸进程数。

# CPU 使用情况(% CPU)

  • us(用户态):用户态运行占用的 CPU 百分比。
  • sy(内核态):内核态运行占用的 CPU 百分比。
  • ni(nice):改变优先级的进程占用的 CPU 百分比。
  • id(空闲):CPU 空闲的百分比。
  • wa(iowait):等待输入 / 输出操作完成的时间百分比。
  • hi(硬件中断):处理硬件中断的时间百分比。
  • si(软件中断):处理软件中断的时间百分比。
  • st(steal time):在虚拟化环境中,其他虚拟机占用的时间百分比。

# 内存使用情况

  • 第四行和第五行的信息等同于 free 命令的输出,提供了内存使用的概览。

# 进程列表部分

  • PID:进程的唯一标识号。
  • USER:运行进程的用户。
  • PR:进程的优先级。
  • NI:进程的 nice 值,影响进程调度优先级。
  • VIRT:进程使用的虚拟内存总量。
  • RES:进程使用的、未被换出的物理内存大小。
  • SHR:共享内存的大小,可能被多个进程共享。
  • S:进程的状态。
  • %CPU:在动态更新的时间段内,进程占用的 CPU 时间百分比。
  • %MEM:进程使用的物理内存百分比。
  • TIME+:进程自启动以来占用的总 CPU 时间。
  • COMMAND:启动进程的命令名称或命令行。

# PID

操作系统为每个进程分配了一个唯一的正整数标识符,称为 进程 ID(PID)。PID 是进程间亲缘关系的关键标识,其中启动其他进程的称为 父进程,被启动的称为 子进程。在 Linux 系统中,PID 和进程控制块(PCB)之间存在一一对应关系,可以通过查看 task_struct 结构体来获取进程的 PID 及其父进程 ID。

使用 ps -l 命令可以查看进程列表,其中包括 shell 进程和 ps 命令进程。通过 PPID 列可以确定 ps 进程的父进程是 bash shell。此外,通过系统调用 getpid() 和 getppid() ,可以在程序中获取当前进程的 PID 和父进程 ID,如下所示:

int main(int argc, char* argv[]) {
    printf("Current PID: %d\n", getpid());
    printf("Current parent process PID: %d\n", getppid());
    return 0;
}

当 Linux 系统启动时,首先执行的是 BIOS 程序,这是一段固件级别的初始化代码,负责初始化硬件并定位启动设备,如硬盘或 USB 等。接着,启动设备上的 Bootloader 代码被加载并执行,Bootloader 的任务是加载 Linux 内核。

一旦 Bootloader 将控制权交给内核,内核便开始初始化硬件设备、设置内存管理,并创建和启动关键的内核线程和服务。内核启动的第一个用户级进程通常是 init 进程,其 PID 通常是 1,负责启动系统上的所有其他进程和服务。 init 进程读取系统配置文件,如 /etc/init.d/ 或 /etc/rc2.d/ 等,初始化系统环境,并按顺序启动程序,使系统达到正常运行状态。

以 Bash shell 为例, init 进程通过读取配置文件 /etc/rc2.d/ 中的 /etc/init.d/ssh 来加载 ssh 程序,随后 ssh 程序加载 Bash shell。通过 Bash shell 启动的任何程序都成为 Bash shell 的子进程。

# Kill 命令

kill 命令是一种在 Linux 和 UNIX 系统中用于向进程发送信号的工具。信号是一种软件中断,可以指示进程执行特定的操作,如终止或暂停。

# 进程的前台与后台状态

  • 当用户从终端启动一个进程时,该进程默认在前台运行,并能接收键盘发送的信号。例如, Ctrl+C 产生终止信号(SIGINT),而 Ctrl+Z 产生暂停信号(SIGTSTP)。
  • 如果进程在后台运行,它不会接收键盘信号,而只能通过 kill 命令发送信号。

# 使用 kill 命令

  • 要查看所有可用的信号列表,可以使用:

    kill -l
  • 发送信号以异常方式终止进程,可以使用:

    kill -9 pid

    其中 -9 对应于 SIGKILL 信号,这是一个强制终止进程的信号。

# 后台进程的启动与管理

  • 通过在命令末尾添加 & 符号,可以直接启动进程到后台。例如:

    vim ./01_test &

    这将启动 vim 并将其置于后台,允许用户继续在终端中执行其他命令。

  • 使用 Ctrl+Z 可以暂停当前运行的前台进程,将其放到后台,并显示任务编号。例如:

    vim ./01_test
    [Ctrl+Z]

    这将暂停 vim 并显示任务编号。

# 后台任务的管理

  • 使用 jobs 命令可以查看当前所有的后台任务。
  • 使用 fg 命令可以将后台任务带到前台继续运行。
  • 使用 bg 命令可以继续在后台运行之前暂停的任务。

# Linux C 函数

# system 执行 shell 命令

system 函数是 C 语言标准库中用于执行 shell 命令的函数。它允许你在 C 程序中启动一个新进程来运行一个 shell 命令,并等待该命令完成执行。

函数原型:

int system(const char *command);

参数: command :一个指向以 null 结尾的字符串的指针,这个字符串包含了要执行的 shell 命令。

返回值:

  • 成功时, system 返回命令的退出状态。在大多数系统上,这通常是一个非负整数,表示命令的终止状态。
  • 如果命令行参数是 NULL ,函数返回 0。
  • 失败时,返回 -1,并设置全局变量 errno 以指示错误类型。

system 函数实际上是调用了 fork 和 exec 函数族来创建一个新的进程,并在这个新进程中执行指定的命令。 system 调用的进程会等待由 command 指定的命令执行完成,然后返回。

以下是使用 system 函数的一个例子:

#include <stdio.h>
#include <stdlib.h>
int main() {
    int status;
    // 执行 shell 命令
    status = system("echo Hello, World!");
    if (status == -1) {
        perror("system");
        return 1;
    }
    printf("命令执行的退出状态为: %d\n", status);
    return 0;
}

在上面的例子中,使用了 system 函数执行了 echo Hello, World! 这个 shell 命令。如果 system 调用失败,打印错误信息并返回。否则打印出命令的退出状态。

使用 system 时需要注意的事项:

  • system 会启动一个 shell 来执行命令,这可能会导致安全风险,特别是如果命令字符串由不可信的输入构成时。
  • system 函数的返回值通常需要通过 WEXITSTATUS 宏和 wait 函数族来解析,以确定子进程的确切退出状态。
  • 在某些环境中,为了避免安全问题,推荐使用更具体的 exec 函数族,或者使用管道和 popen / pclose 对来执行外部命令。

由于 system 函数的这些限制和潜在的安全问题,现代的 C 程序设计中倾向于使用其他方法来执行外部命令。

# fork 复制进程

fork 函数是 POSIX 标准中用于创建进程的系统调用,它在 UNIX 和类 UNIX 系统中广泛使用。 fork 函数通过复制调用它的进程来创建一个新的进程,这个新进程称为子进程(child process),而调用它的进程称为父进程(parent process)。

函数原型:

pid_t fork(void);

参数: fork 函数不接受任何参数。

返回值:

  • 在父进程中, fork 返回新创建子进程的进程 ID(PID),这是一个正整数。
  • 在子进程中, fork 返回 0。
  • 如果函数调用失败,返回 -1 ,并设置全局变量 errno 以指示错误类型。

pid_t 是一个用来表示进程 ID 的数据类型,通常定义为 int 或 long int 。

以下是使用 fork 函数的一个例子:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
    pid_t pid = fork();
    if (pid == -1) {
        //fork 调用失败
        perror("fork");
        return EXIT_FAILURE;
    }
    if (pid == 0) {
        // 这是子进程(子进程的 PID 为 0)
        printf("我是 PID : %ld 的子进程 \n", (long)getpid());
    } else {
        // 这是父进程
        printf("我是 PID 为 %ld 的父进程,我的子进程 PID 为 %ld\n", (long)getpid(), (long)pid);
    }
    // 父进程和子进程可以继续执行其他操作...
    return EXIT_SUCCESS;
}

在上面的例子中,调用 fork 来创建一个新的子进程。如果 fork 调用失败打印错误信息并返回失败状态。如果 fork 成功,则根据返回值判断当前是父进程还是子进程,并打印相应的消息。

fork 函数的行为特点:

  • 子进程继承父进程的许多属性,包括文件描述符、环境变量、当前工作目录等。
  • 子进程获得父进程数据空间、堆和栈的副本,但对这些空间的修改不会影响父进程。
  • fork 之后,子进程的返回地址会接着执行 fork 调用之后的代码,而父进程则继续执行 fork 调用之后的代码。
  • 子进程的 PID 在系统中是唯一的。

# 内存复制

当调用 fork 函数时,它会创建一个新的进程,这个新进程几乎完全复制了当前进程的用户态地址空间。这意味着在 fork 调用时,父进程的内存布局,包括进程上下文、变量、堆栈、程序代码等,都会被复制到子进程中。

子进程的独立性:尽管子进程开始时是父进程的一个副本,但为了确保两个进程的独立性,某些属性和资源需要在子进程中进行适当的修改。这些修改主要涉及进程控制块(PCB)。

进程标识符(PID):子进程的 PID:子进程获得一个唯一的进程标识符(PID),这个 PID 与父进程的 PID 不同,确保了子进程在系统中的唯一性。

父进程标识符(PPID):子进程的 PPID:子进程的父进程标识符(PPID)被设置为创建它的父进程的 PID,这建立了进程间的父子关系。

其他属性和资源:除了 PID 和 PPID 之外,子进程还会继承父进程的许多属性,如打开的文件描述符、信号处理设置、用户和组 ID 等。但是,子进程会拥有自己的独立堆栈和内存空间,以支持其独立执行。

子进程的创建: fork 函数在父进程中返回新创建子进程的 PID,而在子进程中返回 0。这个特性可以用于在代码中区分父进程和子进程的执行路径。

1413469471bcef7270cb0727d47e5682.png

# 写时复制

为了降低数据复制的开销并优化内存管理, fork 函数采用了写时复制(Copy-On-Write,简称 COW)策略。这种策略在 fork 执行时的行为如下:

  • 操作系统不会立即复制父进程的整个内存空间给子进程。相反,它允许父进程和子进程在初始阶段暂时共享相同的物理内存页。
  • 这些共享的内存页在内存中被标记为只读。这意味着,只要父进程或子进程没有尝试修改这些页面,它们就可以继续共享这些内存页,而无需复制。
  • 一旦父进程或子进程尝试写入这些共享的页面,操作系统会介入。它会为发起写操作的进程分配一个新的物理内存页,并执行数据的复制操作。这个新分配的页将只属于发起写操作的进程,而另一个进程仍然保留对原始只读页的访问。

COW 机制确保了只有在进程实际需要修改数据时才进行内存页的复制,从而:

  • 减少了不必要的内存复制操作,降低了内存使用量。
  • 提高了进程创建的效率,尤其是在父进程拥有大量内存页时。

# 文件打开和 fork

在使用 fork() 函数生成子进程之前,如果已经执行了文件打开操作,子进程将继承父进程的文件描述符集合。这些文件描述符实际上是指向操作系统中的文件表项的指针。重要的是,文件表项是由操作系统管理的,它们可以被多个进程共享。这导致了一个结果:父进程和子进程将共享对同一个文件表项的访问。

文件表项的共享特性:在文件表项中,除了包含了指向磁盘上文件 inode 的指针之外,还记录了文件的当前读写偏移量。这意味着父进程和子进程在对文件进行读写操作时,将共享这个偏移量,从而影响彼此的文件操作位置。

文件描述符的独立性:相对地,在 fork() 函数调用之后,如果父进程和子进程各自独立地打开了一个文件,操作系统将分别为它们创建独立的文件表项。即便这两个进程打开的是同一个文件系统中的对象,即 inode 相同,它们的文件表项和读写偏移量也将是独立的。

独立文件操作的意义:这种机制确保了父进程和子进程在文件操作中的独立性。即使它们可能访问相同的文件数据,它们的读写位置和状态是分开维护的,从而避免了相互之间的干扰。

假设父进程打开了一个文件并读取了一些数据,然后调用了 fork() 。子进程将继续从文件的当前位置开始读取,这个位置是父进程留下的位置。如果子进程继续读取或写入文件,那么父进程下次对文件的操作也会从这个新的位置开始。

#include <unistd.h>
#include <fcntl.h>
int main() {
    int fd = open("example.txt", O_RDONLY); // 父进程打开文件
    pid_t pid = fork(); // 创建子进程
    if (pid == 0) {
        // 子进程
        char buffer[10];
        read(fd, buffer, sizeof(buffer)); // 子进程从父进程停止的地方继续读取
    } else if (pid > 0) {
        // 父进程
        // 父进程可以继续对文件进行操作,共享相同的文件读写位置
    }
    return 0;
}

在上述示例中,如果子进程执行了读取操作,父进程在子进程读取后对文件的任何操作都将从子进程停止读取的地方开始。

fork_open_before.png

fork_open_after.png

# exec 函数替换进程映像

# execl 函数

execl 函数是 POSIX 标准中用于执行一个新程序的系统调用,它替换当前进程的映像,而不是创建一个新进程。 execl 是 exec 函数族中的一员,它允许你指定程序的路径和参数列表。

函数原型:

int execl(const char *path, const char *arg0, ... /*, (char *)0 */);

参数:

  • path :要执行的程序的路径。
  • arg0 :程序的第一个命令行参数,通常为主函数 main 的 argv[0] 。
  • 随后的参数:一系列以 null 结尾的字符串,表示程序的其余命令行参数。
  • ((char *)0) :最后一个参数必须是 NULL ,以标记参数列表的结束。

返回值:

  • execl 函数不会返回,因为它会替换当前进程的映像。如果执行成功,当前进程将开始执行新的程序。
  • 如果 execl 调用失败,它不会返回。通常,你会在调用 execl 之前设置一个跳板函数(trampoline),以便在 execl 失败时返回错误。

execl 函数的行为特点:

  • 它加载并运行指定路径的程序,替换当前进程的映像。
  • 当前进程的内存空间、文件描述符、环境变量等都会被新程序的相应内容替换。
  • execl 调用成功后,当前进程的代码和数据将被新程序的代码和数据替换,进程的 ID(PID)保持不变。

以下是使用 execl 函数的一个例子:

#include <stdio.h>
#include <stdlib.h>
int main() {
    // 使用 execl 执行 "ls -l" 命令
    if (execl("/bin/ls", "ls", "-l", NULL) == -1) {
        //execl 调用失败
        perror("execl");
        return EXIT_FAILURE;
    }
    //execl 调用成功时不会返回
    return EXIT_SUCCESS;
}

Code_lWqLJ2OrWt.png

在上面的例子中使用 execl 来执行 /bin/ls -l 命令。如果 execl 调用失败,打印错误信息并返回失败状态。然而,如果 execl 成功,当前进程将被 ls -l 命令替换,并且不会执行 perror 和 return 之后的代码。

使用 execl 时需要注意的事项:

  • 由于 execl 替换当前进程的映像,因此它不会返回。如果需要处理 execl 失败的情况,应该在调用 execl 之前进行错误处理。
  • execl 只接受字符串参数,如果需要传递其他类型的数据,应该使用其他 exec 函数族成员,如 execv 或 execvp 。
  • execl 调用时,程序的当前工作目录和环境变量等属性将被继承。
  • 安全起见,如果路径或参数来自不可信的输入,应该使用 execv 或 execvp 并确保参数经过严格验证。

# execv 函数

execv 函数是 POSIX 标准中用于执行一个新程序的系统调用,它替换当前进程的映像。与 execl 不同, execv 不需要将参数逐个列在函数调用中,而是接受一个参数数组。

函数原型:

int execv(const char *path, char *const argv[]);

参数:

  1. path :要执行的程序的路径,这是一个字符串,指定了可执行文件的路径。
  2. argv :这是一个字符指针数组,包含了传递给新程序的参数列表。数组的最后一个元素必须是空指针( NULL ),以标记参数列表的结束。

返回值:

  • execv 函数在成功执行时不会返回,因为它将当前进程的映像替换为新程序的映像,进程将开始执行新程序。
  • 如果 execv 调用失败,它将返回 -1 并设置全局变量 errno 以指示错误。

execv 函数的行为特点:

  • 它加载并运行指定路径的程序,替换当前进程的映像。
  • 当前进程的内存空间、文件描述符、环境变量等都会被新程序的相应内容替换。
  • execv 调用成功后,当前进程的代码和数据将被新程序的代码和数据替换,进程的 ID(PID)保持不变。

以下是使用 execv 函数的一个例子:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
    char *const argv[] = {"ls", "-l", NULL}; // 命令行参数数组
    const char *path = "/bin/ls";           // 程序路径
    // 使用 execv 执行 "ls -l" 命令
    if (execv(path, argv) == -1) {
        //execv 调用失败
        perror("execv");
        return EXIT_FAILURE;
    }
    //execv 调用成功时不会返回
    return EXIT_SUCCESS;
}

Code_lWqLJ2OrWt.png

在上面的例子中,我们定义了一个参数数组 argv 和程序路径 path 。使用 execv 执行 /bin/ls -l 命令。如果 execv 调用失败,打印错误信息并返回失败状态。如果 execv 成功,当前进程将被 ls -l 命令替换,并且不会执行 perror 和 return 之后的代码。

使用 execv 时需要注意的事项:

  • 由于 execv 替换当前进程的映像,因此它不会返回。如果需要处理 execv 失败的情况,应该在调用 execv 之前进行错误处理。
  • execv 接受一个以空指针结束的参数数组,这使得它在处理来自外部源的参数时更加灵活和安全。
  • execv 调用时,程序的当前工作目录和环境变量等属性将被继承。
  • 安全起见,如果路径或参数来自不可信的输入,应该确保路径的有效性,并对参数进行严格验证。

# 退出进程

在进程阶段,进程总共有 5 种终止方式,其中 3 种是正常终止,还有 2 种是异常终止:

终止方式 终止情况
main 函数中调用 return 正常
调用 exit 函数 正常
调用 _Exit 函数或者 _exit 函数 正常
调用 abort 函数 异常
接收到引起进程终止的信号 异常

46111f5db308a670df7920e2b0130265.png

当进程处于前台的时候,按下 ctrl+c 或者是 ctrl+\ 可以给整个进程组发送键盘中断信号 SIGINT 和 SIGQUIT。

# exit 终止当前进程

exit 函数是 C 语言标准库中用于终止当前进程的函数。当你希望正常或异常地结束程序的执行时,可以调用 exit 函数。

函数原型:

void exit(int status);

参数: status :一个整数,表示程序的退出状态。这个状态可以被父进程或操作系统捕获,用于了解程序是成功完成执行还是遇到了错误。

功能:当 exit 被调用时,它将终止当前进程,并返回给操作系统一个退出状态码。进程的所有资源将被释放,包括打开的文件描述符、内存、锁等。

返回值: exit 函数不返回。一旦调用,控制权将返回给操作系统,当前进程将终止。

status 参数通常遵循以下约定:

  • 0:表示程序正常退出。
  • 非零值:通常表示程序由于某种原因而异常退出,例如错误或异常条件。

以下是使用 exit 函数的一个例子:

#include <stdlib.h>
int main() {
    // 正常退出程序
    exit(0);
    // 以下代码将不会被执行
    return 0;
}

在上面的例子中, exit(0) 被调用,导致程序立即以状态码 0 退出,表示成功完成。

使用 exit 时需要注意的事项:

  • 一旦调用 exit ,当前进程将终止,所有未处理的清理工作将不会被执行。因此,确保在调用 exit 之前完成所有必要的清理工作。
  • 如果在多线程程序中调用 exit ,整个程序的所有线程都将被终止,而不仅仅是调用 exit 的线程。
  • 在某些情况下,可以使用 _exit 或 _Exit 函数作为替代,这些函数不进行任何清理工作,直接终止进程。但通常推荐使用 exit ,因为它允许进行标准的退出前清理。

exit 是程序控制流程的基本部分,确保程序可以在完成工作或遇到错误时正确地终止。

# _Exit 终止当前进程

_Exit 函数是 C 语言标准库中的一个函数,用于立即终止当前进程,而不像 exit 函数那样执行标准 I/O 缓冲区的刷新或其他清理操作。 _Exit 提供了一种快速退出进程的方式,通常用于紧急情况或当确定不需要进行任何清理时。

函数原型:

void _Exit(int status);

参数: status :一个整数,表示程序的退出状态码。这个状态码可以被父进程或操作系统捕获,用于了解程序是成功完成执行还是遇到了错误。

功能: _Exit 函数调用导致当前进程立即终止,并返回给操作系统一个退出状态码。与 exit 不同, _Exit 不会刷新标准 I/O 缓冲区,也不会调用任何退出处理程序(如 exit 函数注册的 atexit 函数)。它直接关闭所有文件描述符并终止进程。

返回值: _Exit 函数不返回。一旦调用,控制权将返回给操作系统,当前进程将终止。

以下是使用 _Exit 函数的一个例子:

#include <stdlib.h>
void terminate_process(int status) {
    // 立即退出进程,不进行任何清理
    _Exit(status);
}
int main() {
    // 正常退出程序
    terminate_process(0);
    // 以下代码将不会被执行
    return 0;
}

在上面的例子中, terminate_process 函数使用 _Exit 立即退出进程。由于 _Exit 不返回,因此 main 函数中的后续代码不会被执行。

使用 _Exit 时需要注意的事项:

  • _Exit 用于紧急情况或当确定不需要进行任何清理时。如果你的程序需要在退出前进行一些清理工作(如释放资源、同步数据等),应该使用 exit 而不是 _Exit 。
  • _Exit 不刷新标准 I/O 缓冲区,因此如果缓冲区中的数据需要被写入磁盘,使用 _Exit 可能导致数据丢失。
  • _Exit 不调用任何退出处理程序,因此如果程序中有注册 atexit 函数或其他退出处理程序,使用 _Exit 将跳过这些处理程序。
  • 在多线程程序中, _Exit 只终止调用它的线程所在的进程,而不是所有线程。

_Exit 提供了一种快速退出进程的方法,但应该谨慎使用,确保在退出前不需要进行其他任何操作。

# _exit 终止当前进程

_exit 函数是定义在 unistd.h 中的一个函数,其原型是:

void _exit(int status);

参数: status :一个整数,表示程序退出时的状态码。这个状态码可以被操作系统或父进程用来确定程序是成功退出还是遇到了错误。

功能: _exit 函数用于立即终止当前进程,并退出到操作系统。与 exit 函数不同, _exit 不会执行任何清理操作,如刷新标准 I/O 缓冲区或调用注册的退出处理程序(例如通过 atexit 注册的函数)。

返回值: _exit 函数不返回。一旦调用,它将立即终止进程,并将指定的退出状态码返回给操作系统。

使用 _exit 的示例代码如下:

#include <unistd.h>
int main() {
    // 正常退出,状态码为 0
    _exit(0);
    // 这行代码不会被执行,因为 _exit 不会返回
}

在上面的示例中, _exit(0) 被调用,程序将立即退出,状态码为 0,表示正常退出。

使用 _exit 时应注意的事项:

  • 由于 _exit 不执行任何清理操作,它比 exit 更适合于紧急情况下的退出,或者在确定不需要任何清理的情况下使用。
  • 如果程序中有未保存的数据或需要执行其他清理工作,应使用 exit 而不是 _exit 。
  • 在多线程程序中, _exit 只会导致当前线程终止,其他线程将继续运行。如果需要终止整个进程,应确保所有线程都正确同步。
  • _exit 直接终止进程,不会进行标准的退出序列,因此使用时需要谨慎,确保不会丢失重要数据或资源。

# abort 异常终止当前进程

abort 函数是 C 语言标准库中的一个函数,用于异常终止当前进程。当 abort 被调用时,它会导致进程立即停止执行,并返回一个退出状态码,表示进程是因为一个信号而终止的。

函数原型:

void abort(void);

参数: abort 函数不接受任何参数。

返回值: abort 函数不返回。一旦调用,它将立即终止当前进程。

abort 函数的行为包括:

  • 向进程发送 SIGABRT 信号(在 POSIX 系统中)。
  • 调用所有已注册的 SIGABRT 信号处理程序。
  • 清理临时文件(如果进程使用的是标准 I/O 库)。
  • 关闭所有打开的文件描述符。
  • 将退出状态码设为 SIGABRT 的值加上 128,或者在某些系统中,可能直接设置为 SIGABRT 的值。

以下是使用 abort 函数的一个例子:

#include <stdlib.h>
#include <stdio.h>
int main() {
    // 触发异常终止
    abort();
    // 下面的代码将不会被执行
    return 0;
}

在上面的例子中,调用 abort() 将立即终止程序,程序不会执行 abort() 之后的任何代码。

使用 abort 时需要注意的事项:

  • abort 通常用于遇到无法恢复的错误时,需要立即停止程序的情况。
  • 由于 abort 会发送 SIGABRT 信号,任何为 SIGABRT 信号注册的处理程序都将被调用。
  • abort 会清理标准 I/O 库使用的临时文件,但不会刷新标准 I/O 缓冲区,这意味着可能存在数据丢失的风险。
  • 在多线程程序中, abort 调用将终止整个进程,不仅仅是当前线程。

abort 是处理严重错误情况的一种方式,但开发者在使用时应该谨慎,确保所有重要的资源都得到了适当的管理,以避免资源泄露或其他副作用。

# wait 等待子进程状态

wait 函数是 POSIX 标准中用于等待子进程状态改变的系统调用。当一个父进程创建了一个或多个子进程后,它可能会调用 wait 函数来挂起执行,直到至少有一个子进程更改其状态,这通常是因为子进程已经终止。

函数原型:

pid_t wait(int *wstatus);

参数: wstatus :一个指向整数的指针,用于接收子进程的退出状态。如果子进程是因为接收到信号而终止的,这个参数会提供有关退出原因的信息。

返回值:

  • 成功时, wait 返回被等待终止的子进程的进程 ID(PID)。
  • 如果调用失败,返回 (-1) ,并设置全局变量 errno 以指示错误。

wait 函数的行为特点:

  • 父进程调用 wait 将挂起,直到有一个子进程终止。
  • wait 可以等待任何一个子进程的状态改变,而不管子进程的 PID。
  • 一旦子进程终止, wait 将返回,并通过 wstatus 参数提供子进程的退出状态。

退出状态可以通过宏 WIFEXITED(status) 检查子进程是否正常退出,如果是, WEXITSTATUS(status) 宏可以获取子进程的退出码。如果子进程是因为信号而终止的, WIFSIGNALED(status) 宏可以用来获取该信号的编号。

以下是使用 wait 函数的一个例子:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main() {
    pid_t pid = fork(); // 创建子进程
    if (pid == -1) {
        //fork 调用失败
        perror("fork");
        return EXIT_FAILURE;
    }
    if (pid == 0) {
        // 这是子进程
        printf("I am the child process with PID: %d\n", getpid());
        exit(EXIT_SUCCESS); // 子进程退出
    } else {
        // 这是父进程
        int wstatus;
        pid_t terminated_pid = wait(&wstatus); // 等待子进程终止
        printf("Child process (PID: %d) terminated\n", terminated_pid);
        if (WIFEXITED(wstatus)) {
            printf("Child exited with status: %d\n", WEXITSTATUS(wstatus));
        }
    }
    return EXIT_SUCCESS;
}

在上面的例子中,父进程调用 fork 创建了一个子进程。子进程打印自己的 PID 并正常退出。父进程调用 wait 等待子进程终止,并接收子进程的退出状态。然后,父进程检查子进程是否正常退出,并打印退出状态码。

使用 wait 时需要注意的事项:

  • wait 可以被多次调用,以等待多个子进程的终止。
  • 如果有多个子进程, wait 将返回第一个终止的子进程的 PID。
  • 如果不希望等待子进程,可以使用 waitpid 函数,它允许指定要等待的子进程的 PID,以及其他选项。
宏 说明
WIFEXITED(wstatus) 子进程正常退出的时候返回真,此时可以使用 WEXITSTATUS(wstatus) ,获取子进程的返回情况
WIFSIGNALED(wstatus) 子进程异常退出的时候返回真,此时可以使用 WTERMSIG(wstatus) 获取信号编号,可以使用 WCOREDUMP(wstatus) 获取是否产生 core 文件
WIFSTOPPED(wstatus) 子进程暂停的时候返回真,此时可以使用 WSTOPSIG(wstatus) 获取信号编号
...

# waitpid 检查子进程状态

waitpid 函数是 POSIX 标准中用于等待或检查一个子进程状态改变的系统调用。与 wait 函数相比, waitpid 提供了更多的灵活性,允许父进程指定要等待的子进程的 PID,以及其他选项来控制等待的行为。

函数原型:

pid_t waitpid(pid_t pid, int *wstatus, int options);

参数:

  1. pid :指定要等待的子进程的进程 ID(PID)。特殊值 -1 表示等待任何子进程。\

    PID 数值 效果
    < -1 等待进程 PID 和 pid 绝对值的进程
    == -1 等待任一个子进程,等价于 wait
    == 0 等待同一进程组的任意子进程
    > 0 等待指定 PID 的进程
  2. wstatus :一个指向整数的指针,用于接收子进程的退出状态。如果子进程是因为接收到信号而终止的,这个参数会提供有关退出原因的信息。

  3. option :指定等待行为的选项,可以是以下宏的组合:

    • WNOHANG :如果没有任何子进程状态改变, waitpid 不会阻塞,而是立即返回 0。
    • WUNTRACED :也会返回状态为 STOPPED (已停止)的子进程的信息。

返回值:

  • 成功时, waitpid 返回被等待状态改变的子进程的 PID。
  • 如果调用失败,返回 (-1) ,并设置全局变量 errno 以指示错误。
  • 如果 WNOHANG 选项被设置,并且没有子进程状态改变,则返回 0。

以下是使用 waitpid 函数的一个例子:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main() {
    pid_t pid = fork(); // 创建子进程
    if (pid == -1) {
        //fork 调用失败
        perror("fork");
        return EXIT_FAILURE;
    }
    if (pid == 0) {
        // 这是子进程
        printf("I am the child process with PID: %d\n", getpid());
        sleep(2); // 子进程休眠 2 秒
        exit(EXIT_SUCCESS); // 子进程退出
    } else {
        // 这是父进程
        int wstatus;
        pid_t terminated_pid = waitpid(pid, &wstatus, 0); // 等待特定子进程终止
        if (terminated_pid == -1) {
            perror("waitpid");
            return EXIT_FAILURE;
        }
        printf("Child process (PID: %d) terminated\n", terminated_pid);
        if (WIFEXITED(wstatus)) {
            printf("Child exited with status: %d\n", WEXITSTATUS(wstatus));
        }
    }
    return EXIT_SUCCESS;
}

在上面的例子中,父进程调用 fork 创建了一个子进程。子进程打印自己的 PID 并休眠 2 秒后正常退出。父进程调用 waitpid 等待特定 PID 的子进程终止,并接收子进程的退出状态。然后,父进程检查子进程是否正常退出,并打印退出状态码。

使用 waitpid 时需要注意的事项:

  • waitpid 可以用来等待特定的子进程或任何子进程的状态改变。
  • 使用 WNOHANG 选项可以进行非阻塞等待,这在某些情况下很有用,例如在循环中检查子进程状态时。
  • 使用 WUNTRACED 选项可以等待子进程停止(例如,因为收到 SIGSTOP 信号)。
  • 父进程应该检查 waitpid 返回的 PID,以确定哪个子进程状态发生了改变,并适当地处理退出状态。

# 用户 ID

# getuid 获取真实用户 ID

getuid 函数是 POSIX 标准中用于获取当前进程的真实用户 ID(UID)的系统调用。这个调用提供了进程启动时所使用的用户身份信息。

函数原型:

uid_t getuid(void);

参数: getuid 函数不接受任何参数。

返回值:

  • 成功时,返回当前进程的真实用户 ID。
  • 如果调用失败,返回 (-1) ,并设置全局变量 errno 以指示错误。

真实用户 ID 是指进程启动时所具有的用户 ID。在 UNIX 和类 UNIX 系统中,这个 ID 用于权限控制和确定哪些资源可以被进程访问。

以下是使用 getuid 函数的一个例子:

#include <stdio.h>
#include <sys/types.h>
int main() {
    uid_t uid = getuid(); // 获取当前进程的真实用户 ID
    printf("The real user ID of this process is: %d\n", uid);
    return 0;
}

在上面的例子中,我们调用 getuid 来获取当前进程的真实用户 ID,并将其打印出来。

使用 getuid 时需要注意的事项:

  • getuid 通常用于安全和权限管理,例如在确定哪些文件可以被访问或哪些操作可以被执行时。
  • 如果程序需要改变其有效用户 ID 或组 ID,可以使用 setuid 和 setgid 函数,但这些操作需要管理员权限或特定的安全策略。
  • 在多线程环境中, getuid 返回的是整个进程的真实用户 ID,而不是特定线程的 ID。

getuid 是一个简单但非常重要的系统调用,它为程序提供了识别和校验用户身份的能力。

# getgid 获取真实组 ID

getgid 函数是 POSIX 标准中用于获取当前进程的真实组 ID(GID)的系统调用。与 getuid 类似, getgid 提供了进程启动时所使用的用户组身份信息。

函数原型:

gid_t getgid(void);

参数: getgid 函数不接受任何参数。

返回值:

  • 成功时,返回当前进程的真实组 ID。
  • 如果调用失败,返回 (-1) ,并设置全局变量 errno 以指示错误。

真实组 ID 是指进程启动时所具有的组 ID。在 UNIX 和类 UNIX 系统中,这个 ID 同样用于权限控制,与用户 ID 一起决定了进程可以访问的资源。

以下是使用 getgid 函数的一个例子:

#include <stdio.h>
#include <sys/types.h>
int main() {
    gid_t gid = getgid(); // 获取当前进程的真实组 ID
    printf("The real group ID of this process is: %d\n", gid);
    return 0;
}

在上面的例子中,我们调用 getgid 来获取当前进程的真实组 ID,并将其打印出来。

使用 getgid 时需要注意的事项:

  • getgid 通常与 getuid 结合使用,以确定进程的完整用户和组权限。
  • 与 getuid 一样,如果程序需要改变其有效用户 ID 或组 ID,可以使用 setgid 函数,但这通常需要管理员权限或特定的安全策略。
  • 在多线程环境中, getgid 返回的是整个进程的真实组 ID,而不是特定线程的 ID。

getgid 是一个重要的系统调用,它允许程序识别和校验进程所属的用户组,从而进行适当的权限控制和资源访问管理。

# geteuid 获取有效用户 ID

geteuid 函数是 POSIX 标准中用于获取当前进程的有效用户 ID(EUID)的系统调用。有效用户 ID 是进程执行操作时用于权限检查的用户 ID。在某些情况下,有效用户 ID 可能与进程的真实用户 ID(由 getuid 获取)不同,例如,当进程使用了设置用户 ID 的文件或调用了 setuid 函数改变了其有效用户 ID。

函数原型:

uid_t geteuid(void);

参数: geteuid 函数不接受任何参数。

返回值:

  • 成功时,返回当前进程的有效用户 ID。
  • 如果调用失败,返回 (-1) ,并设置全局变量 errno 以指示错误。

有效用户 ID 常用于以下几种情况:

  • 执行文件时,如果文件具有设置用户 ID 的权限位(setuid 位),则进程的有效用户 ID 将设置为文件的所有者 ID。
  • 进程使用 setuid 函数显式地改变了其有效用户 ID。

以下是使用 geteuid 函数的一个例子:

#include <stdio.h>
#include <sys/types.h>
int main() {
    uid_t euid = geteuid(); // 获取当前进程的有效用户 ID
    printf("The effective user ID of this process is: %d\n", euid);
    return 0;
}

在上面的例子中,我们调用 geteuid 来获取当前进程的有效用户 ID,并将其打印出来。

使用 geteuid 时需要注意的事项:

  • 如果进程没有改变其有效用户 ID, geteuid 通常返回与 getuid 相同的值。
  • 在权限管理中,有效用户 ID 通常用于确定进程是否可以执行需要特定权限的操作。
  • 与 getuid 和 getgid 一样,在多线程环境中, geteuid 返回的是整个进程的有效用户 ID,而不是特定线程的 ID。

geteuid 是一个重要的系统调用,它为程序提供了识别当前权限上下文的能力,有助于进行安全检查和权限管理。

# getegid 获取有效组 ID

getegid 函数是 POSIX 标准中用于获取当前进程的有效组 ID(EGID)的系统调用。有效组 ID 是进程执行操作时用于权限检查的组 ID。类似于有效用户 ID,有效组 ID 可能与进程的真实组 ID(由 getgid 获取)不同,在某些情况下,例如当进程使用了设置组 ID 的文件或调用了 setgid 函数改变了其有效组 ID。

函数原型:

gid_t getegid(void);

参数: getegid 函数不接受任何参数。

返回值:

  • 成功时,返回当前进程的有效组 ID。
  • 如果调用失败,返回 (-1) ,并设置全局变量 errno 以指示错误。

有效组 ID 常用于以下几种情况:

  • 执行文件时,如果文件具有设置组 ID 的权限位(setgid 位),则进程的有效组 ID 将设置为文件的组所有者 ID。
  • 进程使用 setgid 函数显式地改变了其有效组 ID。

以下是使用 getegid 函数的一个例子:

#include <stdio.h>
#include <sys/types.h>
int main() {
    gid_t egid = getegid(); // 获取当前进程的有效组 ID
    printf("The effective group ID of this process is: %d\n", egid);
    return 0;
}

在上面的例子中,我们调用 getegid 来获取当前进程的有效组 ID,并将其打印出来。

使用 getegid 时需要注意的事项:

  • 如果进程没有改变其有效组 ID, getegid 通常返回与 getgid 相同的值。
  • 在权限管理中,有效组 ID 通常用于确定进程是否可以访问需要特定组权限的资源。
  • 与 getuid 和 getgid 一样,在多线程环境中, getegid 返回的是整个进程的有效组 ID,而不是特定线程的 ID。

getegid 是一个重要的系统调用,它允许程序识别当前的组权限上下文,有助于进行安全检查和权限管理。

# getpgrp 获取进程组 ID

getpgrp 函数是 POSIX 标准中用于获取调用进程的进程组 ID(Process Group ID,PGRP)的系统调用。进程组是一个或多个进程的集合,通常用于作业控制。

函数原型:

pid_t getpgrp(void);

参数: getpgrp 函数不接受任何参数。

返回值:

  • 成功时,返回调用进程的进程组 ID。
  • 如果调用失败,返回 (-1) ,并设置全局变量 errno 以指示错误。

进程组 ID 是一个正整数,用于标识一个进程组。每个进程都属于一个进程组,通常进程组 ID 与进程的会话(session)ID 相同。进程组的概念在多用户环境中非常有用,特别是用于信号的发送和管理。

以下是使用 getpgrp 函数的一个例子:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main() {
    pid_t pgrp = getpgrp(); // 获取当前进程的进程组 ID
    printf("The process group ID of this process is: %d\n", pgrp);
    return 0;
}

在上面的例子中,我们调用 getpgrp 来获取当前进程的进程组 ID,并将其打印出来。

使用 getpgrp 时需要注意的事项:

  • 进程组 ID 通常用于在 shell 脚本和程序中管理作业。
  • 可以通过 setpgid 函数改变进程的进程组 ID,这通常需要特定的权限。
  • getpgrp 返回的进程组 ID 可以用于使用 killpg 函数向整个进程组发送信号。
  • 在多线程环境中, getpgrp 返回的是当前线程所属进程的进程组 ID。

getpgrp 是一个有用的系统调用,它为程序提供了识别和管理系统进程组的能力,有助于进行有效的作业管理和信号处理。

# setpgid 设置进程组 ID

setpgid 函数是 POSIX 标准中用于设置一个进程的进程组 ID 的系统调用。使用 setpgid 可以创建新的进程组或将进程移动到现有的进程组。

函数原型:

int setpgid(pid_t pid, pid_t pgid);

参数:

  1. pid :要设置进程组 ID 的进程的 PID。如果 pid 等于 0 ,则使用调用进程的 PID。
  2. pgid :新的进程组 ID。如果 pgid 等于 pid ,则创建一个新的进程组,并将 pid 指定的进程作为该组的领导进程。

返回值:

  • 成功时,返回 0。
  • 如果调用失败,返回 -1 ,并设置全局变量 errno 以指示错误。

以下是 setpgid 函数的一些关键点:

  • 只有进程的父进程或具有适当权限的进程才能改变进程的进程组 ID。
  • 进程组 ID 一旦设置,除非是该进程组的领导进程,否则不能被改变。
  • 进程组的领导进程可以是该组内任何进程,但一旦设置,就不能改变。
  • 进程创建时,默认的进程组 ID 与父进程的进程组 ID 相同。

使用 setpgid 函数的一个例子:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main() {
    pid_t pid = fork(); // 创建子进程
    if (pid == -1) {
        //fork 调用失败
        perror("fork");
        return 1;
    } else if (pid == 0) {
        // 子进程中
        pid_t new_pgid = getpgrp(); // 获取当前进程组 ID
        printf("Child process with PID %d is in process group %d\n", getpid(), new_pgid);
        // 尝试创建新的进程组并将自己设为领导进程
        if (setpgid(0, getpid()) == -1) {
            perror("setpgid");
            return 1;
        }
        // 再次获取新的进程组 ID
        new_pgid = getpgrp();
        printf("Child process with PID %d is now the leader of process group %d\n", getpid(), new_pgid);
    } else {
        // 父进程中等待子进程结束
        wait(NULL);
    }
    return 0;
}

在上面的例子中,父进程创建了一个子进程。子进程首先打印其当前的进程组 ID,然后尝试使用 setpgid 创建一个新的进程组,并将自身设为该组的领导进程。如果操作成功,子进程将再次打印其新的进程组 ID。

使用 setpgid 时需要注意的事项:

  • 使用 setpgid 可以影响进程接收信号的方式,因为信号可以按进程组发送。
  • 在某些系统中,只有超级用户或具有 CAP_SYS_NICE 权限的用户才能将进程移动到其他进程组。
  • 进程组的概念在多用户环境中非常有用,尤其是在 shell 脚本和作业控制中。

# getsid 获取调用进程的会话 ID

getsid 函数是 POSIX 标准中用于获取调用进程的会话 ID(Session ID)的系统调用。每个进程都属于一个会话,会话是一个或多个进程的集合,通常由一个控制终端、作业控制和登录记录组成。

函数原型:

pid_t getsid(void);

参数: getsid 函数不接受任何参数。

返回值:

  • 成功时,返回调用进程的会话 ID。
  • 如果调用失败,返回 (-1) ,并设置全局变量 errno 以指示错误。

会话 ID 是一个正整数,用于标识一个会话。每个会话都有一个唯一的会话 ID,并且每个会话都有一个或多个进程组,其中至少有一个进程组是会话的 “领导” 进程组。

以下是使用 getsid 函数的一个例子:

#include <stdio.h>
#include <unistd.h>
int main() {
    pid_t sid = getsid(); // 获取当前进程的会话 ID
    printf("The session ID of this process is: %d\n", sid);
    return 0;
}

在上面的例子中,我们调用 getsid 来获取当前进程的会话 ID,并将其打印出来。

使用 getsid 时需要注意的事项:

  • 会话的概念在多用户环境中非常有用,尤其是在终端会话和作业控制中。
  • 一个会话通常由一个登录 shell 创建,并且该 shell 进程是会话的领导进程。
  • 会话的领导进程可以创建新的进程组,并且这些进程组都属于同一个会话。
  • getsid 返回的会话 ID 可以用于使用 killpg 函数向整个会话发送信号。

getsid 是一个有用的系统调用,它为程序提供了识别和管理系统会话的能力,有助于进行有效的作业管理和信号处理。

# syslog 系统日志

syslog 函数是 UNIX 和类 UNIX 系统中用于发送消息到系统日志守护进程 syslogd 的标准 API 函数。这个函数允许程序将日志消息记录到系统日志中,与其他日志记录方式相比, syslog 提供了一种集中式、可配置的日志记录解决方案。

函数原型:

void syslog(int priority, const char *message, ...);

参数:

  1. priority :一个整数,表示日志消息的优先级。优先级是两个部分的组合 ——"等级" 和 "设施"。等级(如 LOG_EMERG, LOG_ALERT, LOG_CRIT 等)表示消息的紧急程度,设施(如 LOG_USER, LOG_DAEMON 等)表示消息的来源类型。
  2. message :一个格式字符串,遵循 printf 风格,指定了要记录的消息格式。
  3. ... :一系列可选参数,提供给 message 字符串的格式说明符。

功能: syslog 函数将程序的输出消息发送到系统日志,通常由 syslogd 守护进程处理。这些消息可以被配置为记录到不同的目的地,如控制台、文件或其他地方。

返回值: syslog 函数不返回任何值( void 类型)。

以下是使用 syslog 函数的一个例子:

#include <syslog.h>
int main() {
    // 打开 syslog,标识为当前程序名
    openlog("my_program", LOG_PID, LOG_USER);
    // 记录一条信息性消息
    syslog(LOG_INFO, "This is an informational message.");
    // 记录一条错误消息
    syslog(LOG_ERR, "This is an error message.");
    // 关闭 syslog
    closelog();
    return 0;
}

在上面的例子中,我们首先使用 openlog 函数初始化 syslog ,指定了程序名、进程 ID 记录选项和消息设施。然后,我们使用 syslog 函数记录了两条消息,一条是信息性消息,另一条是错误消息。最后,我们使用 closelog 函数关闭了 syslog 。

使用 syslog 时需要注意的事项:

  • 通常在程序开始时调用 openlog 并设置适当的参数,程序结束时调用 closelog 。
  • syslog 可以处理不同级别的日志消息,根据配置,这些消息可以被发送到不同的地方。
  • 消息的优先级可以根据需要设置,以确定消息的重要性。
  • 系统日志的配置通常在 /etc/syslog.conf 或 /etc/rsyslog.conf 等配置文件中设置。

syslog 是一种灵活的日志记录方式,适用于需要将日志消息集中管理和配置的应用程序。