一、消息队列
基本概念
System V消息队列是Unix系统中一种进程间通信(IPC)机制,它允许进程互相发送和接收数据块(消息)
操作系统可以在内部申请一个消息队列,可以让不同的进程向消息队列中发送数据块,为了让进程拿到其他进程发送的数据块,所以数据块一定会有一个标记标识是谁发送的数据,不同进程可以根据这个标记拿到自己想要的数据,这样进程间就可以通信了。
操作
这些接口与共享内存的接口都十分相似,因为他们都遵循System V标准
1. 获取消息队列: msgget函数
【解释】:
- key是由用户传入,让内核识别消息队列唯一性的标识
- msgflg表示创建消息队列的方式,可传入 IPC_CREATE IPC_EXCL
- 返回值:成功则返回消息队列的msgid,失败返回-1
2.设置消息队列:msgctl函数
(包括获取消息队列的状态、设置消息队列的属性以及删除消息队列)
- msqid:这是要操作的消息队列的标识符,由之前调用
msgget()
成功返回。 - cmd:指定要执行的操作类型,可以是以下几个值之一:
IPC_STAT
:将消息队列的当前状态复制到buf
指向的结构中。IPC_SET
:使用buf
指向的结构中的某些成员设置消息队列的属性(如权限)。IPC_RMID
:删除该消息队列。
- buf:这是一个指向
struct msqid_ds
结构的指针,用于存放或设置消息队列的属性。当执行IPC_STAT
时,该结构会被填充;执行IPC_SET
时,会根据结构中的有效成员来设置队列的属性。
ps.通过指令也可以删除消息队列
//查看当前所有的消息队列信息
ipcs -q
//删除消息队列
ipcrm -q msgid
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <errno.h>
int main() {
int msqid;
struct msqid_ds buf;
// 假设msqid是你已经知道的有效消息队列ID
msqid = /* your valid message queue ID */;
// 获取消息队列状态
if (msgctl(msqid, IPC_STAT, &buf) == -1) {
perror("msgctl IPC_STAT");
exit(EXIT_FAILURE);
}
printf("Message Queue Information:\n");
printf(" msqid: %d\n", buf.msg_perm.__key); // 注意: msg_perm.__key 在一些系统上可能不直接提供消息队列的key
printf(" Mode: %o\n", buf.msg_perm.mode);
printf(" Owner: %d\n", buf.msg_perm.uid);
printf(" Group: %d\n", buf.msg_perm.gid);
printf(" Bytes in queue: %lu\n", (unsigned long)buf.msg_qbytes);
printf(" Number of messages: %ld\n", buf.msg_cbytes / sizeof(long));
return 0;
}
3.向消息队列发送信息及收取信息:msgsnd函数 msgrcv函数
msgsnd
- msqid:消息队列的标识符,由
msgget()
调用返回。 - msgp:指向要发送消息内容的指针。
- msgsz:消息的字节大小。
- msgflg:标志位,通常用于指定消息发送的行为。如果设置了
IPC_NOWAIT
,则调用将立即返回,而不是等待队列有空闲空间。 - 返回值:成功返回0,失败返回-1,并设置errno
使用举例:
在这个例子中,我们定义了一个结构体my_msgbuf_t
来组织消息内容,包含一个类型字段(mtype
)和一个文本内容字段(mtext
)。然后,我们使用msgsnd()
函数将这个结构体实例发送到消息队列中。注意,消息的大小是以字节为单位计算的,但不包括类型字段的大小(通常是一个长整型,即long
)。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <errno.h>
typedef struct my_msgbuf {
long mtype; // 消息类型
char mtext[100]; // 消息内容
} my_msgbuf_t;
int main() {
int msqid; // 假设这是有效的消息队列ID
my_msgbuf_t message;
int retval;
// 初始化消息内容
message.mtype = 1; // 消息类型
strcpy(message.mtext, "Hello, this is a test message.");
// 发送消息
if ((retval = msgsnd(msqid, &message, sizeof(message) - sizeof(long), 0)) == -1) {
perror("msgsnd");
exit(EXIT_FAILURE);
}
printf("Message sent successfully.\n");
return 0;
}
magrcv
- msqid:消息队列的标识符,由
msgget()
调用获得。 - msgp:指向接收消息缓冲区的指针,消息的实际内容将被复制到这里。
- msgsz:指定接收消息的最大字节数(不包括消息类型)。
- msgtyp:指定要接收的消息类型。可以是特定的消息类型或者使用
MSG_ANY
来接收队列中的第一条消息(不论类型)。 - msgflg:控制消息接收的选项,可以是以下标志的组合:
MSG_NOERROR
:如果消息长度超过msgsz
,超出部分将被丢弃,不会产生错误。IPC_NOWAIT
:如果队列中没有符合条件的消息,立即返回,而不是等待。
- 返回值:成功返回接收到的消息的实际字节数(包括消息类型在内的总字节数)。失败返回-1,并设置errno
使用举例:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <errno.h>
typedef struct my_msgbuf {
long mtype; // 消息类型
char mtext[100]; // 消息内容
} my_msgbuf_t;
int main() {
int msqid; // 假设这是有效的消息队列ID
my_msgbuf_t message;
ssize_t bytes_received;
// 接收消息
if ((bytes_received = msgrcv(msqid, &message, sizeof(message.mtext), 1, 0)) == -1) { // 假设接收类型为1的消息
perror("msgrcv");
exit(EXIT_FAILURE);
} else {
printf("Received message: Type = %ld, Text = \"%s\", Bytes received = %zd\n",
message.mtype, message.mtext, bytes_received);
}
return 0;
}
二、信号量
基本概念
- 进程间通信的前提是两个进程看到同一份空间,这种多执行流都可以看到的资源称为共享资源
- 像申请的共享内存,当它正在被一个进程访问时,另一个进程想要访问它就要先等待,这种被互斥保护起来的资源称为临界资源,访问临界资源的代码称为临界区
- 程序员不能对资源进行保护,保护资源的本质就是保护临界区
信号量(Semaphore)是一种用于操作系统中管理共享资源访问和实现进程间同步与互斥的机制。它是一个包含一个整数值的数据结构,该值表示系统中某种资源的可用数量,其本质就是一个计数器。
我们访问临界资源的步骤一般是:申请信号量---->访问临界资源---->释放信号量
例如,我们将共享内存看做是一份资源,及资源的可用数量为1,当一个进程想要访问该共享内存时,先申请信号量,此时S--,当另一个进程想要访问这个共享内存时,信号量为0,没有可用资源,此时就要等待,直到进程访问完成后释放信号量,等待的进程才可以访问共享内存。所以信号量是用来保护临界资源的。
信号量要保护临界资源,那它一定要让多个进程可以看到他,及信号量自己本身就是一个共享资源,为了保护信号量自己的安全,信号量的操作(PV)一定是原子的,及要么做要么不做、要么成功要么失败,不会做一半被打断。
-
P操作(wait/signal_wait/lock):当一个进程想要访问一个受信号量保护的资源时,它会执行P操作。如果信号量的值大于0,P操作会将信号量减1,并允许进程继续执行。如果信号量为0,则进程会被阻塞,直到信号量变为非零值。
-
V操作(signal/signal_release/unlock):当一个进程完成对共享资源的访问后,会执行V操作,将信号量的值加1。如果此时有其他进程因等待该信号量而被阻塞,至少会有一个进程被唤醒。
操作
1.创建信号量 semget
-
key:一个键值,用于标识信号量集。可以使用
ftok()
函数生成一个唯一的键值,或者使用预定义的键值(如IPC_PRIVATE)来创建私有信号量集。 -
nsems:信号量集中信号量的数量。如果是创建新的信号量集,该参数指定了信号量的数量;如果是打开现有信号量集,则该参数应与现有信号量集的大小相匹配,否则可能导致错误。
-
semflg:控制信号量集创建和访问的标志,IPC_CREAT IPC_EXCL
-
返回值:成功时,返回信号量集的标识符(一个非负整数),出错时,返回-1,并设置
errno
全局变量。
代码举例:
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <errno.h>
int main() {
key_t key = ftok("pathname", 'a'); // 使用ftok生成一个键值,'pathname'应该替换为实际的路径名
int semid;
// 尝试获取或创建一个包含3个信号量的信号量集
if ((semid = semget(key, 3, IPC_CREAT | 0666)) == -1) { // 使用0666设置读写权限
perror("semget");
exit(EXIT_FAILURE);
}
printf("Successfully obtained semaphore set with id: %d\n", semid);
// 接下来可以使用semctl(), semop()等函数进一步操作信号量集...
return 0;
}
2.设置信号量 semctl
-
semid:由
semget()
调用返回的信号量集的标识符。 -
semnum:信号量集中的信号量编号,从0开始。指定要操作的特定信号量。
-
cmd:指定要执行的操作类型,可以是以下几种:
SETVAL
:设置信号量的值。GETVAL
:获取信号量的当前值。GETALL
:获取信号量集所有信号量的值。SETALL
:设置信号量集所有信号量的值。IPC_RMID
:删除信号量集。
-
arg(可变参数):传递给命令的具体参数,类型为
union semun
。这个联合体的结构依赖于cmd
的值。例如,当cmd
为SETVAL
时,arg
应包含一个新的整数值。
- 返回值:成功时,返回执行命令的结果,其类型和含义依赖于
cmd,
出错时,返回-1,并设置errno
全局变量。
代码举例:
在这个示例中,我们首先创建或打开一个信号量集,然后使用semctl()
的SETVAL
命令初始化信号量的值,接着使用GETVAL
命令获取并打印该信号量的当前值。
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
union semun {
int val; /* Value for SETVAL */
struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */
unsigned short *array; /* Array for GETALL, SETALL */
struct seminfo *__buf; /* Buffer for IPC_INFO */
};
int main() {
key_t key = ftok("somefile", 'a'); // 使用ftok生成一个键值
int semid;
// 获取或创建信号量集
if ((semid = semget(key, 1, IPC_CREAT | 0666)) == -1) {
perror("semget");
exit(EXIT_FAILURE);
}
// 初始化信号量为1
union semun arg;
arg.val = 1;
if (semctl(semid, 0, SETVAL, arg) == -1) {
perror("semctl SETVAL");
exit(EXIT_FAILURE);
}
printf("Semaphore initialized.\n");
// 获取并打印信号量的值
int semval;
arg.val = 0; // Initialize to avoid undefined behavior
if ((semval = semctl(semid, 0, GETVAL, arg)) == -1) {
perror("semctl GETVAL");
exit(EXIT_FAILURE);
}
printf("Semaphore value: %d\n", semval);
return 0;
}
3.对信号量集进行操作 semop
-
semid:信号量集的标识符,由
semget()
调用获得。 -
sops:指向
sembuf
结构体数组的指针,每个结构体定义了一个对信号量的操作。 -
nsops:
sembuf
结构体数组的长度,即要执行的操作数量。 - 返回值:成功时,返回0,出错时,返回-1,并设置
errno
全局变量。
sembuf结构体:
struct sembuf {
short sem_num; // 要操作的信号量编号,在信号量集中的位置
short sem_op; // 操作类型,正数为V操作(增加),负数为P操作(减少),0为查询但不改变值
short sem_flg; // 操作标志,通常使用SEM_UNDO来自动解除因进程异常终止导致的锁定
};
代码举例:
在这个例子中,程序首先通过semget()
获取或创建一个信号量集。然后,它使用semop()
执行P操作来等待信号量(这可能会阻塞进程直到信号量的值大于0)。模拟了一些临界区操作后,程序执行V操作来释放信号量,允许其他等待该信号量的进程继续执行。注意,这里使用了SEM_UNDO
标志来确保在进程异常终止时,系统能自动回滚信号量的操作,避免死锁。
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#define SEM_KEY 1234 // 用于semget的键值
#define SEM_COUNT 1 // 信号量集中信号量的数量
#define SEM_OP_WAIT -1 // P操作
#define SEM_OP_SIGNAL 1 // V操作
int main() {
key_t key = (key_t)SEM_KEY;
int semid;
// 创建信号量集(如果不存在)
if ((semid = semget(key, SEM_COUNT, IPC_CREAT | 0666)) == -1) {
perror("semget");
exit(EXIT_FAILURE);
}
// 执行P操作(等待信号量)
struct sembuf op_wait = {0, SEM_OP_WAIT, SEM_UNDO}; // 对第0号信号量执行P操作
if (semop(semid, &op_wait, 1) == -1) {
perror("semop wait");
exit(EXIT_FAILURE);
}
printf("Process waiting...\n");
// 模拟一些需要互斥访问的临界区操作
sleep(5); // 示例中简单睡眠5秒代表临界区操作
// 执行V操作(释放信号量)
struct sembuf op_signal = {0, SEM_OP_SIGNAL, SEM_UNDO}; // 对第0号信号量执行V操作
if (semop(semid, &op_signal, 1) == -1) {
perror("semop signal");
exit(EXIT_FAILURE);
}
printf("Process released semaphore.\n");
return 0;
}
4. 指令
//查看所有信号量信息
ipcs -s
//删除信号量
ipcrm -s semid
三、OS如何管理共享内存、消息队列、信号量等资源的
我们这几篇文章所讲的共享内存、消息队列、信号量等都是遵循System V标准的,我们发现他们存在shmid、semid等信息,操作的函数名及参数都是相似的。而描述共享内存、消息队列、信号量的结构体等也都是相似的,重要的是他们的结构体的第一个成员都是struct ipc_perm的结构体
操作系统内部有一个结构体内部存在一个struct kern_ipc_perm的柔性指针数组,指向的是XXX_id_ds结构体中ipc_perm结构体,也就是XXX_id_ds结构体的第一个成员,相当于XXX_id_ds的地址,这样做意味着我们将所有的IPC结构都统一管理了,不论他是共享内存还是消息队列,而数组的下标就是我们shmid msgid semid
在ipc_perm中,有一个mode变量,存放着创建时的类型,所以访问XXX_id_ds结构体中的其他成员也很简单,只要将指针强转成对应类型的指针,然后通过XXX_ipc_perm->的形式访问数据了