Linux学习笔记(十六)--进程信号

信号量

互斥:任何时候只允许一个执行流访问共享资源

临界资源:共享的,任何时刻只允许一个执行流访问的资源叫做临界资源,一般是内存空间(例如管道)

临界区:访问临界资源的代码

信号量:是同步与互斥工具。用于协调多个进程或线程对共享资源的访问顺序,防止数据竞争。是一种"资源计数器"和"访问许可证"。

核心定义

信号量是一个特殊的计数器,用于控制多个执行流(进程/线程)对共享资源的访问。其核心操作(P操作和V操作)是原子操作,意味着在执行过程中不会被中断,从而保证了操作的唯一性和正确性。

工作原理

P操作(等待/尝试)

功能:尝试申请一个资源。信号量的值(计数器)减1。

行为:如果减1后,信号量的值大于等于0,则进程/线程继续执行,获得资源访问权。如果减1后,值小于0,则说明当前无可用资源,该进程/线程被阻塞,进入等待队列,直到有其他进程执行V操作释放资源后将其唤醒。

V 操作 (增加/发信号):

功能:释放一个资源。信号量的值加1。

行为:执行加1后,如果信号量的值大于0,说明没有进程在等待。如果值小于等于0,说明之前有进程在等待,此时会唤醒等待队列中的一个进程。

信号量类型

二元信号量 (Binary Semaphore):其值只能是0或1。本质上等同于互斥锁 (Mutex),用于实现资源的互斥访问(一次只允许一个执行流进入临界区)。

计数信号量 (Counting Semaphore):其值可以大于1。用于控制对多个同类资源实例的并发访问(例如,一个有5个插座的充电站,信号量初始值为5)。

System V信号量

核心概念

信号量集

这是 System V 信号量的核心。它不是单个的计数器,而是一个信号量数组。当你创建一个信号量时,你创建的是一个包含多个(默认为 1 个)信号量的集合。你可以对这个集合中的任何一个信号量进行独立操作,也可以进行原子性的集合操作。

键值

一个唯一的键(通常是 key_t类型),用于在不同进程间标识同一个信号量集。最常用的生成键值的方法是 ftok函数。

信号量值

每个信号量本质是一个非负整数。其经典含义是:> 0: 表示可用资源的数量。= 0: 表示资源已被用完。其值不能为负(但在某些操作的"等待"语义下,进程会阻塞直到值变为正数)

等待​ 和 发信号​ 操作

这是操作信号量的两个基本原语,通常被称为 P 操作​ 和 V 操作。

P 操作: 尝试将信号量的值减 1。如果值大于 0,则成功减 1 并立即返回。如果值等于 0,则调用进程阻塞,直到信号量值变为大于 0。

V 操作: 将信号量的值加 1。如果有其他进程正在因为这个信号量而阻塞,其中一个会被唤醒。

主要系统调用

semget

功能:根据 key创建新的或获取已有的信号量集。

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

参数:(1)key: 信号量集的键值。可以使用 IPC_PRIVATE创建一个只有当前进程及其子进程能访问的新集合。

(2)nsems: 指定要创建/获取的信号量集中信号量的个数。

(3)semflg: 权限标志位(如 0666)和创建选项(如 IPC_CREAT | IPC_EXCL)。

返回值:成功返回信号量集的标识符(一个非负整数),失败返回 -1。

信号量可以申请多个信号量(与信号量是几是两个概念)即信号量集。

semctl

功能:对信号量集或其内的单个信号量进行控制操作。

复制代码
int semctl(int semid, int semnum, int cmd, ... /* union semun arg */);

参数:

semid: semget返回的信号量集标识符。

semnum: 信号量在集合中的索引(从 0 开始)。对于针对整个集合的操作,此参数被忽略。

cmd: 控制命令,最重要的有:

IPC_RMID: 立即删除信号量集,并唤醒所有因此信号量集而阻塞的进程。

SETVAL: 将第 semnum个信号量的值设置为 arg.val。这是初始化单个信号量最直接的方法。

GETVAL: 获取第 semnum个信号量的当前值。

SETALL: 使用 arg.array指向的数组一次性设置整个集合中所有信号量的值。

GETALL: 获取整个集合中所有信号量的值到 arg.array指向的数组。

IPC_STAT: 获取信号量集的元信息到 arg.buf指向的 struct semid_ds结构。

