进程与线程基础及 Linux 进程间通信(IPC)详解
一、程序与进程
1. 程序(静态文件)
程序是存储在磁盘上的可执行文件,是静态实体,不占用 CPU、内存等运行时资源,仅占用磁盘空间。不同操作系统的可执行文件格式不同:
- Windows:
.exe
- Linux:
ELF
(Executable and Linkable Format) - Android:
.apk
(本质是包含 ELF 可执行文件的压缩包)
2. 进程(动态执行)
进程是程序的动态执行过程,是操作系统进行资源分配和调度的基本单位,拥有独立的生命周期和运行时资源(CPU、内存、文件描述符等)。
(1)ELF 文件解析工具
Linux 下通过以下工具查看和分析 ELF 文件:
-
file
命令 :查看文件类型基本信息示例:
file /bin/ls
输出:
/bin/ls: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, ...
-
readelf
命令:解析 ELF 文件详细结构(需掌握核心选项)选项 功能描述 示例 -h
查看 ELF 文件头信息(位数、字节序等) readelf -h /bin/ls
-S
查看 ELF 节头信息(代码段、数据段等) readelf -S /bin/ls
(2)ELF 文件头关键信息
通过 readelf -h
可获取以下核心信息:
- 文件位数 :第 5 个字节(Magic 字段)标识,
01
表示 32 位,02
表示 64 位。 - 字节序 :第 6 个字节标识,
01
表示小端序,02
表示大端序。- 小端序(Little-Endian) :低位字节存低地址,高位字节存高地址。例如
0x12345678
存储为78 56 34 12
。 - 大端序(Big-Endian) :高位字节存低地址,低位字节存高地址。例如
0x12345678
存储为12 34 56 78
。
- 小端序(Little-Endian) :低位字节存低地址,高位字节存高地址。例如
(3)ELF 文件类型
ELF 格式包含 4 类核心文件,对应不同使用场景:
类型 | 描述 | 示例 |
---|---|---|
可执行文件 | 可直接运行,包含完整的代码和数据,加载后可执行 | /bin/ls 、自己编译的 ./test |
可重定位文件(.o) | 编译器生成的中间文件,需链接后才能执行(单个源文件编译产物) | gcc -c test.c 生成的 test.o |
共享目标文件(.so) | 动态共享库,可被多个程序动态链接复用,节省内存 | /lib/x86_64-linux-gnu/libc.so.6 |
核心转储文件(core) | 程序崩溃时生成的内存快照,用于调试(默认关闭,需 ulimit -c unlimited 开启) |
程序崩溃后生成的 core.12345 |
3. 进程控制块(PCB)------ task_struct
当 ELF 程序被执行时,Linux 内核会创建一个 task_struct
结构体 来描述该进程,即进程控制块(PCB)。它记录了进程的所有运行时信息,包括:
- 进程 ID(PID)、父进程 ID(PPID)
- 内存资源(虚拟地址空间、页表)
- CPU 调度信息(优先级、状态)
- 文件描述符表、信号处理方式
- 锁资源、信号量等
查看 task_struct
定义
task_struct
定义在 Linux 内核头文件中,路径如下:
/usr/src/linux-headers-<版本号>/include/linux/sched.h
查看命令:
bash
cd /usr/src/linux-headers-$(uname -r)/include/linux
vim sched.h
4. 进程查看命令
命令 | 功能描述 | 示例 |
---|---|---|
pstree |
以树状图展示进程间的父子关系 | pstree (查看所有进程树) |
ps -ef |
查看系统中所有进程的详细信息(PID、PPID 等) | `ps -ef |
二、进程状态
Linux 进程有 7 种核心状态,可归纳为 5 大类,状态转换是进程调度的核心逻辑。
1. 进程的"诞生"------ fork()
系统调用
- 触发条件 :父进程调用
fork()
系统调用。 - 核心逻辑:内核复制父进程的上下文(PCB、内存空间等),创建一个几乎完全相同的子进程(子进程 PID 唯一,PPID 为父进程 PID)。
- 初始状态 :子进程创建后进入 就绪态(TASK_RUNNING),等待 CPU 调度。
2. 核心状态解析
状态分类 | 内核标识 | 含义与典型场景 |
---|---|---|
就绪态 | TASK_RUNNING | 进程已准备好运行,等待 CPU 时间片(放在就绪队列中)。 |
执行态 | TASK_RUNNING | 进程正在 CPU 上执行代码(内核复用 TASK_RUNNING 标识,通过是否在 CPU 上区分就绪/执行)。 |
睡眠态(挂起态) | TASK_INTERRUPTIBLE | 可中断睡眠:等待非关键事件(如 sleep(10) 、键盘输入),可被信号(如 SIGINT)唤醒。 |
TASK_UNINTERRUPTIBLE | 不可中断睡眠:等待关键硬件操作(如磁盘修复),仅事件完成后唤醒,ps 显示为 D 状态。 |
|
暂停态 | TASK_STOPPED | 进程被暂停信号(如 SIGSTOP、Ctrl+Z)暂停,可通过 SIGCONT 恢复。 |
TASK_TRACED | 进程被调试器(如 gdb)跟踪,处于暂停调试状态。 | |
退出相关状态 | EXIT_ZOMBIE(僵尸态) | 进程已终止,但父进程未读取其退出状态,保留 PCB(ps 显示为 Z 状态)。 |
EXIT_DEAD(死亡态) | 父进程调用 wait() /waitpid() 读取退出状态后,内核释放所有资源(进程彻底消失)。 |
三、进程控制核心函数
1. fork()
------创建子进程
函数原型
c
#include <unistd.h>
pid_t fork(void);
返回值规则
- 父进程:返回子进程的 PID(正数)。
- 子进程:返回 0。
- 失败:返回 -1(如内存不足)。
示例代码(父子进程区分)
c
#include <stdio.h>
#include <unistd.h>
int main() {
pid_t pid = fork();
if (pid < 0) {
perror("fork error"); // 错误处理
return -1;
} else if (pid == 0) {
// 子进程逻辑
printf("我是子进程,PID:%d,PPID:%d\n", getpid(), getppid());
} else {
// 父进程逻辑
printf("我是父进程,PID:%d,子进程PID:%d\n", getpid(), pid);
pause(); // 暂停父进程,避免子进程先退出
}
return 0;
}
关键特性:写时复制(Copy-On-Write)
父子进程初始共享同一份物理内存,但当任一进程修改数据(栈、堆、全局变量等)时,内核才为修改的页分配新物理内存,避免不必要的复制开销。示例如下:
c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_num = 123; // 全局变量
int main() {
int stack_num = 10; // 栈变量
int *heap_num = malloc(4); // 堆变量
*heap_num = 100;
pid_t pid = fork();
if (pid == 0) {
// 子进程修改数据(触发写时复制)
g_num++;
stack_num++;
(*heap_num)++;
printf("子进程:g_num=%d, stack_num=%d, heap_num=%d\n", g_num, stack_num, *heap_num);
free(heap_num);
} else {
sleep(1); // 等待子进程修改完成
// 父进程数据未被修改
printf("父进程:g_num=%d, stack_num=%d, heap_num=%d\n", g_num, stack_num, *heap_num);
free(heap_num);
}
return 0;
}
输出结果 :
子进程:g_num=124, stack_num=11, heap_num=101
父进程:g_num=123, stack_num=10, heap_num=100
2. exit()
/_exit()
------进程退出
函数区别
函数 | 功能描述 | 缓冲区处理 |
---|---|---|
exit(int status) |
终止进程,执行退出清理(调用 atexit() 注册的函数),刷新标准 I/O 缓冲区。 |
刷新缓冲区 |
_exit(int status) |
直接终止进程,不执行清理,不刷新缓冲区(内核级退出)。 | 不刷新缓冲区 |
退出码规则
exit(0)
/exit(EXIT_SUCCESS)
:正常退出。exit(1)
/exit(EXIT_FAILURE)
:异常退出(非 0 即可,通常用 1)。- 退出码范围:0~255,超出则取模 256。
示例代码(atexit()
注册退出函数)
c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
// 注册的退出函数(栈式调用,反向执行)
void clean1() { printf("clean1: 退出清理1\n"); }
void clean2() { printf("clean2: 退出清理2\n"); }
int main() {
atexit(clean1); // 先注册
atexit(clean2); // 后注册
printf("程序执行中(未刷新缓冲区)"); // 无换行符,缓冲区未刷新
#ifdef USE__EXIT
_exit(0); // 直接退出,不刷新缓冲区,不执行 clean1/clean2
#else
exit(0); // 刷新缓冲区,执行 clean2 → clean1(反向)
#endif
return 0;
}
编译与运行:
- 正常退出(
exit(0)
):
gcc test.c -o test && ./test
输出:程序执行中(未刷新缓冲区)clean2: 退出清理2 clean1: 退出清理1
- 直接退出(
_exit(0)
):
gcc test.c -o test -DUSE__EXIT && ./test
输出:程序执行中(未刷新缓冲区)
(无清理函数执行)
3. wait()
/waitpid()
------回收子进程
父进程通过这两个函数回收子进程的退出状态,避免子进程成为僵尸进程。
函数原型
c
#include <sys/wait.h>
#include <sys/types.h>
// 等待任意子进程退出,获取退出状态
pid_t wait(int *status);
// 等待指定 PID 的子进程退出,可设置非阻塞
pid_t waitpid(pid_t pid, int *status, int options);
核心参数说明(waitpid()
)
pid
:指定等待的子进程 PID(-1
表示等待任意子进程)。status
:存储子进程退出状态的指针(需通过宏解析)。options
:选项(0
表示阻塞,WNOHANG
表示非阻塞)。
退出状态解析宏
通过 status
指针获取子进程退出详情,核心宏如下:
宏 | 功能描述 | 适用场景 |
---|---|---|
WIFEXITED(status) |
判断子进程是否正常退出(exit /_exit ) |
正常退出 |
WEXITSTATUS(status) |
提取正常退出的退出码(需先通过 WIFEXITED 判断) |
正常退出 |
WIFSIGNALED(status) |
判断子进程是否被信号终止 | 信号终止(如 SIGKILL、SIGSEGV) |
WTERMSIG(status) |
提取终止子进程的信号编号(需先通过 WIFSIGNALED 判断) |
信号终止 |
WIFSTOPPED(status) |
判断子进程是否被暂停 | 暂停状态(如 SIGSTOP) |
示例代码(回收正常退出的子进程)
c
#include <stdio.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
// 子进程:正常退出,退出码 42
printf("子进程 PID:%d,即将退出\n", getpid());
exit(42);
} else {
// 父进程:等待子进程退出
int status;
pid_t ret = waitpid(pid, &status, 0); // 阻塞等待
if (ret == -1) {
perror("waitpid error");
return -1;
}
// 解析退出状态
if (WIFEXITED(status)) {
printf("子进程 %d 正常退出,退出码:%d\n", ret, WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("子进程 %d 被信号 %d 终止\n", ret, WTERMSIG(status));
}
}
return 0;
}
输出结果 :
子进程 PID:1234,即将退出
子进程 1234 正常退出,退出码:42
示例代码(回收被信号终止的子进程)
c
#include <stdio.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
// 子进程:触发段错误(SIGSEGV,信号 11)
int *null_ptr = NULL;
*null_ptr = 10; // 非法内存访问
exit(0); // 不会执行
} else {
// 父进程:等待子进程退出
int status;
pid_t ret = waitpid(pid, &status, 0);
if (WIFSIGNALED(status)) {
printf("子进程 %d 被信号 %d 终止(段错误)\n", ret, WTERMSIG(status));
}
}
return 0;
}
输出结果 :
子进程 1235 被信号 11 终止(段错误)
4. exec
系列函数------替换进程映像
exec
系列函数将当前进程的代码段和数据段替换为新的可执行文件(如 /bin/ls
),实现"进程复用"(PID 不变,仅映像替换)。
核心函数(常用 execl
)
c
#include <unistd.h>
// 格式:路径 + argv[0] + 参数列表 + NULL
int execl(const char *path, const char *arg0, ..., (char *)NULL);
示例代码(子进程执行 /bin/ls
)
c
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
// 子进程:替换为 ls 命令(显示当前目录下的文件)
printf("子进程即将执行 ls 命令\n");
execl("/bin/ls", "ls", "-l", NULL); // 路径:/bin/ls,参数:-l
// execl 成功则不返回,失败才执行以下代码
perror("execl error");
exit(1);
} else {
wait(NULL); // 等待子进程执行完成
printf("父进程:子进程执行完毕\n");
}
return 0;
}
输出结果 :
子进程即将执行 ls 命令
total 8
-rwxr-xr-x 1 user user 4568 Aug 10 15:30 test
父进程:子进程执行完毕
四、进程组、会话与终端
1. 进程组(Process Group)
- 定义:由一个或多个进程组成的集合,用于统一管理(如向组内所有进程发送信号)。
- 核心属性 :
- 进程组 ID(PGID):组内所有进程的 PGID 相同。
- 组长进程:PGID 等于其 PID 的进程(组长可终止,但组内有进程则组存在)。
2. 会话(Session)
- 定义:进程组的集合,由会话首进程(创建会话的进程)初始化。
- 核心属性 :
- 会话分为 前台进程组 和 后台进程组。
- 前台进程组可接收终端输入(如 Ctrl+C 发送 SIGINT),后台进程组不可。
- 会话首进程终止时,会话依然存在。
3. 终端(Terminal)
- 定义:用户与系统交互的接口(如 SSH 终端、本地终端)。
- 关联关系:一个控制终端对应一个会话,终端产生的信号(如 Ctrl+Z)发送给前台进程组。
五、守护进程(Daemon)
1. 定义与特点
守护进程是 Linux 中 脱离终端、后台长期运行 的进程,用于执行周期性任务(如日志收集、服务监听)。核心特点:
- 脱离终端:不依赖任何交互窗口,终端关闭不影响其运行。
- 父进程为
init
(或systemd
):启动后断开与原父进程的联系,由系统初始化进程托管。 - 后台运行:
ps
显示为daemon
或无终端关联(TTY 为?
)。
2. 守护进程编写流程(简化版)
fork()
创建子进程,父进程退出(脱离原进程组)。- 子进程调用
setsid()
创建新会话(脱离原终端)。 fork()
创建孙子进程,子进程退出(避免成为会话首进程,无法再次脱离终端)。- 切换工作目录(如
/
),关闭不需要的文件描述符,重定向标准 I/O 到/dev/null
。 - 执行核心业务逻辑(如循环监听端口)。
六、Linux 进程间通信(IPC)
Linux 提供多种 IPC 机制,适用于不同场景,以下是核心方式:
1. 管道(Pipe)------ 匿名管道
- 定义:内核维护的字节流缓冲区,用于父子进程或兄弟进程间的单向通信。
- 核心特点 :
- 半双工:数据只能单向流动(需两个管道实现双向通信)。
- 无名称:仅能在有亲缘关系的进程间使用。
- 基于文件描述符:
pipe(fd)
创建两个描述符,fd[0]
读,fd[1]
写。
示例代码(父子进程管道通信)
c
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
int main() {
int fd[2];
if (pipe(fd) == -1) { // 创建管道
perror("pipe error");
return -1;
}
pid_t pid = fork();
if (pid == 0) {
// 子进程:写数据(关闭读端)
close(fd[0]);
char msg[] = "你好,父进程!";
write(fd[1], msg, strlen(msg));
close(fd[1]);
exit(0);
} else {
// 父进程:读数据(关闭写端)
close(fd[1]);
char buf[100] = {0};
ssize_t n = read(fd[0], buf, sizeof(buf));
if (n > 0) {
printf("父进程收到:%s\n", buf);
}
close(fd[0]);
wait(NULL);
}
return 0;
}
输出结果 :
父进程收到:你好,父进程!
2. 有名管道(FIFO)
- 定义 :有文件系统路径的管道(如
/tmp/myfifo
),可在无亲缘关系的进程间通信。 - 核心特点 :
- 有名称:通过文件路径标识,任意进程可通过路径访问。
- 阻塞特性:
open
时若管道未被另一端打开,会阻塞直到另一端连接。
示例代码(两个独立进程 FIFO 通信)
发送端(Jack)
c
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#define FIFO_PATH "/tmp/jack_rose_fifo"
int main() {
// 若 FIFO 不存在则创建
if (mkfifo(FIFO_PATH, 0666) == -1 && errno != EEXIST) {
perror("mkfifo error");
return -1;
}
// 以只写方式打开 FIFO(阻塞直到读端打开)
int fd = open(FIFO_PATH, O_WRONLY);
if (fd == -1) {
perror("open error");
return -1;
}
// 循环发送消息
char buf[100] = {0};
while (1) {
printf("Jack: ");
fgets(buf, sizeof(buf), stdin);
write(fd, buf, strlen(buf));
}
close(fd);
return 0;
}
接收端(Rose)
c
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#define FIFO_PATH "/tmp/jack_rose_fifo"
int main() {
// 若 FIFO 不存在则创建
if (mkfifo(FIFO_PATH, 0666) == -1 && errno != EEXIST) {
perror("mkfifo error");
return -1;
}
// 以只读方式打开 FIFO(阻塞直到写端打开)
int fd = open(FIFO_PATH, O_RDONLY);
if (fd == -1) {
perror("open error");
return -1;
}
// 循环接收消息
char buf[100] = {0};
while (1) {
memset(buf, 0, sizeof(buf));
ssize_t n = read(fd, buf, sizeof(buf) - 1);
if (n > 0) {
printf("Rose 收到: %s", buf);
}
}
close(fd);
return 0;
}
运行方式:
- 先启动接收端:
gcc rose.c -o rose && ./rose
- 再启动发送端:
gcc jack.c -o jack && ./jack
- 发送端输入消息,接收端实时显示。
3. 共享内存(Shared Memory)
- 定义:内核创建的一块内存区域,多个进程可将其映射到自身虚拟地址空间,实现高效数据共享(无需拷贝,直接访问内存)。
- 核心 API :
shm_open()
:创建或打开共享内存对象(类似文件操作)。ftruncate()
:设置共享内存大小。mmap()
:将共享内存映射到进程虚拟地址空间。munmap()
:解除映射。shm_unlink()
:删除共享内存对象(所有进程解除映射后释放)。
示例代码(父子进程共享内存通信)
c
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <sys/stat.h>
#define SHM_NAME "/my_shared_mem"
#define SHM_SIZE 1024
int main() {
// 1. 创建/打开共享内存对象
int shm_fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, 0666);
if (shm_fd == -1) {
perror("shm_open error");
return -1;
}
// 2. 设置共享内存大小
if (ftruncate(shm_fd, SHM_SIZE) == -1) {
perror("ftruncate error");
return -1;
}
// 3. 映射共享内存到虚拟地址空间
char *shm_ptr = mmap(NULL, SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0);
if (shm_ptr == MAP_FAILED) {
perror("mmap error");
return -1;
}
// 关闭文件描述符(映射后不再需要)
close(shm_fd);
pid_t pid = fork();
if (pid == 0) {
// 子进程:写入数据
strcpy(shm_ptr, "共享内存通信成功!");
printf("子进程写入:%s\n", shm_ptr);
exit(0);
} else {
// 父进程:读取数据
wait(NULL);
printf("父进程读取:%s\n", shm_ptr);
// 4. 解除映射
munmap(shm_ptr, SHM_SIZE);
// 5. 删除共享内存对象(所有进程解除映射后释放)
shm_unlink(SHM_NAME);
}
return 0;
}
编译与运行 :
gcc shm_test.c -o shm_test -lrt && ./shm_test
(-lrt
链接实时库)
输出结果 :
子进程写入:共享内存通信成功!
父进程读取:共享内存通信成功!
4. 消息队列(Message Queue)
- 定义:内核维护的消息链表,进程可按优先级发送/接收消息(类似"邮箱")。
- 核心特点 :
- 消息有序:按优先级或发送顺序排队。
- 非阻塞选项:支持超时机制(
mq_timedsend
/mq_timedreceive
)。 - 基于描述符:通过
mq_open()
获取消息队列描述符。
核心 API 与结构体
-
mq_attr
结构体 :描述消息队列属性(最大消息数、单条消息最大大小等)。cstruct mq_attr { long mq_flags; // 标志(忽略) long mq_maxmsg; // 最大消息数 long mq_msgsize; // 单条消息最大字节数 long mq_curmsgs; // 当前消息数(忽略) };
-
mq_open()
:创建或打开消息队列。 -
mq_timedsend()
:发送消息(支持超时)。 -
mq_timedreceive()
:接收消息(支持超时)。 -
mq_unlink()
:删除消息队列。
示例代码(父子进程消息队列通信)
c
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <mqueue.h>
#include <time.h>
#include <sys/wait.h>
#include <stdlib.h>
#define MQ_NAME "/father_son_mq"
#define MAX_MSG_NUM 10 // 最大消息数
#define MAX_MSG_SIZE 100 // 单条消息最大大小
int main() {
// 1. 初始化消息队列属性
struct mq_attr attr;
attr.mq_maxmsg = MAX_MSG_NUM;
attr.mq_msgsize = MAX_MSG_SIZE;
attr.mq_flags = 0;
attr.mq_curmsgs = 0;
// 2. 创建/打开消息队列
mqd_t mq_fd = mq_open(MQ_NAME, O_CREAT | O_RDWR, 0666, &attr);
if (mq_fd == (mqd_t)-1) {
perror("mq_open error");
return -1;
}
pid_t pid = fork();
if (pid == 0) {
// 子进程:接收消息
char buf[MAX_MSG_SIZE] = {0};
struct timespec timeout;
for (int i = 0; i < 5; i++) {
// 设置超时时间(当前时间 + 15 秒)
clock_gettime(CLOCK_REALTIME, &timeout);
timeout.tv_sec += 15;
// 接收消息(超时返回错误)
ssize_t n = mq_timedreceive(mq_fd, buf, MAX_MSG_SIZE, NULL, &timeout);
if (n == -1) {
perror("mq_timedreceive error");
break;
}
printf("子进程收到:%s\n", buf);
}
exit(0);
} else {
// 父进程:发送消息
char buf[MAX_MSG_SIZE] = {0};
struct timespec timeout;
for (int i = 0; i < 5; i++) {
// 构造消息
sprintf(buf, "父进程的第 %d 条消息", i + 1);
// 设置超时时间(当前时间 + 5 秒)
clock_gettime(CLOCK_REALTIME, &timeout);
timeout.tv_sec += 5;
// 发送消息
if (mq_timedsend(mq_fd, buf, strlen(buf), 0, &timeout) == -1) {
perror("mq_timedsend error");
break;
}
printf("父进程发送:%s\n", buf);
sleep(1); // 间隔 1 秒发送
}
// 等待子进程结束
wait(NULL);
// 3. 关闭消息队列描述符
mq_close(mq_fd);
// 4. 删除消息队列
mq_unlink(MQ_NAME);
}
return 0;
}
编译与运行 :
gcc mq_test.c -o mq_test -lrt && ./mq_test
输出结果 :
父进程发送:父进程的第 1 条消息
子进程收到:父进程的第 1 条消息
父进程发送:父进程的第 2 条消息
子进程收到:父进程的第 2 条消息
...
5. 信号(Signal)
信号是 Linux 中轻量级的进程间通信机制,用于通知进程发生了某种事件(如中断、错误)。
(1)常用信号与编号
信号名称 | 编号 | 含义与默认行为 |
---|---|---|
SIGINT | 2 | 中断(Ctrl+C),默认终止进程 |
SIGKILL | 9 | 强制终止,不可被捕获/阻塞 |
SIGSTOP | 19 | 暂停(Ctrl+Z),不可被捕获/阻塞 |
SIGSEGV | 11 | 段错误(非法内存访问),默认终止并生成 core 文件 |
SIGCONT | 18 | 恢复暂停的进程 |
(2)信号发送函数------ kill()
/sigqueue()
-
kill()
:发送信号给指定进程。原型:
int kill(pid_t pid, int sig);
示例:
kill(1234, SIGINT);
(给 PID 1234 的进程发送中断信号)。 -
sigqueue()
:发送信号并携带数据(仅支持实时信号)。原型:
int sigqueue(pid_t pid, int sig, const union sigval value);
示例:
cunion sigval val; val.sival_int = 1001; // 携带整数数据 sigqueue(1234, SIGUSR1, val); // 发送自定义信号 SIGUSR1
(3)信号捕获函数------ signal()
/sigaction()
-
signal()
:简单信号捕获(不推荐,兼容性差)。原型:
void (*signal(int sig, void (*handler)(int)))(int);
示例:
c#include <stdio.h> #include <signal.h> void sigint_handler(int sig) { printf("捕获到 SIGINT 信号(编号:%d),不终止进程!\n", sig); } int main() { signal(SIGINT, sigint_handler); // 捕获 SIGINT while (1) { sleep(1); } // 循环等待信号 return 0; }
-
sigaction()
:功能强大的信号捕获(推荐,支持携带数据、信号掩码等)。示例(捕获信号并接收数据):
c#include <stdio.h> #include <signal.h> #include <unistd.h> // 信号处理函数(支持接收数据) void sigusr1_handler(int sig, siginfo_t *info, void *arg) { printf("捕获到信号:%d\n", sig); printf("携带的数据:%d\n", info->si_int); // 读取 sigqueue 发送的数据 } int main() { struct sigaction act; act.sa_sigaction = sigusr1_handler; // 设置处理函数 act.sa_flags = SA_SIGINFO; // 启用数据接收 sigemptyset(&act.sa_mask); // 清空信号掩码 // 注册 SIGUSR1 信号的处理函数 sigaction(SIGUSR1, &act, NULL); printf("进程 PID:%d,等待 SIGUSR1 信号...\n", getpid()); while (1) { sleep(1); } return 0; }
(4)信号阻塞------ sigprocmask()
通过信号掩码(sigset_t
)阻塞指定信号,阻塞期间信号会被挂起,解除阻塞后再处理。
示例:
c
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void sigint_handler(int sig) {
printf("捕获到 SIGINT 信号\n");
}
int main() {
// 1. 注册信号处理函数
signal(SIGINT, sigint_handler);
// 2. 初始化信号集,添加 SIGINT
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGINT);
// 3. 阻塞 SIGINT(期间按 Ctrl+C 不会触发处理函数)
printf("开始阻塞 SIGINT,持续 5 秒...\n");
sigprocmask(SIG_BLOCK, &set, NULL);
sleep(5);
// 4. 解除阻塞(挂起的 SIGINT 会立即触发处理函数)
printf("解除阻塞 SIGINT\n");
sigprocmask(SIG_UNBLOCK, &set, NULL);
while (1) { sleep(1); }
return 0;
}
七、线程(Thread)
1. 进程与线程的核心区别
对比维度 | 进程(Process) | 线程(Thread) |
---|---|---|
资源分配单位 | 系统资源分配的基本单位(独立内存、文件描述符等) | CPU 调度的基本单位(共享进程资源) |
资源占用 | 占用资源多,创建/销毁开销大 | 仅需少量栈空间,创建/切换开销小 |
独立性 | 进程间独立,一个崩溃不影响其他进程 | 线程依赖进程,一个线程崩溃可能导致整个进程崩溃 |
通信方式 | 依赖 IPC 机制(管道、共享内存等) | 直接共享进程资源(全局变量、堆内存等) |
2. 线程的共享与非共享资源
(1)共享资源(进程级资源)
- 文件描述符表
- 信号处理方式
- 当前工作目录
- 用户 ID 和组 ID
- 内存地址空间(代码段
.text
、数据段.data
、堆.heap
、共享库)
(2)非共享资源(线程级资源)
- 线程 ID(TID)
- 处理器现场(寄存器、程序计数器)
- 独立的用户栈和内核栈
errno
变量(每个线程独立)- 信号屏蔽字
- 调度优先级
3. 线程核心 API(POSIX 线程库 pthread
)
编译时需链接线程库:gcc test.c -o test -lpthread
。
(1)pthread_self()
------获取当前线程 ID
c
#include <stdio.h>
#include <pthread.h>
int main() {
// 主线程 ID(%lu 对应 unsigned long)
printf("主线程 ID:%lu\n", pthread_self());
return 0;
}
(2)pthread_create()
------创建线程
c
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
// 线程函数(返回值和参数均为 void*,需强制类型转换)
void *thread_func(void *arg) {
char *msg = (char *)arg;
printf("子线程 ID:%lu,收到参数:%s\n", pthread_self(), msg);
sleep(3); // 模拟业务逻辑
pthread_exit("子线程退出!"); // 线程退出并返回数据
}
int main() {
pthread_t tid; // 线程 ID
char *msg = "Hello, Thread!";
// 创建线程(attr 为 NULL 表示默认属性)
int ret = pthread_create(&tid, NULL, thread_func, (void *)msg);
if (ret != 0) {
perror("pthread_create error");
return -1;
}
printf("主线程 ID:%lu,子线程 ID:%lu\n", pthread_self(), tid);
// 等待子线程退出并回收资源(类似 wait())
void *exit_msg;
pthread_join(tid, &exit_msg);
printf("子线程返回:%s\n", (char *)exit_msg);
return 0;
}
输出结果 :
主线程 ID:140709376947008,子线程 ID:140709368554240
子线程 ID:140709368554240,收到参数:Hello, Thread!
子线程返回:子线程退出!
(3)pthread_join()
------回收线程资源
- 功能 :阻塞等待指定线程退出,回收其资源,获取退出状态(类似进程的
waitpid()
)。 - 注意 :仅适用于 可接合属性 的线程(默认属性),若线程为 分离属性 ,
pthread_join()
会直接返回失败。
(4)线程属性------分离属性(Detached)
分离属性的线程退出时会自动释放资源,无需 pthread_join()
回收(避免僵尸线程)。
示例代码:
c
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
void *thread_func(void *arg) {
printf("分离线程 ID:%lu\n", pthread_self());
sleep(2);
return NULL;
}
int main() {
pthread_t tid;
pthread_attr_t attr;
// 1. 初始化线程属性
pthread_attr_init(&attr);
// 2. 设置分离属性
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
// 3. 创建分离线程
pthread_create(&tid, &attr, thread_func, NULL);
// 4. 销毁线程属性(不再需要)
pthread_attr_destroy(&attr);
// 尝试回收分离线程(会失败)
void *exit_msg;
int ret = pthread_join(tid, &exit_msg);
if (ret != 0) {
printf("pthread_join 失败:%s\n", strerror(ret));
}
sleep(3); // 等待分离线程执行完成
return 0;
}
输出结果 :
分离线程 ID:140709368554240
pthread_join 失败:Invalid argument
4. 示例代码(创建两个线程并发执行)
c
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
// 线程 1 函数
void *thread1_func(void *arg) {
while (1) {
printf("线程 1(%lu):%s\n", pthread_self(), (char *)arg);
sleep(1);
}
}
// 线程 2 函数
void *thread2_func(void *arg) {
while (1) {
printf("线程 2(%lu):%s\n", pthread_self(), (char *)arg);
sleep(1);
}
}
int main() {
pthread_t tid1, tid2;
// 创建两个线程
pthread_create(&tid1, NULL, thread1_func, "Hello World!");
pthread_create(&tid2, NULL, thread2_func, "Hello Thread!");
// 等待线程(避免主线程先退出)
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
return 0;
}
输出结果 (两个线程交替执行):
线程 1(140709368554240):Hello World!
线程 2(140709360161536):Hello Thread!
线程 1(140709368554240):Hello World!
线程 2(140709360161536):Hello Thread!
...