linux进程或线程间通信机制主要分为三类:
通信:这些工具关注进程之间的数据交换。
同步:这些进程关注进程和线程操作之间的同步。
信号:在特定场景下可以将信号作为一种同步技术,信号还可以作为一种通信技术。
根据上图总结一下:
用于通信的主要有:管道和FIFO、消息队列(POSIX和SYSTEM V)、共享内存(POSIX和SYSTEM V)、内存映射、socket(数据报和流)、伪终端。
用于同步的主要有: 信号量、互斥量、条件变量等。其实又可以将互斥量,读写锁,自旋锁这一类用于保护临界区的锁归为一类,另一类为信号量和条件变量,用于控制并发线程数量。
信号:单独归为一类,分为标准信号和实时信号。
线程进程的同步机制在linux系统编程-进程和线程相关、锁机制一文中有做详细总结,这里主要说明一下linux的通信机制。
2.1、管道的特点
1、一个管道是一个字节流
管道是不存在消息或消息边界的概念的。从管道中读取数据的进程可以读取任意大小的数据块,而不管写入进程写入管道的数据块的大小是什么。
2、读阻塞和写阻塞
试图从一个当前为空的管道中读取数据将会被阻塞直到至少有一个字节被写入到管道中为止。如果管道的写入端被关闭了,那么从管道中读取数据的进程在读完管道中剩余的所有数据之后将会看到文件结束,(即 read()返回 0)。
同样试图向一个当前已满的管道中写入数据将会被阻塞直到有足够空间的数据被读走为止。如果读取端被关闭了,write会返回EPIPE错误,并收到SIGPIPE信号。
3、管道是单向的
数据的传递方向是单向的。管道的一段用于写入,另一端则用于读取,如果想要实现双向通信的效果,可以创建两条管道。
4、写入不超过 PIPE_BUF 字节的操作是原子的
如果多个进程写入同一个管道,那么如果它们在一个时刻写入的数据量不超过 PIPE_BUF字节,那么就可以确保写入的数据不会发生相互混合的情况在 Linux 上,PIPE_BUF 的值为 4096。
5、管道的容量是有限的。
2.2 示例程序:管道实现父子进程通信
在fork()系统调用后,子进程会继承父进程的文件描述符的副本,如下图所示:
管道数据流是单向的,所以通常两个通信进程需要关闭掉自己不需要使用的端口,如上左右两图,是fork调用后,父进程关闭读取端,子进程关闭写入端。数据流向为父进程往管道里写入数据,子进程读取数据。
下面的示例程序,功能是父进程向管道中写入数据,子进程一小块一小块的从管道中读取数据。在pipe系统调用创建管道后,父进程需要关闭读取端,子进程需要关闭写入端(如上图);父进程将从命令行参数获取到的数据写入管道,子进程通过循环读的方式从管道获取数据,并将获取到的数据打印到屏幕上;当父进程写入数据完成后就会关闭管道的写入端,那么子进程在读取完管道里的剩余数据后,就会读到文件的结尾,这个时候子进程就跳出读循环,同时关闭写入端,程序结束。
#include
#include
#include
#include
#define BUFF_SIZE 1024
int main(int argc,char *argv[])
{
int pfd[2]; //使用pipe创建管道后,pfd[0]表示读取端,pfd[1]表示写入端
char buf[BUFF_SIZE];
ssize_t numread;
if(pipe(pfd)==-1) //pipe创建管道
return -1;
switch(fork()){
case -1:
return -1;
case 0:
if(close(pfd[1])==-1) //子进程关闭写入端
return -1;
while(1){
numread=read(pfd[0],buf,BUFF_SIZE); //子进程循环从读取端读数据到buf数组
if(numread==-1){
printf("read err\n");
return -1;}
if(numread==0){ //read返回0表示写入端已经被父进程关闭,这时跳出循环
break;}
if(write(STDOUT_FILENO,buf,numread)!=numread) //读取到的buf输出到屏幕上STDOUT_FILENO
return -1;
}
write(STDOUT_FILENO,"\n",1);
if(close(pfd[0])==-1) //子进程关闭读取端
return -1;
return 0;
default:
if(close(pfd[0])==-1) //父进程关闭读取端
return -1;
if(write(pfd[1],argv[1],strlen(argv[1]))!=strlen(argv[1])) //父进程向管道写入端写数据,数据为命令行参数
return -1;
if(close(pfd[0])==-1) //父进程关闭写入端,程序结束
return -1;
wait(NULL); //等待子进程返回
return 0;
}
}
程序效果:
2.2 示例程序:管道用于进程同步
管道主要用途是用于进程间通信,但也可用于进程间的同步,这主要是利用了前面讲到的,在关闭管道的写入端后,读取端进程会读到文件的结尾,即read返回0。
下面这个程序的功能是:父进程创建了多个子进程,每个子进程休眠不同的时间,父进程需要等待所有的子进程休眠完毕后,再去执行他自己的操作。
下面的程序数据流向变成了父进程读,子进程写。所以一开始for循环创建了三个子进程,三个子进程都关闭管道的读取端,同时各自休眠2秒、6秒、10秒,休眠完成后,子进程关闭写入端;当所有的子进程完成休眠关闭写入端后,父进程就会读到文件的结尾,这个时候同步操作就完成了,父进程就知道可以做自己的事了。
#include
#include
#include
#include
int main(int argc,char *argv[])
{
int pfd[2];
int sleep_time[3]={2,6,10};
int i,dummy;
if(pipe(pfd)==-1)
return -1;
for(i=0;i<3;i++) //创建三个子进程
{
switch(fork()){
case -1:
return -1;
case 0:
if(close(pfd[0])==-1) //三个子进程关闭读取端
return -1;
sleep(sleep_time[i]); //三个子进程休眠不同的时间2秒、6秒、10秒。
printf("child pid=%d,sleep time=%ds\n",getpid(),sleep_time[i]);
if(close(pfd[1])==-1) //休眠完成后三个子进程关闭写入端
return -1;
return 0;
default:
break;
}
}
if(close(pfd[1])==-1) //父进程关闭写入端
return -1;
if(read(pfd[0],&dummy,1)!=0) //父进程从读取端读取数据,看是否能读到文件结尾。
return -1;
printf("parent get EOF\n"); //这里表示父进程已经知道同步信息,子进程完成休眠。
printf("start parent work now\n");
return 0;
}
程序效果:
2.3 将进程的标准输出、标准输入绑定到管道两端
执行shell命令下的
ls | wc -l
相当于创建了两个进程ls和wc,ls的标准输出stdout被链接到管道的写入端,wc的标准输入stdin被连接到管道的读取端。
还是以上面的图为例,如果我们想要把父进程的标准输出、子进程的标准输入绑定到管道两端,可以
做:
...
char *str_get;
switch(fork()){
...
case 0:
close(pfd[1]); //子进程关闭写端
dup2(pfd[0],STDIN_FILENO); //复制文件描述符,将标准输入和管道读端绑定
close(pfd[0]); //关闭多余的文件描述符
gets(str_get); //从管道读
default:
close(pfd[0]); //父进程关闭读端
dup2(pfd[1],STDOUT_FILENO); //复制文件描述符,将标准输出和管道写端绑定
close(pfd[1]); //关闭多余的文件描述符
puts("test message\n"); //向管道写
}
2.4 与shell命令进行通信
popen系统调用可以执行一条shell 命令并读取其输出或向其发送一些输入。popen()函数的原理就是创建了一个管道,然后创建了一个子进程来执行 shell,而 shell 又创建了一个子进程来执行command 字符串。
FILE *popen(const char *command,const char *mode);
注意popen返回值是一个文件流指针。
举例:通过popen执行shell下的ls命令并读取其输出:
char *buff;
FILE *fp;
fp=popen("ls",r);
fgets(buff,size,fp); //读取ls的输出到buff数组
close(fp);
2.5 FIFO
FIFO和管道的区别:FIFO 在文件系统中拥有一个名称,并且其打开方式与打开一个普通文件是一样的。这样就能够将 FIFO 用于非相关进程之间的通信。管道需要进程有血缘关系才能通信。
使用mkfifo创建一个FIFO:
int mkfifo(const char *pathname,mode_t mode);
例:
int fd1;
int fd2;
#define FIFO_PATH "tmp/fifotest"
mkfifo(FIFO_PATH ,S_IRUSR|S_IWUSR|S_IWGRP);
//进程一需要以只读方式打开FIFO,用于读取数据
fd1=open(FIFO_PATH,O_RDONLY);
//进程二只写方式打开FIFO,用于写数据
fd2=open(FIFO_PATH,O_WRONLY);
//...接下来fd1和fd2就可以使用write和read调用了
...
...
当一个进程打开FIFO的一端,而另一端没有打开,read或write的操作会被阻塞;在写端未被打开,又不希望读取阻塞的情况下,可以在读取进程open打开FIFO时指定O_NONBLOCK实现,这个时候read不会阻塞并且能够返回正确数据;但读端未被打开,在写入进程open打开FIFO时指定O_NONBLOCK是不可以的,open会调用失败。
3.1 system v 消息队列特点
1、引用消息队列的句柄是一个由 msgget()调用返回的标识符。这些标识符与其他形式的 I/O 所使用的文件描述符是不同的。
2、面向消息,即读者接收到由写者写入的整条消息。读取一条消息的一部分而让剩余部分遗留在队列中或一次读取多条消息都是不可能的。
3、消息队列中读取消息既可以按照先入先出的顺序,也可以根据消息的类型来读取消息。
3.2 创建一个消息队列
msgget()系统调用创建一个新消息队列或取得一个既有队列的标识符:
int msgget(key_t key,int msgflag);
key 参数可以使用 ftok()获得,例如:
key_t key; //key值
int id; //消息队列的标识符
key=ftok("/program/test",'x'); // ftok()获取key值
id=msgget(key,IPC_CREAT | S_IRUSR | S_IWUSR); //msgget()系统调用创建一个新消息队
IPC_CREAT 如果没有与指定的 key 对应的消息队列,那么就创建一个新队列。
IPC_EXCL 如果同时还指定了 IPC_CREAT 并且与指定的 key 对应的队列已经存在,那么调用就会失败并返回 EEXIST 错误。
创建一个消息队列还可以使用IPC_PRIVATE的方式:
id=msgget(IPC_PRIVATE,S_IRUSR | S_IWUSR);
3.3 消息队列发送和接受消息
msgsnd()系统调用向消息队列写入一条消息。
int msgsnd(int id,const void *msg,size_t msgsz,int flag);
参数依次是:id为队列标识符,msg为发送的消息(结构体指针),msgsz为消息内容的长度,flag可为IPC_NOWAIT非阻塞发送。
发送程序:
int len;
//发送消息自定义结构体msg
struct msg{
long tpye; //消息的类型
char *text; //消息的内容
};
struct msg msg_send;
msg_send.type=20;
msg_send.text="thi is a message\n";
len=strlen(msg_send.text); //消息内容的长度
msgsnd(id,&msg_send,len,IPC_NOWAIT); //开始发送消息,id是消息队列的标识符
当消息队列满时,msgsnd()会阻塞直到队列中有足够的空间来存放这条消息。但如果指定了IPC_NOWAIT这个标记,那么 msgsnd()就会立即返回 EAGAIN 错误。
msgrcv()系统调用从消息队列中读取(以及删除)一条消息并将其内容复制进 msgp 指向的缓冲区中:
ssize_t msgrcv(int id,void *msg,size_t maxmsgsz,long rec_type,int flag);
参数依次是:id为队列标识符,msg为读取缓冲区,maxmsgsz为缓冲区最大可用字节数,rec_type表示接受消息的类型,flag可设置为IPC_NOWAIT非阻塞读取。
接收程序:
#define maxlen 1024; //指定接受缓冲区最大可用字节数
struct msg_rec{
long tpye_rec; //消息的类型
char *text_rec; //消息的内容
};
struct msg_rec mymsg_rec;
msglen=msgrcv(id,&mymsg_rec,maxlen,20,IPC_NOWAIT); //指定接受类型为20
通常如果队列中没有匹配 rec_type 的消息,那么 msgrcv()会阻塞直到队列中存在匹配的消息为止。指定 IPC_NOWAIT 标记会导致 msgrcv()立即返回 ENOMSG错误。
上面程序在msgrcv中指定了接受数据类型rec_type为20,这个rec_tpye的意义是:
a、如果 rec_type 等于 0,那么会删除队列中的第一条消息并将其返回给调用进程。
b、如果 rec_type 大于 0,那么会将队列中第一条发送信息的type 等于rec_type 的消息删除并将其返回给调用进程。
c、如果rec_type 小于 0,队列中 type 最小并且其值小于或等于rec_type 的绝对值的第一条消息会被删除并返回给调用进程。
例如消息队列中如果有消息和对应的类型如下图
如果让rec_type=0,这时候就相当于把消息队列当作一个先进先出的队列使用,假如我在msgrcv时指定rec_type为-300,那么这些 msgrcv调用会按照 2(类型为 100)、5(类型为 100)、3(类型为 200)、1(类型为300)的顺序读取消息。4不会被读取。
所以我在程序里指定rec_type为20就能匹配到发送程序中的类型为20的消息。
3.4 消息队列的控制操作
msgctl()系统调用在标识符为 id 的消息队列上执行控制操作,msqid_ds是消息队列关联的结构体。
int msgctl(int id,int cmd,struct msqid_ds *buf);
struct msqid_ds
{
struct msqid_ds {
struct ipc_perm msg_perm;
struct msg *msg_first; /* first message on queue,unused */
struct msg *msg_last; /* last message in queue,unused */
__kernel_time_t msg_stime; /* last msgsnd time */
__kernel_time_t msg_rtime; /* last msgrcv time */
__kernel_time_t msg_ctime; /* last change time */
unsigned long msg_lcbytes; /* Reuse junk fields for 32 bit */
unsigned long msg_lqbytes; /* ditto */
unsigned short msg_cbytes; /* current number of bytes on queue */
unsigned short msg_qnum; /* number of messages in queue */
unsigned short msg_qbytes; /* max number of bytes on queue */
__kernel_ipc_pid_t msg_lspid; /* pid of last msgsnd */
__kernel_ipc_pid_t msg_lrpid; /* last receive pid */
};
cmd参数可以是如下:
IPC_RMID
立即删除消息队列对象及其关联的 msqid_ds 数据结构。队列中所有剩余的消息都会丢失。
IPC_STAT
将与这个消息队列关联的 msqid_ds 数据结构的副本放到 buf 指向的缓冲区中。
IPC_SET
使用buf指向的缓冲区提供的值更新与这个消息队列关联的msqid_ds数据结构中被选中的字段。
3.5 显示系统中的消息队列
一种方法:shell命令的ipcs可以显示系统上IPC对象的信息:
ipcrm 命令删除一个 IPC 对象:
ipcrm -X key
ipcrm -x id
第二种方法:
/proc/sysvipc/msg 列出所有消息队列及其特性。
/proc/sysvipc/sem 列出所有信号量集及其特性。
/proc/sysvipc/shm 列出所有共享内存段及其特性。
例如一个/proc/sysvipc/sem 文件的内容:
第三种方法:
也可以用msgctl操作控制来获取。
4.1 posix和system v消息队列区别
posix消息队列的优势:
1、接口更加简单,同时POSIX IPC 对象是引用计数的,只有当所有当前使用队列的进程都关闭了队列以后,才会对队列进行标记以便删除,这样就简化了确定何时删除一个对象的任务。
2、进程能够在一条消息进入之前为空的队列时异步地通过信号或线程的实例化来接收通知。
3、可以使用 poll()、select()以及 epoll 来监控 POSIX消息队列。System V 消息队列并没有这个特性。
System V 消息队列优势:
1、与 POSIX 消息队列严格按照优先级排序相比,System V 消息队列能够根据类型来选择消息的功能的灵活性更强。
4.2posix消息队列的创建和关闭
POSIX消息队列的创建,关闭和删除用到以下三个函数接口:
#include
mqd_t mq_open(const char *name, int oflag, /* mode_t mode, struct mq_attr *attr */); //成功返回消息队列描述符,失败返回-1
mqd_t mq_close(mqd_t mqdes);
mqd_t mq_unlink(const char *name); //成功返回0,失败返回-1
name:表示消息队列的名字,它符合POSIX IPC的名字规则
oflag:表示打开的方式,和open函数的类似。有必须的选项:O_RDONLY,O_WRONLY,O_RDWR,还有可选的选项:O_NONBLOCK,O_CREAT,O_EXCL。
mode:是一个可选参数,在oflag中含有O_CREAT标志且消息队列不存在时,才需要提供该参数。表示默认访问权限。可以参考open。
attr:也是一个可选参数,在oflag中含有O_CREAT标志且消息队列不存在时才需要。该参数用于给新队列设定某些属性,如果是空指针,那么就采用默认属性。
mq_close用于关闭一个消息队列,和文件的close类型,关闭后,消息队列并不从系统中删除。一个进程结束,会自动调用关闭打开着的消息队列。
mq_unlink()函数删除通过 name 标识的消息队列,并将队列标记为在所有进程使用完该队列之后销毁该队列(所有进程关闭消息队列描述符后删除)。
4.3 POSIX消息队列的属性
前面在mq_open中最后一个参数mq_attr结构的定义如下:
#include
struct mq_attr
{
long int mq_flags //消息队列的标志:0或O_NONBLOCK,用来表示是否阻塞
long int mq_maxmsg //消息队列的最大消息数
long int mq_msgsize //消息队列中每个消息的最大字节数
long int mq_curmsgs //消息队列中当前的消息数目
long int __pad[4];
};
POSIX消息队列的属性设置和获取可以通过下面两个函数实现:
#include
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); //成功返回0,失败返回-1
mq_getattr用于获取当前消息队列的属性,mq_setattr用于设置当前消息队列的属性。其中mq_setattr中的oldattr用于保存修改前的消息队列的属性,可以为空。
mq_setattr可以设置的属性只有mq_flags,用来设置或清除消息队列的非阻塞标志。newattr结构的其他属性被忽略。mq_maxmsg和mq_msgsize属性只能在创建消息队列时通过mq_open来设置。mq_open只会设置该两个属性,忽略另外两个属性。mq_curmsgs属性只能被获取而不能被设置。
4.4 posix消息队列发送和接收
POSIX消息队列可以通过以下两个函数来进行发送和接收消息:
#include
mqd_t mq_send(mqd_t mqdes, const char *msg_ptr, size_t msg_len, unsigned msg_prio);
mqd_t mq_receive(mqd_t mqdes, char *msg_ptr,size_t msg_len, unsigned *msg_prio);
mq_send向消息队列中写入一条消息,mq_receive从消息队列中读取一条消息。
mqdes:消息队列描述符;
msg_ptr:指向消息体缓冲区的指针;
msg_len:消息体的长度,其中mq_receive的该参数不能小于能写入队列中消息的最大大小,即一定要大于等于该队列的mq_attr结构中mq_msgsize的大小。如果mq_receive中的msg_len小于该值,就会返回EMSGSIZE错误。POXIS消息队列发送的消息长度必须小于mq_attr结构中mq_msgsize的大小。
msg_prio:消息的优先级;它是一个小于MQ_PRIO_MAX的数,数值越大,优先级越高。POSIX消息队列在调用mq_receive时总是返回队列中最高优先级的最早消息。如果消息不需要设定优先级,那么可以在mq_send是置msg_prio为0,mq_receive的msg_prio置为NULL。
一个简单的例子,下面的程序通过命令行获取信息向posix信号队列里写入一条信息。
./a.out // 执行文件+信号队列名+发送的消息+优先级
#include
#include
#include
#include
#include
int main(int argc, char *argv[])
{
unsigned long prio;
mqd_t mqd;
struct mq_attr attr;
attr.mq_maxmsg=32;
attr.mq_msgsize=512;
if (argc != 4)
{
printf("usage: mqsend \n" );
return -1;
}
mqd = mq_open(argv[1], O_WRONLY | O_CREAT,0666,&attr);
if(mqd == (mqd_t)-1)
{
printf("mq_open() error %d: %s\n", errno, strerror(errno));
return -1;
}
prio = atoi(argv[3]);
if (mq_send(mqd, argv[2], strlen(argv[2]), prio) == -1)
{
printf("mq_send() error %d: %s\n", errno, strerror(errno));
return -1;
}
return 0;
}
下面的程序从posix消息队列里面读取一条信息
用法:./a.out //执行文件+消息队列名
#include
#include
#include
#include
#include
int main(int argc, char *argv[])
{
int prio;
mqd_t mqd;
struct mq_attr attr;
char *buff;
if (argc != 2)
{
printf("usage: mq_receive \n" );
return -1;
}
mqd = mq_open(argv[1], O_RDONLY);
if(mqd == (mqd_t)-1)
{
printf("mq_open() error %d: %s\n", errno, strerror(errno));
return -1;
}
if(mq_getattr(mqd,&attr) == -1)
{
printf("mq_getattr() error %d: %s\n", errno, strerror(errno));
return -1;
}
buff=malloc(attr.mq_msgsize);
if (mq_receive(mqd, buff, attr.mq_msgsize, &prio) == -1)
{
printf("mq_receive() error %d: %s\n", errno, strerror(errno));
return -1;
}
printf("read data is %s\n",buff);
return 0;
}
测试结果:利用发送程序发送3条信息,优先级分别为1,5,10。利用接受程序进行消息接收并打印,可以观察到是按照优先级从高到低的顺序依次读取的:
4.5 posix消息队列异步消息通知
POSIX 消息队列区别于 System V 消息队列的一个特性是 POSIX 消息队列能够接收之前为空的队列上有可用消息的异步通知(即队列从空变成了非空)。这个特性意味着已经无需执行一个阻塞的调用或将消息队列描述符标记为非阻塞并在队列上定期执行 mq_receive()调用了,进程可以选择通过信号的形式或通过在一个单独的线程中调用一个函数的形式来接收通知。
使用mq_notify()函数注册的调用进程在一条消息进入描述符 mqdes 引用的空队列时会接收通知信号:
int mq_notify(mqd_t mqdes, const struct sigevent* notification);
其中sigevent结构体如下:
union sigval { /* Data passed with notification */
int sival_int; /* Integer value */
void *sival_ptr; /* Pointer value */
};
struct sigevent {
int sigev_notify; /* Notification method */
int sigev_signo; /* Notification signal */
union sigval sigev_value; /* Data passed with notification */
void (*sigev_notify_function) (union sigval);/* Function used for thread notification (SIGEV_THREAD) */
void *sigev_notify_attributes;/* Attributes for notification thread (SIGEV_THREAD) */
pid_t sigev_notify_thread_id; /* ID of thread to signal (SIGEV_THREAD_ID) */
};
sigev_notify字段:SIGEV_SIGNAL,通过生成一个在 sigev_signo 字段中指定的信号来通知进程。SIGEV_THREAD,通过调用在 sigev_notify_function 中指定的函数来通知进程,就像是在一个新线程中启动该函数一样。
使用mq_notify的注意事项:
1、在任何一个时刻都只有一个进程(“注册进程”)能够向一个特定的消息队列注册接收通知。如果一个消息队列上已经存在注册进程了,那么后续在该队列上的注册请求将会失败(mq_notify()返回 EBUSY 错误)。
2、只有当一条新消息进入之前为空的队列时注册进程才会收到通知。如果在注册的时候队列中已经包含消息,那么只有当队列被清空之后有一条新消息达到之时才会发出通知。
3、一个进程想要持续地接收通知,那么必须要在每次接收到通知之后再次调用 mq_notify()来注册自己。
4、如果其他进程在 mq_receive()调用中被阻塞了,那么消息队列里新来一条消息后,该进程会读取消息,注册进程会保持注册状态。
通过信号接收通知的程序示例
下面的程序通过非阻塞模式打开了队列,阻塞通知信号(SIGUSR1)并为其建立一个处理器,然后调用 mq_notify()来注册进程接收消息通知,执行一个无限循环,在循环中执行下列任务:
1、调用 sigsuspend(),该函数会解除通知信号的阻塞状态并等待直到信号被捕获(为什么不用pause,因为sigsuspend将解除阻塞和休眠成为了一个原子操作,避免了解除阻塞和休眠pause之间信号的到来,导致进程一直挂起)
2、进程被信号唤醒后调用 mq_notify()重新注册进程接收消息通知。
3、执行一个 while 循环从队列中尽可能多地读取消息以便清空队列。
#include
#include
#include
#include
#include
#include
#define NOTIFY_SIG SIGUSR1
static void handler(int sig){
if(sig == NOTIFY_SIG)
printf("NOTIFY_SIG sig wake up the process!!!\n");
}
int main(int argc, char *argv[])
{
int prio;
mqd_t mqd;
sigset_t blockmask,emptymask;
struct mq_attr attr;
struct sigaction act;
struct sigevent sev;
char *buff;
if (argc != 2)
{
printf("usage: mq_receive \n" );
return -1;
}
//用非阻塞方式打开,用与后面while循环mq_receive将消息队列里的数据取干净
mqd = mq_open(argv[1], O_RDONLY | O_NONBLOCK);
if(mqd == (mqd_t)-1)
{
printf("mq_open() error %d: %s\n", errno, strerror(errno));
return -1;
}
if(mq_getattr(mqd,&attr) == -1)
{
printf("mq_getattr() error %d: %s\n", errno, strerror(errno));
return -1;
}
//接收缓冲区分配内存
buff=malloc(attr.mq_msgsize);
//阻塞NOTIFY_SIG信号
sigemptyset(&blockmask);
sigaddset(&blockmask, NOTIFY_SIG);
sigprocmask(SIG_BLOCK, &blockmask, NULL);
//开始捕捉信号NOTIFY_SIG
act.sa_handler = handler;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(NOTIFY_SIG, &act, NULL);
//注册信号通知,注册完毕后,当空的消息队列进来新的消息,会发送NOTIFY_SIG信号给该进程
sev.sigev_notify=SIGEV_SIGNAL;
sev.sigev_signo=NOTIFY_SIG;
if(mq_notify(mqd,&sev) == -1){
printf("mq_notify error %d: %s\n", errno, strerror(errno));
}
sigemptyset(&emptymask);
while(1){
//在这里挂起,等待NOTIFY_SIG信号唤醒,实际上这里没有阻塞任何信号,任何信号都可以唤醒
sigsuspend(&emptymask);
//执行到这里说明进程已经被NOTIFY_SIG信号唤醒,接下来的操作是重新注册并且从消息队列里读数据
if(mq_notify(mqd,&sev) == -1){
printf("mq_notify error %d: %s\n", errno, strerror(errno));
}
//读数据
while((mq_receive(mqd, buff, attr.mq_msgsize, &prio)) >= 0)
{
printf("read data is %s\n",buff);
}
}
return 0;
}
利用前面的send向消息队列里发送数据,利用上面的程序替代rec接收程序进行测试,首先运行./rec /mq &为后台执行:
通知线程接收通知的方式和代码先不写了,感觉扣的太细了,以后有时间和机会了解一下。
好了,还剩一个共享内存和内存映射了,socket放在单独的一章整理,毕竟内容有点多。
5.1 共享内存的特点
共享内存允许两个或多个进程共享物理内存的同一块区域(通常被称为段)。由于一个共享内存段会成为一个进程用户空间内存的一部分,因此这种IPC 机制无需内核介入,与管道或消息队列要求发送进程将数据从用户空间的缓冲区复制进内核内存和接收进程将数据从内核内存复制进用户空间的缓冲区的做法相比,这种 IPC 技术的速度更快。共享内存这种 IPC 机制不由内核控制意味着通常需要通过某些同步方法使得进程不会出现同时访问共享内存的情况。
5.2 system v共享内存操作流程
1、创建/打开共享内存
2、映射共享内存,即把指定的共享内存映射到进程的地址空间用于访问
3、撤销共享内存的映射
4、删除共享内存对象
创建/打开共享内存:
int shmget(key_t key, size_t size, int shmflg)
key参数不多解释,跟前面的system v消息队列一样。
size参数是要建立共享内存的长度,以字节为单位。
shmfg参数一是指定创建或打开的标志和读写的权限(ipc_perm中的mode成员)。二是可指定IPC_CREAT和IPC_EXCL,IPC_CREAT 如果共享内存不存在,则创建一个共享内存,否则直接打开已存在的。IPC_EXCL 只有在共享内存不存在的时候,新的内存才建立,否则就产生错误。
映射共享内存:
void * shmat(int shmid, const void *shmaddr, int shmflg);
shmid:要映射的共享内存区标示符
shmaddr:将共享内存映射到指定地址(若为NULL,则表示由系统自动完成映射)
shmflg:SHM_RDONLY 共享内存只读,默认0:共享内存可读写
返回值:调用成功返回映射后的地址,出错返回(void *)-1。
撤销共享内存:
int shmdt(const void * shmadr);
注意:当一个进程不再需要共享内存段时,它将调用shmdt()系统调用取消这个段,但是这并不是从内核真正地删除这个段,而是把相关shmid_ds结构的shm_nattch域的值减1,当这个值为0时,内核才从物理上删除这个共享段。
控制共享内存:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmid:共享内存标示符ID
cmd :IPC_STAT得到共享内存的状态;IPC_SET改变共享内存的状态;IPC_RMID删除共享内存
buf :是一个结构体指针。IPC_STAT的时候,取得的状态放在这个结构体中。如果要改变共享内存的状态,用这个结构体 struct shmid_ds 指定:
struct shmid_ds{
struct ipc_perm shm_perm;/* 操作权限*/
int shm_segsz; /*段的大小(以字节为单位)*/
time_t shm_atime; /*最后一个进程附加到该段的时间*/
time_t shm_dtime; /*最后一个进程离开该段的时间*/
time_t shm_ctime; /*最后一个进程修改该段的时间*/
unsigned short shm_cpid; /*创建该段进程的pid*/
unsigned short shm_lpid; /*在该段上操作的最后1个进程的pid*/
short shm_nattch; /*当前附加到该段的进程的个数*/
/*下面是私有的*/
unsigned short shm_npages; /*段的大小(以页为单位)*/
unsigned long *shm_pages; /*指向frames->SHMMAX的指针数组*/
struct vm_area_struct *attaches; /*对共享段的描述*/
};
上一个system v共享内存的示例程序,下面的程序进程创建了一块共享内存区域,并将该区域映射到指定地址,再对共享区域中的数据进行修改:
#include
#include /* For mode constants */
#include /* For O_* constants */
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define PATHNAME "."
int main(int argc, char **argv)
{
int id;
int flag;
char *ptr;
size_t length=1024;
key_t key;
struct shmid_ds buff;
key = ftok(PATHNAME,1);
if(key<0)
{
printf("ftok error\r\n");
return -1;
}
id = shmget(key, length,IPC_CREAT | IPC_EXCL| S_IRUSR | S_IWUSR );
if(id<0)
{
printf("errno: %s\r\n",strerror(errno));
printf("shmget error\r\n");
return -1;
}
ptr = shmat(id, NULL, 0);
if(ptr==NULL)
{
printf("shmat error\r\n");
return -1;
}
shmctl(id,IPC_STAT,&buff);
int i;
for(i=0;i<buff.shm_segsz;i++)
{
*ptr++ = i%256;
}
return 0;
}
下面的程序将同一块共享内存区域打开,并映射到进程的虚拟地址,读取共享内存的数据并打印:
#include
#include /* For mode constants */
#include /* For O_* constants */
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define PATHNAME "."
int main(int argc, char **argv)
{
const char * const pathname=".";
int id;
int flag;
char *ptr;
size_t length=1024;
key_t key;
struct shmid_ds buff;
key = ftok(pathname,1);
if(key<0)
{
printf("ftok error\r\n");
return -1;
}
flag = IPC_CREAT | 0400;
id = shmget(key, length, flag);
if(id<0)
{
printf("shmget error\r\n");
return -1;
}
ptr = shmat(id, NULL, 0);
if(ptr==NULL)
{
printf("shmat error\r\n");
return -1;
}
shmctl(id,IPC_STAT,&buff);
int i;
unsigned char c;
for(i=0;i<buff.shm_segsz;i++)
{
c=*ptr++;
printf("ptr[%d]=%d\r\n",i,c);
}
shmctl(id, IPC_RMID, NULL);
return 0;
}
5.3 内存映射
在学习内存映射的相关知识前,先了解几个概念。
a、文件映射和匿名映射。
文件映射将一个文件的一部分直接映射到调用进程的虚拟内存中。一旦一个文件被映射之后就可以通过在相应的内存区域中操作字节来访问文件内容了。映射的分页会在需要的时候从文件中(自动)加载。
而一个匿名映射没有对应的文件。这种映射的分页会被初始化为 0。
b、私有映射和共享映射
私有映射在映射内容上发生的变更对其他进程不可见,对于文件映射来讲,变更将不会在底层文件上进行。这意味着每当一个进程试图修改一个分页的内容时,内核首先会为该进程创建一个新分页并将需修改的分页中的内容复制到新分页中。
共享映射在映射内容上发生的变更对所有共享同一个映射的其他进程都可见,对于文件映射来讲,变更将会发生在底层的文件上。
将前面的概念两两组合,所以我们常说的内存映射实际上包括文件私有映射、文件共享映射、匿名私有映射、匿名共享映射。
先说文件私有映射:将一个文件的部分内容映射到虚拟空间地址中,映射在映射内容上发生的变更对其他进程不可见,并且内容变更将不会在底层文件上进行。这种映射的主要用途是使用一个文件的内容来初始化一块内存区域。一些常见的例子包括根据二进制可执行文件或共享库文件的相应部分来初始化一个进程的文本和数据段。
文件共享映射:同样是将一个文件的部分内容映射到虚拟空间地址中,但映射在映射内容上发生的变更对其他进程可见,并且内容变更将会在底层文件上进行同步。主要用于两个用途。第一,它允许内存映射 I/O,因为正常的 read()或 write()需要两次传输:比如发起一次read()是先将文件内容复制到内核高速缓冲区,然后内核高速缓冲区和用户的缓冲区再进行数据交换,而使用 mmap()就无需第二次传输了,用户进程仅仅需要修改映射到虚拟空间地址中的内容,然后可以依靠内核内存管理器来自动更新底层的文件。除了节省了内核空间和用户空间之间的一次传输之外,mmap()还能够通过减少所需使用的内存来提升性能,当使用 read()或 write()时,数据将被保存在两个缓冲区中:一个位于用户空间,另一个位于内核空间。当使用 mmap()时,内核空间和用户空间会共享同一个缓冲区,即映射到虚拟空间的区域,节省了内存的消耗。
匿名私有映射:每次调用 mmap()创建一个私有匿名映射时都会产生一个新映射,该映射与同一(或不同)进程创建的其他匿名映射是不同的(即不会共享物理分页)。私有匿名映射的主要用途是为一个进程分配新(用零填充)内存(如在分配大块内存时 malloc()会为此而使用 mmap())。
匿名共享映射:相当于 共享内存,但只有相关进程之间才能这么做。
创建一个内存映射:
void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);
addr 参数指定了映射被放置的虚拟地址。如果将 addr 指定为 NULL,那么内核会为映射选择一个合适的地址。这是创建映射的首选做法。
length 参数指定了映射的字节数。
prot 参数是一个位掩码,它指定了施加于映射之上的保护信息:
flag参数可为MAP_PRIVATE 表示创建一个私有映射,MAP_SHARED 表示创建一个共享映射。
参数 fd 和 offset 是用于文件映射的(匿名映射将忽略它们)。fd 参数是一个标识被映射的文件的文件描述符。offset 参数指定了映射在文件中的起点。
例如创建一个文件共享映射:
#define pathname "./testfile"
#define SIZE 1024
int main(int argc,char argv[]){
int fd;
char *addr;
fd=open(pathname,O_RDWR);
if(fd<0){
printf("open file err\n");
return -1;
}
addr=mmap(NULL,SIZE,PROT_READ | PROTT_WRITE,MAP_SHARED,fd,0);
if(addr==MAP_FAILED)
{
printf("mmap err\n");
return -1;
}
return 0;
}
解除映射区域:
munmap()系统调用执行与 mmap()相反的操作,即从调用进程的虚拟地址空间中删除一个映射。
int munmap(void *start, size_t length);
同步映射区域:
内核会自动将发生在 MAP_SHARED 映射内容上的变更写入到底层文件中,但在默认情况下,内核不保证这种同步操作会在何时发生,msync()系统调用让应用程序能够显式地控制何时完成共享映射与映射文件之间的同步,调用 msync()还允许一个应用程序确保在可写入映射上发生的更新会对在该文件上执行 read()的其他进程可见:
int msync(void *addr,size_t len,int flags)
传给 msync()的 addr 和 length 参数指定了需同步的内存区域的起始地址和大小,flag参数如下:
MS_SYNC:内存区域会与磁盘同步。
MSASYNC:内存区域仅仅是与内核高速缓冲区同步。
posix共享内存
学习完前面的内存映射,posix共享内存实际上非常简单,实际上posix共享内存就是属于一种特殊的共享映射,也是通过mmap实现的,他的fd文件描述符指代的不是具体的文件,而是一个由shm_open()创建打开的一个共享内存对象,所以它不需要向System v共享内存那样使用的是键和标识符,也不需要像共享文件映射那样创建实际的磁盘文件。
通过如下的示例代码创建一个posix共享内存:
int fd;
char *addr;
fd=shm_open("./shm",flags,mode);
ftruncate(fd,size);
addr=mmap(NULL,size,PROT_READ | PROTT_WRITE,MAP_SHARED,fd,0);
好了、至此,linux通信机制和同步机制差不多都捋过一遍了,面试的时候应该不会问这么细吧,哈哈,希望来年2月份可以找个好工作,也不枉我背了这么多八股文QAQ…