野火嵌入式Linux——内核编程模块 (进程)

程序进程虚拟内存之间的核心关系

这张图清晰地展示了 Linux 系统中程序进程虚拟内存之间的核心关系

1. 左侧:程序(可执行文件)

这是磁盘上的静态文件,包含了代码和数据,主要分为几个段:

  • .text:存放可执行的机器指令(代码段),是只读的。
  • .data:存放已初始化的全局变量和静态变量。
  • .rodata:存放只读数据,如字符串常量。
  • .bss:存放未初始化的全局变量和静态变量,在程序加载时会被自动清零。

这些段只是文件中的数据,本身不代表一个正在运行的实体。


2. 中间:task_struct(进程描述符)

当程序被加载执行时,内核会为它创建一个进程 ,并用 task_struct 结构体来描述这个进程的所有信息,这就是进程的 "身份证"。

  • 图中 mm* 是一个指向 mm_struct 的指针,mm_struct 是内存描述符,它管理着进程的整个虚拟地址空间。
  • 每个进程都有自己独立的 task_structmm_struct,这是进程间隔离的基础。

3. 右侧:虚拟内存(进程地址空间)

这是每个进程独立拥有的虚拟地址空间,它是一个逻辑上的连续地址范围,由内核的内存管理单元(MMU)映射到物理内存。

  • 程序被 "加载" 到这里:.text.data 等段会被映射到虚拟地址空间的对应区域。
  • 进程的代码执行、数据读写都发生在这个虚拟地址空间中,而不是直接操作物理内存。
  • task_struct 通过 mm 指针,将进程的执行上下文和它的虚拟内存空间关联起来。

4. 整体流程

  1. 加载 :内核将磁盘上的程序文件(.text, .data 等段)加载到进程的虚拟地址空间中。
  2. 关联 :内核创建 task_struct,并通过其中的 mm 指针,将进程描述符与新创建的虚拟内存空间关联。
  3. 执行:CPU 执行进程的代码时,通过虚拟地址访问内存,内核负责将虚拟地址透明地转换为物理地址。

简单来说,程序是静态的代码和数据文件,进程是程序的一次执行实例,而虚拟内存则是这个实例运行时的 "舞台",task_struct 则是管理这个舞台的 "管理员"。

查看进程之间的关系(ubuntu中)

pstree

fork函数

Fork函数https://blog.csdn.net/weixin_73503631/article/details/154989083?spm=1011.2415.3001.5331

⭐⭐⭐⭐⭐

执行fork函数之前,操作系统只有一个进程,fork函数之前的代码只会被执行一次
在执行fork函数之后,操作系统有两个几乎一样的进程,fork函数之后的代码会被执行两次。

子进程的无缝替换

exechan'shu'zu

常用后缀:

l:代表以列表形式传参(list)

v:代表以矢量数组形式传参(vector)

p:代表使用环境变量Path来寻找指定执行文件

e:代表用户提供自定义的环境变量

#include <unistd.h>

函数原型:

复制代码
int execl(const char *path, const char *arg, ...)
复制代码
int execlp(const char *file, const char *arg, ...)
复制代码
int execv(const char *path, char *const argv[])
复制代码
int execve(const char *path, char *const argv[],char *const envp[])

返回值:

成功:不返回

失败:-1

要点总结

  • l后缀和v后缀必须两者选其一来使用

  • p后缀和e后缀是可选的,可用可不用

  • 组合后缀的相关函数还有很多,可自己进一步了解

exce函数有可能执行失败,需要预防

  • 新程序的文件路径出错

  • 传参或者是自定义环境变量时,没有加NULL

  • 新程序没有执行权限

execlp示例:

进程的退出

正常退出:

  • 从main函数return

  • 调用exit()函数终止(退出时考虑IO缓冲区),标准库函数 (属于 stdlib.h),终止进程前会做善后清理工作

  • 调用_exit()函数终止(直接退出,不考虑IO缓冲区),系统调用 (属于 unistd.h),直接终止进程,不做任何清理,是更底层的操作

exit和_exit退出函数

头文件:

复制代码
#include <unistd.h>
#include <stdlib.h>

原型:

复制代码
void _exit(int status);
void exit(int status);

返回值:

不返回

复制代码
#include <stdio.h>
#include <stdlib.h>   // exit() 头文件
#include <unistd.h>   // _exit() 头文件

int main() {
    printf("测试exit和_exit的区别");  // 注意:没有换行符\n,printf会缓存内容
    
    // 测试1:调用exit()
    // exit(0);  // 执行后,缓冲区会被刷新,能看到打印内容
    
    // 测试2:调用_exit()
    _exit(0);  // 执行后,缓冲区未刷新,看不到打印内容
}

等待子进程的终结

只有当子进程exit成功退出后,父进程wait ( ) 后面的内容才会执行,否则父进程会一直阻塞在wait这里

  • 父进程调用 wait(&status) 后,会阻塞在这个位置,暂停执行后续代码。
  • 只有当子进程通过 exit(0) 正常退出(或被信号终止)后,内核才会唤醒父进程,让它继续执行 wait() 之后的代码。
  • 如果子进程没有退出,父进程会一直阻塞在 wait() 这里,直到子进程结束。

wait函数

头文件

复制代码
#include <sys/wait.h>

函数原型

复制代码
pid_t wait(int *status)

返回值

  • 成功:退出的子进程的pid

  • 失败:-1

处理子进程退出状态值的宏
  • WIFEXITED(status) :如果子进程正常退出,则该宏为真

  • WEXITSTATUS(status):如果子进程正常退出,则该宏获取子进程的退出值

进程的生老病死

核心状态说明

  • 就绪态:进程已获得除 CPU 外的所有必要资源,等待系统调度器分配时间片。
  • 运行态:进程正在 CPU 上执行指令。在单核心系统中,同一时间只有一个进程处于运行态。
  • 睡眠态:进程因等待某事件(如 I/O 完成、sleep ()、信号量)而主动放弃 CPU,进入阻塞状态,直到事件发生后被唤醒。
  • 暂停态:进程收到调试信号(如 SIGSTOP)后暂停执行,等待继续信号(如 SIGCONT)。
  • 僵尸态:进程已经执行结束(调用 exit ()),但父进程尚未通过 wait ()/waitpid () 回收其退出状态和资源,此时进程描述符仍保留在系统中。
  • 死亡态:父进程调用 wait () 回收了子进程的所有资源,进程彻底从系统中消失。

进程组、会话、终端

进程组:对相同类型的进程进行管理

进程组的诞生:

  • 在shell里面直接执行一个应用程序,对于大部分进程来说,自己就是进程组的首进程**,进程组只有一个进程**
  • 如果进程调用了fork函数,那么父子进程同属于一个进程组父进程为首进程
  • 在shell中通过管道执行连接起来的应用程序,两个程序同属于一个进程组,第一个程序为进程组的首进程

进程组ID(pgid),由首进程pid决定

会话

作用:管理进程组

会话的诞生

  • 调用setsid函数,新建一个会话,应用程序作为会话的第一个进程,称为会话首进程
  • 用户在终端正确登录后,启动shell时linux系统会创建一个新的会话,shell进程作为会话首进程

会话id:会话首进程id,SID

前台进程组

shell进程启动时,默认是前台进程组的首进程

前台进程组的首进程会占用会话所关联的终端来运行,shell启动其他应用程序时,其他程序成为首进程

先搞懂几个核心概念(用生活例子类比)

