1. 环境表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
| // 1. 进程启动时 会将环境表作为第三个参数传入(环境表就是环境变量)
#include <stdio.h>
extern char **environ; // 声明环境表指针
int main() {
for (char **env = environ; *env != NULL; env++) {
printf("%s\n", *env); // 打印所有环境变量
}
return 0;
}
// 2. 输出
SSH_CONNECTION=111.46.57.97 49558 192.168.1.8 22
LESSCLOSE=/usr/bin/lesspipe %s %s
LANG=C.UTF-8
HISTTIMEFORMAT=%F %T xm
XDG_SESSION_ID=1040
USER=xm
PWD=/tmp/tmp.17PXDZ8IS3/03-APUE/chapter7
HOME=/home/xm
LC_CTYPE=C.UTF-8
SSH_CLIENT=111.46.57.97 49558 22
XDG_DATA_DIRS=/usr/local/share:/usr/share:/var/lib/snapd/desktop
SSH_TTY=/dev/pts/1
MAIL=/var/mail/xm
TERM=xterm-256color
SHELL=/bin/bash
SHLVL=1
LANGUAGE=en_US:
LOGNAME=xm
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus
XDG_RUNTIME_DIR=/run/user/1000
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
HISTSIZE=1000
LESSOPEN=| /usr/bin/lesspipe %s
OLDPWD=/tmp/tmp.17PXDZ8IS3/03-APUE
_=/usr/bin/printenv
// 3. 与printenv 输出一致
|
2. 进程初探
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| int globvar = 6;
char buf[] = "a write to stdout\n";
int main() {
int var;
pid_t pid;
var = 88;
// 在 Unix/Linux 系统编程中,write(STDOUT_FILENO, buf, len) 的返回值检查是一个关键错误处理点。
if (write(STDOUT_FILENO, buf, sizeof(buf) - 1) != sizeof(buf) - 1) { // sizeof 编译决定 strlen运行决定 直接写入,绕过缓冲
err_sys("write error");
}
/*
* 1. 终端交互式运行: 输出一次 重定向到文件:输出两次
* 2. 终端(TTY) 行缓冲 遇到 \n 立即刷新缓冲区,fork() 前内容已输出
* 3. 文件/管道 全缓冲 缓冲区满或程序退出时才刷新,fork() 时未刷新的缓冲区会被子进程继承并重复输出
*/
printf("before fork\n"); // 内容暂存缓冲区
if ((pid = fork()) < 0) {
err_sys("fork error");
} else if (pid == 0) {
globvar++;
var++;
} else {
sleep(2);
}
printf("pid = %ld, glob = %d, var = %d\n", (long)getpid(), globvar, var); // 子进程的改变 并不影响父进程的值
exit(0);
}
// 输出
a write to stdout
before fork
pid = 2213, glob = 7, var = 89
pid = 2212, glob = 6, var = 88
|
1
2
3
4
| // 几点问题说明
1. 子进程获得父进程的空间 & 堆 & 栈副本
2. 子父进程 只共享正文段
3. 写时复制
|
| 变量类型 | 存储区域 | fork() 后行为 | 修改后影响 | |—————|—————|———————————-|—————-| | 全局变量 | 数据段(Data) | 初始共享(COW),修改时分离 | 父子进程独立 | | 局部变量 | 栈(Stack) | 完全独立拷贝 | 父子进程独立 | | 静态局部变量 | 数据段(Data) | 同全局变量 | 父子进程独立 |
| 特性 | 全局变量 | 静态局部变量 |
|---|
| 存储位置 | 数据段(Data Segment) | 数据段(Data Segment) |
| 作用域 | 整个程序可见 | 仅在定义函数内可见 |
| 生命周期 | 程序启动到终止 | 程序启动到终止 |
| 初始化 | 默认零初始化 | 默认零初始化 |
| 访问速度 | 快(直接访问) | 快(直接访问) |
| 特性 | 全局变量 | 静态局部变量 |
|---|
| 线程共享 | ✅ 所有线程共享 | ✅ 被多个线程调用时共享 |
| 线程安全 | ❗ 必须加锁 | ❗ 必须加锁(若被并发调用) |
| 典型问题 | 数据竞争 | 数据竞争(当函数被并发调用) |
| 解决方案 | 互斥锁/原子操作 | 互斥锁/原子操作 |
| 特性 | 全局变量 | 静态局部变量 |
|---|
| 初始状态 | 共享(COW机制) | 共享(COW机制) |
| 修改影响 | 父子进程独立 | 父子进程独立 |
| 内存消耗 | 修改时触发内存复制 | 修改时触发内存复制 |
| 典型用途 | 进程间显式共享(需IPC) | 函数内状态保持 |
3. vfork 问题
vfork 会改变父进程变量!!!!!!!!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
| /*
vfork 系统调用详解
vfork 是 Unix/Linux 中用于创建子进程的系统调用,与 fork 类似但设计更高效(也更危险)。它专为 exec 类操作优化,适用于子进程立即执行新程序的场景。
1. 函数原型
c
#include <unistd.h>
pid_t vfork(void);
返回值
成功:父进程返回子进程的 PID,子进程返回 0。
失败:返回 -1 并设置 errno。
2. vfork 的核心特性
(1) 共享地址空间(与 fork 的关键区别)
vfork:子进程共享父进程的地址空间,直到调用 exec 或 _exit。
fork:子进程获得父进程地址空间的独立拷贝(写时复制 COW)。
(2) 执行顺序保证
父进程会被挂起,直到子进程调用 exec 或 _exit。
避免父子进程同时运行导致数据竞争。
(3) 高性能
无需复制页表或内存,适合频繁创建短暂子进程的场景(如 Shell 命令执行)。
*/
特性 fork vfork
地址空间 子进程获得父进程的独立拷贝(写时复制) 子进程共享父进程的地址空间(直接操作同一内存)
执行顺序 父子进程并行执行 父进程被挂起,直到子进程结束或调用 exec
性能 较高(现代 Linux 优化后) 更高(无内存拷贝,但风险大)
安全性 安全(隔离性好) 危险(子进程可能破坏父进程数据)
典型用途 通用多进程场景 仅限子进程立即调用 exec 或 _exit
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
pid_t pid = vfork();
if (pid == -1) {
perror("vfork failed");
exit(1);
}
if (pid == 0) { // 子进程
execlp("ls", "ls", "-l", NULL); // 替换为 ls 命令
_exit(1); // 若 exec 失败,必须用 _exit 退出
} else { // 父进程
printf("Parent PID: %d, Child PID: %d\n", getpid(), pid);
}
return 0;
}
|
4. 进程退出
| 终止类型 | 具体方式 | 关键特性 | 示例代码 | 资源清理(用户态清理) |
|---|
| 正常终止 | | | | |
| | 从main函数返回 | 等效于调用exit(),返回值作为退出码 | int main() { return 42; } | 完全清理 |
| | 调用exit() | 1. 执行atexit()注册函数 2. 刷新缓冲区 3. 关闭文件描述符 | exit(42); | 完全清理 |
| | 调用_exit()或_Exit() | 直接进入内核,不执行任何清理 | _exit(1); | 不清理 |
| | 最后一个线程从启动例程返回 | 进程退出码固定为0 | 多线程自然退出 | 线程资源清理 |
| | 最后一个线程调用pthread_exit() | 进程退出码固定为0 | pthread_exit(NULL); | 线程资源清理 |
| 异常终止 | | | | |
| | 调用abort() | 产生SIGABRT信号,生成core dump | abort(); | 部分清理 |
| | 接收到终止信号 | 信号编号决定终止类型(如SIGSEGV=段错误) | kill(pid, SIGTERM); | 内核强制清理 |
| | 线程被取消 | 最后一个线程响应pthread_cancel() | pthread_cancel(thread_id); | 线程资源部分清理 |
- 资源清理 分为 内核态清理 & 用户态清理, 上述表格中描述的是用户态清理。
- 统一终止路径:无论进程通过何种方式终止(正常/异常),最终都会执行内核中的同一段清理代码。 - 关闭所有打开的描述符(文件、套接字等资源) - 释放占用的存储器(内存、堆栈等)
- wait 存在的意义? 获取子进程的状态呀 (退出 & 资源占用等状态)
- 父进程挂了 子进程会由 init 进程负责回收
- 父进程不回收子进程(not wait), 子进程退出后会成为僵尸进程(占用pid, 但是资源已经释放,内核已完成清理操作关闭文件描述符、释放内存等)
5. 进程竞争问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| #include "../apue.h"
static void charatatime(char *);
int main() {
pid_t pid;
// TELL_WAIT();
if ((pid = fork()) < 0) {
err_sys("fork error");
} else if (pid == 0) {
// WAIT_PARENT();
charatatime("output from child\n");
} else {
charatatime("output from parent\n");
// TELL_CHILD(pid);
}
exit(0);
}
static void charatatime(char *str) {
char *ptr; // 字符指针,用于遍历字符串
int c; // 存储当前字符的整型变量
setbuf(stdout, NULL); // 禁用标准输出的缓冲
for (ptr = str; (c = *ptr++) != 0;) { // 遍历字符串直到空字符
putc(c, stdout); // stdout 是进程间共享资源
}
}
|
1
2
3
4
| // 输出
xm@hcss-ecs-4208:/tmp/tmp.17PXDZ8IS3/03-APUE/chapter8$ ./test2
ooutput from child
utput from parent
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // 哪些东西多有多进程竞争问题?
1. 文件系统相关
文件描述符:打开的文件、管道、socket
文件内容:数据库文件、配置文件
文件元信息:inode、目录项
2. 内存相关
共享内存段:shmget创建的内存区域
内存映射文件:mmap映射的文件内存
3. 进程控制相关
进程表项:PID分配与状态记录
信号处理:信号处理器注册表
4. 网络相关
端口号:TCP/UDP端口绑定
套接字缓存:网络数据缓冲区
5. 系统全局资源
系统时钟:时间戳获取
用户/组信息:/etc/passwd等系统文件
|
6. 进程权限管理
| ID类型 | 作用 | 获取函数 | 修改函数 | 关联文件/命令 |
|---|
| 实际用户ID (RUID) | 进程的原始所有者(谁启动了进程),用于审计和资源归属(如文件创建者)。 | getuid() | setuid() | /etc/passwd 第3字段 |
| 有效用户ID (EUID) | 进程当前操作的权限身份(决定能访问哪些资源)。 | geteuid() | seteuid() | ps -eo ruid,euid,cmd |
| 设置用户ID (SUID) | 可执行文件的特殊权限位,允许进程运行时将 EUID 临时设置为文件所有者。 | - | chmod u+s | ls -l 中的 s 标志 |
| ID类型 | exec(设置用户ID位关闭) | exec(设置用户ID位打开) | setuid(uid)(超级用户) | setuid(uid)(非特权用户) |
|---|
| 实际用户ID (RUID) | 不变 | 不变 | 设为 uid | 不变 |
| 有效用户ID (EUID) | 不变 | 设置为程序文件的用户ID | 设为 uid | 设为 uid(需 uid=RUID 或 EUID) |
| 保存的设置用户ID (SUID) | 从有效用户ID复制 | 从有效用户ID复制 | 设为 uid | 不变 |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| // sudo ping 等程序都会设置s位
// ping 的执行逻辑是啥? fork()+exec()
# 伪代码:模拟 ping 的权限控制流程
def ping(destination):
# 1. 检查当前权限(实际用户 vs 有效用户)
ruid = get_real_user_id() # 实际用户ID(如普通用户UID=1000)
euid = get_effective_user_id() # 有效用户ID(因SUID,此时euid=0)
# 2. 创建原始套接字(需root权限)
try:
raw_socket = create_raw_socket() # 此操作需要 CAP_NET_RAW 或 root
except PermissionError:
print("Error: No permission to create raw socket")
exit(1)
# 3. 降权:恢复为普通用户权限(安全措施)
set_effective_user_id(ruid) # EUID从root(0) → 当前用户(1000)
# 4. 发送ICMP请求(后续操作以普通用户身份运行)
while True:
send_icmp_echo_request(raw_socket, destination)
response = wait_for_icmp_reply(raw_socket)
print(f"Reply from {destination}: time=10ms")
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
| // suid 再探究 举例说明
/**
* at程序权限控制流程(基于SUID机制)
*
* 前提:程序文件由root拥有且设置用户ID位已设置(权限:-rwsr-xr-x)
*/
// -------------------------------------------------------------------
// 阶段1:程序启动(exec加载后)
// -------------------------------------------------------------------
/**
* 实际用户ID (RUID) = 当前用户ID(如1000) // 始终不变
* 有效用户ID (EUID) = root (0) // SUID位生效,切换为文件所有者
* 保存的设置用户ID (SUID) = root (0) // 从新EUID复制
*/
// -------------------------------------------------------------------
// 阶段2:降低特权(初始安全措施)
// -------------------------------------------------------------------
/**
* at程序首先调用 setuid(getuid()) 降权:
* - 将EUID从root改为当前用户ID
* - SUID保持root不变(关键!为后续提权保留凭证)
*
* 此时:
* RUID = 当前用户ID
* EUID = 当前用户ID
* SUID = root
*/
// -------------------------------------------------------------------
// 阶段3:访问配置文件(临时提权)
// -------------------------------------------------------------------
/**
* 当需要访问受保护的配置文件(如/var/at/jobs)时:
* 调用 setuid(SUID) 将EUID恢复为root:
* - 参数SUID=root,因此允许提权(需特权进程或SUID=参数)
*
* 此时:
* RUID = 当前用户ID
* EUID = root
* SUID = root
*
* 注:此阶段可操作特权文件(如记录待执行命令)
*/
// -------------------------------------------------------------------
// 阶段4:再次降权(安全回收)
// -------------------------------------------------------------------
/**
* 文件操作完成后,调用 seteuid(getuid()) 降权:
* - 仅修改EUID,保持RUID/SUID不变
*
* 此时:
* RUID = 当前用户ID
* EUID = 当前用户ID
* SUID = root
*/
// -------------------------------------------------------------------
// 阶段5:守护进程执行命令(最终权限切换)
// -------------------------------------------------------------------
/**
* 守护进程(root权限)fork子进程后:
* 子进程调用 setuid(getuid()) 彻底放弃特权:
* - 修改所有ID为当前用户ID(因父进程是root,有权修改全部ID)
*
* 最终状态:
* RUID = 当前用户ID
* EUID = 当前用户ID
* SUID = 当前用户ID
*
* 注:此时完全以用户权限运行命令,确保安全
*/
// -------------------------------------------------------------------
// 关键总结:
// 1. SUID的作用:保存初始特权状态,支持动态权限升降
// 2. 安全设计原则:最小权限 + 及时回收特权
// -------------------------------------------------------------------
|
1
2
3
4
5
6
| // uid & gid 等问题
cat /etc/passwd
用户名:密码占位符:UID:GID:描述信息:家目录:登录Shell
root:x:0:0:root:/root:/bin/bash
xm:x:1000:1000:xm,xm,xm,xm,xm:/home/xm:/bin/bash
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| // 实际用户 ID(Real UID 谁启动了进程) 有效用户 ID(Effective UID 进程能做什么)
setuid(uid_t uid) // 同时设置 RUID 和 EUID(需权限)
seteuid(uid_t uid) // 仅修改 EUID
setreuid(ruid, euid) //原子化设置 RUID 和 EUID
// ruid euid 的作用?
// 举例说明如下:
// passwd 命令需要修改 /etc/shadow(仅 root 可写),但普通用户也能运行它,这是如何实现的?
xm@hcss-ecs-4208:/etc$ ls -l /usr/bin/passwd
-rwsr-xr-x 1 root root 59640 Nov 29 2022 /usr/bin/passwd
// s 权限位:表示该程序设置了 SUID(Set User ID),运行时会将 EUID 临时改为文件所有者(这里是 root)
// 过程如下:
1. 用户 alice(RUID=1000)执行 passwd。
2. 由于 SUID 存在,进程的:
- RUID = 1000(仍是 alice)
- EUID = 0(临时变成 root)
3. 进程因此可以修改 /etc/shadow。
|
7. 解释器文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // 1. 内核执行解释器文件过程
内核读取文件的第一行
如果以#!开头,内核会提取解释器路径和可选参数
内核启动解释器程序,将文件路径作为参数传递
解释器读取并执行文件内容
// 2.常见的解释器文件
#!/bin/bash
echo "Hello, World!"
#!/usr/bin/env python3
print("Hello, World!")
// 3. 在 Python 脚本中,#!/usr/bin/env python(Shebang 行)的作用是告诉系统如何执行该脚本。有的 .py 文件需要它,有的不需要,主要取决于脚本的执行方式。
./script.py // 可以直接运行
|
8. system
1
2
3
| system() 会启动一个子 Shell(fork 后),该 Shell 继承调用进程的所有用户ID(RUID/EUID/SUID)。
会以 root 用户执行命令。
不要在fork 中执行system(会存在越权等问题)
|
9. 进程统计 acct
1
2
3
4
5
6
| SYNOPSIS
#include <unistd.h>
int acct(const char *filename);
DESCRIPTION
The acct() system call enables or disables process accounting. If called with the name of an existing file as its argument, accounting is turned on, and records for each terminating process are appended to filename as it terminates. An argument of NULL causes accounting to be turned off.
|
9. 进程调度
1
2
3
4
5
6
7
8
9
10
| // 进程调度
#include <unistd.h>
int nice(int inc);
int getpriority(int which, id_t who);
int setpriority(int which, id_t who, int prio);
// 几点说明
1. man page 有的说明为进程调度 有的说明为线程调度(历史遗留原因, 不纠结), 实际上表现为线程调度!
|
1
| // 补充一章 调度举例 & nice 值 与oom 值区别?
|
10. 进程时间
| 时间类型 | 说明 |
|---|
| 墙上时钟时间(Wall Clock Time) | 真实世界流逝的时间(从开始到结束的总时间)。 |
| 用户 CPU 时间(User CPU Time) | 进程在 用户态 执行的时间(如计算、逻辑处理)。 |
| 系统 CPU 时间(System CPU Time) | 进程在 内核态 执行的时间(如系统调用、I/O 操作)。 |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // manpage 的描述可能有bug
SYNOPSIS
#include <sys/times.h>
clock_t times(struct tms *buf);
DESCRIPTION
times() stores the current process times in the struct tms that buf points to. The struct tms is as defined in <sys/times.h>:
struct tms {
clock_t tms_utime; /* user time */
clock_t tms_stime; /* system time */
clock_t tms_cutime; /* user time of children */
clock_t tms_cstime; /* system time of children */
};
|