参数 arg: 一个可选的 union semun联合体,根据 cmd不同而使用不同的成员。这个联合体需要用户自己定义。

例如:

复制代码
union semun {
    int              val;    /* 用于 SETVAL */
    struct semid_ds *buf;    /* 用于 IPC_STAT, IPC_SET */
    unsigned short  *array;  /* 用于 GETALL, SETALL */
    struct seminfo  *__buf;  /* 用于 IPC_INFO (Linux特有) */
};
semop

功能:对一个或多个信号量执行原子性的操作。这是实现 P 操作和 V 操作的关键。

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

参数:semid: 信号量集标识符。

sops: 指向一个 struct sembuf结构体数组的指针,每个结构体描述一个操作。

nsops: sops数组的长度。

struct sembuf结构体:

复制代码
struct sembuf {
    unsigned short sem_num;  /* 信号量在集合中的索引 */
    short          sem_op;   /* 操作值 */
    short          sem_flg;  /* 操作标志,如 IPC_NOWAIT, SEM_UNDO */
};

sem_op决定了操作:

sem_op > 0: V 操作。将信号量的值增加 sem_op。这通常对应于释放资源。

sem_op == 0: 等待零。调用者将阻塞,直到该信号量的值变为 0。用于同步场景。

sem_op < 0: P 操作。尝试从信号量的值中减去 |sem_op|。如果信号量的当前值 >= |sem_op|,则减法立即成功。否则,调用者将阻塞,直到信号量值增长到足够大。这对应于获取/请求资源。

sem_flg标志:

IPC_NOWAIT: 如果操作会导致阻塞,则函数立即返回错误 EAGAIN,而不是阻塞。

SEM_UNDO: 非常重要!启用"撤消"机制。如果进程异常终止(未执行 V 操作释放信号量),内核会自动"撤消"该进程对信号量所做的所有更改,防止死锁和资源泄漏。在大多数需要长期持有信号量的场景下都应使用此标志。

信号

基本概念

在 Linux 中,信号(Signal)​ 是进程间通信(IPC)或内核向进程通知事件的一种基本机制,用于异步处理事件(如中断、异常、终止等)。

前/后台进程

前台进程

终端控制:占用终端(标准输入、输出、错误)