可以把 终端 想象成你家的客厅(唯一的活动空间),shell (比如 bash)就是客厅里的 "主人",进程组 是一群要占用客厅的人,前台进程组首进程 就是当前占用客厅的 "负责人",只有他能使用客厅的资源(比如沙发、电视)。

1. "shell 进程启动时,默认是前台进程组的首进程"

当你打开终端(比如 Linux 的 Terminal、macOS 的终端),系统会启动一个 shell 进程(比如 bash)。

  • 此时这个 shell 就是 "客厅负责人"(前台进程组首进程),独占终端的输入输出:你输入 lscd 这些命令,shell 能接收到;命令的执行结果,也能显示在终端上。
  • 简单说:刚打开终端时,shell 是 "老大",终端完全听它的。
2. "前台进程组的首进程会占用会话所关联的终端来运行"

"占用终端" 就是指:这个进程能接收你在终端敲的键盘输入,也能把输出打印到终端屏幕上。

  • 比如你没运行其他程序时,shell 作为首进程,你敲 echo hello,shell 接收到这个输入,执行后把 hello 输出到终端 ------ 这就是 "占用终端" 的体现;
  • 反之,如果是后台进程(比如 sleep 10 &),它不会占用终端,你敲键盘依然能被 shell 接收。
3. "shell 启动其他应用程序时,其他程序成为首进程"

当你在 shell 里输入并运行一个程序(比如 vimpythonls),shell 会 "把客厅的控制权让出去":

  • 原来的 shell 不再是前台进程组首进程,刚启动的这个程序 会变成新的前台进程组首进程,独占终端;
  • 比如你输入 vim test.txt 后:
    • shell 会暂停(不再接收你的键盘输入);
    • vim 成为前台进程组首进程,占用终端:你敲的所有按键(比如编辑文字、按 esc)都被 vim 接收,终端显示的也是 vim 的界面;
    • 当你退出 vim(按 :wq),vim 进程结束,shell 又会重新成为前台进程组首进程,终端的控制权回到 shell 手里,你又能输入 shell 命令了。

后台进程组

后台进程中的程序不占用终端
在shell进程里面启动程序时,加上&的符号可以指定程序运行在后台进程组中

ctrl+z 会使进程进入后台,同时停止执行

jobs:查看有哪些后台进程组

fg+job id 可以把后台进程组切换为前台进程组

终端

物理终端

  • 串口终端
  • lcd终端

伪终端

  • ssh远程连接产生的终端
  • 桌面系统启动的终端

虚拟终端

  • Linux内核自带的,ctrl+Alt+f0-f6可以打开7个虚拟终端

守护进程

会话用来管理前后台进程组,会话一般关联着一个终端

当终端被关闭之后,会话中的所有进程都会被关掉

守护进程(Daemon)是脱离终端、在后台长期运行的进程(比如 Linux 里的 sshdnginx 都是守护进程)。它的核心特点是:

  • 不占用终端,即使启动它的终端关闭,进程依然运行;
  • 不受终端信号(比如 Ctrl+C)影响;
  • 在后台独立运行。

如何来写一个守护进程

1、创建一个子进程,父进程直接退出 (通过fork函数实现)

为什么要这么做?
  1. 脱离父进程的控制 :启动守护进程的初始进程(比如你在 shell 里敲命令启动)是前台进程组的一部分,父进程退出后,子进程会被操作系统的 init 进程(PID=1)接管,不再依赖原启动终端;
  2. 避免成为进程组首进程 :只有进程组首进程才能创建新会话(下一步要做的),但如果直接用原进程创建会话,它依然和终端关联;先 fork 出子进程,子进程不是进程组首进程,为下一步创建新会话做准备;
  3. 让 shell 认为命令执行完毕:父进程退出后,shell 会立刻返回(不会卡住),子进程则在后台继续运行。

2、创建一个新的会话

先理解:什么是 "会话(Session)"?

会话是一组进程组的集合,每个会话都和一个终端关联(比如你打开的 Terminal 窗口就是一个会话,关联着 shell 进程)。

  • 会话有一个 "控制终端",会话里的前台进程组会占用这个终端;
  • 当终端关闭时,内核会给会话里的所有进程发送 SIGHUP 信号,导致进程退出(这就是为什么普通后台进程(加 &)终端关了也会退出)。
为什么要创建新会话?

setsid() 函数会做 3 件关键事,彻底让进程脱离原终端:

  1. 创建一个新的会话 ,子进程成为这个新会话的会话首进程
  2. 创建一个新的进程组,子进程成为这个进程组的首进程
  3. 脱离原会话的控制终端(新会话没有关联任何终端)。

这样一来,进程就不再受原终端的任何影响(终端关闭、Ctrl+C 都和它无关)。

3、改变守护进程的当前工作目录,改为"/"

为什么要这么做?

守护进程启动时,默认的当前工作目录是启动它的那个目录 (比如你在 /home/yourname 下运行守护进程,它的工作目录就是 /home/yourname)。如果不修改,会带来两个严重问题:

  1. 无法卸载挂载的文件系统 举个例子:如果你的守护进程工作目录是 /mnt/usb(U 盘挂载目录),只要这个守护进程还在运行,系统就无法卸载这个 U 盘(提示 "设备忙")。而根目录 / 是系统最顶层目录,永远不会被卸载,能避免这个问题。

  2. 原工作目录被删除 / 移动导致异常 如果守护进程的工作目录被手动删除(比如 /home/yourname 被删),虽然进程不会立刻崩溃,但后续如果进程需要创建文件、访问相对路径,都会失败(比如想创建 ./log.txt,但当前目录已不存在)。

4、重设文件权限掩码

先搞懂:文件权限掩码(umask)是什么?

umask 是系统的 "权限过滤器" ,作用是限制新建文件 / 目录的默认权限

  • 新建文件的默认权限 = 0666 - umask(文件默认不能执行,所以是 666)
  • 新建目录的默认权限 = 0777 - umask(目录需要执行权限才能进入,所以是 777)

