简介
线程与进程对比
多线程模型主要优势为线程间切换代价较小,因此适用于I/O密集型的工作场景(如上传下载),因为I/O密集型的工作场景经常会由于I/O阻塞导致频繁的切换线程。
多进程模型的优势是CPU,多进程模型适用于需要频繁的计算场景,比如多机分布式,其实网络编程也可以说是一种跨主机进程通信。
多进程场景
一.多进程场景,进程间需要通信
1.数据传输
一个进程需要将它的数据发送给另一个进程;
2.资源共享
多个进程之间共享同样的资源;
3.通知事件
一个进程需要向另一个或一组进程发送消息,通知它们发生了某种事件;
4.进程控制
有些进程希望完全控制另一个进程的执行(如Debug进程),
该控制进程希望能够拦截另一个进程的所有操作,并能够及时知道它的状态改变。
基于以上几个原因,特定的环境下我们需要进程间能通信。怎么才能通信?
二.进程间通信的基本原理
每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信机制(IPC)。
三.进程间通信的几种常用方式概述
无名管道( pipe )
:管道是一种半双工的通信方式,数据只能单向流动,
而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。有名管道 (named pipe)
: 有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。消息队列( message queue )
: 消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。
消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。信号量( semophore )
: 信号量是一个计数器,可以用来控制多个进程对共享资源的访问。
它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。
因此,主要作为进程间以及同一进程内不同线程之间的同步手段。共享内存( shared memory )
:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。
共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。
它往往与其他通信机制,如信号,配合使用,来实现进程间的同步和通信。
进程通信方式的详细讲解
1.无名管道(pipe),也可直接叫管道
这是一种最基本的IPC(Inter-Process Communication,进程间通信)机制,由pipe函数创建:
#include <unistd.h>
int pipe(int pipefd[2]);
返回值:成功返回0,失败返回-1;
调用pipe函数时在内核中开辟一块缓冲区用于通信,它有一个读端,一个写端:
pipefd[0]指向管道的读端,
pipefd[1]指向管道的写端。
所以管道在用户程序看起来就像一个打开的文件,通过read(pipefd[0])或者write(pipefd[1])向这个文件读写数据,其实是在读写内核缓冲区。
使用管道的通信过程:
1)父进程调用pipe开辟管道,得到两个文件描述符指向管道的两端。
2)父进程调用fork创建子进程
,那么子进程也有两个文件描述符指向同一管道。
3)父进程关闭管道读端,子进程关闭管道写端。或者反过来,父进程关闭管道写端,子进程关闭管道读端。
父进程可以往管道里写,子进程可以从管道里读,父进程传信息给子进程,反之亦然。
上面是让父子进程通信,也可以fork两次,让两个子进程,以类似的方式通信。
这里为什么只能是单向,不能双向呢?
管道是用环形队列实现的,数据从写端流入从读端流出,假设所有端口开启,父、子进程同时读写数据,那么管道中的信息就会参杂在一块,读出的数据必定不是乱的,无法区分,通信就没有意义了。
管道可能出现的四种特殊情况:
- 写端关闭,读端不关闭;
那么管道中剩余的数据都被读取后,再次read会返回0,就像读到文件末尾一样。 - 写端不关闭,但是也不写数据,读端不关闭;
此时管道中剩余的数据都被读取之后再次read会被阻塞,直到管道中有数据可读了才重新读取数据并返回; - 读端关闭,写端不关闭;
此时该进程会收到信号SIGPIPE,通常会导致进程异常终止。 - 读端不关闭,但是也不读取数据,写端不关闭;
此时当写端被写满之后再次write会阻塞,直到管道中有空位置了才会写入数据并重新返回。
使用管道的缺点:
- 两个进程通过一个管道只能实现单向通信。
- 只能用于具有亲缘关系的进程间通信,例如父子,兄弟进程。
无名管道例子:父进程向子程进发送信息,子进程接收信息
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main(){
//创建管道
int _pipe[2]; //0,读端,1,写端
int ret = pipe(_pipe);
if(ret!=0){
printf("管道创建有错!\n");
return 1;
}
pid_t pid = fork(); //创建子进程
if(pid < 0){
printf("子进程创建异常!\n");
return 2;
}
if(pid == 0){ //等于0,创建子进程成功
close(_pipe[1]); //关闭写端,保留读端
char _mesg[100];
int j =0;
while(j<10){
memset(_mesg,'\0',sizeof(_mesg));
read(_pipe[0],_mesg,sizeof(_mesg));
printf("读到的父进程数据:%s------pid:%d\n",_mesg,pid);
j++;
}
}
if(pid > 0){
//父进程
close(_pipe[0]); //关闭读端,保留写端,达到向子进程写信息
//写入数据
int i=0;
char * _mesg = NULL;
while (i<100)
{
_mesg = "I am father!";
write(_pipe[1],_mesg,strlen(_mesg)+1);
sleep(1);
i++;
}
}
return 0;
}
//编译执行
root@ubuntu:/mnt/hgfs/workspaceC/process/ipc# gcc -g pipe.c -o pipe
root@ubuntu:/mnt/hgfs/workspaceC/process/ipc# ./pipe
读到的父进程数据:I am father!------pid:0
读到的父进程数据:I am father!------pid:0
读到的父进程数据:I am father!------pid:0
读到的父进程数据:I am father!------pid:0
读到的父进程数据:I am father!------pid:0
读到的父进程数据:I am father!------pid:0
读到的父进程数据:I am father!------pid:0
读到的父进程数据:I am father!------pid:0
读到的父进程数据:I am father!------pid:0
读到的父进程数据:I am father!------pid:0
2.命名管道(FIFO)
概念
管道的一个不足之处是没有名字,因此,只能用于具有亲缘关系的进程间通信,在命名管道(named pipe 或FIFO)提出后,该限制得到了克服。
命名管道不同于管道之处在于它提供一个路径名与之关联,以命名管道的文件形式存储于文件系统中。
命名管道是一个设备文件,因此,即使某进程与创建命名管道的进程不存在亲缘关系,只要可以访问该路径,就能够通过命名管道相互通信。
值得注意的是,命名管道总是按照先进先出的原则操作(保留匿名管道的特性),第一个被写入的数据将先从管道中读出。
命名管道的创建与读写
Linux 下有两种方式创建命名管道。
一是在Shell下交互地建一个命名管道,二是在程序中使用系统函数建命名管道。
Shell 方式下可使用mknod 或mkfifo命令,下面命令使 mknod 创建了一个命名管道:
mknod namedpipe
创建命名管道的系统函数有两个:mknod 和mkfifo。
两个函数均定义在头件sys/stat.h ,
函数原型如下:
#include <sys/types.h>
#include <sys/stat.h>
int mknod(const char *path,mode_t mod,dev_t dev);
int mkfifo(const char *path,mode_t mode);
函数mknod 参数:
path 为创建的命名管道的全路径名;
mod 为创建的命名管道的模式和明其存取权限,一般用 S_IFIFO表示要创建一个FIFO文件,0666就可以了;
dev为设备值,它只在创建设备时才会用到,一般文件用0就可以了。
函数mkfifo两个参数的含义和mknod前两个相同。
mknod 是较老的函数,使用mkfifo函数更加简单和规范,所以建议尽量使用mkfifo而不是mknod 。
这两个函数调用成功都返回0,失败都返回-1。
下面使用 mknod 函数创建了个命名管道:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
//创建一个命名管道‘文件’,它是一个设备文件,linux中该文件有p标志
int main(){
umask(0);
if(mknod("/tmp/fifo",S_IFIFO | 0666,0) == -1){
perror("mkfifo error!\n");
exit(1);
}
umask(0002);
return 0;
}
//编译与执行后
root@ubuntu:/mnt/hgfs/workspaceC/process/ipc# cd /tmp
root@ubuntu:/tmp# ll
total 84
drwxrwxrwt 18 root root 4096 May 21 18:42 ./
drwxr-xr-x 20 root root 4096 Apr 6 10:39 ../
prw-rw-rw- 1 root root 0 May 21 18:41 fifo|
下面是使用 mkfifo的代例代码:是我们推荐用的
umask(0);
if (mkfifo("/tmp/fifo",S_IFIFO | 0666) == -1) {
perror("mkfifo error!");
exit(1);
}
umask(0002);
使用命名管道时,必须先调open()将其打开。
因为命名管道是一个存在于硬盘上的文件,匿名管道是存在于内存中的特殊文件。
需要注意的是,调用open()打开命名管道的进程可能会被阻塞。
但如果同时用读、写方式(O_RDWR)打开,则一定不会导致阻塞;
如果以只读方式(O_RDONLY)打开,则调open()函数的进程将会被阻塞,直到有写方式打开管道;
同样只以写方(O_WRONLY)打开也会阻塞直到有读方式打开管道。
程序实例:
server写端,并创建命名管道文件:
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#define _PATH_ "/tmp/file.tmp"
#define _SIZE_ 100
int main(){
int ret = mkfifo(_PATH_,S_IFIFO|0666);
if(ret !=0){
printf("mkfifo error!\n");
exit(1);
}
printf("写端(服务端)打开文件阻塞!\n");
int fd = open(_PATH_,O_WRONLY); //只写模式,会阻塞其它进程,直到另一个进程以只读方式打开之
if(fd < 0){
printf("open error!\n");
exit(1);
}
char buf[_SIZE_];
memset(buf,'\0',sizeof(buf)); //初始化缓冲区为0
while(1){
printf("下面是scanf等待输入阻塞:\n");
scanf("%s",buf);
int ret = write(fd,buf,strlen(buf)+1);
if(ret < 0){
printf("写入文件错误 !\n");
break;
}
if(strncmp(buf,"quit",4)==0){ //比较buf和quit是否相同,相同返回0,如果输入quit即退出循环
break;
}
}
close(fd);
return 0;
}
client读端:
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#define _PATH_ "/tmp/file.tmp"
#define _SIZE_ 100
int main(){
printf("下面是读端打开文件阻塞:\n");
int fd = open(_PATH_,O_RDONLY);
if(fd <0){
printf("open file error!\n");
exit(1);
}
char buf[_SIZE_];
memset(buf,'\0',sizeof(buf));
while(1){
int ret = read(fd,buf,sizeof(buf));
if(ret <0){
printf("读错或读完了buf!\n");
}
printf("%s\n",buf);
if(strncmp(buf,"quit",4)==0){
break;
}
}
close(fd);
return 0;
}
编译与执行
打开两个终端(linux),先在一个终端中编译server.c,然后在另一个终端中编译client.c
3.消息队列
消息队列是Linux IPC中很常用的一种通信方式,它通常用来在不同进程间发送特定格式的消息数据。
区别管道通信的主要特点:
可以进行进程间的双向通信
消息队列是基于消息的,管道是基于字节流的
消息队列不一定是先入先出
POSIX消息队列相关函数:
1、mq_open
#include <mqueue.h>
#include <fcntl.h>
#include <sys/stat.h>
mqd_t mq_open(const char *name, int oflag, mode_t mode, struct mq_attr *attr);
用于打开或创建一个消息队列,成功返回消息队列描述符,失败返回-1。
- name:表示消息队列的名字,它符合POSIX IPC的名字规则。以/开头,并且不能再有/。
- oflag:表示打开的方式,和open函数的类似。
有必须的选项:O_RDONLY,O_WRONLY,O_RDWR,
还有可选的选项:O_NONBLOCK,O_CREAT,O_EXCL。 - mode:是一个可选参数,在oflag中含有O_CREAT标志且消息队列不存在时,才需要提供该参数。
表示默认访问权限,0666。 - attr:也是一个可选参数,在oflag中含有O_CREAT标志且消息队列不存在时才需要。该参数用于给新队列设定某些属性,如果是空指针,那么就采用默认属性。
mq_open返回值是mqd_t类型的值,被称为消息队列描述符。
2、mq_close
#include <mqueue.h>
#include <fcntl.h>
#include <sys/stat.h>
mqd_t mq_close(mqd_t mqdes);
用于关闭一个消息队列,关闭后,消息队列并不从系统中删除。
一个进程结束,会自动关闭打开着的消息队列。
3、mq_unlink
#include <mqueue.h>
#include <fcntl.h>
#include <sys/stat.h>
mqd_t mq_unlink(const char *name);
用于删除一个消息队列。消息队列创建后可以通过调用该函数进行删除。
4、mq_getattr和mq_setattr
POSIX消息队列的属性
POSIX标准规定消息队列属性mq_attr必须要含有以下四个内容:
long mq_flags //消息队列的标志:0阻塞,O_NONBLOCK不阻塞
long mq_maxmsg //消息队列的最大消息数
long mq_msgsize //消息队列中每个消息的最大字节数
long mq_curmsgs //消息队列中当前的消息数目,用户不可修改
函数原型:
#include <mqueue.h>
#include <fcntl.h>
#include <sys/stat.h>
mqd_t mq_getattr(mqd_t mqdes, struct mq_attr *attr);
mqd_t mq_setattr(mqd_t mqdes, struct mq_attr *newattr, struct mq_attr *oldattr);
- mq_setattr可以设置的属性只有mq_flags,用来设置消息队列的非阻塞和阻塞,newattr结构的其他属性被忽略。
- mq_maxmsg和mq_msgsize属性只能在创建消息队列时通过mq_open来设置,
- mq_open只会设置该两个属性,忽略另外两个属性。
- mq_curmsgs属性只能被获取而不能被设置。
下面是测试代码,注意:编译依赖动态库:librt.so:
#include <mqueue.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
mqd_t mqID;
mqID = mq_open("/anonymQueue", O_RDWR | O_CREAT, 0666, NULL);
if (mqID < 0) {
printf("mq_opene error\n");
exit(1);
}
struct mq_attr mqAttr;
if (mq_getattr(mqID, &mqAttr) < 0) {
printf("omq_getattr error\n");
exit(1);
}
printf("mq_flags:%ld \n",mqAttr.mq_flags);
printf("mq_maxmsg:%ld \n",mqAttr.mq_maxmsg);
printf("mq_msgsize:%ld \n",mqAttr.mq_msgsize);
printf("mq_curmsgs:%ld \n",mqAttr.mq_curmsgs);
mqAttr.mq_flags = O_NONBLOCK;
if (mq_setattr(mqID, &mqAttr,NULL) < 0) {
printf("mq_setattr error\n");
exit(1);
}
printf("mq_flags:%ld \n",mqAttr.mq_flags);
printf("mq_maxmsg:%ld \n",mqAttr.mq_maxmsg);
printf("mq_msgsize:%ld \n",mqAttr.mq_msgsize);
printf("mq_curmsgs:%ld \n",mqAttr.mq_curmsgs);
}
//编译执行,依赖系统的动态库librt.so
# gcc xiaoxitest.c -o xiaoxitest -lrt
#./xiaoxitest
mq_flags:0 //默认是阻塞的
mq_maxmsg:10
mq_msgsize:8192
mq_curmsgs:0
mq_flags:2048 //修改为非阻塞
mq_maxmsg:10
mq_msgsize:8192
mq_curmsgs:0
5、mq_send和mq_receive两函数发送和接收消息:
#include <mqueue.h>
#include <fcntl.h>
#include <sys/stat.h>
mqd_t mq_send(mqd_t mqdes, const char *msg_ptr, size_t msg_len, unsigned msg_prio);
成功返回0,出错返回-1
mqd_t mq_receive(mqd_t mqdes, char *msg_ptr, size_t msg_len, unsigned *msg_prio);
成功返回接收到消息的字节数,出错返回-1
mq_send
向消息队列中写入一条消息,mq_receive
从消息队列中读取一条消息。
- mqdes:消息队列描述符;
- msg_ptr:指向消息体缓冲区的指针;
- msg_len:消息体的长度。
- msg_prio:消息的优先级;它是一个小于MQ_PRIO_MAX的数,数值越大,优先级越高。
POSIX消息队列在调用mq_receive时总是返回队列中最高优先级的最早消息。
如果消息不需要设定优先级,那么可以在mq_send是置msg_prio为0,
mq_receive的msg_prio置为NULL。
下面是消息队列使用的测试代码:
send.c,先发后收
#include <fcntl.h>
#include <mqueue.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdlib.h>
#define MQ_FILE "/hello"
int main(){
char input[100]={0};
struct mq_attr attr; //消息队列属性
mqd_t mqID;
int get_num =0; //接收消息的字节数
int prio = 0; //消息队列中的消息优先级,数越大,优先级越高
//创建消息队列,后执行程序,先启动发送端
mqID = mq_open(MQ_FILE,O_RDWR | O_CREAT |O_EXCL,0666,NULL); //NULL表示默认消息属性
if(mqID <0){
if(mqID == -1){
mq_unlink(MQ_FILE); //删除消息队列
mqID = mq_open(MQ_FILE,O_RDWR | O_CREAT,0666,NULL);
}else{
printf("mq_open error!\n");
exit(1);
}
}
while(1){ //要一直发消息,接收消息
//先发
printf("请输入要发送的消息!\n");
scanf("%s",input);
if(strncmp(input,"quit",4)==0){ //如果发送的信息头四个是quit,则退出发送阻塞
mq_send(mqID,input,strlen(input),0);
mq_unlink(MQ_FILE); //不玩了,删除消息队列
break; //退出循环
}
mq_send(mqID,input,strlen(input),0); //发送除 quit的消息
//后收回复的消息
mq_getattr(mqID,&attr);
get_num = mq_receive(mqID,input,attr.mq_msgsize,&prio);
if(get_num <0){
printf("mq_receive error!\n");
sleep(1); //不能退出(exit),要不然的话会结束会话
}
if(strncmp(input,"quit",4)==0){
break; //即然回复的是quit,说明发送方退出了,我也要退出了
}
printf("收到的消息内容: %s,字节数:%d,优先级: %d\n",input,get_num,prio);
}
return 0;
}
rec.c先收再发,打开消息队列,而不创建之
#include <fcntl.h>
#include <mqueue.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#define MQ_FILE "/hello"
int main(){
char buf[100]={0};
struct mq_attr attr; //消息队列属性
mqd_t mqID;
int get_num =0; //接收消息的字节数
int prio = 0; //消息队列中的消息优先级,数越大,优先级越高
mqID = mq_open(MQ_FILE,O_RDWR); //只打开消息队列
if(mqID <0){
printf("open error!\n");
}
while(1){
//先收发送来的消息
mq_getattr(mqID,&attr);
get_num = mq_receive(mqID,buf,attr.mq_msgsize,&prio);
if(get_num <0){
printf("mq_receive error!\n");
sleep(1); //不能退出(exit),要不然的话会结束会话
}
if(strncmp(buf,"quit",4)==0){
break; //即然回复的是quit,说明发送方退出了,我也要退出了
}
printf("收到的消息内容: %s,字节数:%d,优先级: %d\n",buf,get_num,prio);
//后发
printf("请输入要发送的消息!\n");
scanf("%s",buf);
if(strncmp(buf,"quit",4)==0){ //如果发送的信息头四个是quit,则退出发送阻塞
mq_send(mqID,buf,strlen(buf),0);
mq_unlink(MQ_FILE); //不玩了,删除消息队列
break; //退出循环
}
mq_send(mqID,buf,strlen(buf),0); //发送除 quit的消息
}
return 0;
}
编译行执,打开两个linux终端,分别编译执行之
# gcc -g send.c -o send -lrt #gcc -g recive.c -o rec -lrt
#./hello #./rec
请输入要发送的消息! 收到的消息内容: hello,字节数:5,优先级: 0
hello 请输入要发送的消息!
k l
收到的消息内容: k l,字节数:3,优先级: 0
注意:POSIX消息队列的限制
POSIX消息队列本身的限制就是mq_attr中的mq_maxmsg和mq_msgsize,
这两个参数可以在调用mq_open创建一个消息队列的时候设定。
但这个设定是受到系统内核限制的。
# ulimit -a |grep message
限制大小为800KB,该大小是整个消息队列的大小,不仅仅是最大消息数*消息的最大大小;还包括消息队列的额外开销。
MQ_OPEN_MAX:一个进程能同时打开的消息队列的最大数目,POSIX要求至少为8
MQ_PRIO_MAX:消息的最大优先级,POSIX要求至少为32
4.信号量
信号量的本质是一种数据操作锁,只是一种外部资源的标识,信号量本身不具备通信能力。
简单说一下信号量的工作机制:
信号量只能进行两种操作等待和发送信号,即P(sv)
和V(sv)
他们的行为是这样的:
P(sv) :
如果sv 的值大于零,就给它减掉一个值;值减少到零,就阻塞进程。
V(sv) :
恢复进程运行,给sv值增加。
Linux下信号量相关函数
信号量函数由semget、semop、semctl三个函数组成,这个三个函数来自System V标准体系。
(1)semget,得到一个信号量集标识符或创建一个信号量集对象
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
-
key:0(IPC_PRIVATE):会建立新信号量集对象
大于0的32位整数:通常要求此值来源于ftok返回的IPC键值,这个key和semflg有关联 -
nsems:创建信号量集中信号量的个数,该参数只在创建信号量集时有效
-
semflg:0:取信号量集标识符,若不存在则函数会报错
IPC_CREAT:当semflg&IPC_CREAT为真时,如果内核中不存在键值与key相等的信号量集,则新建一个信号量集;如果存在这样的信号量集,返回此信号量集的标识符
IPC_CREAT|IPC_EXCL:如果内核中不存在键值与key相等的信号量集,则新建一个信号量集;如果存在这样的信号量集则报错
(2)semop,对信号量集标识符为semid中的一个或多个信号量进行P操作或V操作
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semop(int semid, struct sembuf *sops, unsigned nsops)
-
semid:信号量集标识符
-
sops:指向进行操作的信号量集结构体数组的首地址,此结构的具体说明如下:
struct sembuf {
short semnum; 信号量集合中的信号量编号,0代表第1个信号量
short val;
若val>0进行V操作信号量值加val,表示进程释放控制的资源
若val<0进行P操作信号量值减val,若(semval-val)<0,则调用进程阻塞,直到资源可用;
//若设置IPC_NOWAIT不会睡眠,进程直接返回EAGAIN错误*/
若val==0时,调用进程进入睡眠状态,直到信号值为0;若设置IPC_NOWAIT,进程不会睡眠,直接返回EAGAIN错误
short flag;
0 设置信号量的默认操作
PC_NOWAIT设置信号量操作不等待
SEM_UNDO 选项会让内核记录一个与调用进程相关的UNDO记录,如果该进程崩溃,则根据这个进程的UNDO记录自动恢复相应信号量的计数值
}; -
nsops:进行操作信号量的个数,即sops结构变量的个数,需大于或等于1。
最常见设置此值等于1
,只完成对一个信号量的操作
成功:返回信号量集的标识符,失败返回-1
sops为指向sembuf数组,定义所要进行的操作序列。
下面是信号量操作举例:
struct sembuf sem_get={0,-1,IPC_NOWAIT}; /*将信号量对象中序号为0的信号量减1*/
struct sembuf sem_get={0,1,IPC_NOWAIT}; /*将信号量对象中序号为0的信号量加1*/
struct sembuf sem_get={0,0,0}; /*进程被阻塞,直到对应的信号量值为0*/
flag一般为0,若flag包含IPC_NOWAIT,则该操作为非阻塞操作。
若flag包含SEM_UNDO,则当进程退出的时候会还原该进程的信号量操作,这个标志在某些情况下是很有用的,比如某进程做了P操作得到资源,但还没来得及做V操作时就异常退出了,此时,其他进程就只能都阻塞在P操作上,于是造成了死锁。若采取SEM_UNDO标志,就可以避免因为进程异常退出而造成的死锁。
(3)semctl,可以对信号量集中的单个信号量进行很多操作
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, union semun arg)
- semid:信号量集标识符
- semnum:信号量集数组上的下标,表示某一个信号量
- cmd:
IPC_RMID
:从内核中删除信号量集合
SETVAL
:用联合体中val成员的值设置信号量集合中单个信号量的值
IPC_STAT
:从信号量集上检索semid_ds结构,并存到semun联合体参数的成员buf的地址中semid_ds内核为每个信号量集合维护着一个结构体
IPC_SET
:设置一个信号量集合的semid_ds里的值,并从semun的buf中取出值
GETALL和SETALL
:GETALL从信号量集合中获得所有信号量的值,并把其整数值存到semun联合体, SETALL反过来设置
arg:
union semun {
short val; /*SETVAL用的值*/
struct semid_ds* buf; /*IPC_STAT、IPC_SET用的semid_ds结构*/
unsigned short* array; /*SETALL、GETALL用的数组值*/
struct seminfo *buf; /*为控制IPC_INFO提供的缓存*/
} arg;
信号量本身不具备通信的能力,例子我们放到下面共享内存,联合演示。
5.共享内存(最高效的通信方式)
(1)什么是共享内存
特别提醒:共享内存并未提供同步机制,也就是说,在第一个进程结束对共享内存的写操作之前,并无自动机制可以阻止第二个进程开始对它进行读写。所以我们通常需要用其他的机制来同步对共享内存的访问,例如前面说到的信号量
。
(2)共享内存的使用涉及到的函数
与信号量一样,在Linux中也提供了一组函数接口用于使用共享内存,而且使用共享共存的接口还与信号量的非常相似,而且比使用信号量的接口来得简单。
1)shmget函数,创建共享内存
原型为:
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
第一个参数,与信号量的semget函数一样,程序需要提供一个参key(非0 整数),
它有效地为共享内存段命名。
第二个参数,size以字节为单位指定需要共享的内存容量
第三个参数,shmflg是权限标志,它的作用与open函数的mode参数一样,
如果要想在key标识的共享内存不存在时,创建它的话,可以与IPC_CREAT做或操作。
共享内存的权限标志与文件的读写权限一样,0644表示允许一个进程创建的共享内存被
内存创建者所拥有的进程向共享内存读取和写入数据,
同时其他用户创建的进程只能读取共享内存。
shmget函数成功时返回一个与key相关的共享内存标识符(非负整数),用于后续的共享内存函数。失败返回-1
2)shmat函数,启动对该共享内存的访问,并把共享内存连接到当前进程的地址空间。
原型:
void *shmat(int shm_id, const void *shm_addr, int shmflg);
第一个参数,shm_id是由shmget函数返回的共享内存标识。
第二个参数,shm_addr指定共享内存连接到当前进程中的地址,通常为空,让系统来选择共享内存的地址。
第三个参数,shm_flg是一组标志位,通常为0,详情到linux中,用man shmat查看。
调用成功时返回一个指向共享内存第一个字节的指针,如果调用失败返回-1.
3)shmdt函数,将共享内存从当前进程中分离。
注意,将共享内存分离并不是删除它,只是使该共享内存对当前进程不再可用。
原型如下:
int shmdt(const void *shmaddr);
参数shmaddr是shmat函数返回的地址指针,调用成功时返回0,失败时返回-1.
4)shmctl函数,与信号量的semctl函数一样,用来控制共享内存,
原型如下:
int shmctl(int shm_id, int command, struct shmid_ds *buf);
- 第一个参数,shm_id是shmget函数返回的共享内存标识符。
- 第二个参数,command是要采取的操作,它可以取下面的三个值 :
IPC_STAT:把shmid_ds结构中的数据取出来,放到后面的buf
IPC_SET:如果进程有足够的权限,把后面的buf的值设置到内核shmid_ds结构去。
IPC_RMID:删除共享内存段 - 第三个参数,buf是一个结构指针,它指向结构体是内核中用来维护共享内存的内部结构体。
使用共享内存进行进程间通信
大概思路:假如两个进程,一个往共享内中写,一个从共享内存中读,为了两个进程同步,我们设置两个信号量,当信号量值为1,0时,进程只可写,另一个进程看到1,0,就不读了,当写完操作做好了,再把信号量减去为0,1了,释放共享内存,让另一个进程读。
文件shmread.c
创建共享内存,并读取其中的信息,另一个文件shmwrite.c
向共享内存中写入数据。
为了方便操作和数据结构的统一,为这两个文件定义了相同的数据结构,定义在文shmdata.h
中。
shmdata.h的源代码如下:定义了一些宏和共用体
# ifndef _SHM_SEM_H
# define _SHM_SEM_H
# define SHM_KEY 38111
# define SEM_KEY 38222
# define SHM_SIZE 2048
# define SEM_NUM 2
union semun
{
int val; /*for SETVAL*/
struct semid_ds *buf; /*for IPC_STAT ans IPC_SET*/
unsigned short *array; /*for GETALL ans SET_ALL*/
};
# endif
源文件shmread.c的源代码如下:
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>
#include "shmdata.h"
/*sem P*/
int sem_p(int semid, int semnum) {
/*int semop(int semid, struct sembuf semoparray[], size_t cnts); */
struct sembuf semOpr;
semOpr.sem_num = semnum;
semOpr.sem_op = -1;
semOpr.sem_flg = SEM_UNDO;
return semop(semid, &semOpr, 1);
}
/*sem V*/
int sem_v(int semid, int semnum) {
/*int semop(int semid, struct sembuf semoparray[], size_t cnts); */
struct sembuf semOpr;
semOpr.sem_num = semnum;
semOpr.sem_op = 1;
semOpr.sem_flg = SEM_UNDO;
return semop(semid, &semOpr, 1);
}
/*get semval*/
int get_semval(int semid, int semnum) { return semctl(semid, semnum, GETVAL); }
int main(void) {
int ret;
int shmid;
int semid;
void *ptrShm = NULL;
struct shmid_ds shm_ds;
struct semid_ds sem_ds;
char buf[1024] = {0};
union semun arg;
unsigned short semvalArr[SEM_NUM];
memset(&shm_ds, 0x00, sizeof(shm_ds));
memset(&sem_ds, 0x00, sizeof(sem_ds));
/*semget*/
semid = semget((key_t)SEM_KEY, SEM_NUM, 0660 | IPC_CREAT | IPC_EXCL);
if (semid == -1) {
if (errno == EEXIST) /*no need to init sem*/
{
printf("semget() warning: %s\n", strerror(errno));
semid = semget((key_t)SEM_KEY, 0, 0660 | IPC_CREAT);
if (semid == -1) {
printf("semget() error: %s\n", strerror(errno));
return -1;
}
printf("semget() success. semid=[%d]\n", semid);
} else {
printf("semget() error: %s\n", strerror(errno));
return -1;
}
} else /*need to init sem*/
{
printf("semget() success. semid=[%d]\n", semid);
/*semctl(): init sem, set semvalArr[0]=1 semvalArr[1]=0*/
semvalArr[0] = (unsigned short)1;
semvalArr[1] = (unsigned short)0;
arg.array = semvalArr;
ret = semctl(semid, 0, SETALL, arg);
if (ret == -1) {
printf("semctl() SETALL error: %s\n", strerror(errno));
printf("init sem error.\n");
ret = semctl(semid, 0, IPC_RMID);
if (ret == -1) {
printf("semctl() error: %s\n", strerror(errno));
}
printf("semctl() success. Sem is deleted.\n");
return -1;
}
printf("semget() success.\n");
}
printf("semval[0]=[%d] semval[1]=[%d].\n", get_semval(semid, 0), get_semval(semid, 1));
/*shmget*/
shmid = shmget((key_t)SHM_KEY, SHM_SIZE, 0660 | IPC_CREAT | IPC_EXCL);
if (shmid == -1) {
if (errno == EEXIST) {
printf("shmget() warning: %s\n", strerror(errno));
shmid = shmget((key_t)SHM_KEY, SHM_SIZE, 0660 | IPC_CREAT);
if (shmid == -1) {
printf("shmget() error: %s\n", strerror(errno));
return -1;
}
} else {
printf("shmget() error: %s\n", strerror(errno));
return -1;
}
}
printf("shmget() success. shmid=[%d]\n", shmid);
/*shmat*/
ptrShm = shmat(shmid, NULL, 0);
if (ptrShm == NULL) {
printf("shmat() error: %s\n", strerror(errno));
goto FAILED;
}
printf("shmat() success.\n");
printf("Begin to read shared memory.\n");
while (1) {
/*test semvalArr[0]*/
ret = get_semval(semid, 1);
if (ret < 0) /**/
{
printf("get_semval() error.\n");
goto FAILED;
} else if (ret == 0) /*It suggests that the server hasn't start yet*/
continue;
else
break;
}
/*##########################################*/
/*Begin to read*/
while (1) {
/*P*/
ret = sem_p(semid, 1);
if (ret == -1) {
printf("sem_p() error\n");
goto FAILED;
}
/*read shm*/
memset(&buf, 0x00, sizeof(buf));
memcpy(buf, (char *)ptrShm, sizeof(buf));
/*V*/
ret = sem_v(semid, 0);
if (ret == -1) {
printf("sem_v() error\n");
goto FAILED;
}
printf("client: %s", buf);
if (buf[0] == '\0') printf("\n");
if (memcmp(buf, "exit", 4) == 0) {
break;
}
}
/*shmdt*/
ret = shmdt(ptrShm);
if (ret == -1) {
printf("shmdt() error: %s\n", strerror(errno));
}
printf("shmdt() success.\n");
/*shmctl IPC_RMID*/
ret = shmctl(shmid, IPC_RMID, &shm_ds);
if (ret == -1) {
printf("shmctl() error: %s\n", strerror(errno));
}
printf("shmctl() success. Shm is deleted.\n");
/*semctl IPC_RMID*/
ret = semctl(semid, 0, IPC_RMID);
if (ret == -1) {
printf("semctl() error: %s\n", strerror(errno));
}
printf("semctl() success. Sem is deleted.\n");
return 0;
FAILED:
/*shmctl IPC_RMID*/
ret = shmctl(shmid, IPC_RMID, &shm_ds);
if (ret == -1) {
printf("shmctl() error: %s\n", strerror(errno));
}
printf("shmctl() success. Shm is deleted.\n");
/*semctl IPC_RMID*/
ret = semctl(semid, 0, IPC_RMID);
if (ret == -1) {
printf("semctl() error: %s\n", strerror(errno));
}
printf("semctl() success. Sem is deleted.\n");
return -1;
}
源文件shmwrite.c的源代码如下:
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>
#include "shmdata.h"
/*sem P*/
int sem_p(int semid, int semnum) {
struct sembuf semOpr;
semOpr.sem_num = semnum;
semOpr.sem_op = -1;
semOpr.sem_flg = SEM_UNDO;
return semop(semid, &semOpr, 1);
}
/*sem V*/
int sem_v(int semid, int semnum) {
struct sembuf semOpr;
semOpr.sem_num = semnum;
semOpr.sem_op = 1;
semOpr.sem_flg = SEM_UNDO;
return semop(semid, &semOpr, 1);
}
/*get semval*/
int get_semval(int semid, int semnum) { return semctl(semid, semnum, GETVAL); }
int main(void) {
int ret;
int shmid;
int semid;
void *ptrShm = NULL;
struct shmid_ds shm_ds;
struct semid_ds sem_ds;
char buf[1024] = {0};
union semun arg;
unsigned short semvalArr[SEM_NUM] = {1, 0};
memset(&shm_ds, 0x00, sizeof(shm_ds));
memset(&sem_ds, 0x00, sizeof(sem_ds));
/*semget*/
semid = semget((key_t)SEM_KEY, SEM_NUM, 0660 | IPC_CREAT | IPC_EXCL);
if (semid == -1) {
if (errno == EEXIST){
printf("semget() warning: %s\n", strerror(errno));
semid = semget((key_t)SEM_KEY, 0, 0660 | IPC_CREAT); /*nsems 可以是 0 当semid已存在*/
if (semid == -1) {
printf("semget() error: %s\n", strerror(errno));
return -1;
}
printf("semget() success. semid=[%d]\n", semid);
} else {
printf("semget() error: %s\n", strerror(errno));
return -1;
}
} else /*need to init sem*/
{
printf("semget() success. semid=[%d]\n", semid);
arg.array = semvalArr;
ret = semctl(semid, 0, SETALL, arg);
if (ret == -1) {
printf("semctl() SETALL error: %s\n", strerror(errno));
printf("init sem error.\n");
/*semctl IPC_RMID*/
ret = semctl(semid, 0, IPC_RMID);
if (ret == -1) {
printf("semctl() error: %s\n", strerror(errno));
}
printf("semctl() success. Sem is deleted.\n");
return -1;
}
printf("semget() success.\n");
}
printf("init sem success. semval[0]=[%d] semval[1]=[%d].\n", get_semval(semid, 0), get_semval(semid, 1));
/*shmget*/
shmid = shmget((key_t)SHM_KEY, SHM_SIZE, 0660 | IPC_CREAT | IPC_EXCL);
if (shmid == -1) {
if (errno == EEXIST) {
printf("shmget() warning: %s\n", strerror(errno));
shmid = shmget((key_t)SHM_KEY, SHM_SIZE, 0660 | IPC_CREAT);
if (shmid == -1) {
printf("shmget() error: %s\n", strerror(errno));
return -1;
}
} else {
printf("shmget() error: %s\n", strerror(errno));
return -1;
}
}
printf("shmget() success. shmid=[%d]\n", shmid);
/*shmat*/
ptrShm = shmat(shmid, NULL, !SHM_RDONLY); /*void *shmat(int shmid, const void *shmarr, int flag);*/
if (ptrShm == NULL) {
printf("shmat() error: %s\n", strerror(errno));
goto FAILED;
}
printf("shmat() success.\n");
/*Begin to write*/
printf("Begin to write shared memory.\n");
while (1) {
/*input*/
memset(&buf, 0x00, sizeof(buf));
printf("server: ");
if (fgets(buf, sizeof(buf) - 1, stdin) < 0) {
printf("fgets() error\n");
fflush(stdin);
continue;
}
fflush(stdin);
/*P*/
ret = sem_p(semid, 0);
if (ret == -1) {
printf("sem_p() error\n");
goto FAILED;
}
/*write into shm*/
memset(ptrShm, 0x00, SHM_SIZE);
memcpy((char *)ptrShm, buf, strlen(buf));
/*V*/
ret = sem_v(semid, 1);
if (ret == -1) {
printf("sem_v() error\n");
goto FAILED;
}
if (memcmp(buf, "exit", 4) == 0) {
break;
}
}
/*shmdt*/
ret = shmdt(ptrShm);
if (ret == -1) {
printf("shmdt() error: %s\n", strerror(errno));
}
printf("shmdt() success.\n");
return 0;
FAILED:
/*shmctl IPC_RMID*/
ret = shmctl(shmid, IPC_RMID, &shm_ds);
if (ret == -1) {
printf("shmctl() error: %s\n", strerror(errno));
}
printf("shmctl() success. Shm is deleted.\n");
/*semctl IPC_RMID*/
ret = semctl(semid, 0, IPC_RMID);
if (ret == -1) {
printf("semctl() error: %s\n", strerror(errno));
}
printf("semctl() success. Sem is deleted.\n");
return -1;
}
Makefile
CC = gcc
编译与执行
- 先make shmwrite,
- 然后make shmread
- 先执执行写 ./shmwrite
- 然后读 ./shmread