Linux中进程和线程常用的API详解

进程与线程基础及 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
(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. 守护进程编写流程(简化版)

  1. fork() 创建子进程,父进程退出(脱离原进程组)。
  2. 子进程调用 setsid() 创建新会话(脱离原终端)。
  3. fork() 创建孙子进程,子进程退出(避免成为会话首进程,无法再次脱离终端)。
  4. 切换工作目录(如 /),关闭不需要的文件描述符,重定向标准 I/O 到 /dev/null
  5. 执行核心业务逻辑(如循环监听端口)。

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

运行方式

  1. 先启动接收端:gcc rose.c -o rose && ./rose
  2. 再启动发送端:gcc jack.c -o jack && ./jack
  3. 发送端输入消息,接收端实时显示。

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 结构体 :描述消息队列属性(最大消息数、单条消息最大大小等)。

    c 复制代码
    struct 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);

    示例:

    c 复制代码
    union 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!

...

相关推荐
小猪写代码2 小时前
Ubuntu C编程 (make工具和Makefile的引用)
linux·运维·ubuntu
肖爱Kun2 小时前
LINUX中USB驱动架构—设备驱动
linux·驱动
白鹭2 小时前
apache实现LAMP+apache(URL重定向)
linux·运维·apache·url重定向·apache实现lamp架构
荣光波比2 小时前
MySQL数据库(一)—— 数据库基础与MySQL安装管理指南
运维·数据库·mysql·云计算
aramae3 小时前
终端之外:解锁Linux命令行的魔法与力量
linux·服务器·apache
椰子今天很可爱3 小时前
线程分离和线程同步互斥
linux·c++
小柯J桑_3 小时前
Linux:线程控制
linux·c++·算法
arron88993 小时前
CentOS配置vsftpd服务器
linux·服务器·centos