交互性:可接收键盘输入(如 Ctrl+C

启动方式:直接输入命令运行

Shell行为:Shell 等待进程结束才返回提示符

信号影响:受终端信号影响(如 SIGINTSIGTSTP

后台进程

终端控制:不占用标准输入(可输出到终端)

交互性:无法通过键盘直接交互

启动方式:在命令后加 &Ctrl+Z+ bg

Shell行为:Shell 立即返回提示符

信号影响:默认忽略 SIGINT,但接收 SIGHUP(可能)

Linux中,一次登录中一个终端一般会配上一个bash,每一个登录只允许一个进程是前台进程,可以允许多个进程是后台进程。

常见信号列表

|------|---------|----------|-------------------------|
| 信号编号 | 信号名 | 默认行为 | 说明 |
| 1 | SIGHUP | 终止 | 终端挂起或控制进程终止 |
| 2 | SIGINT | 终止 | 键盘中断(Ctrl+C) |
| 3 | SIGQUIT | 终止+核心转储 | 键盘退出(Ctrl+\) |
| 9 | SIGKILL | 终止(不可捕获) | 强制杀死进程 |
| 15 | SIGTERM | 终止 | 优雅终止进程(默认 kill 命令发送的信号) |
| 18 | SIGCONT | 继续 | 恢复暂停的进程 |
| 19 | SIGSTOP | 暂停(不可捕获) | 强制暂停进程 |
| 20 | SIGTSTP | 暂停 | 终端挂起(Ctrl+Z) |

(1)每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到,例如其中有定义 #define SIGINT 2

(2)编号34以上的是实时信号,本章只讨论编号34以下的信号,不讨论实时信号。这些信号各自在什么条件下 产生,默认的处理动作是什么,在signal(7)中都有详细说明: man 7 signal

信号的产生方式

终端输入:Ctrl+C(SIGINT)、Ctrl+\`(SIGQUIT)、Ctrl+Z`(SIGTSTP)。

命令发送:kill -信号 PID(如 kill -9 1234)。

系统函数:

kill():向指定进程发送信号。

raise():向自身发送信号。

alarm():定时发送 SIGALRM。

sigqueue():发送带附加数据的信号。

硬件部分

键盘数据如何输入给内核?crtl+c又是如何变成信号的?

背景

CPU和外设(如键盘)的通信分为数据层面和控制层面:

数据层面:CPU不会主动读取外设数据(外设数据需"推"给CPU)。

控制层面:CPU通过硬件针脚(集成在主板上)与外设通信,外设也插在主板上,因此物理上可通过主板电路与CPU间接连接。

键盘数据输入的内核流程

键盘向内核输入数据,核心是"硬件中断 + 驱动处理":

  1. 硬件层:键盘触发中断

当用户按下键盘按键时:

键盘控制器(硬件模块)会检测到按键事件,通过硬件中断线(主板上的电路)向CPU发送硬件中断信号。

CPU的"中断引脚"(物理针脚)接收到该信号,触发CPU暂停当前任务,转去处理中断。

  1. CPU层:中断响应与向量表

中断号:每个硬件中断对应唯一的"中断号"(如键盘中断可能对应中断号1 ,不同硬件/系统可能有差异)。

中断向量表:CPU内部有一张"中断向量表",存储了每个中断号对应的处理程序入口地址。当CPU收到中断号时,会根据中断号查向量表,跳转到对应的中断处理程序(如键盘驱动的处理函数)。

寄存器的作用:CPU寄存器本质是高低电平的组合(物理上通过电路存储0/1状态),可临时保存中断上下文(如程序计数器、状态寄存器),保证中断处理完成后能恢复原任务。

  1. 内核层:驱动处理与数据拷贝

操作系统(内核)的键盘驱动会被调用,执行以下操作:

从键盘控制器的硬件缓冲区中,拷贝按键数据到内核的输入缓冲区。

判断数据类型:如果是普通按键(如字母、数字),则将数据放入"输入队列",供上层应用(如Shell)读取;如果是控制键组合(如Ctrl+C),则进入"信号处理分支"。

crtl+c转换为信号的过程

Ctrl+C 是"终端控制序列",内核会将其识别为终止信号(SIGINT,信号编号通常为2),并发送给前台进程:

  1. 控制序列识别

当用户按下 Ctrl+C 时,键盘驱动会将这组按键(Ctrl + C)解析为控制命令,而非普通字符数据。

  1. 信号生成与发送

内核根据控制序列的语义,确定要发送的信号(如 SIGINT )。

内核通过进程控制块(PCB)找到前台进程(当前终端的前台进程组),将信号( SIGINT )标记到该进程的信号位图中(PCB的 struct signal_struct 或类似结构,每个信号对应一个比特位,1表示该信号待处理)。

示例图:

信号处理方式

默认动作:执行系统预设操作(终止、暂停、忽略等)。

忽略:直接丢弃信号(SIGKILL、SIGSTOP 不可忽略)。

自定义动作:通过 signal()sigaction()注册信号处理器。

示例:

复制代码
#include <signal.h>
#include <stdio.h>
#include <unistd.h>

void handler(int sig) {
    printf("收到信号: %d\n", sig);
}

int main() {
    signal(SIGINT, handler);  // 捕获 Ctrl+C
    while(1) pause();
    return 0;
}
相关推荐
李昊哲小课2 小时前
Python 文件路径操作详细教程
linux·服务器·python
小小小米粒2 小时前
k8s网络通信ip申请如何层级同步进行pod网络层级网络访问请求路由流程
linux·运维·服务器
云边散步2 小时前
godot2D游戏教程系列二(8)
笔记·学习·游戏·游戏开发
Cyber4K2 小时前
【妙招系列】在Linux中测试自己邮箱是否可接收邮件?
linux·运维·python
xhyyvr2 小时前
VR科普学习一体机|开启智慧安全教育新时代
学习·安全·vr
s6516654962 小时前
linux-寄存器
linux
Ama_tor2 小时前
FLASK|完整版学习(ALL)
python·学习·flask
航Hang*2 小时前
第2章:进阶Linux系统——第1节:配置与管理Samba服务器
linux·运维·服务器·笔记·学习
乐观勇敢坚强的老彭2 小时前
本周C++编程课笔记:for循环练习
java·c++·笔记