二元信号量和计数信号量的区别:
二元信号量和计数信号量在嵌入式系统和多任务环境中都是重要的同步机制,用于控制对共享资源的访问。它们之间的主要区别体现在以下几个方面:
1. 状态表示
- 二元信号量(Binary Semaphore):二元信号量只有两种状态,通常用于表示某个共享资源是否被占用。当资源被占用时,二元信号量的值为0;当资源未被占用时,二元信号量的值为1。这种信号量适用于只允许一个任务或线程访问的共享资源。
- 计数信号量(Counting Semaphore):计数信号量可以有多个状态,用于表示某个共享资源的可用数量。当资源被占用时,计数信号量的值会减少;当资源被释放时,计数信号量的值会增加。计数信号量允许多个任务或线程并发访问共享资源,但数量受到信号量初始值的限制。
2. 应用场景
- 二元信号量:由于其简单性,二元信号量特别适用于那些只能被一个任务或线程独占访问的资源。例如,在嵌入式系统中,用于控制对硬件设备的独占访问。
- 计数信号量:计数信号量更适用于允许多个任务或线程并发访问的资源。例如,在操作系统中,用于控制对共享内存区域、信号量集或其他类型共享资源的访问。
3. 实现复杂度
- 二元信号量:由于只有两种状态,二元信号量的实现相对简单。在硬件层面,可以利用简单的寄存器或位操作来实现。
- 计数信号量:计数信号量需要维护一个计数器,因此实现上相对复杂一些。在软件层面,需要额外的逻辑来处理计数器的增减和线程间的同步。
4. 同步机制
- 两者都是通过信号量的获取(wait/take)和释放(signal/give)操作来实现对共享资源的同步访问。然而,由于计数信号量允许多个任务或线程并发访问,因此在同步机制上需要更复杂的逻辑来确保资源的正确分配和释放。
5. 示例
- 二元信号量:假设有一个打印机资源,在任何时候只能被一个任务使用。此时可以使用二元信号量来控制对打印机的访问。当任务需要打印时,首先尝试获取二元信号量;如果成功获取(信号量值为1),则进行打印操作;打印完成后释放信号量(将信号量值设置为0)。
- 计数信号量:假设有一个包含多个座位的会议室,允许一定数量的任务同时进入。此时可以使用计数信号量来控制对会议室的访问。信号量的初始值可以设置为会议室的座位数。当任务需要进入会议室时,首先尝试获取计数信号量;如果信号量的值大于0,则允许任务进入并减少信号量的值;任务离开会议室时释放信号量(增加信号量的值)。
PV操作对二元信号量和计数信号量的影响
PV操作是信号量机制中的两个基本操作,用于实现对共享资源的同步访问。
P操作(Wait/Semaphore Decrement)
- 功能:P操作主要用于申请资源。当一个进程或线程需要访问某个共享资源时,它会执行P操作来尝试获取信号量。
- 实现:在P操作中,信号量的值会减1。如果减1后的信号量值仍然大于等于0,则表示还有可用资源,进程或线程可以继续执行;如果减1后的信号量值小于0,则表示资源已被全部占用,进程或线程将被阻塞,并放入等待队列中等待资源释放。
V操作(Signal/Semaphore Increment)
- 功能:V操作主要用于释放资源。当一个进程或线程完成了对共享资源的访问后,它会执行V操作来释放信号量。
- 实现:在V操作中,信号量的值会加1。如果加1前的信号量值小于0,则表示有等待该资源的进程或线程被阻塞在等待队列中,此时操作系统会从等待队列中唤醒一个或多个进程或线程,让它们有机会继续执行。
应用于二元信号量和计数信号量
- 二元信号量:在二元信号量中,由于信号量的取值只有0和1,P操作实际上是在检查资源是否可用(值为1表示可用,值为0表示不可用),而V操作则是在释放资源(将值从0变为1),从而允许等待的进程或线程继续执行。
- 计数信号量:在计数信号量中,信号量的取值可以是任意非负整数,表示可用资源的数量。P操作会尝试减少资源数量,如果减少后资源数量仍然大于等于0,则进程或线程可以继续执行;如果小于0,则进程或线程被阻塞。V操作则会增加资源数量,并可能唤醒等待队列中的进程或线程。
生产者消费者模型:
任何并发问题都可以用二元信号量解决;
用二元信号量解决生产者消费者问题:
错误示例:
内核为单核时,PV有效,produce和space加起来等于10;
(多个生产者或多个消费者)多核情况下,p[0]>0未被信号量保护起来;
子程序可以同时进入临界区,当第一个子进程解锁后把p[0]减少1,另一个子进程立刻加锁,又把p[0]减少1;最终导致仓库出现负数;
正确实例:
把PV操作移入外面,先加锁再判断资源是否大于0;n'g
二元信号量实现生产者消费者;
#include <43func.h>
int main()
{
int shmid = shmget(IPC_PRIVATE, 4096, IPC_CREAT | 0600);
ERROR_CHECK(shmid, -1, "shmget");
int *p = shmat(shmid, NULL, 0);
ERROR_CHECK(p, (void *)-1, "shmat");
// p[0]->仓库,p[1]->goods
p[0] = 10;
p[1] = 0;
int semid = semget(1000, 1, IPC_CREAT | 0600);
ERROR_CHECK(semid, -1, "semget");
int semc_val = semctl(semid, 0, SETVAL, 1);
ERROR_CHECK(semc_val, -1, "semctl SETVAL");
semc_val = semctl(semid, 0, GETVAL);
ERROR_CHECK(semc_val, -1, "semctl GETVAL")
printf("semc_val = %d\n", semc_val);
struct sembuf P, V;
P.sem_num = 0;
P.sem_op = -1;
P.sem_flg = SEM_UNDO; // 在进程终止时把减去的资源加回,避免出现死锁
V.sem_num = 0;
V.sem_op = 1;
V.sem_flg = SEM_UNDO;
if (fork() == 0)
{
while (1)
{
semop(semid, &P, 1);
if (p[1] > 0)
{
printf("before consumer space = %d,goods = %d\n", p[0], p[1]);
--p[1];
++p[0];
printf("after consumer space = %d,goods = %d\n", p[0], p[1]);
}
semop(semid, &V, 1);
}
}else if(fork() == 0){
while (1)
{
semop(semid, &P, 1);
if (p[1] > 0)
{
printf("before consumer space = %d,goods = %d\n", p[0], p[1]);
--p[1];
++p[0];
printf("after consumer space = %d,goods = %d\n", p[0], p[1]);
}
semop(semid, &V, 1);
}
}
else
{
while (1)
{
semop(semid, &P, 1);
if (p[0] > 0)
{
printf("before produce space = %d,goods = %d\n", p[0], p[1]);
++p[1];
--p[0];
printf("after produce space = %d,goods = %d\n", p[0], p[1]);
}
semop(semid, &V, 1);
}
wait(NULL);
}
shmdt(p);
shmctl(shmid, IPC_RMID, NULL);
}
死锁:
(1)pipe死锁,若果一个行为需要多种资源,获取资源顺序不对导致死锁;
(2)处于加锁状态的进程异常终止:
SEM_UNDO:避免异常终止的处于加锁状态的进程出现死锁;在进程终止时,把减去的资源加回来;
侠义的消息队列(本机通信):
侠义上讲,消息队列(Message Queue)是进程间通信(IPC,Inter-Process Communication)的一种方式,特指在UNIX/Linux系统中,用于在不同进程之间传递消息的队列型数据结构。消息队列是一种先进先出的数据结构,能够确保消息的顺序性,同时允许消息按照类型进行分类和接收。
消息队列在本机中通信的函数
在UNIX/Linux系统中,消息队列的通信主要依赖于几个关键的系统调用函数,这些函数允许进程创建、发送、接收和控制消息队列。以下是这些函数的基本介绍和使用方法:
msgget() - 创建或打开消息队列
- 功能:打开或创建一个消息队列。
- 原型:
int msgget(key_t key, int msgflg);
- 参数:
key
:消息队列的键值,用于唯一标识一个消息队列。msgflg
:控制消息队列的创建和打开行为的标志位,如IPC_CREAT
表示创建新队列,IPC_EXCL
与IPC_CREAT
结合使用表示如果队列已存在则不创建。- 返回值:成功时返回消息队列的标识符(ID),失败时返回-1。
msgsnd() - 发送消息到消息队列
- 功能:向指定的消息队列发送一条消息。
- 原型:
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
- 参数:
msqid
:消息队列的标识符。msgp
:指向要发送的消息的指针。msgsz
:消息的大小(不包括消息类型字段)。msgflg
:控制消息发送行为的标志位。- 返回值:成功时返回0,失败时返回-1。
msgrcv() - 从消息队列接收消息
- 功能:从指定的消息队列接收一条消息。
- 原型:
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
- 参数:
msqid
:消息队列的标识符。msgp
:指向接收消息的缓冲区的指针。msgsz
:接收缓冲区的大小。msgtyp
:指定接收消息的类型,0表示接收队列中的第一条消息。msgflg
:控制消息接收行为的标志位。- 返回值:成功时返回接收到的消息的实际字节数(不包括消息类型字段),失败时返回-1。
msgctl() - 控制消息队列
- 功能:对消息队列执行各种控制操作,如删除队列、获取或设置队列的属性等。
- 原型:
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
- 参数:
msqid
:消息队列的标识符。cmd
:指定要执行的操作,如IPC_RMID
表示删除队列。buf
:指向msqid_ds
结构的指针,用于获取或设置队列的属性。- 返回值:成功时返回0,失败时返回-1。
广义的的消息队列:网络中间件;网络通信,kcfka;
IPC:只能在本机通信;可以保留消息的边界;
流式消息:管道消息没有边界;TCP
消息队列以数据包的形式发送:UDP
FIFO(先进先出):System V版本
msgget:创建消息队列;
msgsnd:发送消息
#include <43func.h>
typedef struct MsgBuf
{
long mtype;
char mtext[256];
} MyMsg;
int main()
{
int msgid = msgget(1004, IPC_CREAT | 0600);
ERROR_CHECK(msgid, -1, "msgget");
MyMsg msg1 = {1, "hello I am msg1"};
MyMsg msg2 = {2, "haha I am msg2"};
MyMsg msg3 = {3, "nihao I am msg3"};
MyMsg msg4;
msg4.mtype = 4;
strcpy(msg4.mtext, "msg4 msg4 msg4");
int msg_snd1 = msgsnd(msgid, &msg1, strlen(msg1.mtext), 0);
int msg_snd2 = msgsnd(msgid, &msg2, sizeof(MyMsg), 0);
int msg_snd3 = msgsnd(msgid, &msg3, sizeof(MyMsg), 0);
int msg_snd4 = msgsnd(msgid, &msg4, sizeof(MyMsg), 0);
puts("send over!");
// msgctl(msgid, IPC_RMID, NULL);
}
msgflg的作用
msgflg
在msgsnd
函数中主要用于指定发送消息时的行为,包括是否阻塞、是否立即返回等。具体来说,它可以包含以下标志位(这些标志位通常是通过位或操作组合使用的):
IPC_NOWAIT:如果消息队列已满,且设置了此标志位,则
msgsnd
调用将不会阻塞等待空间可用,而是立即返回,并设置errno
为EAGAIN
。这允许程序在无法立即发送消息时继续执行其他操作,而不是无限期地等待。0(或其他未指定IPC_NOWAIT的情况):如果消息队列已满,且未设置IPC_NOWAIT标志位,则
msgsnd
调用将阻塞,直到队列中有足够的空间来容纳待发送的消息为止。这是默认行为,确保消息能够可靠地发送到队列中,但可能会导致发送者在队列满时暂停执行。
msgrcv:接收消息;
#include <43func.h>
typedef struct MsgBuf
{
long mtype;
char mtext[256];
} MyMsg;
int main()
{
int msgid = msgget(1004, IPC_CREAT | 0600);
ERROR_CHECK(msgid, -1, "msgget");
long type;
printf("msg1,2,3,4\n");
scanf("%ld",&type);
MyMsg msg;
memset(&msg,0,sizeof(msg));
int ret = msgrcv(msgid,&msg,sizeof(msg.mtext),type,0);
ERROR_CHECK(ret,-1,"msgrcv");
printf("%ld,msg = %s\n",type,msg.mtext);
}
IPC_NOWAIT:防止等待阻塞;
ssize_t nbytes;
MyMsg msg;
nbytes = msgrcv(msgid, &msg, sizeof(msg.mtext), 0, IPC_NOWAIT);
if (nbytes == -1) {
if (errno == EAGAIN || errno == ENOMSG) {
// 没有消息可接收,且设置了 IPC_NOWAIT
// 处理错误或继续执行其他操作
} else {
// 处理其他可能的错误
perror("msgrcv");
// 可能需要退出或进行其他恢复操作
}
}
// 处理接收到的消息
IPC_NOWAIT
标志通常与msgrcv
(接收消息)函数一起使用。当您调用msgrcv
并传递IPC_NOWAIT
标志时,如果消息队列中没有符合条件的消息可供接收,msgrcv
会立即返回一个错误(通常是EAGAIN
或ENOMSG
),而不是阻塞等待消息的到来。
proc文件系统:
伪文件系统:操作系统的运行状态在文件系统的映射;
process‘;
可以像修改文件一样,修改操作系统的属性;
cat:读
cat /proc/cpuinfo:查看cpu信息;
echo:写;
数字标识进程;
在sys目录下改操作系统设置:
net:网络
kernel:内核(消息队列,共享内存)
fs:文件