共享内存和消息队列在通信的时候都有一个前提,就是看到同一份资源。所以看到同一份资源为通信提供了前提条件,但是看到同一份资源进行通信仍然存在问题,例如“没有保护机制”,这样会使得和我们想象逻辑接收到的数据不一致!这个由多个执行流(进程)能看到的同一份公共资源就叫做共享资源。
为什么会得到的数据不一致?
被保护的共享资源叫做临界资源,进程中涉及到共享资源的程序段叫做临界区。
保护共享资源的方法就是保护临界区代码,约束临界区代码从而保护临界资源。
怎么通过约束代码来保护临界资源?为什么是约束代码而不是直接保护资源?
如图,引入锁的机制。
假设有多个进程要访问共享资源,那么这些进程中执行访问的代码部分就是临界区。当他们要访问时就要申请锁,谁申请到了锁就进行加锁,之后其他进程的临界区代码会被卡住,只有申请锁成功的临界区代码可以执行。然后改临界区代码执行完成后再进行解锁,其他进程临界区代码再继续进行申请锁,如此循环。
在任何时候,只允许一个执行流(进程)访问资源,这种保护机制就叫互斥。
还有一种保护机制叫做同步。
mutex
)来实现。当一个线程需要访问临界资源时,它会请求锁,获取锁后,其他线程必须等待,直到这个线程释放锁。例子:
lock(); // 获取锁
counter++; // 临界区 后文信号量中会指出,计数器
unlock(); // 释放锁
例子:
Semaphore s = 1; // 初始化信号量
// 线程A执行任务
wait(s); // 等待信号量
taskA(); // 执行任务A
signal(s); // 释放信号量
// 线程B执行任务
wait(s); // 等待信号量
taskB(); // 执行任务B
signal(s); // 释放信号量
在很多情况下,互斥和同步是协同工作的。比如,在一个多线程程序中,互斥保证线程对共享资源的独占访问,而同步保证线程之间按正确的顺序执行。
mutex
)。所以对于临界资源也叫做互斥资源。
所以说,所谓的对共享资源进行保护,本质上就是对访问共享资源的代码进行保护。
还有一个细节问题,锁本身也是共享的,要被进程竞争申请,在所有进程都在申请锁的时候如何保护锁的安全?
申请锁的时候必须是原子的,保持原子性!
也就是说当一个进程开始申请锁的时候其他进程就不能申请了,一旦开始申请,就已经锁定。
信号量本质上就是计数器!
信号量作为计数器,用来表明临界资源中,资源的数量还剩多少。
与其他system V通信方式不同,信号量的机制是将共享资源初始化成若干个子资源。
可以将子资源假设为电影院的位置,电影院在售卖电影票的时候最怕的两个问题就是:
所以需要通过一系列机制来约定规则,从而使每个位置对应的票独立。
这个机制也就是信号量的机制。
想要访问临界资源,必须先买票预定位置。信号量描述临界资源中资源数量的多少。所有进程如果想访问临界资源中的一小块,就必须先申请信号量。
所以,进程访问资源前,先申请信号量,本质是:对资源的预定机制!
这样就可以保证并发访问不会出现问题!
资源给你了,等你随时访问,没有人再跟你竞争。
struct sem
{
// lock
// int count; 计数
// task_struct *waitqueue;
}
sem--
,P操作sem++
,V操作计数器使用PV操作来完成资源的预定机制。
当count不为0,说明还有资源,可以进行申请,为0则说明没有资源了,阻塞挂起。
当信号量只有1或者0两种状态的时候,叫做二元信号量。也就是使用一整块资源,也就是互斥!区分于上述所提的多元信号量。
每个进程都会先看到同一个信号量,从信息量的角度看,通信不仅仅是数据传输,还包括进程间的状态传递(比如通过信号量),进行消息的通知。UNIX System V 提供的信号量机制(P和V操作)是一种优雅的解决方案,可以实现进程间的同步和互斥,从而“移动”相同的信息量(状态信息)。
当一个进程退出的时候,会进行V操作,计数器++,传递状态使得等待队列的进程进入执行。
信号量的操作主要通过以下几个系统调用完成:
int semget(key_t key, int nsems, int semflg);
key_t key
:信号量的键值(key),用于唯一标识一个信号量集合。int nsems
:信号量集合中的信号量数量。int semflg
:标志位,常见的值包括: IPC_CREAT
:如果不存在,则创建新信号量。IPC_EXCL
:与 IPC_CREAT
一起使用,若信号量已存在,则返回错误。-1
并设置 errno
。int semop(int semid, struct sembuf *sops, size_t nsops);
int semid
:信号量集合的 ID。struct sembuf *sops
:信号量操作数组。**size_t nsops**
:操作数组中的元素个数。0
,失败返回 -1
并设置 errno
。struct sembuf
** 结构体**
struct sembuf {
unsigned short sem_num; /* 信号量编号 */
short sem_op; /* 操作值(+1 释放(V操作),-1 请求(P操作),0 等待) */
short sem_flg; /* 操作标志,例如 IPC_NOWAIT */
};
sem_op
取值: int semctl(int semid, int semnum, int cmd, ...);
int semid
:信号量集合 ID。int semnum
:要操作的信号量编号。int cmd
:控制命令。...
可选参数,根据 cmd
决定是否传递 union semun
结构。cmd
命令: SETVAL
:设置单个信号量的值。IPC_RMID
:删除信号量集合。GETVAL
:获取某个信号量的值。IPC_STAT
:获取信号量信息。union semun
** 结构体**
union semun {
int val; /* SETVAL 时使用 */
struct semid_ds *buf; /* IPC_STAT/IPC_SET 时使用 */
unsigned short *array; /* GETALL/SETALL 时使用 */
};
int semid = semget(ftok("/tmp", 'A'), 1, IPC_CREAT | 0666);
信号量的初始值是在创建之后再调用smctl
进行初始化。
union semun arg;
arg.val = 1; // 设置信号量初值为 1
semctl(semid, 0, SETVAL, arg);
struct sembuf p = {0, -1, SEM_UNDO};
semop(semid, &p, 1);
// 临界区代码
struct sembuf v = {0, 1, SEM_UNDO};
semop(semid, &v, 1);
semctl(semid, 0, IPC_RMID);
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <unistd.h>
int main() {
key_t key = ftok("/tmp", 'A'); // 生成 key
int semid = semget(key, 1, IPC_CREAT | 0666); // 创建信号量
union semun arg;
arg.val = 0; // 初始化为 0,表示资源不可用
semctl(semid, 0, SETVAL, arg);
pid_t pid = fork();
if (pid == 0) {
// 子进程
printf("子进程等待信号量...\n");
struct sembuf p = {0, -1, 0}; // P 操作
semop(semid, &p, 1);
printf("子进程获得信号量,继续执行\n");
exit(0);
} else {
sleep(2);
printf("父进程释放信号量...\n");
struct sembuf v = {0, 1, 0}; // V 操作
semop(semid, &v, 1);
wait(NULL);
semctl(semid, 0, IPC_RMID); // 删除信号量
}
return 0;
}
运行结果:
子进程等待信号量...
(2秒后)
父进程释放信号量...
子进程获得信号量,继续执行
**struct semid_ds**
。这个结构体包含了关于一个特定信号量集合的所有元数据信息。**struct semid_ds**
:#include <sys/ipc.h> // 需要包含它以定义 struct ipc_perm
#include <time.h> // 需要包含它以定义 time_t
struct semid_ds {
struct ipc_perm sem_perm; /* Ownership and permissions - 包含所有权和权限信息 */
time_t sem_otime; /* Last semop time - 最后一次调用 semop() 的时间 */
time_t sem_ctime; /* Last change time - 最后一次改变该结构体信息的时间(例如通过 semctl 的 IPC_SET 或 IPC_RMID) */
unsigned long sem_nsems; /* No. of semaphores in set - 此信号量集合中信号量的数量 */
/* 以下可能是 Linux 特有的或内部使用的字段,不一定在所有 POSIX 系统中都存在 */
// unsigned long __unused1;
// unsigned long __unused2;
};
struct ipc_perm sem_perm
: 这是一个嵌套的结构体,包含了与IPC(进程间通信)对象相关的通用权限和所有权信息。time_t sem_otime
: 最后一次执行 semop
操作(改变信号量值)的时间。time_t sem_ctime
: 最后一次改变该结构体信息(例如权限变更)的时间。unsigned long sem_nsems
: 这个信号量集合中包含的信号量(semaphores)的数量。这对应ipcs -s
输出中的nsems
列。struct ipc_perm
**): **#include <sys/types.h> // 需要包含它以定义 key_t, uid_t, gid_t
struct ipc_perm {
key_t __key; /* Key supplied to semget(2) or other IPC get functions - 创建或获取 IPC 对象时提供的键 */
uid_t uid; /* Effective UID of owner - 所有者的有效用户 ID */
gid_t gid; /* Effective GID of owner - 所有者的有效组 ID */
uid_t cuid; /* Effective UID of creator - 创建者的有效用户 ID */
gid_t cgid; /* Effective GID of creator - 创建者的有效组 ID */
unsigned short mode; /* Permissions - 访问权限位 (e.g., 0666) */
unsigned short __seq; /* Sequence number - 序列号,用于内部跟踪 */
/* 以下可能是内部使用的或用于填充的字段 */
// unsigned long __unused1;
// unsigned long __unused2;
};
struct semid_ds
内部的 sem_perm
成员(类型为 struct ipc_perm
)存储了更具体的权限和标识信息 **key_t key**
: 创建或获取该信号量集合时使用的键(key)。这是用户空间用来标识和访问IPC资源(如信号量、共享内存、消息队列)的一种方式。它对应ipcs -s
输出中的key
列。uid_t uid
, gid_t gid
: 信号量集合的所有者的有效用户ID和组ID。uid_t cuid
, gid_t cgid
: 信号量集合的创建者的有效用户ID和组ID。unsigned short mode
: 访问权限位(例如读、写权限)。这对应ipcs -s
输出中的perms
列。unsigned short seq
: 序列号,用于区分被删除后又重新创建的同key
的IPC对象。semid_ds
描述符。semid
(Semaphore ID),对应ipcs -s
输出的semid
列。当用户通过semget
等系统调用使用key
创建或获取信号量集合时,内核会分配或查找对应的semid_ds
结构,并返回其semid
给用户进程。后续的操作(如semop
、semctl
)通常使用这个semid
来指定要操作的信号量集合。semctl
等): int semctl(int semid, int semnum, int cmd, ...);
函数。这是一个用于控制信号量的系统调用。semid
来指定目标信号量集合。cmd
参数指定要执行的操作,例如IPC_STAT
就是用来获取信号量集合的状态信息,内核会把对应的的semid_ds
结构体的内容拷贝给用户提供的缓冲区(可变参数传入的自己创建的semid_ds
)。其他cmd
可以用来设置权限、获取/设置信号量值等。ipcrm -s <semid>
命令则用于从系统中删除指定的信号量集合。ipcs -s <semid>
查询总结:
操作系统通过定义struct semid_ds
数据结构来详细“描述”每一个信号量集合(包含权限、所有者、信号量数量、操作时间以及唯一标识key
等信息)。然后,操作系统在内核中将这些semid_ds
结构组织起来(通常使用semid
作为内部索引),形成一个所有信号量集合的清单。通过semget
, semop
, semctl
, ipcrm
等系统调用,用户进程可以基于key
或semid
来创建、操作和销毁这些信号量集合,而内核则利用这些结构体信息来执行相应的管理和控制。