前言:
资源竞争:
资源竞争(Race Condition)是多线程或多进程环境中的一种常见问题,它发生在多个进程或线程并发访问和修改同一资源(如内存位置、文件、数据库记录等)时,而最终结果依赖于竞争者的执行顺序。如果不同的执行顺序会导致不同的结果,那么就存在资源竞争问题。
资源竞争可能导致多种问题,包括数据损坏、不一致性和系统崩溃。为了解决资源竞争问题,通常需要使用同步机制来确保在任何时候只有一个线程或进程可以访问特定的资源
临界资源:
临界资源(Critical Resource)是指在多线程或多进程环境中,一次只能被一个线程或进程访问的资源。这种资源通常包括硬件设备、文件、数据库、内存区域等,它们不能被多个执行线程或进程同时使用,否则可能会导致数据不一致、资源损坏或其他未定义的行为。
为了安全地使用临界资源,必须采取措施来确保一次只有一个线程或进程可以访问它。
临界区:
临界区(Critical Section)是指程序中访问临界资源的代码区域。由于临界资源一次只能被一个线程或进程安全地访问,因此必须确保在任何时候,只有一个线程可以执行临界区代码。如果多个线程同时执行临界区,可能会导致数据不一致或竞态条件。
为了保护临界区,通常需要使用同步机制,这些同步机制确保了在任何时候,只有一个线程可以进入临界区。
同步与互斥的概念:
同步(Synchronization)和互斥(Mutual Exclusion)是并发编程中的两个核心概念,它们用于控制多个线程或进程对共享资源的访问,以防止数据不一致性和竞态条件。
互斥(Mutual Exclusion):
互斥是指在任何时候,只允许一个线程或进程访问特定的临界资源。这是为了防止多个线程同时修改同一数据,从而导致数据不一致的问题。互斥通常通过锁机制实现,比如互斥锁(mutexes)或信号量(semaphores)。
互斥的目的是确保临界区代码一次只能被一个线程执行。临界区是指访问共享资源的代码段,如果不加以控制,可能会被多个线程同时执行,从而导致不确定的行为。
同步(Synchronization):
同步是指协调多个线程或进程的执行顺序,使得它们按照预定的规则合作完成任务。同步机制允许线程或进程在某些情况下等待或通知其他线程,以便它们能够协调工作。
同步和互斥是相辅相成的。互斥是同步的一种特殊情况,它专注于确保对共享资源的访问是互斥的。而同步则是更广泛的概念,它不仅包括互斥,还包括线程之间的协调和合作。
信号量概述:
信号量(Semaphore)是一种用于多线程环境下同步和互斥控制的机制,它是一个计数器,用于控制对共享资源的访问。信号量可以解决多个线程或进程同时访问同一资源时可能出现的竞争条件问题。
信号量的核心概念是计数器,它表示可用资源的数量。
信号量的类型:
-
二进制信号量:
- 用于实现互斥,计数器只有两个值:0和1。
- 通常用于保护临界资源,确保一次只有一个线程可以访问。
-
计数信号量:
- 用于同步,计数器可以有多个值。
- 可以用来控制对有限资源的访问数量
信号量机制的核心:
信号量在操作系统中通常是由内核维护的,它是一个整数值,用于控制对共享资源的访问。
信号量的操作:
-
初始化(Set Value):
- 在创建信号量时,可以将其初始化为一个具体的值,这个值表示可用资源的数量或信号量的初始状态。
-
增加(Increment):
- 在信号量当前值的基础上加上一个数值,这通常对应于信号量的V操作(释放操作)。当一个资源被释放时,信号量的值会增加,表示现在有更多的资源可用。
-
减少(Decrement):
- 在信号量当前值的基础上减去一个数值,这通常对应于信号量的P操作(等待操作)。当一个资源被占用时,信号量的值会减少,表示现在可用的资源减少了。
-
等待(Wait):
- 如果信号量的值为0,则执行等待操作的进程或线程会被阻塞,直到信号量的值变为正数。这表示进程或线程在等待资源变得可用
在 Linux 系统中查询信号量使用 ipcs -s
1.创建信号量集合
semget()
函数是 Linux 系统中用于创建新的信号量集合或获取现有信号量集合标识符的 System V IPC 函数。这个函数允许进程创建或访问一组相关的信号量。
semget()函数描述:
函数头文件:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
函数原型:
int semget(key_t key, int nsems, int semflg);
函数参数:
key_t key:这是一个键值,用于标识信号量集合。如果key是 IPC_PRIVATE,则创建一个只由调用进程及其子进程访问的信号量集合。否则key通常由ftok()函数生成,用于创建一个可以被其他进程访问的信号量集合。
int nsems:这指定了信号量集合中信号量的数量。
int semflg:这是一组标志和权限位,用于控制信号量集合的创建和访问。可以使用 IPC_CREAT 来创建信号量集合(如果它尚不存在),并且可以使用 IPC_EXCL 与 IPC_CREAT 结合使用,以确保信号量集合是新创建的(如果已经存在,则semget()调用失败)。权限位通常使用八进制数来设置,例如 0644 表示用户可读写,组和其他用户可读
函数返回值:
成功:返回信号量集标识符(一个非负整数)
失败:返回-1,设置errno表示错误原因
错误原因:
EACCES(权限不足)
EEXIST(IPC_CREAT | IPC_EXCL 被设置,但键已存在)
ENOENT(未找到键)
ENOMEM(内存不足)
示例代码:
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdio.h>
int main() {
key_t key;
int semid;
int nsems = 3; // 需要3个信号量
// 生成唯一的键值
key = ftok("data", 'a');
if (key == -1) {
perror("ftok");
exit(EXIT_FAILURE);
}
// 创建信号量集合
semid = semget(key, nsems, 0666 | IPC_CREAT);
if (semid == -1) {
perror("semget");
exit(EXIT_FAILURE);
}
printf("信号量集合的标识符是: %d\n", semid);
// ... 使用信号量集合 ...
return 0;
}
2.控制信号量集合:
semctl()
函数是 System V 信号量的一部分,用于控制信号量集合。它可以用于执行多种操作,包括获取信号量集合的信息、设置信号量的值、删除信号量集合等
semctl()函数描述:
函数头文件:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
函数原型:
int semctl(int semid, int semnum, int cmd, ...);
函数参数:
int semid:信号量集合的标识符,由semget()函数创建时返回。
int semnum:信号量集合中特定信号量的索引号,范围从 0 到 nsems - 1(nsems 是创建信号量集合时指定的数量)。
int cmd:指定要执行的操作。不同的命令需要不同的额外参数。
...:根据 cmd 指定的命令,可能需要额外的参数。
cmd命令:
IPC_STAT:获取信号量集合的状态信息。需要提供指向 struct semid_ds 的指针作为额外参数。
IPC_SET:设置信号量集合的属性。需要提供指向 struct semid_ds 的指针作为额外参数。
IPC_RMID:删除信号量集合。成功删除后,所有信号量都将被销毁。
GETVAL:获取信号量 semnum 的当前值。
SETVAL:设置信号量 semnum 的值为额外参数指定的值。
GETALL:获取信号量集合中所有信号量的值,结果存放在数组中。
SETALL:设置信号量集合中所有信号量的值,值存放在数组中
函数返回值:
成功:返回非负值。对于 IPC_STAT、IPC_SET、GETVAL、GETALL 等命令,返回的是一个值,它取决于具体的命令。
失败:返回-1,并设置 errno 以指示错误原因
示例代码:
a.初始化信号量
int semid;
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
} arg;
...创建信号量集合
// 初始化信号量的值
arg.val = 1; // 设置信号量的初始值为1
if (semctl(semid, 0, SETVAL, arg) == -1) {
perror("semctl");
exit(EXIT_FAILURE);
}
b.删除信号量集合
if (semctl(semid, 0, IPC_RMID, arg) == -1) {
perror("semctl IPC_RMID");
exit(EXIT_FAILURE);
}
semctl() 函数的最后一个参数是一个 union semun 类型的变量,这个联合体用于兼容不同类型的命令和参数。
semctl() 函数的最后一个参数是表示没有额外的参数传递给 semctl()
函数.
3.信号量的操作:
semop()函数描述:
在 Linux 系统中,semop()
函数是用于对 System V 信号量进行操作的函数。它可以对信号量集合中的一个或多个信号量执行原子操作,如 P(等待)操作和 V(信号)操作
函数头文件:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
函数原型:
int semop(int semid, struct sembuf *sops, size_t nsops);
函数参数:
int semid:信号量集合的标识符,由 semget() 函数创建时返回。
struct sembuf *sops:指向 sembuf 结构数组的指针,每个结构指定了对信号量集合中某个信号量的操作。
size_t nsops:sops 数组中的元素数量,即要执行的操作数量。
函数返回值:
成功:返回 0。
失败;返回 -1,并设置 errno 以指示错误原因
E2BIG(sem_op 值超出范围)
EACCES(权限不足)
EINTR(操作被信号中断)
EINVAL(无效的信号量编号或操作)
sem_num
:指定信号量集合中信号量的索引号。sem_op
:指定要执行的操作。可以是 -1(等待,P 操作),0(测试,非阻塞等待),或 +1(信号,V 操作)。sem_flg
:指定操作的标志。通常使用IPC_NOWAIT
使操作非阻塞,或者使用SEM_UNDO
在进程退出时自动释放信号量
示例代码:
struct sembuf sop;
// 等待信号量 0
sop.sem_num = 0;
sop.sem_op = -1; // P 操作
sop.sem_flg = 0; // 阻塞模式
// 执行信号量操作
if (semop(semid, &sop, 1) == -1) {
perror("semop wait");
exit(EXIT_FAILURE);
}
// ... 临界区代码 ...
// 信号量 0 释放
sop.sem_op = 1; // V 操作
if (semop(semid, &sop, 1) == -1) {
perror("semop signal");
exit(EXIT_FAILURE);
}
semtimedop()
函数描述:
semtimedop()
函数是 Linux 系统中用于对 System V 信号量进行带超时的操作的函数。它类似于 semop()
,但它允许您指定一个超时时间,这样在信号量的值在指定的时间内没有变为正值时,操作不会无限期地阻塞。
函数头文件:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
函数原型:
int semtimedop(int semid, struct sembuf *sops, size_t nsops,
const struct timespec *timeout);
函数参数:
int semid:信号量集合的标识符,这个标识符是通过 semget() 函数创建信号量集合时返回的。
struct sembuf *sops:指向 sembuf 结构体数组的指针,该数组包含了要执行的操作。
size_t nsops:sops 数组中结构体的数量,即要执行的操作数。
const struct timespec *timeout:指向 timespec 结构的指针,该结构指定了操作的最大等待时间。如果 timeout 为 NULL,则表示如果没有信号量可用,操作将无限期地等待
函数返回值:
成功:返回 0。
失败:返回 -1,并设置 errno 以指示错误原因
EAGAIN(超时)
E2BIG(请求的操作数超出信号量集合的大小)
EACCES(权限不足)
EINTR(操作被信号中断)
EINVAL(无效的信号量编号或操作)。
结语:
无论你是初学者还是有经验的开发者,我希望我的博客能对你的学习之路有所帮助。如果你觉得这篇文章有用,不妨点击收藏,或者留下你的评论分享你的见解和经验,也欢迎你对我博客的内容提出建议和问题。每一次的点赞、评论、分享和关注都是对我的最大支持,也是对我持续分享和创作的动力