2017-2018-1 20155320第十周课下作业-IPC
研究Linux下IPC机制:原理,优缺点,每种机制至少给一个示例,提交研究博客的链接
- 共享内存
- 管道
- FIFO
- 信号
- 消息队列
共享内存
共享内存允许两个或更多进程访问同一块内存。当一个进程改变了这块内存中的内容的的时候,其他进程都会察觉到这个更改。
进程间需要共享的数据放入内核的共享内存区,进程可以把共享内存映射到自己进程的地址空间去,所以进程可以直接读取内存,不需要任何数据的拷贝。
共享内存原理
system V IPC机制下的共享内存本质是一段特殊的内存区域,进程间需要共享的数据被放在该共享内存区域中,所有需要访问该共享区域的进程都要把该共享区域映射到本进程的地址空间(虚拟地址空间)中去。这样一个使用共享内存的进程可以将信息写入该空间,而另一个使用共享内存的进程又可以通过简单的内存读操作获取刚才写入的信息,使得两个不同进程之间进行了一次信息交换,从而实现进程间的通信。共享内存允许一个或多个进程通过同时出现在它们的虚拟地址空间的内存进行通信,而这块虚拟内存的页面被每个共享进程的页表条目所引用,同时并不需要在所有进程的虚拟内存都有相同的地址。进程对象对于共享内存的访问通过key(键)来控制,同时通过key进行访问权限的检查。
相关函数
1、shmget
- 函数说明:得到一个共享内存标识符或创建一个共享内存对象并返回共享内存标识符。
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
- 参数:
key ftok函数返回的I P C键值
size 大于0的整数,新建的共享内存大小,以字节为单位,获取已存在的共享内存块标识符时,该参数为0
shmflg IPC_CREAT||IPC_EXCL 执行成功,保证返回一个新的共享内存标识符,附加参数指定IPC对象存储权限,如|0666
返回值:成功返回共享内存的标识符,出错返回-1,并设置error错误位。
2、shmat
- 函数说明:连接共享内存标识符为shmid的共享内存,连接成功后把共享内存区对象映射到调用进程的地址空间
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void shmaddr, int shmflg);
- 参数:
shmid: 共享内存标识符
shmaddr: 指定共享内存出现在进程内存地址的什么位置,通常指定为NULL,让内核自己选择一个合适的地址位置
shmflg: SHM_RDONLY 为只读模式,其他参数为读写模式
返回值:成功返回附加好的共享内存地址,出错返回-1,并设置error错误位
3、shmdt
-函数说明:与shmat函数相反,是用来断开与共享内存附加点的地址,禁止本进程访问此片共享内存,需要注意的是,该函数并不删除所指定的共享内存区,而是将之前用shmat函数连接好的共享内存区脱离目前的进程
#include <sys/types.h>
#include <sys/shm.h>
void *shmdt(const void* shmaddr);
- 参数:shmddr 连接共享内存的起始地址
- 返回值:成功返回0,出错返回-1,并设置error。
4、shmctl
-
函数说明:控制共享内存块
-
参数:
shmid:共享内存标识符IPC_STAT:得到共享内存的状态,把共享内存的shmid_ds结构赋值到buf所指向的buf中
IPC_SET:改变共享内存的状态,把buf所指向的shmid_ds结构中的uid、gid、mode赋值到共享内存的shmid_ds结构内
IPC_RMID:删除这块共享内存
buf:共享内存管理结构体 -
返回值:成功返回0,出错返回-1,并设置error错误位。
#include <sys/types.h>
#Include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds* buf);
实例(参考Linux-IPC之共享内存的实例)
以下为删除共享内存实例
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <stdlib.h>
#include <sys/shm.h>
#define PROJ_ID 1
int main(int argc,char* argv[])
{
if(argc!=2)
{
printf("error args
");
return -1;
}
key_t skey;
skey=ftok(argv[1],PROJ_ID);
if(-1==skey)
{
perror("ftok");
return -1;
}
printf("the key is %d
",skey);
int shmid;
shmid=shmget(skey,1<<12,0600|IPC_CREAT);
if(-1==shmid)
{
perror("shmget");
return -1;
}
printf("the shmid is %d
",shmid);
int ret;
if(-1==ret)
{
perror("shmctl_rmid");
return -1;
}
return 0;
}
- 运行结果为
管道(PIPE)
管道两端可分别用描述字fd[0]以及fd[1]来描述,需要注意的是,管道的两端是固定了任务的。即一端只能用于读,由描述字fd[0]表示,称其为管道读端;另一端则只能用于写,由描述字fd[1]来表示,称其为管道写端。如果试图从管道写端读取数据,或者向管道读端写入数据都将导致错误发生。一般文件的I/O函数都可以用于管道,如close、read、write等等。
管道特点
- 只支持单向数据流;
- 只能用于具有亲缘关系的进程之间;
- 没有名字;
- 管道的缓冲区是有限的(管道制存在于内存中,在管道创建时,为缓冲区分配一个页面大小);
- 管道所传送的是无格式字节流,这就要求管道的读出方和写入方必须事先约定好数据的格式,比如多少字节算作一个消息(或命令、或记录)等等;
函数
- 管道的创建
#include <unistd.h>
int pipe(int fd[2]);
- 利用popen和pclose函数可以创建和关闭管道
fd为两个文件描述符:fd[0]用来读,fd[1]用来写。
1.父子进程的单向通信方式如下图:
一个进程创建一个管道——>派生一个自身的拷贝——>父进程关闭管道的读出端,子进程的写入端关闭(上图中的虚线)——>父子进程就建立了单向通信了。
- 父子进程的双向通信方式如下图:
实例
#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main()
{
int p[2];
int pid;
char *str = "HelloWorld";
char buf[128];
memset(buf,' ',128);
if(pipe(p) == -1)
{
printf("function pipe() calls failed.");
return -1;
}
if((pid=fork()) == -1) //创建一个子进程
{
printf("function fork() calls failed.
");
return -1;
}
else if(pid == 0) //在子进程中
{
printf("In sub : pid=%d
",getpid());
write(p[1],str,strlen(str)); //向无名管道中写入str
}else { //在父进程中
printf("In father : pid=%d
",getpid());
read(p[0],buf,strlen(str)); //读取无名管道
printf("In father : buf=%s
",buf);
}
}
FIFO(有名管道)
- FIFO不同于管道之处在于它提供一个路径名与之关联,以FIFO的文件形式存在于文件系统中。这样,即使与FIFO的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过FIFO相互通信(能够访问该路径的进程以及FIFO的创建进程之间)
FIFO的应用
(1)在shell中时常会用到管道(作为输入输入的重定向),在这种应用方式下,管道的创建对于用户来说是透明的;
(2)用于具有亲缘关系的进程间通信,用户自己创建管道,并完成读写操作。
相关函数
- 有名管道的创建
#include
#include
int mkfifo(const char * pathname, mode_t mode)
int mknod
(const char * pathname, mode_t mode,dev_t dev)
该函数的第一个参数是一个普通的路径名,也就是创建后FIFO的名字。第二个参数与打开普通文件的open()函数中的mode 参数相同。
实例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
/*在这里设置打开管道文件的mode为只读形式*/
#define FIFOMODE (O_CREAT | O_RDWR | O_NONBLOCK)
#define OPENMODE (O_RDONLY | O_NONBLOCK)
#define FIFO_SERVER "myfifo"
int main(void)
{
char buf[100];
int fd;
int readnum;
/*创建有名管道,设置为可读写,无阻塞,如果不存在则按照指定权限创建*/
if ((mkfifo(FIFO_SERVER, FIFOMODE) < 0) && (errno != EEXIST)) {
printf("cannot create fifoserver/n");
exit(1);
}
printf("Preparing for reading bytes... .../n");
/*打开有名管道,并设置非阻塞标志*/
if ((fd = open(FIFO_SERVER, OPENMODE)) < 0) {
perror("open");
exit(1);
}
while (1) {
/*初始化缓冲区*/
bzero(buf, sizeof(buf));
/*读取管道数据*/
if ((readnum = read(fd, buf, sizeof(buf))) < 0) {
if (errno == EAGAIN) {
printf("no data yet/n");
}
}
/*如果读到数据则打印出来,如果没有数据,则忽略*/
if (readnum != 0) {
buf[readnum] = '/0';
printf("read %s from FIFO_SERVER/n", buf);
}
sleep(1);
}
return 0;
}
信号
信号量是进程/线程同步的一种方式,有时候我们需要保护一段代码,使它每次只能被一个执行进程/线程运行,这种工作就需要一个二进制开关;
有时候需要限制一段代码可以被多少个进程/线程执行,这就需要用到关于计数信号量。信号量开关是二进制信号量的一种逻辑扩展,两者实际调用的函数都是一样。
工作原理
-
由于信号量只能进行两种操作等待和发送信号,即P(sv)和V(sv),他们的行为是这样的:
P(sv):如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行
V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加1.
相关函数
- semget函数
作用是创建一个新信号量集或取得一个已有信号量集
int semget(key_t key, int num_sems, int sem_flags);
第一个参数key是整数值(唯一非零),不相关的进程可以通过它访问一个信号量
第二个参数num_sems指定需要的信号量数目,它的值几乎总是1。
第三个参数sem_flags是一组标志,当想要当信号量不存在时创建一个新的信号量,可以和值IPC_CREAT做按位或操作。设置了IPC_CREAT标志后,即使给出的键是一个已有信号量的键,也不会产生错误。而IPC_CREAT | IPC_EXCL则可以创建一个新的,唯一的信号量,如果信号量已存在,返回一个错误。
- semop函数
它的作用是改变信号量的值,原型为:
int semop(int sem_id, struct sembuf *sem_opa, size_t num_sem_ops);
- semctl函数
该函数用来直接控制信号量信息,它的原型为:
- 前两个参数与前面一个函数中的一样,command通常是下面两个值中的其中一个
- SETVAL:用来把信号量初始化为一个已知的值。p 这个值通过union
- semun中的val成员设置,其作用是在信号量第一次使用前对它进行设置。
- IPC_RMID:用于删除一个已经无需继续使用的信号量标识符。
int semctl(int sem_id, int sem_num, int command, ...);
实例
// 加入信号量操作后的程序
#include "mysem.h"
#include <stdio.h>
#include <unistd.h>
int main()
{
int semid = create_sems(10); // 创建一个包含10个信号量的信号集
init_sems(semid, 0, 1); // 初始化编号为 0 的信号量值为1
pid_t id = fork(); // 创建子进程
if( id < 0)
{
perror("fork");
return -1;
}
else if (0 == id)
{// child
int sem_id = get_sems();
while(1)
{
P(sem_id, 0); // 对该信号量集中的0号信号 做P操作
printf("你");
fflush(stdout);
sleep(1);
printf("好");
printf(":");
fflush(stdout);
sleep(1);
V(sem_id, 0);
}
}
else
{// father
while(1)
{
P(semid,0);
printf("在");
sleep(1);
printf("吗");
printf("?");
fflush(stdout);
V(semid, 0);
}
wait(NULL);
}
destroy_sems(semid);
return 0;
}
消息队列
消息队列就是一个消息的链表。可以把消息看作一个记录,具有特定的格式以及特定的优先级。对消息队列有写权限的进程可以向其中按照一定的规则添加新消息;对消息队列有读权限的进程则可以从消息队列中读走消息
相关函数
1、ftok函数
#include <sys/ipc.h>
#include <sys/types.h>
key_t ftok(const char* path, int id);
- ftok 函数把一个已存在的路径名和一个整数标识转换成一个key_t值,即IPC关键字
- path 参数就是你指定的文件名(已经存在的文件名),一般使用当前目录。当产生键时,只使用id参数的低8位。
id 是子序号, 只使用8bit (1-255) - 返回值:若成功返回键值,若出错返回(key_t)-1
在一般的UNIX实现中,是将文件的索引节点号取出(inode),前面加上子序号的到key_t的返回值
2、msgget函数
#include <sys/msg.h>
#include <sys/ipc.h>
int msgget(key_t key, int msgflag);
- msgget 通常是调用的第一个函数,功能是创建一个新的或已经存在的消息队列。此消息队列与key相对应。
- key 参数 即ftok函数生成的关键字
flag参数 :
IPC_CREAT: 如果IPC不存在,则创建一个IPC资源,否则打开已存在的IPC。
IPC_EXCL :只有在共享内存不存在的时候,新的共享内存才建立,否则就产生错误。
IPC_EXCL与IPC_CREAT一起使用,表示要创建的消息队列已经存在。如果该IPC资源存在,则返回-1。
IPC_EXCL标识本身没有太大的意义,但是和IPC_CREAT标志一起使用可以用来保证所得的对象时新建的,而不是打开已有的对象。
- 返回值 若成功返回消息队列ID,若出错则返回-1
3、msgsnd函数和msgrcv函数
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgsnd(int msqid, const void* msgp, size_t msgsz, int msgflag);
ssize_t msgrcv(int msqid, void *ptr, size_t nbytes, long type, int msgflag);
- msgsnd 将数据放到消息队列中 msgrcv 从消息队列中读取数据
- msqid:消息队列的识别码
- msgp:指向消息缓冲区的指针,用来暂时存储发送和接受的消息。是一个允许用户定义的通用结构
4、msgctl函数
#include <sys/types.g>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
- msgctl 函数 可以直接控制消息队列的行为
- msqid 消息队列id
- cmd :命令
- IPC_STAT 读取消息队列的数据结构msqid_ds, 并将其存储在 buf指定的地址中
- IPC_SET 设置消息队列的数据结构msqid_ds 中的ipc_perm元素的值,这个值取自buf 参数
- IPC_RMID 从内核中移除消息队列。
返回值:如果成功返回0,失败返回-1
实例
消息发送端:send.c
1 /*send.c*/
2 #include <stdio.h>
3 #include <sys/types.h>
4 #include <sys/ipc.h>
5 #include <sys/msg.h>
6 #include <errno.h>
7
8 #define MSGKEY 1024
9
10 struct msgstru
11 {
12 long msgtype;
13 char msgtext[2048];
14 };
15
16 main()
17 {
18 struct msgstru msgs;
19 int msg_type;
20 char str[256];
21 int ret_value;
22 int msqid;
23
24 msqid=msgget(MSGKEY,IPC_EXCL); /*检查消息队列是否存在*/
25 if(msqid < 0){
26 msqid = msgget(MSGKEY,IPC_CREAT|0666);/*创建消息队列*/
27 if(msqid <0){
28 printf("failed to create msq | errno=%d [%s]
",errno,strerror(errno));
29 exit(-1);
30 }
31 }
32
33 while (1){
34 printf("input message type(end:0):");
35 scanf("%d",&msg_type);
36 if (msg_type == 0)
37 break;
38 printf("input message to be sent:");
39 scanf ("%s",str);
40 msgs.msgtype = msg_type;
41 strcpy(msgs.msgtext, str);
42 /* 发送消息队列 */
43 ret_value = msgsnd(msqid,&msgs,sizeof(struct msgstru),IPC_NOWAIT);
44 if ( ret_value < 0 ) {
45 printf("msgsnd() write msg failed,errno=%d[%s]
",errno,strerror(errno));
46 exit(-1);
47 }
48 }
49 msgctl(msqid,IPC_RMID,0); //删除消息队列
50 }
消息接收端 receive.c
1 /*receive.c */
2 #include <stdio.h>
3 #include <sys/types.h>
4 #include <sys/ipc.h>
5 #include <sys/msg.h>
6 #include <errno.h>
7
8 #define MSGKEY 1024
9
10 struct msgstru
11 {
12 long msgtype;
13 char msgtext[2048];
14 };
15
16 /*子进程,监听消息队列*/
17 void childproc(){
18 struct msgstru msgs;
19 int msgid,ret_value;
20 char str[512];
21
22 while(1){
23 msgid = msgget(MSGKEY,IPC_EXCL );/*检查消息队列是否存在 */
24 if(msgid < 0){
25 printf("msq not existed! errno=%d [%s]
",errno,strerror(errno));
26 sleep(2);
27 continue;
28 }
29 /*接收消息队列*/
30 ret_value = msgrcv(msgid,&msgs,sizeof(struct msgstru),0,0);
31 printf("text=[%s] pid=[%d]
",msgs.msgtext,getpid());
32 }
33 return;
34 }
35
36 void main()
37 {
38 int i,cpid;
39
40 /* create 5 child process */
41 for (i=0;i<5;i++){
42 cpid = fork();
43 if (cpid < 0)
44 printf("fork failed
");
45 else if (cpid ==0) /*child process*/
46 childproc();
47 }
48 }
49
ps:消息队列与管道的区别
消息队列与管道的区别以及有名管道相比,具有更大的灵活性,首先,它提供有格式字节流,有利于减少开发人员的工作量;
其次,消息具有类型,在 实际应用中,可作为优先级使用。这两点是管道以及有名管道所不能比的。同样,消息队列可以在几个进程间复用,而不管这几个进程是否具有亲缘关系,这一点与 有名管道很相似;但消息队列是随内核持续的,与有名管道(随进程持续)相比,生命力更强,应用空间更大。