比如默认 umask 是 0022(常见的默认值):

  • 新建文件权限:0666 - 0022 = 0644(即 -rw-r--r--
  • 新建目录权限:0777 - 0022 = 0755(即 drwxr-xr-x
为什么要重设守护进程的 umask?

守护进程继承了启动它的 shell 的 umask(比如 shell 的 umask 是 0077,会导致新建文件只有所有者能读写)。如果不重置:

  1. 权限不符合预期:比如守护进程想创建一个让其他用户也能读取的日志文件,但继承的 umask 会把权限限制死;
  2. 权限过严 / 过松:可能导致守护进程创建的文件无法被其他程序访问(过严),或权限太开放导致安全风险(过松)。

因此,守护进程通常会将 umask 设为 0(即取消所有权限限制),让进程手动控制新建文件 / 目录的权限,而不是依赖系统默认值。

5、关闭不需要的文件描述符

复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/stat.h>

#define MAXFILE 3

int main()
{
    pid_t pid;
    int fd, len, i, num;
    char *buf = "the dameon is running\n";
    len = strlen(buf) + 1;

    /*1.创建子进程,销毁父进程*/
    pid = fork();
    if (pid < 0) {
        printf("fork fail\n");
        exit(1);
    }

    if (pid > 0) {
        exit(0);
    }

    /*2.创建新会话,摆脱终端的影响*/
    setsid();

    /*3.改变当前工作目录*/
    chdir("/");

    /*4.重设文件掩码*/
    umask(0);

    /*5.关闭默认的文件描述符*/
    for (i = 0; i < MAXFILE; i++) {
        close(i);
    }

    /*6.实现守护进程的功能*/
    while (1) {
        fd = open("/var/log/dameon.log", O_CREAT | O_WRONLY | O_APPEND, 0666);
        write(fd, buf, len);
        close(fd);
        sleep(5);
    }

    return 0;
}

普通进程伪装成守护进程

复制代码
# 格式:nohup 你的程序命令 > 输出日志文件 2>&1 &
nohup ./your_program > /var/log/normal_daemon.log 2>&1 &

nohup(no hang up)的核心作用是忽略终端挂断信号(SIGHUP) ,配合 & 让进程在后台运行,从而模拟守护进程 "脱离终端、后台持续运行" 的核心特征。它和手动编写守护进程代码的区别在于:

  • nohup 是 "用户态伪装",进程本质还是普通进程,只是不受终端退出影响;
  • 手动编写的守护进程是 "内核态改造",完全脱离终端会话,更符合标准守护进程规范。
特性 nohup + &(伪装) 手动编写守护进程(标准)
脱离终端 ✅(忽略 SIGHUP) ✅(完全脱离会话)
后台运行
占用终端进程组 ✅(仍属于原进程组) ❌(独立进程组)
开机自启 需手动配置 可集成到系统服务(systemd)
代码侵入性 0(无需改代码) 需修改程序(fork/setsid 等)
稳定性 较好(但可能被意外 kill) 更高(完全独立)

PS命令

ps(Process Status)是 Linux/Unix 系统中查看进程静态快照的核心命令,用于列出当前运行的进程、PID、状态、资源占用等信息。

静态快照 :只显示执行命令瞬间的进程状态,不实时刷新(区别于 top)。

复制代码
# BSD 风格(推荐)
ps aux
# System V 风格
ps -ef
过滤指定进程(排查是否运行)
复制代码
# 查找 nginx 进程
ps aux | grep nginx
# 查找 PID 为 1234 的进程
ps -p 1234
按资源排序(定位高占用)
复制代码
# 按 CPU 使用率降序
ps aux --sort=-%cpu | head -10
# 按内存使用率降序
ps aux --sort=-%mem | head -10
自定义输出列(-o
复制代码
# 只显示 PID、用户、CPU、命令
ps -eo pid,user,%cpu,cmd
按用户 / 组筛选
复制代码
# 查看 root 用户的所有进程
ps -u root
# 查看指定组的进程
ps -G wheel
查看线程信息
复制代码
# 显示进程的线程(LWP)
ps -L -p 1234

ps aux 输出字段(关注进程本身)

  • USER:进程所有者
  • PID:进程 ID(唯一标识)
  • %CPU:CPU 使用率
  • %MEM:内存使用率
  • VSZ:虚拟内存大小(KB)
  • RSS:物理内存大小(KB)
  • TTY:终端设备(? 表示无终端,后台进程)
  • STAT:进程状态(见下文)
  • START:启动时间
  • TIME:累计 CPU 时间
  • COMMAND:进程命令与参数

进程状态码(STAT 列)

  • R:Running(运行中)
  • S:Sleeping(可中断睡眠,等待事件)
  • D:Uninterruptible Sleep(不可中断,通常 I/O 等待)
  • T:Stopped(停止,如 Ctrl+Z)
  • Z:Zombie(僵尸进程,已结束但未被父进程回收)
  • <:高优先级
  • N:低优先级
  • s:会话领导者
  • l:多线程
  • +:前台进程组

ps axjf(关注进程间的联系)

一个组合参数的 ps 命令,核心作用是以树状结构显示系统中所有进程的父子层级关系,非常适合排查进程之间的调用 / 派生关系。

直接执行即可看到完整的进程树:

复制代码
ps axjf
核心输出字段(重点关注):
  • PPID:父进程 ID(可以看到谁创建了这个进程)
  • PID:当前进程 ID
  • PGID:进程组 ID
  • SID:会话 ID
  • TTY:进程关联的终端(? 为后台进程)
  • PGRP:进程组号
  • UID:进程所属用户 ID
  • COMMAND:进程命令,树状结构中用 ├─/└─ 表示层级

僵尸进程和托孤进程

1. 进程正常退出流程

子进程退出时,会调用 exit() 函数,内核会释放其大部分资源,但会保留进程描述符(task_struct),其中包含退出状态等信息。父进程需要调用 wait()waitpid() 等函数,来读取子进程的退出状态,并由内核彻底清理该子进程的所有资源。

2. 僵尸进程(Zombie Process)

  • 产生原因 :子进程已经调用 exit() 退出,但父进程没有调用 wait()waitpid() 来回收它的退出状态和资源。
  • 表现 :在 ps 命令中,状态码为 Z。此时子进程的进程描述符仍然存在,占用少量系统资源(如 PID),但无法被正常调度执行。
  • 危害:大量僵尸进程会占用系统 PID 资源,导致无法创建新的进程。
  • 处理方式
    • 终止父进程:当父进程退出后,init 进程(PID=1)会接管这些僵尸进程,并自动调用 wait() 清理它们。
    • 让父进程显式调用 wait()waitpid(),或通过信号处理函数(如捕获 SIGCHLD)来自动回收。

3. 托孤进程(孤儿进程,Orphan Process)

  • 产生原因:父进程先于子进程退出,子进程就变成了 "孤儿"。
  • 处理机制 :内核会将孤儿进程的父进程 "过继" 给 init 进程(PID=1),由 init 进程负责在它们退出时调用 wait() 回收资源,因此孤儿进程不会变成僵尸进程。
特性 僵尸进程 (Zombie) 孤儿进程 (Orphan)
谁先退出 子进程先退出 父进程先退出
状态码 Z 正常运行状态(如 S, R
资源占用 占用 PID 等少量资源 正常占用资源
危害 大量存在会耗尽 PID 资源 无直接危害,由 init 进程接管
解决方法 终止父进程或让父进程调用wait() 由 init 进程自动回收,无需干预

子进程 "退出" 的两层含义

子进程调用 exit() 后,它的退出分为两个阶段:

  1. 用户态退出

    • 子进程的代码停止执行,用户空间的内存、文件描述符等资源被释放。
    • 从用户的角度看,这个程序 "已经结束了",不再做任何事情。
  2. 内核态残留

    • 内核中仍然保留着该进程的进程描述符(task_struct),里面记录了它的 PID、退出状态码、资源使用统计等信息。
    • 这些信息是留给父进程的 "遗产",父进程需要通过 wait() 来 "认领" 这份遗产,内核才会彻底删除这个进程描述符。

1. 为什么会变成 "僵尸"?

  • 子进程退出后,内核会向父进程发送一个 SIGCHLD 信号,通知它 "你的孩子走了,快来收尸"。
  • 如果父进程没有 处理这个信号,也没有主动调用 wait()waitpid(),内核就会一直保留那个进程描述符,这个进程就变成了僵尸进程(Zombie Process)
  • 它的状态码是 Z,表示它已经死了,但 "尸体" 还在系统里占着位置。

2. 僵尸进程的危害

虽然僵尸进程不占用 CPU 和大量内存,但它会:

  • 占用一个唯一的 PID 号。
  • 如果系统中产生了大量僵尸进程,会耗尽可用的 PID 资源,导致无法创建新的进程。

3. 如何 "收尸"?

  • 父进程主动回收 :在代码中调用 wait()waitpid(),或者注册信号处理函数来捕获 SIGCHLD 并在处理函数中回收。
  • 终止父进程 :如果父进程退出,init 进程(PID=1)会自动接管所有僵尸子进程,并调用 wait() 清理它们。

进程间通信

IPC 是操作系统提供的机制,用于让独立的进程之间交换数据、同步行为或传递控制信号,主要解决以下四类问题:

  1. 数据传输:在进程之间传递字节流或消息块,例如管道、消息队列。
  2. 资源共享:让多个进程访问同一块内存或文件,例如共享内存。
  3. 事件通知:一个进程向另一个或一组进程发送信号,告知特定事件发生,例如信号(Signal)。
  4. 进程控制:一个进程完全控制另一个进程的执行(如调试、挂起、恢复),例如通过调试器或 ptrace。

Linux 系统下的 IPC 分类与特点

1. 早期 Unix 系统 IPC
  • 管道(Pipe):半双工的单向通信通道,只能在具有亲缘关系的进程(如父子进程)间使用。
  • 信号(Signal):用于通知进程发生了异步事件,是一种 "软中断" 机制,不适合传输大量数据。
  • FIFO(命名管道):是管道的一种扩展,允许无亲缘关系的进程间通信,在文件系统中以特殊文件形式存在。
2. System V IPC(贝尔实验室)

这是一套经典的 IPC 机制,主要用于本地进程间通信:

  • System V 消息队列:消息以链表形式在内核中维护,支持按类型筛选消息,比管道更灵活。
  • System V 信号量:用于实现进程间的同步与互斥,防止多个进程同时访问临界资源。
  • System V 共享内存 :允许多个进程共享同一块物理内存,是所有 IPC 中速度最快的方式,因为数据无需在用户态和内核态之间拷贝。
3. Socket IPC(BSD)
  • 套接字(Socket):起源于 BSD Unix,是一种通用的 IPC 接口。它不仅支持本地进程间通信(Unix 域套接字),还支持跨网络的主机间通信(网络套接字),是网络编程的基础。
4. POSIX IPC(IEEE)

为了统一不同 Unix 系统的 IPC 接口,IEEE 制定了 POSIX 标准,提供了与 System V 功能类似但接口更现代、更易用的实现:

  • POSIX 消息队列:接口更简洁,支持消息优先级,并且可以与文件系统事件(如 select/poll)集成。
  • POSIX 信号量:分为命名信号量和无名信号量,无名信号量可用于线程间同步。
  • POSIX 共享内存 :通过 mmap() 系统调用将文件或设备映射到进程地址空间,使用更灵活。

💡 选型建议

  • 追求速度 :优先选择 共享内存(无论是 System V 还是 POSIX)。
  • 需要跨网络 :使用 Socket
  • 简单的父子进程通信 :使用 管道 最方便。
  • 事件通知 :使用 信号

无名管道

一、pipe 函数核心信息

  • 头文件#include <unistd.h>
  • 函数原型int pipe(int pipefd[2]);
  • 返回值
    • 成功:返回 0,并在 pipefd 数组中填充两个文件描述符:
      • pipefd[0]:管道的读端
      • pipefd[1]:管道的写端
    • 失败:返回 -1,并设置 errno

二、无名管道的特点

  1. 特殊文件 :它是内核中的一块缓冲区,没有名字,因此无法用 open() 创建,但必须用 close() 关闭。
  2. 单向通信 :数据只能从写端(pipefd[1])流向读端(pipefd[0])。
  3. 亲缘限制只能在具有共同祖先的进程(如父子、兄弟进程)间使用,因为文件描述符需要通过 fork() 继承。
  4. 阻塞特性
    • 当管道为空时,read() 操作会阻塞,直到有数据写入。
    • 当管道满时,write() 操作会阻塞,直到有数据被读出。
  5. 生命周期:当所有指向管道的文件描述符都被关闭后,管道才会被内核销毁。

三、标准使用步骤(父进程 → 子进程通信)

  1. 父进程创建管道 :调用 pipe(),得到读写两端的文件描述符。

  2. 父进程 fork 子进程:子进程会继承父进程的这两个文件描述符。

  3. 关闭无用端口

    • 若父进程要写、子进程要读:父进程关闭 pipefd[0],子进程关闭 pipefd[1]
    • 若父进程要读、子进程要写:父进程关闭 pipefd[1],子进程关闭 pipefd[0]
  4. 进行读写操作 :使用 write()read() 在两端传输数据。

  5. 关闭读写端口:通信结束后,关闭各自持有的文件描述符。

    示例代码(父写子读)
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <string.h>

    int main() {
    int pipefd[2];
    if (pipe(pipefd) == -1) {
    perror("pipe");
    exit(EXIT_FAILURE);
    }

    复制代码
     pid_t pid = fork();
     if (pid == -1) {
         perror("fork");
         exit(EXIT_FAILURE);
     }
    
     if (pid == 0) { // 子进程
         close(pipefd[1]); // 关闭写端
    
         char buf[1024];
         ssize_t n = read(pipefd[0], buf, sizeof(buf)-1);
         if (n == -1) {
             perror("read");
             exit(EXIT_FAILURE);
         }
         buf[n] = '\0';
         printf("子进程收到: %s\n", buf);
    
         close(pipefd[0]);
         exit(EXIT_SUCCESS);
     } else { // 父进程
         close(pipefd[0]); // 关闭读端
    
         const char *msg = "Hello from parent!";
         if (write(pipefd[1], msg, strlen(msg)) == -1) {
             perror("write");
             exit(EXIT_FAILURE);
         }
    
         close(pipefd[1]);
         wait(NULL); // 等待子进程结束
         exit(EXIT_SUCCESS);
     }

    }

有名管道

一、mkfifo 函数核心信息

  • 头文件

    复制代码
    #include <sys/types.h>
    #include <sys/stat.h>
  • 函数原型

    复制代码
    int mkfifo(const char *filename, mode_t mode);
    • filename:要创建的有名管道文件路径(如 /tmp/myfifo)。
    • mode:文件权限(如 0666),最终权限会受系统 umask 影响。
  • 返回值

    • 成功:返回 0,在文件系统中创建一个特殊的管道文件。
    • 失败:返回 -1,并设置 errno

二、有名管道的特点

  1. 有文件名:在文件系统中以特殊文件形式存在,因此可以被任意进程通过路径名打开,突破了无名管道只能在亲缘进程间使用的限制。
  2. 任意进程通信:只要知道管道文件的路径,无亲缘关系的进程也可以通过它进行数据传输。
  3. 单向通信:本质上仍是半双工的,数据单向流动。若要双向通信,需要创建两个 FIFO。
  4. 阻塞特性
    • 当管道为空时,read() 操作会阻塞,直到有数据写入。
    • 当管道满时,write() 操作会阻塞,直到有数据被读出。
    • write 操作具有原子性,保证了多进程写入时数据不会被交错。一个 write 操作要么完整地把数据写入管道,要么完全不写入,不会出现 "写了一半" 的情况。
  5. 生命周期:管道文件本身是持久化的,除非被手动删除,否则会一直存在于文件系统中,即使所有进程都关闭了它。

三、标准使用步骤

  1. 创建管道 :一个进程调用 mkfifo() 创建有名管道文件。

  2. 打开管道

    • 写进程以 O_WRONLY 模式打开管道。
    • 读进程以 O_RDONLY 模式打开管道。
    • 注意:open() 操作会阻塞,直到另一端也打开了管道。
  3. 数据传输 :使用 write()read() 进行读写。

  4. 关闭管道:通信结束后,关闭文件描述符。

  5. 删除管道 :不再需要时,使用 unlink() 删除文件系统中的管道文件。

    写进程(writer.c)
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <fcntl.h>
    #include <sys/stat.h>
    #include <sys/types.h>

    int main() {
    const char *fifo_path = "/tmp/myfifo";

    复制代码
     // 创建有名管道,如果已存在则忽略
     if (mkfifo(fifo_path, 0666) == -1) {
         perror("mkfifo");
         // 如果错误是因为文件已存在,可以继续执行
     }
    
     int fd = open(fifo_path, O_WRONLY);
     if (fd == -1) {
         perror("open");
         exit(EXIT_FAILURE);
     }
    
     const char *msg = "Hello from FIFO writer!";
     if (write(fd, msg, sizeof(msg)) == -1) {
         perror("write");
         exit(EXIT_FAILURE);
     }
    
     close(fd);
     return 0;

    }

    读进程(reader.c)
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <fcntl.h>
    #include <sys/stat.h>
    #include <sys/types.h>

    int main() {
    const char *fifo_path = "/tmp/myfifo";

    复制代码
     int fd = open(fifo_path, O_RDONLY);
     if (fd == -1) {
         perror("open");
         exit(EXIT_FAILURE);
     }
    
     char buf[1024];
     ssize_t n = read(fd, buf, sizeof(buf)-1);
     if (n == -1) {
         perror("read");
         exit(EXIT_FAILURE);
     }
     buf[n] = '\0';
     printf("收到: %s\n", buf);
    
     close(fd);
     unlink(fifo_path); // 删除管道文件
     return 0;

    }

信号

一、信号的产生信号(Signal)是软件或硬件触发的异步通知,用于告知进程发生了特定事件。

硬件触发

  • 执行非法指令:如除零操作、未定义指令等。
  • 访问非法内存:如访问空指针、越界内存地址。
  • 驱动程序:如硬件设备(键盘、定时器)通过中断触发信号。

软件触发

  • 控制台上的快捷键:
    • Ctrl+C:发送中断信号(SIGINT),用于终止前台进程。
    • Ctrl+\\:发送退出信号(SIGQUIT),用于终止进程并生成核心转储。
    • Ctrl+Z:发送停止信号(SIGTSTP),用于暂停前台进程。
  • kill 命令:向指定进程发送任意信号。
  • 程序调用 kill() 函数:在代码中主动向自身或其他进程发送信号。

二、信号的处理方式

当进程接收到信号时,有三种主要的处理方式:

忽略(Ignore)

  • 进程告知操作系统,当该信号发生时,不做任何处理,就像信号从未出现过一样。
  • 注意:SIGKILL(终止进程)和 SIGSTOP(暂停进程)这两个信号无法被忽略

捕获并处理(Catch and Handle)

  • 进程注册一个自定义的信号处理函数。
  • 当信号发生时,操作系统会暂停进程的正常执行,转而执行这个自定义函数,处理完成后再恢复原执行流程。

默认(Default)

  • 进程不做任何特殊设置,使用系统为每种信号预设的默认行为。
  • 常见的默认行为包括:终止进程、忽略信号、暂停进程、生成核心转储等。

常用信号分析表解读

信号名 信号编号 产生原因 默认处理方式
SIGHUP 1 关闭终端 终止
SIGINT 2 Ctrl+C 终止
SIGQUIT 3 Ctrl+\ 终止 + 转储
SIGABRT 6 abort() 终止 + 转储
SIGFPE 8 算术错误 终止
SIGKILL 9 kill -9 pid 终止,不可捕获 / 忽略
SIGUSR1 10 自定义 忽略
SIGSEGV 11 段错误 终止 + 转储
SIGUSR2 12 自定义 忽略
SIGALRM 14 alarm() 终止
SIGTERM 15 kill pid 终止
SIGCHLD 17 (子) 状态变化 忽略
SIGSTOP 19 Ctrl+Z 暂停,不可捕获 / 忽略

signal、kill、raise 三个函数

1. signal 函数

作用用于为指定信号设置自定义的处理函数,是最基础的信号注册接口。

  • 头文件#include <signal.h>

  • 函数原型

    复制代码
    typedef void (*sighandler_t)(int);
    sighandler_t signal(int signum, sighandler_t handler);
  • 参数说明

    • signum:要设置的信号编号,如 SIGINTSIGUSR1 等。
    • handler:信号处理方式,有三种选择:
      • SIG_IGN:忽略该信号。
      • SIG_DFL:使用系统默认处理方式。
      • 自定义函数指针:指向 void (*)(int) 类型的函数,作为自定义处理逻辑。
  • 返回值

    • 成功:返回上一次设置的 handler。

    • 失败:返回 SIG_ERR

      #include <stdio.h>
      #include <stdlib.h>
      #include <signal.h>
      #include <sys/types.h>
      #include <sys/wait.h>
      #include <unistd.h>

      /* 信号处理函数 */
      void signal_handler(int sig)
      {
      printf("\nthis signal number is %d \n", sig);

      复制代码
      if (sig == SIGINT)
      {
          printf("I have get SIGINT!\n");
          printf("The signal has been restored to the default processing mode!\n");
          /* 恢复信号为默认处理 */
          signal(SIGINT, SIG_DFL);
      }

      }

      int main(void)
      {
      printf("\nthis is an alarm test function\n");

      复制代码
      /* 设置信号处理的回调函数 */
      signal(SIGINT, signal_handler);
      
      while (1)
      {
          printf("waiting for the SIGINT signal, please enter \"ctrl + c\"...\n");
          sleep(1);
      }
      
      exit(0);

      }

2. kill 函数

作用:向指定进程(或进程组)发送一个信号,常用于进程间通信或终止其他进程。

  • 头文件#include <signal.h>#include <sys/types.h>

  • 函数原型

    复制代码
    int kill(pid_t pid, int signum);
  • 参数说明

    • pid:目标进程 ID。特殊值:
      • pid > 0:发送给进程 ID 为 pid 的进程。
      • pid = 0:发送给与当前进程同组的所有进程。
      • pid = -1:发送给系统中除 init 进程外的所有进程(需权限)。
      • pid < -1:发送给进程组 ID 为 -pid 的所有进程。
    • signum:要发送的信号编号。
  • 返回值 :成功返回 0,失败返回 -1 并设置 errno

    #include <stdio.h>
    #include <stdlib.h>
    #include <signal.h>
    #include <sys/types.h>
    #include <sys/wait.h>
    #include <unistd.h>

    int main(void)
    {
    pid_t pid;

    复制代码
      /* 创建子进程 */
      if ((pid = fork()) < 0)
      {
          printf("Fork error!\n");
          exit(1);
      }
    
      if (pid == 0)
      {
          /* 子进程中使用 raise 函数发送 SIGSTOP 信号,使子进程暂停 */
          printf("Child is waiting for SIGSTOP signal!\n");
          /* 子进程停在这里 */
          raise(SIGSTOP);
    
          /* 子进程没有机会运行到这里 */
          printf("Child won't run here forever!\n");
          exit(0);
      }
      else
      {
          /* 睡眠5秒,让子进程先执行并暂停 */
          sleep(5);
    
          /* 发送 SIGKILL 信号杀死子进程 */
          if (kill(pid, SIGKILL) < 0)
          {
              printf("Parent kill %d failed!\n", pid);
          }
    
          /* 一直阻塞直到子进程退出(杀死) */
          wait(NULL);
    
          /* 父进程退出运行 */
          printf("Parent exit!\n");
          exit(0);
      }

    }

3. raise 函数

作用 :向当前进程自身发送一个信号,等价于 kill(getpid(), signum)

  • 头文件#include <signal.h>

  • 函数原型

    复制代码
    int raise(int signum);
  • 参数说明

    • signum:要发送的信号编号。
  • 返回值:成功返回 0,失败返回非 0 值。


核心应用场景

  • signal :在程序启动时注册信号处理函数,用于优雅地处理 Ctrl+C、程序退出等事件。
  • kill :在父进程中向子进程发送控制信号,或在命令行中使用 kill 命令管理进程。
  • raise:在程序内部主动触发信号,模拟外部事件,或触发自身的信号处理逻辑。

信号集处理函数

一、核心概念

  1. 屏蔽信号集(Signal Mask / Blocked Set)

    • 也叫 "阻塞信号集",是当前进程中被阻塞、暂时不会被递送的信号集合。
    • 当一个信号被加入屏蔽集后,即使它产生了,也不会被进程处理,直到它被从屏蔽集中移除。
  2. 未处理信号集(Pending Set)

    • 也叫 "待处理信号集",记录了已经产生但因为被屏蔽而尚未被处理的信号。
    • 一旦信号从屏蔽集中移除,未处理信号集中对应的信号就会被递送并处理。
    • 非实时信号(1-31) :不排队,只保留一个。这意味着如果同一个非实时信号(如 SIGINT)在被屏蔽期间多次触发,进程只会收到一次通知。
    • 实时信号(34-64) :排队,保留全部。
      • 实时信号支持可靠排队,多次触发会按顺序 被记录和递送,不会丢失。
函数 / 宏 作用
sigemptyset(sigset_t *set) 将信号集合初始化为空集
sigfillset(sigset_t *set) 将信号集合初始化为包含所有信号
sigaddset(sigset_t *set, int signum) 将指定信号 signum 添加到集合 set
sigdelset(sigset_t *set, int signum) 将指定信号 signum 从集合 set 中移除
sigismember(const sigset_t *set, int signum) 判断信号 signum 是否在集合 set 中,是则返回 1
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset) 检查或修改进程的信号屏蔽字(屏蔽信号集)
复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

void my_func(int signo)
{
    sigset_t set;
    sigemptyset(&set);
    sigaddset(&set, SIGINT);
    sigprocmask(SIG_UNBLOCK, &set, NULL);

    printf("hello\n");
    sleep(5);
    printf("world\n");
}

int main()
{
    signal(SIGINT, my_func);

    while (1);
    return 0;
}

上述函数的执行过程解析

  1. 主程序初始化

    • main 函数中,通过 signal(SIGINT, my_func) 注册了对 SIGINTCtrl+C)信号的自定义处理函数 my_func
    • 程序进入死循环 while(1);,等待信号触发。
  2. 第一次按下 Ctrl+C

    • 触发 SIGINT 信号,主程序被打断,执行 my_func
    • my_func 中:
      1. 初始化一个信号集 set,并将 SIGINT 加入其中。
      2. 调用 sigprocmask(SIG_UNBLOCK, &set, NULL),解除对 SIGINT 的阻塞。
      3. 打印 hello
      4. 调用 sleep(5),使进程休眠 5 秒。
    • 关键点 :在 sleep(5) 期间,进程对 SIGINT 信号不再阻塞。
  3. sleep(5) 期间再次按下 Ctrl+C

    • 再次触发 SIGINT 信号。由于此时信号未被阻塞,my_func 会被再次递归调用
    • 第二次执行 my_func
      1. 再次解除对 SIGINT 的阻塞。
      2. 打印第二个 hello
      3. 再次进入 sleep(5)
  4. 休眠结束

    • 第二次 sleep(5) 结束,打印第二个 world,第二次 my_func 调用返回。
    • 第一次 sleep(5) 结束,打印第一个 world,第一次 my_func 调用返回。
    • 程序回到死循环,继续等待。

SIGINT 信号本身不是 "天生阻塞" 的 ,它的阻塞状态完全由进程的信号屏蔽集(Signal Mask)决定,默认情况下(进程未做任何信号屏蔽操作),SIGINT 是未被阻塞的。

1. 信号的 "默认阻塞状态"

Linux 系统中,所有信号(包括 SIGINT)的默认状态都是:

  • 不在进程的屏蔽信号集(Blocked Set)中 → 未被阻塞
  • 未出现在未处理信号集(Pending Set)中 → 无待处理信号

这意味着:默认情况下,你按下 Ctrl+C 发送 SIGINT,进程会立即响应(执行默认行为:终止进程,或执行你注册的自定义处理函数)。

2. 信号处理时的 "自动阻塞"(核心易错点)

当你为 SIGINT 注册了自定义处理函数后,在处理函数执行期间 ,内核会自动将 SIGINT 加入进程的屏蔽集------ 这是 Linux 的默认保护机制,目的是防止信号处理函数被递归调用。

system-V 消息队列

system-V ipc特点

  • 独立于进程

  • 没有文件名和文件描述符

  • IPC对象具有key和ID

消息队列用法

  • 定义一个唯一key(ftok)

  • 构造消息对象(msgget)

  • 发送特定类型消息(msgsnd)

  • 接受特定类型消息(msgrcv)

  • 删除消息队列(msgctl)

ftok函数

功能:获取一个key

函数原型:

复制代码
key_t ftok(const char *path,int proj_id)

参数:

  • path:一个合法路径

  • proj_id:一个整数

返回值:

成功:合法键值

失败:-1

msgget函数

功能:获取消息队列ID

函数原型:

复制代码
int msgget(key_t key,int msgflg)

参数:

  • key:消息队列的键值

  • msgflg:

    • IPC_CREAT:如果消息队列不存在,则创建

    • mode:访问权限

返回值:

成功:该消息队列的ID

失败:-1

msgsnd函数

功能:发送消息到消息队列

函数原型:

复制代码
int msgsnd(int msqid,const void *msgp,size_t msgsz,int msgflg);

参数:

  • msqid:消息队列ID

  • msgp:消息缓存区

    • struct msgbuf

      {

      long mtype; //消息标识

      char mtext[1]; //消息内容

      }

  • msgsz:消息正文的字节数

  • msgflg:

    • IPC_NOWAIT:非阻塞发送

    • 0:阻塞发送

返回值:

成功:0

失败:-1

msgrcv函数

功能:从消息队列读取消息

函数原型:

复制代码
ssize_t msgrcv(int msqid,void *msgp,size_t msgsz,long msgtyp,int msgflg)

参数:

  • msqid:消息队列ID

  • msgp:消息缓存区

  • msgsz:消息正文的字节数

  • msgtyp:要接受消息的标识

  • msgflg:

    • IPC_NOWAIT:非阻塞读取

    • MSG_NOERROR:截断消息

    • 0:阻塞读取

返回值:

成功:0

失败:-1

msgctl函数

功能:设置或获取消息队列的相关属性

函数原型:

复制代码
int msgctl(int msgqid,int cmd,struct maqid_ds *buf)
  • msgqid:消息队列的ID

  • cmd

    • IPC_STAT:获取消息队列的属性信息

    • IPC_SET:设置消息队列的属性

    • IPC_RMID:删除消息队列

  • buf:相关结构体缓冲区

    消息发送端(msg_send.c)
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <sys/msg.h>
    #include <sys/ipc.h>

    // 定义消息结构体(需与接收端一致)
    struct msgbuf {
    long mtype; // 消息类型(≥1)
    char mtext[1024]; // 消息内容
    };

    int main() {
    key_t key;
    int msgid;
    struct msgbuf msg;

    复制代码
      // 1. 生成唯一 key
      key = ftok("./test_ipc", 100); // 需确保 ./test_ipc 文件存在
      if (key == -1) {
          perror("ftok failed");
          exit(1);
      }
    
      // 2. 创建/获取消息队列
      msgid = msgget(key, IPC_CREAT | 0666);
      if (msgid == -1) {
          perror("msgget failed");
          exit(1);
      }
    
      // 3. 构造并发送消息
      msg.mtype = 1; // 消息类型设为 1
      strcpy(msg.mtext, "Hello, System-V Message Queue!");
      
      // 阻塞发送消息(msgflg=0)
      if (msgsnd(msgid, &msg, strlen(msg.mtext), 0) == -1) {
          perror("msgsnd failed");
          exit(1);
      }
      printf("发送消息成功:%s\n", msg.mtext);
    
      return 0;

    }

    消息接收端(msg_recv.c)
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <sys/msg.h>
    #include <sys/ipc.h>

    // 与发送端一致的消息结构体
    struct msgbuf {
    long mtype;
    char mtext[1024];
    };

    int main() {
    key_t key;
    int msgid;
    struct msgbuf msg;
    ssize_t recv_len;

    复制代码
      // 1. 生成与发送端相同的 key
      key = ftok("./test_ipc", 100);
      if (key == -1) {
          perror("ftok failed");
          exit(1);
      }
    
      // 2. 获取消息队列(无需创建,直接获取)
      msgid = msgget(key, 0666);
      if (msgid == -1) {
          perror("msgget failed");
          exit(1);
      }
    
      // 3. 接收类型为 1 的消息(阻塞接收)
      recv_len = msgrcv(msgid, &msg, sizeof(msg.mtext), 1, 0);
      if (recv_len == -1) {
          perror("msgrcv failed");
          exit(1);
      }
      // 手动补全字符串结束符
      msg.mtext[recv_len] = '\0';
      printf("接收消息成功:%s(长度:%zd 字节)\n", msg.mtext, recv_len);
    
      // 4. 删除消息队列(仅需一端执行)
      if (msgctl(msgid, IPC_RMID, NULL) == -1) {
          perror("msgctl remove failed");
          exit(1);
      }
      printf("消息队列已删除\n");
    
      return 0;

    }

system-V 信号量

本质

计数器

作用

保护共享资源

  • 互斥

  • 同步

信号量用法

  • 定义一个唯一key(ftok)

  • 构造一个信号量(semget)

  • 初始化信号量(semctl SETVA)

  • 对信号量进行P/V操作(semop)

  • 删除信号量(semctl RMID)

重要概念补充

  • P 操作(减 1)sem_op = -1,申请资源,若信号量值为 0 则阻塞(直到值 > 0)。
  • V 操作(加 1)sem_op = 1,释放资源,唤醒等待该信号量的进程。
  • SEM_UNDO 标志:进程异常退出时,内核会自动撤销该进程对信号量的操作,避免信号量值异常(必加!)。
  • 信号量初始化semctlSETVAL 仅需初始化一次,通常由第一个创建信号量的进程执行。
  • nsems 参数semgetnsems 表示创建的信号量集中包含的信号量个数,互斥场景下设为 1 即可。

semget函数

功能:获取信号量的ID

函数原型:

复制代码
int semget(key_t key,int nsems,int semflg)

参数:

  • key:信号量键值

  • nsems:信号量数量

  • semflg:

    • IPC_CREATE:信号量不存在则创建

    • mode:信号量的权限

返回值:

成功:信号量ID

失败:-1

semctl函数

功能:获取或设置信号量的相关属性

函数原型:

复制代码
int semctl(int semid,int semnum,int cmd,union semun arg)

参数:

  • semid:信号量ID

  • semnum:信号量编号

  • cmd:

    • IPC_STAT:获取信号量的属性信息

    • IPC_SET:设置信号量的属性

    • IPC_RMID:删除信号量

    • IPC_SETVAL:设置信号量的值

  • arg:

    union semun { int val; struct semid_ds *buf; }

返回值:

成功:由cmd类型决定

失败:-1

semop函数

函数原型:

复制代码
int semop(int semid,struct sembuf *sops,size_t nsops)

参数:

  • semid:信号量ID

  • sops:信号量操作结构体数组

    struct sembuf

    {

    short sem_num; //信号量编号

    short sem_op;//信号量P/V操作

    short sem_flg; //信号量行为,SEM_UNDO

    }

    • nsops:信号量数量

返回值:

成功:0

失败:-1

复制代码
头文件 sem.h(信号量操作封装)
#include <sys/ipc.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/shm.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/sem.h>

union semun
{
    int val;
    struct semid_ds *buf;
};

/* 初始化信号量 */
int init_sem(int sem_id, int init_value)
{
    union semun sem_union;
    sem_union.val = init_value;

    if (semctl(sem_id, 0, SETVAL, sem_union) == -1)
    {
        printf("Initialize semaphore");
        return -1;
    }

    return 0;
}

/* 删除信号量 */
int del_sem(int sem_id)
{
    union semun sem_union;
    if (semctl(sem_id, 0, IPC_RMID, sem_union) == -1)
    {
        perror("Delete semaphore");
        return -1;
    }
}

/* P 操作 */
int sem_p(int sem_id)
{
    struct sembuf sops;
    sops.sem_num = 0;    /* 单个信号量的编号为 0 */
    sops.sem_op = -1;    /* 表示 P 操作(申请资源)*/
    sops.sem_flg = SEM_UNDO; /* 系统自动释放残留信号量 */

    if (semop(sem_id, &sops, 1) == -1)
    {
        perror("P operation");
        return -1;
    }
    return 0;
}

/* V 操作 */
int sem_v(int sem_id)
{
    struct sembuf sops;
    sops.sem_num = 0;    /* 单个信号量的编号为 0 */
    sops.sem_op = 1;     /* 表示 V 操作(释放资源)*/
    sops.sem_flg = SEM_UNDO; /* 系统自动释放残留信号量 */

    if (semop(sem_id, &sops, 1) == -1)
    {
        perror("V operation");
        return -1;
    }
    return 0;
}

主程序 main.c(进程同步示例)
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include "sem.h"

#define DELAY_TIME 3

int main(void)
{
    pid_t result;
    int sem_id;

    /* 创建一个信号量集(包含1个信号量) */
    sem_id = semget((key_t)6666, 1, 0666 | IPC_CREAT);
    if (sem_id == -1) {
        perror("semget failed");
        exit(1);
    }

    /* 初始化信号量值为 0 */
    init_sem(sem_id, 0);

    /* 调用 fork() 创建子进程 */
    result = fork();
    if (result == -1)
    {
        perror("Fork\n");
        exit(1);
    }
    else if (result == 0) /* 子进程 */
    {
        printf("Child process will wait for some seconds...\n");
        sleep(DELAY_TIME);
        printf("The child process is running...\r\n");
        sem_v(sem_id); // 子进程执行完后,执行V操作,释放信号量
    }
    else /* 父进程 */
    {
        sem_p(sem_id); // 父进程先执行P操作,阻塞等待子进程释放信号量
        printf("The father process is running...\r\n");
        sem_v(sem_id); // 父进程执行完后,执行V操作
        del_sem(sem_id); // 删除信号量
    }

    exit(0);
}

这是一个典型的进程同步示例,通过信号量实现 "子进程先执行,父进程后执行" 的顺序控制:

  1. 信号量初始化

    • 信号量初始值设为 0,这意味着父进程执行 sem_p 时会立即阻塞,因为信号量值为 0,无法申请到资源。
  2. 子进程行为

    • 子进程先睡眠 3 秒,模拟耗时操作。
    • 睡眠结束后,打印提示信息,然后执行 sem_v,将信号量值从 0 增加到 1,释放资源。
  3. 父进程行为

    • 父进程一开始就执行 sem_p,由于信号量初始值为 0,父进程会阻塞等待。
    • 当子进程执行 sem_v 后,信号量值变为 1,父进程的 sem_p 操作成功,解除阻塞,开始执行自己的代码。
    • 父进程执行完后,再次执行 sem_v 恢复信号量,并调用 del_sem 删除信号量集。

system-V 共享内存

作用

高效率传输大量数据

共享内存用法

  • 定义一个唯一key(ftok)

  • 构造一个共享内存对象(shmget)

  • 共享内存映射(shmat)

  • 解除共享内存映射(shmdt)

  • 删除共享内存(shmctl RMID)

shmget函数

功能:获取共享内存对象的ID

函数原型:

复制代码
int shmget(key_t key,int size,int shmflg)

参数:

  • key:共享对象键值

  • size:共享内存大小

  • shmflg:

    • IPC_CREATE:共享内存不存在则创建

    • mode:共享内存的权限

返回值:

成功:共享内存ID

失败:-1

shmat函数

功能:映射共享内存

函数原型:

复制代码
int shmat(int shmid,const void *shmaddr,int shmflg)

参数:

  • shmid:共享内存ID

  • shmaddr:映射地址,NULL为自动分配

  • shmflg:

    • SHM_RDONLY:只读方式映射

    • 0:可读可写

返回值:

成功:共享内存首地址

失败:-1

shmdt函数

功能:解除共享内存映射

函数原型:

复制代码
int shmdt(const void *shmaddr)

参数:

shmaddr:映射地址

返回值:

成功:0

失败:-1

shmctl函数

功能:获取或设置共享内存的相关属性

函数原型:

复制代码
int shmctl(int shmid,int cmd,struct shmid_ds *buf)

参数:

  • shmid:共享内存ID

  • cmd:

    • IPC_STAT:获取共享内存的属性信息

    • IPC_SET:设置共享内存的属性

    • IPC_RMID:删除共享内存

  • buf:属性缓冲区

返回值:

成功:由cmd类型决定

失败:-1

复制代码
System-V 信号量 + 共享内存 的组合使用场景,实现了父子进程间的同步通信。
核心逻辑是:子进程向共享内存写入数据,父进程在信号量同步下读取数据。

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/sem.h>
#include <sys/shm.h>
#include <sys/ipc.h>
#include <unistd.h>
#include <sys/wait.h>

// 必须手动声明的 semun 联合体
union semun {
    int val;
    struct semid_ds *buf;
};

// 信号量操作函数(复用之前的封装)
int init_sem(int sem_id, int init_value) {
    union semun sem_union;
    sem_union.val = init_value;
    if (semctl(sem_id, 0, SETVAL, sem_union) == -1) {
        perror("init_sem failed");
        return -1;
    }
    return 0;
}

int sem_p(int sem_id) {
    struct sembuf sops = {0, -1, SEM_UNDO};
    if (semop(sem_id, &sops, 1) == -1) {
        perror("sem_p failed");
        return -1;
    }
    return 0;
}

int sem_v(int sem_id) {
    struct sembuf sops = {0, 1, SEM_UNDO};
    if (semop(sem_id, &sops, 1) == -1) {
        perror("sem_v failed");
        return -1;
    }
    return 0;
}

int del_sem(int sem_id) {
    union semun sem_union;
    if (semctl(sem_id, 0, IPC_RMID, sem_union) == -1) {
        perror("del_sem failed");
        return -1;
    }
    return 0;
}

#define DELAY_TIME 3

int main(void) {
    pid_t result;
    int sem_id;
    int shm_id;
    char* addr;  // 共享内存映射地址

    // 1. 创建信号量集(key=6666,1个信号量,权限0666)
    sem_id = semget((key_t)6666, 1, 0666 | IPC_CREAT);
    if (sem_id == -1) {
        perror("semget failed");
        exit(1);
    }

    // 2. 创建共享内存(key=7777,大小1024字节,权限0666)
    shm_id = shmget((key_t)7777, 1024, 0666 | IPC_CREAT);
    if (shm_id == -1) {
        perror("shmget failed");
        exit(1);
    }

    // 3. 初始化信号量为0(实现同步:父进程先阻塞)
    init_sem(sem_id, 0);

    // 4. 创建子进程
    result = fork();
    if (result == -1) {
        perror("Fork failed");
        exit(1);
    }
    else if (result == 0) {  // 子进程
        printf("Child process will wait for some seconds...\n");
        sleep(DELAY_TIME);

        // 映射共享内存到子进程地址空间
        addr = shmat(shm_id, NULL, 0);
        if (addr == (void*)-1) {
            printf("shmat111 error!\n");
            exit(-1);
        }

        // 向共享内存写入数据("helloworld"含结束符共11字节)
        memcpy(addr, "helloworld", 11);
        printf("The child process is running...\r\n");

        // V操作:释放信号量,唤醒父进程
        sem_v(sem_id);

        // 子进程解除共享内存映射(图片中缺失,需补充)
        shmdt(addr);
        exit(0);
    }
    else {  // 父进程
        // P操作:阻塞,等待子进程V操作唤醒
        sem_p(sem_id);
        printf("The father process is running...\r\n");

        // 映射共享内存到父进程地址空间
        addr = shmat(shm_id, NULL, 0);
        if (addr == (void*)-1) {
            printf("shmat222 error!\n");
            exit(-1);
        }

        // 读取并打印共享内存中的数据
        printf("shared memory string:%s\r\n", addr);

        // 解除共享内存映射
        shmdt(addr);

        // 等待子进程退出(避免僵尸进程)
        wait(NULL);

        // 删除信号量和共享内存(图片中缺失,需补充)
        del_sem(sem_id);
        shmctl(shm_id, IPC_RMID, NULL);

        exit(0);
    }
}
相关推荐
古月-一个C++方向的小白2 小时前
Linux——进程控制
linux·运维·服务器
文静小土豆2 小时前
CentOS 7 OpenSSH 10.2p1 升级全攻略(含离线安装与回退方案)
linux·运维·centos·ssh
五阿哥永琪3 小时前
进程的调度算法
linux·运维·服务器
小杜的生信筆記3 小时前
生信技能技巧小知识,Linux多线程压缩/解压工具
linux·数据库·redis
nzxzn3 小时前
LVS(Linux virual server)知识点
linux·运维·lvs
菜鸟别浪3 小时前
内存管理-第1章-Linux 内核内存管理概述
linux·运维·云计算·虚拟化·内存管理
nxb5563 小时前
云原生keepalived实验设定
linux·运维·云原生
luoshanxuli20103 小时前
Linux UVC Camera的介绍与实践应用(二)
linux
xianyudx3 小时前
Linux 服务器 DNS 配置指南 (CentOS 7 / 麒麟 V10)
linux·服务器·centos