(1)进程间通信的原理
尽管进程空间是各自独立的,相互之间没有任何可以共享的空间,但是至少还有一样东西是所有
进程所共享的,那就是OS,因为甭管运行有多少个进程,但是它们共用OS只有一个。
既然大家共用的是同一个OS,那么显然,所有的进程可以通过大家都共享第三方OS来实现数据的
转发。
因此进程间通信的原理就是,OS作为所有进程共享的第三方,会提供相关的机制,以实现进程间数
据的转发,达到数据共享的目的。
(2)广义上的进程间通信
其实广义上来说,任何一种能够实现进程间数据交换的方式,都可以被称为进程间通信,比如
A进程——————文件———————B进程
A进程—————数据库——————B进程
不过一般来说,这种广义的进程间通信,并不被算作真正的“进程间通信”。
只有OS所提供的专门的通信机制,才能算作是真正的“进程间通信”,我们本章所讲的就是狭义上
的真正的“进程间通信”。
(3)Linux提供的“进程通信”方式有哪些
Linux的父亲是Unix,所以Linux的进程间通信,其实都是继承于Unix。
不管继承自谁,Linux所提供的进程间通信机制到底有哪些呢?
1)信号
上一章讲的信号其实也是进程间通信的一种,只不过信号是非精确通信,而本章讲的
IPC是精确通信。所谓精确通信,就是能告诉你详细信息,而信号这种非精确通信,只能
通知某件事情发生了,但是无法告诉详细信息。
2)本章的进程间通信
(a)管道
· 无名管道
· 有名管道
OS在进程之间建立一个“管道”,通过这个管道来实现进程间数据的交换。
(b)system V IPC
· 消息队列:通过消息队列来通信
· 共享内存:通过共享内存来通信
· 信号量:借助通信来实现资源的保护(一种加锁机制)
3)域套接字
讲网络编程时再介绍。
具体来说就是,内核会开辟一个“管道”,通信的进程通过共享这个管道,从而实现通信。
(1)到底什么是管道
内核的代码也是运行在物理内存上的,内核创建一个“管道”,其实就是在内核自己所在的物理
内存空间中开辟出一段缓存空间,比如char buf[1024];
(2)如何操作无名管道
以文件的方式来读写管道,以文件方式来操作时
1)有读写用的文件描述符
2)读写时会用write、read等文件Io函数。
(3)为什么叫无名管道
既然可以通过“文件描述符”来操作管道,那么它就是一个文件(管道文件),但是无名管道
文件比较特殊,它没有文件名,正是因为没有文件名,所有被称为无名管道。
没有文件名,我们怎么操作这个文件呢?
后面再讲这个问题。
#include
int pipe(int pipefd[2]);
(1)功能
创建一个用于亲缘进程(父子进程)之间通信的无名管道(缓存),并将管道与两个读写文件描述符
关联起来。
无名管道只能用于亲缘进程之间通信,为什么只能用于亲缘进程之间通信呢?
后面再详细介绍。
(2)参数:缓存地址,缓存用于存放读写管道的文件描述符。
从这个参数的样子可以看出,这个缓存就是一个拥有两个元素的int型数组。
1)元素[0]:里面放的是读管道的读文件描述符
2)元素[1]:里面放的是写管道的写文件描述符。
特别需要注意的是,这里的读和写文件描述符,是两个不同的文件描述符。
从这里大家也可以看出,并不是所有的文件描述符,都是通过open函数打开文件得到的。
这里无名管道的读、写文件描述符,就是直接在创建管道时得到的,与open没有任何关系。
而且这里也根本没办法使用open函数,因为open函数需要文件路径名,无名管道连文件名
都没有,所以说根本就没办法使用open来打开文件,返回文件描述符。
(3)返回值:成功返回0,失败则返回-1,并且errno被设置。
(1)无名管道只能用于亲缘进程之间通信,为什么?
由于没有文件名,因此进程没办法使用open打开管道文件,从而得到文件描述符,所以只有一种办法,
那就是父进程先调用pipe创建出管道,并得到读写管道的文件描述符。
然后再fork出子进程,让子进程通过继承父进程打开的文件描述符,父子进程就能操作同一个管道,
从而实现通信。
对子进程继承父进程属性这一点不清楚的同学,说明“进程控制”这一章你没有学好,你需要回去
复习一下。
通过前面的描述,我们自然就能理解,为什么无名管道只能用于亲缘进程之间通信了。
什么样的进程之间,我们可以称为亲缘进程呢?
只要是存在继承关系的进程就是亲缘进程,继承关系分为两种。
(1)直接继承关系
父进程————>子进程
(2)间接继承关系
父进程————>子进程————>子进程————>...
(2)读管道时,如果没有数据的话,读操作会休眠(阻塞)
实现步骤:
(a)父进程在fork之前先调用pipe创建无名管道,并获取读、写文件描述符
(b)fork创建出子进程,子进程继承无名管道读、写文件描述符
(c)父子进程使用各自管道的读写文件描述符进行读写操作,即可实现通信
代码演示:
#include
#include
#include
#include
#include
void print_err(char *estr)
{
perror(estr);
exit(-1);
}
int main(void)
{
int ret = 0;
//[0]:读文件描述符
//[1]:写文件描述符
int pipefd[2] = {0};//用于存放管道的读写文件描述符
ret = pipe(pipefd);
if(ret == -1) print_err("pipe fail");
ret = fork();
if(ret > 0)
{
close(pipefd[0]);
while(1)
{
write(pipefd[1], "hello", 5);
sleep(1);
}
}
else if(ret == 0)
{
close(pipefd[1]);
while(1)
{
char buf[30] = {0};
bzero(buf, sizeof(buf));
read(pipefd[0], buf, sizeof(buf));
printf("child, recv data:%s\n", buf);
}
}
return 0;
}
为了避免干扰,通常会把没有使用的文件描述关闭。
SIGPIPE信号:
(a)我们讲信号时介绍过这个信号,这里再说一说这个信号,为什么讲这个信号?
· 与管道有关
· 回顾信号的内容,进行知识的综合运用
(b)什么时候会产生在这个信号?
写管道时,如果管道的读端被close了话,向管道“写”数据的进程会被内核发送一个
SIGPIPE信号,发这个信号的目的就是想通知你,管道所有的“读”都被关闭了。
这就好比别人把水管的出口(读)给堵住了,结果你还一直往里面灌水(写),别人肯定
会警告你,因为你这样可能会对水管造成损害,道理其实是类似的。
由于这个信号的默认动作是终止,所以收到这个信号的进程会被终止,如果你不想被终止的
话,你可以忽略、捕获、或者屏蔽这个信号。
只有当管道所有的读端都被关闭时,才会产生这个信号,只有还有一个读端开着,就不会产生。
1)单个无名管道无法实现双向通信,为什么?
因为使用单个无名管道来实现双向通信时,自己发送给对方的数据,就被自己给抢读到。
2)如何实现无名管来实现双向通信
使用两个无名管道,每个管道负责一个方向的通信。
代码演示:
#include
#include
#include
#include
#include
void print_err(char *estr)
{
perror(estr);
exit(-1);
}
int main(void)
{
int ret = 0;
//[0]:读文件描述符
//[1]:写文件描述符
int pipefd1[2] = {0};//用于存放管道的读写文件描述符
int pipefd2[2] = {0};//用于存放管道的读写文件描述符
ret = pipe(pipefd1);
if(ret == -1) print_err("pipe fail");
ret = pipe(pipefd2);
if(ret == -1) print_err("pipe fail");
ret = fork();
if(ret > 0)
{
close(pipefd1[0]);
close(pipefd2[1]);
char buf[30] = {0};
while(1)
{
write(pipefd1[1], "hello", 5);
sleep(1);
bzero(buf, sizeof(buf));
read(pipefd2[0], buf, sizeof(buf));
printf("parent, recv data:%s\n", buf);
}
}
else if(ret == 0)
{
close(pipefd1[1]);
close(pipefd2[0]);
char buf[30] = {0};
while(1)
{
sleep(1);
write(pipefd2[1], "world", 5);
bzero(buf, sizeof(buf));
read(pipefd1[0], buf, sizeof(buf));
printf("child, recv data:%s\n", buf);
}
}
return 0;
}
(1)无法用于非亲缘进程之间
因为非亲缘进程之间没办法继承管道的文件描述符。
(2)无法实现多进程之间的网状通信
如果非要使用无名管道实现多进程之间的网状通信的话,文件描述符的继承关系将非常的复杂。
所以无名管道基本只适合两个进程间的通信。
如果通信的进程只有两个,而且还是亲缘进程时,那么可以使用无名管道来通信。
比如:
1)直接继承父子进程之间的通信
父进程 ————————————————> 子进程
| |
|—————————无名管道————————|
2)间接继承关系的两进程之间的通信
父进程 ——————> 子进程 ——————> 子进程 ———————> 子进程
| |
|————————————————————无名管道———————————————————|
无名管道因为没有文件名,被称为了无名管道,同样的道理,有名管道之所以叫“有名管道”,是因为
它有文件名。
也就是说当我们调用相应的API创建好“有名管道”后,会在相应的路径下面看到一个叫某某名字的
“有名管道文件”。
不管是有名管道,还是无名管道,它们的本质其实都是一样的,它们都是内核所开辟的一段缓存空间。
进程间通过管道通信时,本质上就是通过共享操作这段缓存来实现,只不过操作这段缓存的方式,是以
读写文件的形式来操作的。
1)能够用于非亲缘进程之间的通信
因为有文件名,所以进程可以直接调用open函数打开文件,从而得到文件描述符,不需要像无名管道
一样,必须在通过继承的方式才能获取到文件描述符。
所以任何两个进程之间,如果想要通过“有名管道”来通信的话,不管它们是亲缘的还是非亲缘的,只要
调用open函数打开同一个“有名管道”文件,然后对同一个“有名管道文件”进行读写操作,即可实现通信。
A进程 —————————> 有名管道 ————————> B进程
总之,不管是亲缘进程还是非亲缘进程,都可以使用有名管道来通信。
2)读管道时,如果管道没有数据的话,读操作同样会阻塞(休眠)
3)当进程写一个所有读端都被关闭了的管道时,进程会被内核返回SIGPIPE信号
如果不想被该信号终止的话,我们需要忽略、捕获、屏蔽该信号。
不过一般情况下,不需要对这个信号进行处理,除非你有必须要处理的理由。
(1)进程调用mkfifo创建有名管道
(2)open打开有名管道
(3)read/write读写管道进行通信
对于通信的两个进程来说,创建管道时,只需要一个人创建,另一个直接使用即可。
为了保证管道一定被创建,最好是两个进程都包含创建管道的代码,谁先运行就谁先创建,后运行的
发现管道已经创建好了,那就直接open打开使用。
#include
#include
int mkfifo(const char *pathname, mode_t mode);
(1)功能
创建有名管道文件,创建好后便可使用open打开。
如果是创建普通文件的话,我们可以使用open的O_CREAT选项来创建,比如:
open("./file", O_RDWR|O_CREAT, 0664);
但是对于“有名管道”这种特殊文件,这里只能使用mkfifo函数来创建。
(2)参数
1)pathname:被创建管道文件的文件路径名。
2)mode:指定被创建时原始权限,一般为0664(110110100),必须包含读写权限。
使用open函数创建普通文件时,指定原始权限是一样的。
open("./file", O_RDWR|O_CREAT, 0664);
不过我们学习第2章时讲过,创建新文件时,文件被创建时的真实权限=mode &
(~umask)umask是文件权限掩码,一般默认为002或者022,对umask不清楚的同学
,请会看第2章。
mkfifo(“./fifo”, 0664);
(3)返回值:成功返回0,失败则返回-1,并且errno被设置。
mkfifo1.c代码如下:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define FIFONAME1 "./fifo1"
#define FIFONAME2 "./fifo2"
void print_err(char *estr)
{
perror(estr);
exit(-1);
}
int creat_open_fifo(char *fifoname, int open_mode)
{
int ret = -1;
int fd = -1;
ret = mkfifo(fifoname, 0664);
//如果mkfifo函数出错了,但是这个错误是EEXIST,不报这个错误(忽略错误)
if(ret == -1 && errno!=EEXIST) print_err("mkfifo fail");
fd = open(fifoname, open_mode);
if(fd == -1) print_err("open fail");
return fd;
}
void signal_fun(int signo)
{
//unlink();
remove(FIFONAME1);
exit(-1);
}
int main(void)
{
char buf[100] = {0};
int ret = -1;
int fd1 = -1;
fd1 = creat_open_fifo(FIFONAME1, O_WRONLY);
signal(SIGINT, signal_fun);
while(1)
{
bzero(buf, sizeof(buf));
scanf("%s", buf);
write(fd1, buf, sizeof(buf));
}
return 0;
}
mkfifo2.c代码如下:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define FIFONAME1 "./fifo1"
#define FIFONAME2 "./fifo2"
void print_err(char *estr)
{
perror(estr);
exit(-1);
}
int creat_open_fifo(char *fifoname, int open_mode)
{
int ret = -1;
int fd = -1;
ret = mkfifo(fifoname, 0664);
//如果mkfifo函数出错了,但是这个错误是EEXIST,不报这个错误(忽略错误)
if(ret == -1 && errno!=EEXIST) print_err("mkfifo fail");
fd = open(fifoname, open_mode);
if(fd == -1) print_err("open fail");
return fd;
}
void signal_fun(int signo)
{
//unlink();
remove(FIFONAME1);
exit(-1);
}
int main(void)
{
char buf[100] = {0};
int ret = -1;
int fd1 = -1;
fd1 = creat_open_fifo(FIFONAME1, O_RDONLY);
signal(SIGINT, signal_fun);
while(1)
{
bzero(buf, sizeof(buf));
read(fd1, buf, sizeof(buf));
printf("%s\n", buf);
}
return 0;
}
同样的,使用一个“有名管道”是无法实现双向通信的,因为也涉及到抢数据的问题。
所以双向通信时需要两个管道。
(1)实现网状通信
面对众多进程网状通信,有名管道依然实现起来很吃力,所以基本也只适合于两个进程之间
的通信。你自己可以尝试下,看看能不能使用有名管道来实现多进程的网状通信,在实现过程
中,你自己就会发现,实现起来很困难。
(2)什么时候合适使用有名管道
当两个进程需要通信时,不管是亲缘的还是非亲缘的,我们都可以使用有名管道来通信。
至于亲缘进程,你也可以选择前面讲的无名管道来通信。
(1)回顾有名管道双向通信
在使用有名管道实现双向通信时,由于读管道是阻塞读的,为了不让“读操作”阻塞“写操作”,使用了
父子进程来多线操作,
1)父进程这条线:读管道1
2)子进程这条线:写管道2
实际上我们后面学习了线程以后,凡是涉及到多线操作的,基本都使用多线程来实现,比如
1)主线程:读管道1
2)次线程:写管道2
(2)对比多进程和多线程各自使用的场合
我们前面讲了进程,虽然线程还没有讲,但是大家大致也能理解线程是一个什么样的东西,事实上
线程和进程都是并发运行的,但是线程和进程各自的使用的场合有所不同。
1)线程
凡是涉及多线时,我们使用线程来并发实现,比如我们讲的“有名管道”双向通信的例子,这
个多线操作理论上就应该使用多线程来实现,只不过我们还没讲多线程而已。
因为多线使用线程更省计算机cpu和内存的开销。
也就是说创建出并发运行次线程的目的,是为了多线操作。
2)进程
一般情况下,我们的程序并不会涉及到多进程,当涉及多线操作时,我们会直接使用线程来并发
实现。
(a)那什么时候我们的程序才会涉及到多进程呢?
一个简单的判断标准就是,如果你发现你的程序必须要去运行一个新程序时,此时必须
涉及到多进程,因为此时如果你不创建一个子进程,你是没有办法来执行新程序的。
新创建的子进程和父进程肯定是并发运行的,只不过这里并发运行的主要目的并不是
为了多线操作,而是为了单独的去执行新程序,执行新程序时,我们只能使用多进程来操作
,你是没有办法使用多线程来操作的,因为线程是不可能去执行一个新程序的。
(b)一般开发的应用程序不涉及执行新程序
除非你开发的是比较大型框架,或者拥有众多功能套件的大型应用软件,在你的程序中必
须开辟新的子进程去执行具有独立功能的新程序,否则们自己写的程序一般都是单进程,根本
不涉及开辟一个并发运行的子进程,然后在子进程里面去执行新程序。
也就是说创建一个并发执行的子进程的目的,是为了执行一个全新的程序。
(1)什么是System V IPC
前面讲的无名管道和有名管道,都是UNIX系统早期提供的比较原始的一种进程间通信(IPC)方式,
早到Unix系统设计之初就有了。
后来Unix系统升级到第5版本时,又提供了三种新的IPC通信方式,分别是:
· 消息队列
· 信号量
· 共享内存
System V就是系统第5版本的意思,后来的Linux也继承了unix的这三个通信方式,Unix是非常
早期的而且非常优秀OS,所以其它os也借鉴了这三种的System V IPC。
(2)System V IPC的特点
1)管道(原始IPC)
管道的本质就是一段缓存,不过Linux OS内核是以文件的形式来管理的,所以我们操作管
道时,不管是无名管道,还是有名管道,我们都是使用文件描述符以文件的形式来操作的。
所以我们操作管道时,除了pipe和mkfifo这两个函数外,其它的像read、write、open
都是我们第1章所学的文件io函数,所以大家在学习管道时,会觉得比较容易。
2)System V IPC
System V IPC与管道有所不同,它完全使用了不同的实现机制,与文件没任何的关系,
也就是说内核不再以文件的形式来管理System V IPC,所以不能再使用文件的方式来操作。
对于System V IPC,OS内核提供了全新的API,对于这些API来说,我们的要求是理解而
不是记忆,因为在以后的开发中,确实用的不多。就算真的用到了,只要你理解了,你自然
能够很快的用起来。
3)使用System V IPC时,不存在亲缘进程一说
任何进程之间通信时,都可以使用System V IPC来通信。
(3)System V IPC标识符
我们前面说过,System V IPC不再以文件的形式存在,因此没有文件描述符这个东西,但是它
有类似的“标识符”。
你完全可以认为这个“标识符”就是文件描述符的替代者,但是它是专门给System V IPC使用的,
所以我们不能使用文件IO函数来操作“标识符”,只能使用System V IPC的特有API才能操作。
1)怎么才能得到这个“标识符”
调用某API创建好某个“通信结构”以后,API就会返回一个唯一的“标识符”。
比如创建好了一个“消息队列”后,创建的API就会返回一个唯一标识消息队列的“标识符”。
2)System V IPC标识符的作用?
比如,如果创建的是消息队列的话,进程通过消息队列唯一的标识符,就能找到创建好的
“消息队列”,使用这个消息队列,进程就能读写数据,然后实现进程间通信。
(1)消息队列的本质
消息队列的本质就是由内核创建的用于存放消息的链表,由于是存放消息的,所以我们就把这个链表
称为了消息队列。通信的进程通过共享操作同一个消息队列,就能实现进程间通信。
(2)消息是如何存放在消息队列中的呢?
消息队列这个链表有很多的节点,链表上的每一个节点就是一个消息。
从图中可以看出,每个消息由两部分组成,分别是消息编号(消息类型)和消息正文。
1)消息编号:识别消息用
2)消息正文:真正的信息内容
(3)收发数据的过程
1)发送消息
(a)进程先封装一个消息包
这个消息包其实就是如下类型的一个结构体变量,封包时将消息编号和消息正文
写到结构体的成员中。
struct msgbuf
{
long mtype; /* 放消息编号,必须> 0 */
char mtext[msgsz]; /* 消息内容(消息正文) */
};
(b)调用相应的API发送消息
调用API时通过“消息队列的标识符”找到对应的消息队列,然后将消息包发送给消息队列
,消息包(存放消息的结构体变量)会被作为一个链表节点插入链表。
2)接收消息
调用API接收消息时,必须传递两个重要的信息,
(a)消息队列标识符
(b)你要接收消息的编号
有了这两个信息,API就可以找到对应的消息队列,然后从消息队列中取出你所要编号
的消息,如此就收到了别人所发送的信息。
“消息队列”有点像信息公告牌,发送信息的人把某编号的消息挂到公告牌上,接收消息的人自己到公
告牌上去取对应编号的消息,如此,发送者和接受者之间就实现了通信。
(4)使用消息队列实现网状交叉通信
对于前面讲的管道来说,很难实现网状交叉通信,但是使用消息队列确非常容易实现。
(1)使用msgget函数创建新的消息队列、或者获取已存在的某个消息队列,并返回唯一标识消息队列的
标识符(msqID),后续收发消息就是使用这个标识符来实现的。
(2)收发消息
· 发送消息:使用msgsnd函数,利用消息队列标识符发送某编号的消息
· 接收消息:使用msgrcv函数,利用消息队列标识符接收某编号的消息
(3)使用msgctl函数,利用消息队列标识符删除消息队列
对于使用消息队列来通信的多个进程来说,只需要一个进程来创建消息队列就可以了,对于其它要参与
通信的进程来说,直接使用这个创建好的消息队列即可。
为了保证消息队列的创建,最好是让每一个进程都包含创建消息队列的代码,谁先运行就由谁创建,
后运行的进程如果发现它想用的那个消息队列已经创建好了,就直接使用,当众多进程共享操作同一个
消息队列时,即可实现进程间的通信。
所有system V ipc的API都是相似的,如果你能把消息队列的API搞清楚,后面的共享内存和信号量
API,理解起来很容易。
#include
#include
#include
int msgget(key_t key, int msgflg);
(a)功能:利用key值创建、或者获取一个消息队列。
· 如果key没有对应任何消息队列,那就创建一个新的消息队列
· 如果key已经对应了某个消息队列,说明你要的消息队列已经存在了,那就获取这个消息队
列来使用
估计你也感觉到了,key值也能够唯一的标识消息队列,那key值到底是个啥?
后面再介绍。
(b)返回值
· 成功:返回消息队列标识符(消息队列的ID)
对于每一个创建好的消息队列来说,ID是固定的。
· 失败:失败返回-1,并设置errno。
(c)参数
int msgget(key_t key, int msgflg);
·key值
用于为消息队列生成(计算出)唯一的消息队列ID。
我们可以指定三种形式的key值:
- 第一种:指定为IPC_PRIVATE宏,指定这个宏后,每次调用msgget时都会创建一个新的消息队列。
如果你每次使用的必须是新消息队列的话,就可以指定这个,不过这个用的很少。
因为一般来说,只要有一个消息队列可以用来通信就可以了,并不需要每次都创建一个全新
的消息队列。
- 第二种:可以自己指定一个整形数,但是容易重复指定
本来我想创建一个新的消息队列,结果我所指定的这个整形数,之前就已经被用于创建
某个消息队列了,当我的指定重复时,msgget就不会创建新消息队列,而是使用的是别人
之前就创建好的消息队列。
所以我们也不会使用这种方式来指定key值。
- 第三种:使用ftok函数来生成key
#include
#include
key_t ftok(const char *pathname, int proj_id);
ftok通过指定路径名和一个整形数,就可以计算并返回一个唯一对应的key值,
只要路径名和整形数不变,所对应的key值就唯一不变的。
不过由于ftok只会使用整形数(proj_id)的低8位,因此我们往往会指定为一个ASCII码值,
因为ASCII码值刚好是8位的整形数。
int msgget(key_t key, int msgflg);
· msgflg
指定创建时的原始权限,比如0664
创建一个新的消息队列时,除了原始权限,还需要指定IPC_CREAT选项。
msgid = msgget(key, 0664|IPC_CREAT);
如果key值没有对应任何消息队列,就会创建一个新的消息队列,此时就会用到msgflg参数,
但是如果key已经对应了某个早已存在消息队列,就直接返回这个已存在消息队列的ID(标识符),
此时不会用到msgflg参数。
(d)多个进程是如何共享到同一个消息队列的
1)创建进程
如果创建者使用"./file", 'a'生成一个key值,然后调用msgget创建了一个消息队列,比如:
key = ftok("./file", 'a');
msgid = msgget(key, 0664|IPC_CREAT);
当创建者得到msgid后,即可操作消息队列。
2)其它共享操作消息队列的进程
共享的方法很简单,只要你能拿到别人创建好的消息队列的ID,即可共享操作同一个消
息队列,实现进程间通信。
获取别人创建好的消息队列的ID,有两个方法:
(a)创建者把ID保存到某文件,共享进程读出ID即可
这种情况下,共享进程根本不需要调用msgget函数来返回ID。
(b)调用msgget获取已在消息队列的ID
· 使用ftok函数,利用与创建者相同的“路径名”和8位整形数,生成相同的key值
· 调用msgget函数,利用key找到别人创建好的消息队列,返回ID
key = ftok("./file", 'a');
msgid = msgget(key, 0664|IPC_CREAT);
拿到了消息队列的ID后就能共享操作了。
这种方法是最常用的方法,因为ftok所用到的“路径名”和“8位的整形数”比较好记忆,
所以,你只要记住别人生成key值时所用的“路径名”和“8位的整形数”,你就一定能共享
操作别人创建好的消息队列。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define MSG_FILE "./msgfile"
#define MSG_SIZE 1024
struct msgbuf
{
long mtype; /* 放消息编号,必须 > 0 */
char mtext[MSG_SIZE]; /* 消息内容(消息正文) */
};
void print_err(char *estr)
{
perror(estr);
exit(-1);
}
int creat_or_get_msgque(void)
{
int msgid = -1;
key_t key = -1;
int fd = 0;
/* 创建一个消息队列的专用文件,ftok会用到这个文件的路径名 */
fd = open(MSG_FILE, O_RDWR|O_CREAT, 0664);
if(fd == -1) print_err("open fail");
/* 利用存在的文件路径名和8位整形数,计算出key */
key = ftok(MSG_FILE, 'a');
if(key == -1) print_err("ftok fail");
/* 利用key创建、或者获取消息队列 */
msgid = msgget(key, 0664|IPC_CREAT);
if(msgid == -1) print_err("msgget fail");
return msgid;
}
int msgid = -1;
//用于退出后删除消息队列
void signal_fun(int signo)
{
msgctl(msgid, IPC_RMID, NULL);
remove(MSG_FILE);
exit(-1);
}
int main(int argc, char **argv)
{
int ret = -1;
long recv_msgtype = 0;
if(argc != 2)
{
printf("./a.out recv_msgtype\n");
exit(-1);
}
recv_msgtype = atol(argv[1]);
msgid = creat_or_get_msgque();
ret = fork();
if(ret > 0) //发送消息
{
signal(SIGINT, signal_fun);
struct msgbuf msg_buf = {0};
while(1)
{
bzero(&msg_buf, sizeof(msg_buf));
/* 封装消息包 */
scanf("%s", msg_buf.mtext);
printf("input snd_msgtype:\n");
scanf("%ld", &msg_buf.mtype);
/* 发送消息包 */
msgsnd(msgid, &msg_buf, MSG_SIZE, 0);
}
}
else if(ret == 0)//接收消息
{
struct msgbuf msg_buf = {0};
int ret = 0;
while(1)
{
bzero(&msg_buf, sizeof(msg_buf));
ret = msgrcv(msgid, &msg_buf, MSG_SIZE, recv_msgtype, 0);
if(ret > 0)
{
printf("%s\n", msg_buf.mtext);
}
}
}
return 0;
}
(a)如何验证消息队列是否被创建成功?
使用ipcs命令即可查看,可跟接的选项有:
- a 或者 什么都不跟:消息队列、共享内存、信号量的信息都会显示出来
- m:只显示共享内存的信息
- q:只显示消息队列的信息
- s:只显示信号量的信息
(b)system v ipc的缺点
进程结束时,system v ipc不会自动删除,进程结束后,使用ipcs依然能够查看到。
如何删除?
· 方法1:重启OS,很麻烦
· 方法2:进程结束时,调用相应的API来删除,后面再讲
· 方法3:使用ipcrm命令删除
- 删除共享内存
+ M:按照key值删除
ipcrm -M key
+ m:按照标识符删除
ipcrm -m msgid
- 删除消息队列
+ Q:按照key值删除
+ q:按照标识符删除
- 删除信号量
+ S:按照key值删除
+ s:按照标识符删除
函数原型
#include
#include
#include
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
(a)功能:发送消息到消息队列上。
说白了就是将消息挂到消息队列上。
(b)返回值
· 成功:返回0,
· 失败:返回-1,errno被设置
(c)参数
· msqid:消息队列的标识符。
· msgp:存放消息的缓存的地址,类型struct msgbuf类型
这个缓存就是一个消息包(存放消息的结构体变量)。
struct msgbuf
{
long mtype; /* 放消息编号,必须 > 0 */
char mtext[msgsz]; /* 消息内容(消息正文) */
};
· msgsz:消息正文大大小。
· msgflg:
- 0:阻塞发送消息
也就是说,如果没有发送成功的话,该函数会一直阻塞等,直到发送成功为止。
- IPC_NOWAIT:非阻塞方式发送消息,不管发送成功与否,函数都将返回
也就是说,发送不成功的的话,函数不会阻塞。
函数原型
#include
#include
#include
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
(a)功能:接收消息
说白了就是从消息队列中取出别人所放的某个编号的消息。
(b)返回值
成功:返回消息正文的字节数
失败:返回-1,errno被设置
(c)参数
· msqid:消息队列的标识符。
· msgp:缓存地址,缓存用于存放所接收的消息
类型还是struct msgbuf:
struct msgbuf
{
long mtype; /* 存放消息编号*/
char mtext[msgsz]; /*存放 消息正文内容 */
};
· msgsz:消息正文的大小
· msgtyp:你要接收消息的编号
· int msgflg:
- 0:阻塞接收消息
也就是说如果没有消息时,接收回阻塞(休眠)。
- IPC_NOWAIT:非阻塞接收消息
也就是说没有消息时,该函数不阻塞
我们需要调用msgctl函数来实现。
msgctl函数原型
#include
#include
#include
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
(a)功能
ctl就是控制contrl的意思,从这个名字我们就能猜出,这个函数的功能是根据cmd指定的要求,
去控制消息队列,比如进行哪些控制呢?
· 获取消息队列的属性信息
· 修改消息队列的属性信息
· 删除消息队列
· 等等
我们调用msgctl函数的最常见目的就是删除消息队列,事实上,删除消息队列只是各种消息队列
控制中的一种。
(b)参数
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
· msqid:消息队列标识符
· cmd:控制选项,其实cmd有很多选项,我这里只简单介绍三个
- IPC_STAT:将msqid消息队列的属性信息,读到第三个参数所指定的缓存。
- IPC_SET:使用第三个参数中的新设置去修改消息队列的属性
+ 定一个struct msqid_ds buf。
+ 将新的属性信息设置到buf中
+ cmd指定为IPC_SET后,msgctl函数就会使用buf中的新属性去修改消息队列原有的属性。
- IPC_RMID:删除消息队列
删除消息队列时,用不到第三个参数,用不到时设置为NULL。
- ... :略
· buf:存放属性信息
有的时候需要给第三个参数,有时不需要,取决于cmd的设置。
buf的类型为struct msqid_ds,有关这个结构体类型,这里这里只进行简单了解。
结构体中的成员都是用来存放消息队列的属性信息的。
struct msqid_ds
{
struct ipc_perm msg_perm; /* 消息队列的读写权限和所有者 */
time_t msg_stime; /* 最后一次向队列发送消息的时间*/
time_t msg_rtime; /* 最后一次从消息队列接收消息的时间 */
time_t msg_ctime; /* 消息队列属性最后一次被修改的时间 */
unsigned long __msg_cbytes; /* 队列中当前所有消息总的字节数 */
msgqnum_t msg_qnum; /* 队列中当前消息的条数*/
msglen_t msg_qbytes; /* 队列中允许的最大的总的字节数 */
pid_t msg_lspid; /* 最后一次向队列发送消息的进程PID */
pid_t msg_lrpid; /* 最后一次从队列接受消息的进程PID */
};
struct ipc_perm
{
key_t __key; /* Key supplied to msgget(2):消息队列的key值 */
uid_t uid; /* UID of owner :当前这一刻正在使用消息队列的用户 */
gid_t gid; /* GID of owner :正在使用的用户所在用户组 */
uid_t cuid; /* UID of creator :创建消息队列的用户 */
gid_t cgid; /* GID of creator :创建消息队列的用户所在用户组*/
unsigned short mode; /* Permissions:读写权限(比如0664) */
unsigned short __seq; /* Sequence number :序列号,保障消息队列ID不被立即
重复使用 */
};
实际上消息队列这种通信方式,使用起来还是蛮方便的,因为不管是两个进程之间的通信,还是n多个进
程的网状交叉通信,消息队列都能搞定,完全可以替代前面讲的管道,
特别是当你的程序必须涉及到多进程网状交叉通信时,消息队列是上上之选。
与管道一样,不能实现大规模数据的通信,大规模数据的通信,必须使用后面讲的“共享内存”来实现。
共享内存的API与消息队列的API非常相似,应该System V IPC的API都是差不多的,所以只要大家把
前面的消息队列拎清楚了,大家学习本小节的共享内存和之后的信号量时,你会觉非常的容易。
共享内存就是OS在物理内存中开辟一大段缓存空间,不过与管道、消息队列调用read、write、
msgsnd、msgrcv等API来读写所不同的是,使用共享内存通信时,进程是直接使用地址来共享读写的。
当然不管使用那种方式,只要能够共享操作同一段缓存,就都可以实现进程间的通信。
不过如果直接使用地址来读写缓存时,效率会更高,但是如果是调用API来读写的话,中间必须经过重重
的OS函数调用之后,直到调用到最后一个函数时,该函数才会通过地址去读写共享的缓存,中间的调用过程
会降低效率。
对于小数据量的通信来说,使用管道和消息队列这种使用API读写的通信方式很合适,但是如果进程涉及到
超大量的数据通信时,必须使用“共享内存”这种直接使用地址操作的通信方式,如果使用API来读写的话,
效率会非常的低。
共享内存的实现原理很简单,进程空间不是没有交集吗,让他们的空间有交集不就行了吗。
以两个进程使用共享内存来通信为例,实现的方法就是:
(1)调用API,让OS在物理内存上开辟出一大段缓存空间。
(2)让各自进程空间与开辟出的缓存空间建立映射关系
就让虚拟地址和物理内存的实际物理地址建立一对一的对应关系,使用虚拟地址读写缓存时,虚拟
地址最终是要转为物理地址的,转换时就必须参考这个映射关系。
总之建立映射关系后,每个进程都可以通过映射后的虚拟地址来共享操作实现通信了。
多个进程能不能映射到同一片空间,然后数据共享呢?
答:当然是可以的
不过当多个进程映射并共享同一个空间时,在写数据的时候可能会出现相互干扰,
比如A进程的数据刚写了一半没写完,结果切换到B进程后,B进程又开始写,A的数据就被中间B的
数据给岔开了这时往往需要加保护措施,让每个进程在没有操作时不要被别人干扰,等操作完以后,
别的进程才能写数据。比如可以使用信号或者信号量来解决这种同步的问题。
(1)进程调用shmget函数创建新的或获取已有共享内存
shm是share memory的缩写。
(2)进程调用shmat函数,将物理内存映射到自己的进程空间
说白了就是让虚拟地址和真实物理地址建议一一对应的映射关系。
建立映射后,就可以直接使用虚拟地址来读写共享的内存空间了。
(3)shmdt函数,取消映射
(4)调用shmctl函数释放开辟的那片物理内存空间
和消息队列的msgctl的功能是一样的,只不过这个是共享内存的。
多个进程使用共享内存通信时,创建者只需要一个,同样的,一般都是谁先运行谁创建,其它后运行的
进程发现已经被创建好了,就直接获取共享使用,大家共享操作同一个内存,即可实现通信。
#include
#include
int shmget(key_t key, size_t size, int shmflg);
1)功能:创建新的,或者获取已有的共享内
· 如果key值没有对应任何共享内存
创建一个新的共享内存,创建的过程其实就是os在物理内存上划出(开辟出)一段物理内存
空间出来。
· 如果key值有对应某一个共享内存
说明之前有进程调用msgget函数,使用该key去创建了某个共享内存,既然别人之前就创
建好了,那就直接获取key所对应的共享内存。
2)返回值
(a)成功:返回共享内存的标识符,以后续操作
(b)失败:返回-1,并且errno被设置。
int shmget(key_t key, size_t size, int shmflg);
3)参数
(a)key:用于生成共享内存的标识符
可以有三种设置:
· IPC_PRIVATE:指定这个后,每次调用shmget时都会创建一个新共享内存。
· 自己指定一个长整型数
· 使用ftok函数,通过路径名和一个8位的整形数来生成key值
(b)size:指定共享内存的大小,我们一般要求size是虚拟页大小的整数倍
一般来说虚拟页大小是4k(4096字节),如果你指定的大小不是虚拟页的整数倍,也会自动
帮你补成整数倍。
(c)semflg:与消息队列一样
指定原始权限和IPC_CREAT,比如0664|IPC_CREAT。
只有在创建一个新的共享内存时才会用到,否者不会用到。
共享内存写数据代码:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define SHM_FILE "./shmfile"
#define SHM_SIZE 4096
int shmid = -1;
void *shmaddr = NULL;
void print_err(char *estr)
{
perror(estr);
exit(-1);
}
void create_or_get_shm(void)
{
int fd = 0;
key_t key = -1;
fd = open(SHM_FILE, O_RDWR|O_CREAT, 0664);
if(fd == -1) print_err("open fail");
key = ftok(SHM_FILE, 'b');
if(key == -1) print_err("ftok fail");
shmid = shmget(key, SHM_SIZE, 0664|IPC_CREAT);
if(shmid == -1) print_err("shmget fail");
//write(fd, &shmid, sizeof(shmid));
}
char buf[300] = {"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222\
ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff2222222222"};
void signal_fun(int signo)
{
shmdt(shmaddr);
shmctl(shmid, IPC_RMID, NULL);
remove("./fifo");
remove(SHM_FILE);
exit(-1);
}
int get_peer_PID(void)
{
int ret = -1;
int fifofd = -1;
/* 创建有名管道文件 */
ret = mkfifo("./fifo", 0664);
if(ret == -1 && errno != EEXIST) print_err("mkfifo fail");
/* 以只读方式打开管道 */
fifofd = open("./fifo", O_RDONLY);
if(fifofd == -1) print_err("open fifo fail");
/* 读管道,获取“读共享内存进程”的PID */
int peer_pid;
ret = read(fifofd, &peer_pid, sizeof(peer_pid));
if(ret == -1) print_err("read fifo fail");
return peer_pid;
}
int main(void)
{
int peer_pid = -1;
/* 给SIGINT信号注册捕获函数,用于删除共享内存、管道、文件等 */
signal(SIGINT, signal_fun);
/* 使用有名管道获取读共享内存进程的PID */
peer_pid = get_peer_PID();
/* 创建、或者获取共享内存 */
create_or_get_shm();
//建立映射
shmaddr = shmat(shmid, NULL, 0);
if(shmaddr == (void *)-1) print_err("shmat fail");
while(1)
{
memcpy(shmaddr, buf, sizeof(buf));
//保证写完后再读数据,当共享内存没有数据时,读进程休眠,当写进程把数据写完后,
//将读进程唤醒。
kill(peer_pid, SIGUSR1);
sleep(1);
}
return 0;
}
共享内存读数据代码:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define SHM_FILE "./shmfile"
#define SHM_SIZE 4096
int shmid = -1;
void *shmaddr = NULL;
void print_err(char *estr)
{
perror(estr);
exit(-1);
}
void create_or_get_shm(void)
{
int fd = 0;
key_t key = -1;
fd = open(SHM_FILE, O_RDWR|O_CREAT, 0664);
if(fd == -1) print_err("open fail");
key = ftok(SHM_FILE, 'b');
if(key == -1) print_err("ftok fail");
shmid = shmget(key, SHM_SIZE, 0664|IPC_CREAT);
if(shmid == -1) print_err("shmget fail");
//read(fd, &shmid, sizeof(shmid));
}
void signal_fun(int signo)
{
if(SIGINT == signo)
{
shmdt(shmaddr);
shmctl(shmid, IPC_RMID, NULL);
remove("./fifo");
remove(SHM_FILE);
exit(-1);
}
else if(SIGUSR1 == signo)
{
}
}
void snd_self_PID(void)
{
int ret = -1;
int fifofd = -1;
/* 创建有名管道文件 */
mkfifo("./fifo", 0664);
if(ret == -1 && errno != EEXIST) print_err("mkfifo fail");
/* 以只写方式打开文件 */
fifofd = open("./fifo", O_WRONLY);
if(fifofd == -1) print_err("open fifo fail");
/* 获取当前进程的PID, 使用有名管道发送给写共享内存的进程 */
int pid = getpid();
ret = write(fifofd, &pid, sizeof(pid));//发送PID
if(ret == -1) print_err("write fifo fail");
}
int main(void)
{
/*给SIGUSR1注册一个空捕获函数,用于唤醒pause()函数 */
signal(SIGUSR1, signal_fun);
signal(SIGINT, signal_fun);
/* 使用有名管道,讲当前进程的PID发送给写共享内存的进程 */
snd_self_PID();
/* 创建、或者获取共享内存 */
create_or_get_shm();
//建立映射
shmaddr = shmat(shmid, NULL, 0);
if(shmaddr == (void *)-1) print_err("shmat fail");
while(1)
{
//无数据休眠,有数据唤醒
pause();
printf("%s\n", (char *)shmaddr);
bzero(shmaddr, SHM_SIZE);
}
return 0;
}
说明:
(a)使用ipcs命令即可查看创建的共享内存:
- a 或者 什么都不跟:消息队列、共享内存、信号量的信息都会显示出来
- m:只显示共享内存的信息
- q:只显示消息队列的信息
- s:只显示信号量的信息
(b)共享内存的删除
进程结束时,system v ipc不会自动删除,进程结束后,使用ipcs依然能够查看到。
如何删除?
· 方法1:重启OS,很麻烦
· 方法2:进程结束时,调用相应的API来删除,后面再讲
· 方法3:使用ipcrm命令删除
- 删除共享内存
+ M:按照key值删除
ipcrm -M key
+ m:按照标识符删除
ipcrm -m msgid
- 删除消息队列
+ Q:按照key值删除
+ q:按照标识符删除
- 删除信号量
+ S:按照key值删除
+ s:按照标识符删除
(1)函数原型
#include
#include
void *shmat(int shmid, const void *shmaddr, int shmflg);
1)功能
将shmid所指向的共享内存空间映射到进程空间(虚拟内存空间),并返回影射后的起始
地址(虚拟地址)。
有了这个地址后,就可以通过这个地址对共享内存进行读写操作。
2)参数
(a)shmid:共享内存标识符。
(b)shmaddr:指定映射的起始地址
有两种设置方式
· 自己指定映射的起始地址(虚拟地址)。
我们一般不会这么做,因为我们自己都搞不清哪些虚拟地址被用了,哪些没被用。
· NULL:表示由内核自己来选择映射的起始地址(虚拟地址)。
这是最常见的方式,也是最合理的方式,因为只有内核自己才知道哪些虚拟地址
可用,哪些不可用。
void *shmat(int shmid, const void *shmaddr, int shmflg);
(c)shmflg:指定映射条件。
· 0:以可读可写的方式映射共享内存
也就是说映射后,可以读、也可以写共享内存。
· SHM_RDONLY:以只读方式映射共享内存
也就是说映射后,只能读共享内存,不能写。
3)返回值
(a)成功:则返回映射地址
(b)失败:返回(void *)-1,并且errno被设置。
(1)函数原型
#include
#include
int shmdt(const void *shmaddr);
1)功能:取消建立的映射。
2)返回值:调用成功返回0,失败返回-1,且errno被设置。
3)参数
shmaddr:映射的起始地址(虚拟地址)。
(1)函数原型
#include
#include
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
(a)功能:根据cmd的要求,对共享内存进行相应控制。
比如:
· 获取共享内存的属性信息
· 修改共享内存的属性信息
· 删除共享内存
· 等等
删除共享内存是最常见的控制。
(b)参数
· shmid:标识符。
· cmd:控制选项
- IPC_STAT:从内核获取共享内存属性信息到第三个参数(应用缓存)。
- IPC_SET:修改共享内存的属性。
修改方法与消息队列相同。
- IPC_RMID:删除共享内存,不过前提是只有当所有的映射取消后,才能删除共享内存。
删除时,用不着第三个参数,所以设置为NULL
· buf
buf的类型为struct shmid_ds。
- cmd为IPC_STAT时
buf用于存储原有的共享内存属性,以供查看。
- cmd为IPC_SET时
buf中放的是新的属性设置,用于修改共享内存的属性。
- struct shmid_ds结构体
struct shmid_ds
{
struct ipc_perm shm_perm; /* Ownership and permissions:权限 */
size_t shm_segsz; /* Size of segment (bytes):共享内存大小 */
time_t shm_atime; /* Last attach time:最后一次映射的时间 */
time_t shm_dtime; /* Last detach time:最后一次取消映射的时间 */
time_t shm_ctime; /* Last change time:最后一次修改属性信息的时间 */
pid_t shm_cpid; /* PID of creator:创建进程的PID */
pid_t shm_lpid; /* PID of last shmat(2)/shmdt(2) :当前正在使用进程的PID*/
shmatt_t shm_nattch; /* No. of current attaches:映射数量,
* 标记有多少个进程空间映射到了共享内存上
* 每增加一个映射就+1,每取消一个映射就-1 */
...
};
struct ipc_perm,这个结构体我们在讲消息队列时已经讲过,这里不再重复讲。
struct ipc_perm
{
key_t __key; /* Key supplied to shmget(2) */
uid_t uid; /* UID of owner */
gid_t gid; /* GID of owner */
uid_t cuid; /* UID of creator */
gid_t cgid; /* GID of creator */
unsigned short mode; /* Permissions + SHM_DEST andSHM_LOCKED flags */
unsigned short __seq; /* Sequence number */
};
4)返回值
调用成功0,失败则返回-1,并且errno被设置。
当多个进程/线程进行共享操作时,用于资源保护,以防止出现相互干扰的情况。
再间简洁一点,信号量用于“资源的保护“。
(1)进程信号量
实现的是进程所操作资源的保护。
(2)线程信号量
实现的是线程所操作资源的保护。
我们讲的是进程的资源保护,实际上线程的资源保护也是类似的原理。
为了更直观的讲解,我们直接通过例子来介绍什么是“进程资源保护”。
(1)例子1:多进程操作共享内存
比如,多个进程同时向共享内存里面写数据时,可能会出现数据相互干扰的情况。
比如,某个进程写数据操作还没有写完时,进程的时间片就到了,然后被切换到另一个写
“共享内存”的进程上运行,这个进程会接着往共享内存里面写数据,此时显然就把第一个进程写
的数据给隔断,这就形成了数据相互干扰。
如果只是普通数据的话无所谓,但是如果是很重要的数据的话,这种干扰是无法接受。
(2)例子2:多进程操作文件
比如当多个进程同时共享向文件里面写数据时,同样会出现和共享写“共享内存”相同的情况
为了避免出现以上所说的相互干扰的问题,就需要加入资源保护的措施,保护的目的就是,保证每个
进程在没有把数据读、写完整之前,其它进程不能进行读、写操作,以防止干扰别人。
疑问:资源保护,这个“资源”到底指的是谁?
答:这个资源指的就是你操作的数据,保护的目的就是不要出现相互干扰,导致紊乱和错误数据的产生。
资源保护的操作分两种,一种叫互斥,另一个种叫同步。
(1)互斥
对于互斥操作来说,多进程共享操作时,多个进程间不关心谁先操作、谁后操作的先后顺序问题,
它们只关心一件事,那就是我在操作时别人不能操作。
就算当前正在操作的进程它的时间片到了,切换到了其它进程上,但是当该进程检测到上一个进程
还没有操作完时,该进程在当前的时间片内会休眠,直到再次切换会上一个进程,将操作完成后再切
换回来,此时才能进行操作。
这跟上厕所时把门关起来是一样的,我在蹲坑时你不能蹲,你在蹲坑时我不能蹲,这就是互斥,至于
蹲坑先后顺序并没有要求。
(2)同步
同步其实本身就包含了互斥,不过同步不仅仅只互斥,同步对于谁先操作、谁后操作的先后顺序有
要求,比如规定A进程先写,然后是B进程写,然后是C进程写,绝对不能出现这操作顺序以外的顺序。
所以所谓同步就是,多个共享操作时,进程必须要有统一操作的步调,按照一定的顺序来操作。
疑问:同步有意义吗?
答:我们讲共享内存时,例子代码不就需要同步吗。
(3)实现同步、互斥,其实就是加锁
这个很形象,我要操作我就上把锁,我上锁的过程中你就不能操作,直到我把锁打开了,你才能操作,
你操作时也会加锁,加锁后我就不能操作了。
所以说信号量就是一个加锁机制,通过加锁来实现同步和互斥。
说到加锁,我们讲到后面“高级IO”时,我们还会讲到“文件锁”这个东西,顾名思义,文件锁就是专门用
来给文件时上锁的,讲到时在详细介绍。
其实,不管是进程还是线程,都存在同步和互斥的问题,同步和互斥的目的其实就是为了实现“资源”的
保护,不要让数据(资源)出现紊乱。
(4)疑问:信号量既然是一种加锁机制,为什么进程信号量会被归到了进程间通信里面呢?
资源保护时,某个进程的操作没有完全完成之前,别人是不能操作的,那么进程间必须相互知道
对方的操作状态,必须会涉及到通信过程。
所以信号量实现资源保护的本质就是,通过通信让各个进程了解到操作状态,然后查看自己能不能操作。
进程信号量既能实现进程的互斥,也能实现进程的同步,不过有些“资源保护机制”就只能实现互斥,
而不能实现同步。
虽然我们这里主要是讲互斥,但是也会捎带的提到同步,为后面讲同步打基础。
semaphore.h:
#ifndef H_SEM_H
#define H_SEM_H
extern void print_err(char *estr);
extern int creat_or_get_sem(int nsems);
extern void init_sem(int semid, int semnum, int val);
extern void del_sem(int semid, int nsems);
extern void p_sem(int semid, int semnum_buf[], int nsops);
extern void v_sem(int semid, int semnum_buf[], int nsops);
#endif
semaphore.c:
#include
#include
#include
#include
#include
#include
#include
#include
#include
union semun
{
int val;
struct semid_ds *buf;
unsigned short *array; /* 不做要求 */
struct seminfo *__buf; /* 不做要求 */
};
#define SEM_FILE "./semfile"
void print_err(char *estr)
{
perror(estr);
//exit(-1);
}
int creat_or_get_sem(int nsems)
{
int semid;
int fd = -1;
key_t key = -1;
fd = open(SEM_FILE, O_RDWR|O_CREAT, 0664);
if(fd == -1) print_err("open ./semfile fail");
key = ftok(SEM_FILE, 'a');
if(key == -1) print_err("ftok fail");
semid = semget(key, nsems, 0664|IPC_CREAT);
if(semid == -1) print_err("semget fail");
return semid;
}
void init_sem(int semid, int semnum, int val)
{
int ret = -1;
union semun sem_un;
/* semnum:信号量编号
* SETVAL:设置信号量初始值cmd
* sem_un:初始值
*/
sem_un.val = val;
ret = semctl(semid, semnum, SETVAL, sem_un);
if(ret == -1) print_err("semctl fail");
}
void del_sem(int semid, int nsems)
{
int ret = 0;
int i = 0;
for(i=0; i<nsems; i++)
{
ret = semctl(semid, i, IPC_RMID);
if(ret == -1) print_err("semctl del sem fail");
}
remove(SEM_FILE);
}
void p_sem(int semid, int semnum_buf[], int nsops)
{
int i = 0;
int ret = -1;
struct sembuf sops[nsops];
for(i=0; i<nsops; i++)
{
sops[i].sem_num = semnum_buf[i];//信号量编号
sops[i].sem_op = -1;//-1 p操作
sops[i].sem_flg = SEM_UNDO;//防止死锁
}
ret = semop(semid, sops, nsops);
if(ret == -1) print_err("semop p fail");
}
void v_sem(int semid, int semnum_buf[], int nsops)
{
int i = 0;
int ret = -1;
struct sembuf sops[nsops];
for(i=0; i<nsops; i++)
{
sops[i].sem_num = semnum_buf[i];//信号量编号
sops[i].sem_op = 1;//+1 v操作
sops[i].sem_flg = SEM_UNDO;//防止死锁
}
ret = semop(semid, sops, nsops);
if(ret == -1) print_err("semop p fail");
}
share_write_file.c:
#include
#include
#include
#include
#include
#include
#include
#include "semaphore.h"
#define NSEMS 1
int semid;
void signal_fun(int signo)
{
del_sem(semid, NSEMS);
exit(-1);
}
int main(void)
{
int i = 0;
int ret = 0;
int fd = -1;
int semnum_buf[1] = {0};
fd = open("./file", O_RDWR|O_CREAT|O_TRUNC, 0664);
if(fd == -1) print_err("open file fail");
semid = creat_or_get_sem(NSEMS);
for(i=0; i<NSEMS; i++)
{
init_sem(semid, i, 1);
}
ret = fork();
if(ret > 0)
{
signal(SIGINT, signal_fun);
while(1)
{
semnum_buf[0] = 0;//设置要操作的信号量的编号
p_sem(semid, semnum_buf, 1); //P操作
write(fd, "hello ", 6);
write(fd, "world\n", 6);
semnum_buf[0] = 0; //设置要操作的信号量的编号
v_sem(semid, semnum_buf, 1);//v操作
}
}
else if(ret == 0)
{
while(1)
{
semnum_buf[0] = 0;//设置要操作的信号量的编号
p_sem(semid, semnum_buf, 1); //P操作
write(fd, "hhhhh ", 6);
write(fd, "wwwww\n", 6);
semnum_buf[0] = 0; //设置要操作的信号量的编号
v_sem(semid, semnum_buf, 1);//v操作
}
}
return 0;
}
(1)什么是进程信号量
简单理解的话,信号量其实是OS创建的一个共享变量,进程在进行操作之前,会先检查这个变量的值,
这变量的值就是一个标记,通过这个标记就可以知道可不可以操作,以实现互斥。
(2)多值信号量和二值信号量
1)二值信号量
同步和互斥时使用的都是二值信号量。
二值信号量的值就两个,0和1,0表示不可以操作,1表示可以操作。
通过对变量进行0、1标记,就可以防止出现相互干扰情况。
2)多值信号量
信号量的最大值>1,比如为3的话,信号量允许的值为0、1、2、3。
多值信号量用的不是很多,所这里只简单的提一下。
(3)信号量集合
我们说信号量其实是一个OS创建的,供相关进程共享的int变量,只不过我们在调用相关API
创建信号量时,我们创建的都是一个信号量集合,所谓集合就是可能会包含好多个信号量。
用于互斥时,集合中只包含一个信号量。
用于同步时,集合中会包含多个信号量,至于多少个,需要看情况。
(4)信号量的使用步骤
1)进程调用semget函数创建新的信号量集合,或者获取已有的信号量集合。
2)调用semctl函数给集合中的每个信号量设置初始值
3)调用semop函数,对集合中的信号量进行pv操作
什么是pv操作?
pv操作其实说白了就是加锁、解锁操作。
(a)P操作(加锁):对信号量的值进行-1,如果信号量的值为0,p操作就会阻塞
(b)V操作(解锁):对信号量的值进行+1,V操作不存在阻塞的问题
总之通过pv操作(加锁、解锁),就能够实现互斥,以防止出现干扰。
正如我们前面总结的,加锁、解锁就跟上厕所蹲坑把门栓起来,完事了再把门打开是
一样的,上厕所时通过门栓的加锁和解锁,就实现了上厕所蹲坑的互斥,防止上厕所的相互
干扰。
4)调用semctl删除信号量集合
1)函数原型
#include
#include
#include
int semget(key_t key, int nsems, int semflg);
sem就是semaphore的缩写。
(a)功能:根据key值创建新的、或者获取已有的信号量集合,并返回其标识符。
· 实现互斥时:集合中只需要一个信号量
· 实现同步时:集合中需要多个信号量
(b)参数
· key:设置同消息队列和共享内存。
一般都使用ftok获取key值。
· nsems:指定集合中信号量的个数。
用于互斥时,数量都指定为1,因为只需要一个信号量。
如果是同步的话就需要至多为多个,至于到底是多少个,讲到同步时再说。
· semflg:设置同消息队列和共享内存。
一般都设置为0664|IPC_CREAT。
(c)返回值:调用成功则返回信号量集合的标识符,失败则返回-1,并且errno被设置。
1)函数原型
#include
#include
#include
int semctl(int semid, int semnum, int cmd, ...);
(a)功能
根据cmd的要求对集合中的各个信号量进行控制,...表示它是一个变参函数,如果第四
个参数用不到的话,可以省略不写。
(b)返回值:调用成功返回非-1值,失败则返回-1,errno被设置。
(c)参数说明
· semid:信号量标识符。
通过标识符就能找到信号量集合。
· semnum:集合中某个信号量的编号。
信号量的编号为非负整数,而且是自动从0开始编号的。
通过信号量编号就能找到集合中对应信号量,然后对这个具体的信号量进行控制操作。
int semctl(int semid, int semnum, int cmd, ...);
· cmd:控制选项。
- IPC_STAT:将信号量的属性信息从内核读到第四个参数所以指定的
struct semid_ds缓存中。
- IPC_SET:修改属性信息,此时也会用到struct semid_ds结构体变量
具体的修改方法同消息队列和共享内存。
- IPC_RMID:删除信号量集合时,并不需要把所有的信号量都删除掉后才能删除,只需要指定semid
和IPC_RMID就可以不把整个信号量集合删除,其中第二个参数semnum没有被用到,所以
semnum的值可以随便写,不过我们一般都是把它写为0。
比如:semctl(semid, 0, IPC_RMID);
int semctl(int semid, int semnum, int cmd, ...);
- SETVAL:通过第四个参数,给集合中semnu编号的信号量设置一个int初始值。
在前面就说过,如果是二值信号量的话,设置初始值要么是0,要么是1,如果信号量的
目的是互斥的话,基本都是设置为1。
当设置为1后,多几个进程互斥操作时,那就是谁先运行就谁先操作。
如果是同步的话,初值是1还是0,这要就要看具体的情况了。
- 其它选项:省略
其中信号量的IPC_STAT、IPC_SET、IPC_RMID与消息队列和共享内存的
IPC_STAT、IPC_SET、IPC_RMID是一样的。
但是SETVAL确属于进程信号量所独有的选项。
对于信号量来说,IPC_RMID、SETVAL是最常用的两个选项。
int semctl(int semid, int semnum, int cmd, ...);
· ...
...表示,如果用不到时可以省略不写。
通过前面cmd的介绍我们可以看出,第四个参数具体设置为什么其实是不一定的,比如
- cmd为IPC_STAT:第四个参数应为struct semid_ds类型的缓存。
有关struct semid_ds结构体我们不再介绍,因为与共享内存的
struct shmid_ds,以及消息队列的struct msqid_ds结构体是类似的。
- cmd为SETVAL:第四个参数应该设置为一个int的值,用于初始化信号量。
从以上可以看出,第四个参数对应内容是变着的,为了应对这种变化就用到了一个联合体。
union semun {
int val;
struct semid_ds *buf;
unsigned short *array; /* 不做要求 */
struct seminfo *__buf; /* 不做要求 */
};
这个联合体类型并没有被定义在信号量相关的系统头文件中,我们使用这个联合体时,
我们需要自己定义这个类型,至于联合体类型名可以自己定,不过一般都是直接沿用
semun这个名字。
成员:
val:存放用于初始化信号量的值
buf:存放struct semid_ds结构体变量的地址
有关联合体的详细讲解,请看《C深度解析课》这门课,联合体的介绍属于基础
课程的内容,所以我们这里不再赘述。
疑问:这个联合怎么用?
+ 例1:当需要指定struct semid_ds缓存时
union semun sem_un; //定义一个联合体变量
struct semid_ds buff; //定义一个struct semid_ds缓存
sem_un.buf = &buff; //现在整个联合体的值就是buf中缩放的buff的地址
semctl(semid, 0, IPC_STAT, sem_un); //这里将联合体传递给semctl函数,
//其实就是将buff的地址传递给了semctl函数
+ 例2:当需要指定信号量的int初始值时
union semun sem_un;
sem_un.val = 1; //现在整个联合体的值就是1
semctl(semid, 0, IPC_STAT, sem_un);
1)函数原型
#include
#include
#include
int semop(int semid, struct sembuf *sops, unsigned nsops);
op是operate操作的意思。
(a)功能:对指定的信号量进行p操作、或者是v操作。
· p操作:将信号量的值-1
当信号量的值为0时,p操作默认是阻塞的。
· v操作:将信号量的值+1
v操作不存在阻塞的问题。
对于二值信号量来说,v操作后,值就从0变为了1,这就表示我操作完了,其它进程运行时
就可以进行p操作了。
(b)返回值:调用成功返回0,失败则返回-1,errno被设置。
int semop(int semid, struct sembuf *sops, unsigned nsops);
(c)参数
· semid:信号量集合的标识符。
· sops:这个参数更好理解的写法是struct sembuf sops[],
第三个参数nsops就是用于指定数组元素个数的。
每一个数组成员对应一个信号量,每一个元素都是一个struct sembuf结构体变量,内部成员的
决定着:
- 你要对集合中哪一个信号量进行操作
- 要进行的是p操作呢,还是v操作
- 结构体成员
struct sembuf
{
unsigned short sem_num;
short sem_op;
short sem_flg;
}
这个结构体不需要我们自己定义,因为在semop的头文件中已经定义了。
如果你无法判断这个结构体是否需要我们自己定义,那你就不要定义,如果编译提示这
个结构体类型不存在,就说明需要自己定义,编译通过就说明在系统头文件中早就定义好了。
+ sem_num:信号量编号,决定对集合中哪一个信号量进行pv操作
+ sem_op:设置为-1,表示想-1进行p操作,设置1表示想+1进行v操作
+ sem_flg:
· IPC_NOWAIT:
一般情况下,当信号量的值为0时进行p操作的话,semop的p操作会阻塞。
如果你不想阻塞的话,可以指定这个选项,NOWAIT就是不阻塞的意思。
不过除非某些特殊情况,否则我们不需要设置为非阻塞。
· SEM_UNDO:防止死锁
还是以二值信号量为例,当进程在v操作之前就结束时,信号量的值就会一直保持
为0,那么其它进程将永远无法p操作成功,会使得进程永远休眠下去,这造成就是死锁。
但是设置了SEM_UNDO选项后,如果进程在结束时没有V操作的话,OS会自动帮忙
V操作,防止死锁。
让多个进程按照固定的步调做事,我们前面就说过,同步本身就是互斥的。
实现同步时,同步的进程可以是亲缘进程,也可以是非亲缘进程。
通过同步让三个亲缘进程按照顺序打印出111111、222222、333333。
代码演示:
semaphore.h:
#ifndef H_SEM_H
#define H_SEM_H
extern void print_err(char *estr);
extern int creat_or_get_sem(int nsems);
extern void init_sem(int semid, int semnum, int val);
extern void del_sem(int semid, int nsems);
extern void p_sem(int semid, int semnum_buf[], int nsops);
extern void v_sem(int semid, int semnum_buf[], int nsops);
#endif
semaphore.c:
#include
#include
#include
#include
#include
#include
#include
#include
#include
union semun
{
int val;
struct semid_ds *buf;
unsigned short *array; /* 不做要求 */
struct seminfo *__buf; /* 不做要求 */
};
#define SEM_FILE "./semfile"
void print_err(char *estr)
{
perror(estr);
//exit(-1);
}
int creat_or_get_sem(int nsems)
{
int semid;
int fd = -1;
key_t key = -1;
fd = open(SEM_FILE, O_RDWR|O_CREAT, 0664);
if(fd == -1) print_err("open ./semfile fail");
key = ftok(SEM_FILE, 'a');
if(key == -1) print_err("ftok fail");
semid = semget(key, nsems, 0664|IPC_CREAT);
if(semid == -1) print_err("semget fail");
return semid;
}
void init_sem(int semid, int semnum, int val)
{
int ret = -1;
union semun sem_un;
/* semnum:信号量编号
* SETVAL:设置信号量初始值cmd
* sem_un:初始值
*/
sem_un.val = val;
ret = semctl(semid, semnum, SETVAL, sem_un);
if(ret == -1) print_err("semctl fail");
}
void del_sem(int semid, int nsems)
{
int ret = 0;
ret = semctl(semid, 0, IPC_RMID);
if(ret == -1) print_err("semctl del sem fail");
remove(SEM_FILE);
}
void p_sem(int semid, int semnum_buf[], int nsops)
{
int i = 0;
int ret = -1;
struct sembuf sops[nsops];
for(i=0; i<nsops; i++)
{
sops[i].sem_num = semnum_buf[i];//信号量编号
sops[i].sem_op = -1;//-1 p操作
sops[i].sem_flg = SEM_UNDO;//防止死锁
}
ret = semop(semid, sops, nsops);
if(ret == -1) print_err("semop p fail");
}
void v_sem(int semid, int semnum_buf[], int nsops)
{
int i = 0;
int ret = -1;
struct sembuf sops[nsops];
for(i=0; i<nsops; i++)
{
sops[i].sem_num = semnum_buf[i];//信号量编号
sops[i].sem_op = 1;//+1 v操作
sops[i].sem_flg = SEM_UNDO;//防止死锁
}
ret = semop(semid, sops, nsops);
if(ret == -1) print_err("semop p fail");
}
sync.c
#include
#include
#include
#include
#include
#include
#include
#include "semaphore.h"
#define NSEMS 3
int semid;
void signal_fun(int signo)
{
del_sem(semid, NSEMS);
exit(-1);
}
int main(void)
{
int i = 0;
int ret = 0;
int fd = -1;
int semnum_buf[1] = {0};
//创建信号量集合
semid = creat_or_get_sem(NSEMS);
//初始化信号量集合中的每个信号量
for(i=0; i<NSEMS; i++)
{
if(i == 0) init_sem(semid, i, 1);
else init_sem(semid, i, 0);
}
ret = fork();
if(ret > 0)
{
ret = fork();
if(ret > 0) //父进程
{
while(1)
{
semnum_buf[0] = 2;
p_sem(semid, semnum_buf, 1);
printf("333333\n");
sleep(1);
semnum_buf[0] = 0;
v_sem(semid, semnum_buf, 1);
}
}
else if(ret == 0) //子进程2
{
while(1)
{
semnum_buf[0] = 1;
p_sem(semid, semnum_buf, 1);
printf("222222\n");
sleep(1);
semnum_buf[0] = 2;
v_sem(semid, semnum_buf, 1);
}
}
}
else if(ret == 0)//子进程1
{
signal(SIGINT, signal_fun);
while(1)
{
semnum_buf[0] = 0;
p_sem(semid, semnum_buf, 1);
printf("111111\n");
sleep(1);
semnum_buf[0] = 1;
v_sem(semid, semnum_buf, 1);
}
}
return 0;
}
使用信号量来解决共享内存的同步问题
semaphore.h:
#ifndef H_SEM_H
#define H_SEM_H
extern int creat_or_get_sem(int nsems);
extern void init_sem(int semid, int semnum, int val);
extern void del_sem(int semid);
extern void p_sem(int semid, int semnum_buf[], int nsops);
extern void v_sem(int semid, int semnum_buf[], int nsops);
#endif
semaphore.c:
#include
#include
#include
#include
#include
#include
#include
#include
#include
union semun
{
int val;
struct semid_ds *buf;
unsigned short *array; /* 不做要求 */
struct seminfo *__buf; /* 不做要求 */
};
#define SEM_FILE "./semfile"
static void print_err(char *estr)
{
perror(estr);
//exit(-1);
}
int creat_or_get_sem(int nsems)
{
int semid;
int fd = -1;
key_t key = -1;
fd = open(SEM_FILE, O_RDWR|O_CREAT, 0664);
if(fd == -1) print_err("open ./semfile fail");
key = ftok(SEM_FILE, 'a');
if(key == -1) print_err("ftok fail");
semid = semget(key, nsems, 0664|IPC_CREAT);
if(semid == -1) print_err("semget fail");
return semid;
}
void init_sem(int semid, int semnum, int val)
{
int ret = -1;
union semun sem_un;
/* semnum:信号量编号
* SETVAL:设置信号量初始值cmd
* sem_un:初始值
*/
sem_un.val = val;
ret = semctl(semid, semnum, SETVAL, sem_un);
if(ret == -1) print_err("semctl fail");
}
void del_sem(int semid)
{
int ret = 0;
ret = semctl(semid, 0, IPC_RMID);
if(ret == -1) print_err("semctl del sem fail");
remove(SEM_FILE);
}
void p_sem(int semid, int semnum_buf[], int nsops)
{
int i = 0;
int ret = -1;
struct sembuf sops[nsops];
for(i=0; i<nsops; i++)
{
sops[i].sem_num = semnum_buf[i];//信号量编号
sops[i].sem_op = -1;//-1 p操作
sops[i].sem_flg = SEM_UNDO;//防止死锁
}
ret = semop(semid, sops, nsops);
if(ret == -1) print_err("semop p fail");
}
void v_sem(int semid, int semnum_buf[], int nsops)
{
int i = 0;
int ret = -1;
struct sembuf sops[nsops];
for(i=0; i<nsops; i++)
{
sops[i].sem_num = semnum_buf[i];//信号量编号
sops[i].sem_op = 1;//+1 v操作
sops[i].sem_flg = SEM_UNDO;//防止死锁
}
ret = semop(semid, sops, nsops);
if(ret == -1) print_err("semop p fail");
}
shm1.c
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "semaphore.h"
#define SHM_FILE "./shmfile"
#define SHM_SIZE 4096
int shmid = -1;
int semid = -1;
void *shmaddr = NULL;
static void print_err(char *estr)
{
perror(estr);
exit(-1);
}
void create_or_get_shm(void)
{
int fd = 0;
key_t key = -1;
fd = open(SHM_FILE, O_RDWR|O_CREAT, 0664);
if(fd == -1) print_err("open fail");
key = ftok(SHM_FILE, 'b');
if(key == -1) print_err("ftok fail");
shmid = shmget(key, SHM_SIZE, 0664|IPC_CREAT);
if(shmid == -1) print_err("shmget fail");
//write(fd, &shmid, sizeof(shmid));
}
char buf[300] = {"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222\
ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff2222222222"};
void signal_fun(int signo)
{
shmdt(shmaddr);
shmctl(shmid, IPC_RMID, NULL);
del_sem(semid);//删除信号量集合
remove("./fifo");
remove(SHM_FILE);
exit(-1);
}
int main(void)
{
int peer_pid = -1;
/* 给SIGINT信号注册捕获函数,用于删除共享内存、管道、文件等 */
signal(SIGINT, signal_fun);
/* 创建、或者获取共享内存 */
create_or_get_shm();
//创建信号量集合
semid = creat_or_get_sem(2);
/* 初始化信号量集合 */
int i = 0;
for(i=0; i<2; i++)
{
//将编号0的信号量初始化为1,其它初始化为0
if(i == 0) init_sem(semid, i, 1);
else init_sem(semid, i, 0);
}
//建立映射
shmaddr = shmat(shmid, NULL, 0);
if(shmaddr == (void *)-1) print_err("shmat fail");
int semnum_buf[1] = {0};//存放信号量的编号
while(1)
{
//p sem 0
semnum_buf[0] = 0;
p_sem(semid, semnum_buf, 1);
/* 向共享内存写数据 */
memcpy(shmaddr, buf, sizeof(buf));
sleep(1);
//v sem 1
semnum_buf[0] = 1;
v_sem(semid, semnum_buf, 1);
}
return 0;
}
shm2.c:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "semaphore.h"
#define SHM_FILE "./shmfile"
#define SHM_SIZE 4096
int shmid = -1;
int semid = -1;
void *shmaddr = NULL;
void print_err(char *estr)
{
perror(estr);
exit(-1);
}
void create_or_get_shm(void)
{
int fd = 0;
key_t key = -1;
fd = open(SHM_FILE, O_RDWR|O_CREAT, 0664);
if(fd == -1) print_err("open fail");
key = ftok(SHM_FILE, 'b');
if(key == -1) print_err("ftok fail");
shmid = shmget(key, SHM_SIZE, 0664|IPC_CREAT);
if(shmid == -1) print_err("shmget fail");
//read(fd, &shmid, sizeof(shmid));
}
void signal_fun(int signo)
{
if(SIGINT == signo)
{
shmdt(shmaddr);
shmctl(shmid, IPC_RMID, NULL);
remove("./fifo");
remove(SHM_FILE);
exit(-1);
}
else if(SIGUSR1 == signo)
{
}
}
int main(void)
{
signal(SIGINT, signal_fun);
/* 创建、或者获取共享内存 */
create_or_get_shm();
//获取别人创建号的信号量
semid = creat_or_get_sem(2);
//建立映射
shmaddr = shmat(shmid, NULL, 0);
if(shmaddr == (void *)-1) print_err("shmat fail");
int semnum_buf[1] = {0};//存放信号量编号
while(1)
{
//p sem 1
semnum_buf[0] = 1;
p_sem(semid, semnum_buf, 1);
//从共享内存去除数据并打印显示
printf("%s\n", (char *)shmaddr);
bzero(shmaddr, SHM_SIZE);//清空共享内存
//v sem 0
semnum_buf[0] = 0;
v_sem(semid, semnum_buf, 1);
}
return 0;
}