IPC-内存映射、信号量、消息队列
内存映射
常见接口
mmap: 创建一个新映射
munmap :解除映射区域
实例:利用mmap打印文件的指定内容
描述
思路
- 获取文件大小,判断offset和lenth是否合理
- 根据参数offset,length,fd,实现私有文件映射mmap(页对齐)
- 调用write将内存中的映射文件写入stdout
- 解除映射区域
代码
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
// 定义处理出错函数
#define handle_error(msg) \
do { perror(msg); exit(EXIT_FAILURE); } while (0)
int main(int argc, char *argv[]) {
char *addr;
int fd;
struct stat sb;
off_t offset, pa_offset;
size_t length;
ssize_t s;
if (argc < 3 || argc > 4) {
fprintf(stderr, "%s file offset [length]\n", argv[0]);
exit(EXIT_FAILURE);
}
fd = open(argv[1], O_RDONLY); // 只读
if (fd == -1)
handle_error("open");
if (fstat(fd, &sb) == -1) /* To obtain file size */
handle_error("fstat"); /* 获得文件大小也可才用fseek,ftell*/
offset = atoi(argv[2]);
pa_offset = offset & ~(sysconf(_SC_PAGE_SIZE) - 1); // 位运算, 页取整,4k一页
/* offset for mmap() must be page aligned */
if (offset >= sb.st_size) {
fprintf(stderr, "offset is past end of file\n");
exit(EXIT_FAILURE);
}
if (argc == 4) {
length = atoi(argv[3]);
if (offset + length > sb.st_size)
length = sb.st_size - offset;
/* Can't display bytes past end of file */
} else { /* No length arg ==> display to end of file */
length = sb.st_size - offset; // 缺省
}
/* 创建一个私有映射, 对于映射区域内容可读取,参数默认为NULL
* 内核会将fd对应的文件选择一个合适的地址并将虚拟首地址返回
* 给addr, 文件映射的起点是分页大小的倍数,文件映射的长度是
* (offset - pa_offset + lenth)*/
addr = mmap(NULL, length + offset - pa_offset, PROT_READ,
MAP_PRIVATE, fd, pa_offset);
if (addr == MAP_FAILED)
handle_error("mmap");
/* offset - pa_offset : 分页后,偏移量相对页首的相对偏移量 */
/* 调用mmap省略了read函数,减少一次内核空间和用户空间的一次传输
,且内核空间和用户空间共享一个缓存区*/
s = write(STDOUT_FILENO, addr + offset - pa_offset, length);
if (s != length) {
if (s == -1) handle_error("write");
fprintf(stderr, "partial write");
exit(EXIT_FAILURE);
}
// 解除映射区域
munmap(addr, length + offset - pa_offset);
close(fd);
exit(EXIT_SUCCESS);
}
mmap与快速IPC
思考
- 共享内存与内存映射有什么区别和联系?
- mmap为什么适用于大型文件执行重复随机访问?
信号量
常见接口
semget : 创建或打开一个信号量集
semop : 获取或释放共享资源
semctl : 初始化或删除信号量集
示例: 用信号量实现PV操作
描述
洗衣房只有3个洗衣机,但是有5个人的准备使用,每个人都需要用3次,每一次用完都需要释放洗衣机的使用资源。释放资源后,进程需要竞争,抢到继续使用,没抢到则阻塞,直到所有人都使用洗衣机3次。
思路
- 父进程进行创建,初始化信号量:semget, semclt
- 子进程负责利用和释放资源: fork,semop
- 父进程负责收尸与删除信号量:semclt, wait
代码
/*************************************************************************
> File Name: 1.sem.c
> Mail: 1136984246@qq.com
************************************************************************/
#include "head.h"
#define VALUE 3 // 定义资源数
/* caller must define this union*/
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 (Linux-specific) */
};
int create_sem() {
key_t key = ftok(".", 2021); /*创建键key,用于用于标识IPC对象*/
int sem_id;
/*根据key创建或关联一个信号量数目为1的集合,权限是666, P操作需要写权限,V操作需要读权限*/
if ((sem_id = semget(key, 1, IPC_CREAT | 0666)) < 0) {
perror("semget");
return -1;
}
return sem_id; // 与semget的返回值保持一致
}
/*初始化过程只需要一个进程完成,否则会出现竞争*/
int init_sem(int sem_id) {
union semun arg;
arg.val = VALUE; /* 有VALUE个共享资源*/
return semctl(sem_id, 0, SETVAL, arg); // 函数原型中...随着cmd
/*将sem_id指定的第0个信号量初始化为arg.val*/
}
int sem_P(int sem_id) {
struct sembuf sbuff;
sbuff.sem_num = 0; /*集合中的第0个信号量执行操作*/
sbuff.sem_op = -1; /*利用资源,减少信号量*/
sbuff.sem_flg = SEM_UNDO; /*进程终止时撤销该进程的调整总和*/
/**/
if (semop(sem_id, &sbuff, 1) == -1) {
perror("shmop");
return -1;
}
return semctl(sem_id, 0, GETVAL); /*返回P后的信号量值semval*/
}
int sem_V(int sem_id) {
struct sembuf sbuff;
sbuff.sem_num = 0;
sbuff.sem_op = 1; /*释放资源,增加信号量*/
sbuff.sem_flg = SEM_UNDO;
if (semop(sem_id, &sbuff, 1) == -1) {
perror("shmop");
return -1;
}
return semctl(sem_id, 0, GETVAL);
}
int main(int argc, char **argv) {
int sem_id;
if ((sem_id = create_sem()) < 0) {
perror("create_sem()");
exit(1);
}
if (init_sem(sem_id) < 0) {
perror("init_sem");
exit(1);
}
int cnt = 0;
unsigned short semval;
int child_num;
pid_t pid;
for (int i = 1; i <= 5; i++) {
if ((pid = fork()) < 0) {
perror("fork");
exit(1);
}
if (pid == 0) {
child_num = i;
break;
}
}
if (pid == 0) {
while (1) {
semval = sem_P(sem_id);// P:使用资源,信号量减1
// printf("<P> : semval = %d\n", semval);
// 输出信号量的值
cnt++;
printf("<Child[%d]> : cnt = %d\n", child_num, cnt);
sleep(2);
semval = sem_V(sem_id); // V: 释放资源,信号量加1
// printf("<V> : semval = %d\n", semval);
if (cnt == VALUE) break;
}
exit(0);
} else {
for (int i = 0; i < 5; i++) {
wait(NULL);
}
}
// 删除信号量只能由一个进程完成
if (semctl(sem_id, 0, GETVAL) == VALUE) {
if (semctl(sem_id, 0, IPC_RMID) == -1) {
perror("semctl");
exit(1);
}
}
return 0;
}
思考与拓展
哲学家进餐问题和生产者消费者模式
CompareAndSet(CAS)
如何用pv信号量确保共享内存的互斥和交替访问?
消息队列
常见接口
msgget : 创建或打开一个队列
msgclt : 对消息队列进行删除,修改,初始化
msgsnd : 把新消息加入队列尾部
msgrcv : 从队列中取消息
用消息队列实现C/S的IPC
实例:利用消息队列收发信息
描述
思路
- usage函数: -s, -r, -t, -k : getopt, mode
- 创建消息队列
- 发送消息与接收消息
代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
// mtext的大小
struct msgbuf {
long mtype; /*根据mtype的值选择发送和接受的字段*/
char mtext[80]; /*用于存储消息内容,自定义大小,可为零*/
};
// usage
static void usage(char *prog_name, char *msg) {
if (msg != NULL)
fputs(msg, stderr);
fprintf(stderr, "Usage: %s [options]\n", prog_name);
fprintf(stderr, "Options are:\n");
fprintf(stderr, "-s send message using msgsnd()\n");
fprintf(stderr, "-r read message using msgrcv()\n");
fprintf(stderr, "-t message type (default is 1)\n"); // 消息类型
fprintf(stderr, "-k message queue key (default is 1234)\n"); // 如何查看已有的key值?
exit(EXIT_FAILURE);
}
static void send_msg(int qid, int msgtype) { // static:函数的定义域只在本文件
struct msgbuf msg;
time_t t;
msg.mtype = msgtype;
time(&t); // 当前时间
snprintf(msg.mtext, sizeof(msg.mtext), "a message at %s",
ctime(&t));
/*向消息队列中写入信息,当消息队列满时,直接返回EAGAIN,而不是阻塞到队列非满*/
if (msgsnd(qid, (void *) &msg, sizeof(msg.mtext), IPC_NOWAIT) == -1) {
/*msgsnd 放回值是0, 而不是发送的字节数write*/
/*sizeof(msg.mtext) 而不是 sizeof(msg)*/
perror("msgsnd error");
exit(EXIT_FAILURE);
}
printf("sent: %s\n", msg.mtext);
}
static void get_msg(int qid, int msgtype) {
struct msgbuf msg;
/*从消息队列msgtype中删除一条信息并且将内容复制到msg指向的缓存区*/
if (msgrcv(qid, (void *) &msg, sizeof(msg.mtext), msgtype,
MSG_NOERROR | IPC_NOWAIT) == -1) {
/* MSG_NOERROR: mtext超过可用空间进行截取而不是报错*/
/*除了ENOMSG之外的错误*/
if (errno != ENOMSG) {
perror("msgrcv");
exit(EXIT_FAILURE);
}
/*队列中某一个msgtype无消息且调用IPC_NOWAIT报错*/
printf("No message available for msgrcv()\n");
} else
printf("message received: %s\n", msg.mtext);
}
int main(int argc, char *argv[]) {
int qid, opt;
int mode = 0; /* 1 = send, 2 = receive */
int msgtype = 1;
int msgkey = 1234; // ftok
// getopt
while ((opt = getopt(argc, argv, "srt:k:")) != -1) {
switch (opt) {
case 's':
mode = 1;
break;
case 'r':
mode = 2;
break;
case 't':
msgtype = atoi(optarg);
if (msgtype <= 0)
usage(argv[0], "-t option must be greater than 0\n");
break;
break;
case 'k':
msgkey = atoi(optarg);
break;
default:
usage(argv[0], "Unrecognized option\n");
}
}
if (mode == 0)
usage(argv[0], "must use either -s or -r option\n");
/*创建一个新的消息队列并返回标识符,不是文件描述符*/
qid = msgget(msgkey, IPC_CREAT | 0666);
if (qid == -1) {
perror("msgget");
exit(EXIT_FAILURE);
}
if (mode == 2)
get_msg(qid, msgtype);
else
send_msg(qid, msgtype);
exit(EXIT_SUCCESS);
}
思考
- 信号量用key, 消息队列和共享内存段都用key,key和fd的区别是什么?为什么说用消息队列不能用select, epoll等,反而需要两套IO操作?