20155322 2017-2018-1《信息安全系统设计》第十周 课下作业-IPC

20155322 2017-2018-1《信息安全系统设计》课下作业-IPC

作业内容

研究Linux下IPC机制:原理,优缺点,每种机制至少给一个示例,提交研究博客的链接。

  • 共享内存
  • 管道
  • FIFO
  • 信号
  • 消息队列

作业完成

内存共享

  • 是什么:
    共享内存是最快的进程间通信方式, 因为进程可以直接读写内存。对于像管道和消息队列等通信方式,则需要在内核和用户空间进行四次的数据拷贝,而共享内存则只拷贝两次数据: 一次从输入文件到共享内存区,另一次从共享内存区到输出文件。
    实际上,进程之间在共享内存时,并不总是读写少量数据后就解除映射,有新的通信时,再重新建立共享内存区域,而是保持共享区域,直到通信完毕为止,这样,数据内容一直保存在共享内存中,并没有写回文件。
    共享内存中的内容往往是在解除映射时才写回文件的(当然操作系统会以一些策略来写文件,就是是说如果你没有显示调用munmap 或者 msync的话,也会写文件,但这个过程是不可控的,是由操作系统决定)。
  • linux的共享内存实现:
    实现方式有两种: posixsystem v
    • POSIX的共享内存,通过用户空间挂载的tmpfs文件系统实现的,通过文件映射的方式,并且会写回到文件,其它进程也可以看到文件的修改;
    • system v 是由内核本身的tmpfs实现的,内核直接实现了shmget/at系统调用,调用了一个shm的特殊文件,对于其它进程来说是看不到这个文件的,也不会写盘。但共享的内容是随内核持续的,就是说只要机器不重启或主动删除,那么共享区一直存在(这也是tmpfs的特性)。

posix

  • 函数原型:
//这两个函数分别是用于内存映射与解除内存映射
void *mmap(void*start,size_t length,int prot,int flags,int fd,off_t offset);
int munmap(void* start,size_t length);

*mmap函数:

  1. 第一个参数是用户指定的文件被映射到进程地址空间的虚地址,如果为0,表示让内核自己选地址
  2. 第二个参数是映射的长度, 注意:映射之后不会立刻占用物理内存空间,但会占用虚存空间
  3. 第三个参数指定被映射对象的类型,注意:这里的权限不能超过打开文件的权限,比如打开的时候是只读,而这里设置PORT_WRITE就会出错:
prot:
PROT_EXEC 表示映射的内存可执行
PROT_WRITE 表示映射的内存可写
PROT_READ 表示映射的内存可读
PROT_NONE 表示映射的页不能被访问
  1. 第四个参数是flags
    MAP_FIXED,MAP_SHARED,MAP_PRIVATE,MAP_ANONYMOUS
    尽量不用MAP_FIXED。如果参数start指定的地址无法建立映射时就会放弃。
    MAP_SHARED 表示与其它映射该文件/设备的进程共享映射,即对文件的修改在其它进程中也可见,共享内存的时候拿来用的,会把内存的数据写回到文件(如果调用了mumap, msync 会强制写,如果没有调用,操作系统也会以一定的机制写,只是不能保证数据完全被写进文件)
    MAP_PRIVATE 表示对创建一个专门的写时复制映射,即对映射的修改不会影响到被映射的文件,这个不能作为内存共享来用,因为其他的进程看不到这块内存,也不会写文件。
    用户只能指定MAP_SHARED与MAP_PRIVATE之一
    MAP_ANONYMOUS是一个匿名映射
  2. 第五个参数是文件标识符
  3. 第六个参数是被映射文件的起始地址
    这个函数返回的是被映射到进程地址空间的地址

munmap函数

  1. 第一个参数被映射到进程地址空间的地址
  2. 第二个参数映射的长度
  3. 函数返回的是被映射到进程地址空间的地址
    如果映射的长度大于文件的长度,大于的部分页会被清0
    mmap有三种映射方式:
  • 使用普通文件提供的内存映射:适用于任何进程之间;此时,需要打开或创建一个文件,然后再调用mmap();典型调用代码如下:
fd=open(name, flag, mode);
if(fd<0)
    ptr=mmap(NULL, len , PROT_READ|PROT_WRITE, MAP_SHARED , fd , 0);
  • 映射shm_open 打开的对象,
    fd=shm_open(name, flag, mode);
    创建的文件直接在 /dev/shm/下面, 接下来的操作和open的差不多,但并不调用close来关闭文件,是调用 shm_unlink 来减计数器,当打开的文件计数器为0的时候这个文件就会被删除。
  • 使用特殊文件提供匿名内存映射:
    适用于具有亲缘关系的进程之间;由于父子进程特殊的亲缘关系,在父进程中先调用mmap(),然后调用fork()。那么在调用fork()之后,子进程继承父进程匿名映射后的地址空间,同样也继承mmap()返回的地址,这样,父子进程就可以通过映射区域进行通信了。注意,这里不是一般的继承关系。一般来说,子进程单独维护从父进程继承下来的一些变量。而mmap()返回的地址,却由父子进程共同维护。
  • linux采用页式管理机制:
    对于用mmap()映射普通文件来说,进程会在自己的地址空间新增一块空间,空间大小由mmap()的len参数指定,注意,进程并不一定能够对全部新增空间都能进行有效访问。进程能够访问的有效地址大小取决于文件被映射部分的大小。简单的说,能够容纳文件被映射部分大小的最少页面个数决定了进程从mmap()返回的地址开始,能够有效访问的地址空间大小。超过这个空间大小,内核会根据超过的严重程度返回发送不同的信号给进程。可用如下图示说明:

    共享内存简单实现:
/*shm_write.c写入/读出共享内存区*/  
#include <stdio.h>  
#include <string.h>  
#include <stdlib.h>  
#include <sys/stat.h>  
#include <fcntl.h>  
#include <sys/mman.h>  
#include <unistd.h>  

int main(int argc,char **argv)  
{  
    int fd;  
    struct stat buf;  
    char *ptr;  

    if(argc!=2)  
    {  
        printf("usage:open <pathname>
");  
        exit(1);  
    }  
    fd=open(argv[1],O_RDWR|O_CREAT,0644);/*创建共享内存区*/  
    ftruncate(fd,100);/*修改共享区大小*/  
    fstat(fd,&buf);  
    ptr=mmap(NULL,buf.st_size,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);/*连接共享内存区*/  
    close(fd);//文件已经被映射,即便关闭也可以写  
    strcpy(ptr,"hello linux");/*写入共享内存区*/  
    printf("%s
",ptr);/*读出共享内存区*/  
    munmap(<span style="font-family:Arial,Helvetica,sans-serif">ptr</span>, buf.st_size);
    return 0;
  • system v
    /proc/sys/kernel/目录下,记录着system V共享内存的一下限制,如一个共享内存区的最大字节数shmmax,系统范围内最大共享内存区标识符数shmmni等,可以手工对其调整,但不推荐这样做。

对于system v的共享内存可以用ipcs -m来查看共享内存信息,用 ipcrm -m id来删除对应shmid的共享内存,当然这两个命令不仅仅用于共享内存,其他system v的包括消息队列等都是适用的,只是参数不一样,如下图所示。

- 对于`system v`共享内存,主要有以下几个API:shmget()、shmat()、shmdt()及shmctl()。
1. shmget()用来获得共享内存区域的ID,如果不存在指定的共享区域就创建相应的区域。
2. shmat()把共享内存区域映射到调用进程的地址空间 中去,这样,进程就可以方便地对共享区域进行访问操作。
3. shmdt()调用用来解除进程对共享内存区域的映射。
4. shmctl实现对共享内存区域的控制操 作。

注:shmget的内部实现包含了许多重要的系统V共享内存机制;shmat在把共享内存区域映射到进程空间时,并不真正改变进程的页 表。当进程第一次访问内存映射区域访问时,会因为没有物理页表的分配而导致一个缺页异常,然后内核再根据相应的存储管理机制为共享内存映射区域分配相应的 页表。

  • system V共享内存限制

/proc/sys/kernel/目录下,记录着system v共享内存的一下限制,如一个共享内存区的最大字节数shmmax,系统范围内最大共享内存区标识符数shmmni等,可以手工对其调整,但不推荐这样做。

#include <sys/ipc.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>

#include <string.h>
typedef struct {
    char name[4];
    int age;
}people;


int main(int argc,char *argv[]){

    int shm_id ,i;
    key_t key;
    char temp;
    people *p_map;
    char* name = "./myshm";
    key = ftok(name,0);
    if(key==-1){
        perror("ftok error!");
        return -1;
    }
    shm_id = shmget(key,4096,IPC_CREAT|0644);
    if(shm_id ==-1){
        perror("shmget error");
        return -1;
    }
    p_map = (people*)shmat(shm_id,NULL,0);
    if((int)p_map == -1){
        perror("shmat error");
        return -1;
    }

    temp = 'a';
    for(i = 0;i<10;i++)
    {
        temp +=1;
        memcpy((*(p_map +i )).name,&temp, 1);
        (*(p_map + i)).age = 20+i;
    }

    if(shmdt(p_map)==-1)
        perror("detach error");
}
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>

typedef struct {
    char name[4];
    int  age;
}people;
int main(int argc,char *argv[]){
    int shm_id ,i;
    key_t key;
    char temp;
    people *p_map;
    char* name = "./myshm";
    key = ftok(name,0);
    if(key==-1){
        perror("ftok error!");
        return -1;
    }
    shm_id = shmget(key,0,0);
    if(shm_id ==-1){
        perror("shmget error");
        return -1;
    }
    p_map = (people*) shmat(shm_id,NULL,0);
    if((int)p_map == -1){
        perror("shmat error");
        return -1;
    }

    for(i = 0;i<10;i++){
        printf("Name: %s,Age: %d
",(*(p_map+i)).name,(*(p_map+i)).age);
    }
    if(shmdt(p_map)==-1)
    {
        perror("detach error");
    }
}

注意:

  1. system V共享内存中的数据,从来不写入到实际磁盘文件中去;而通过mmap()映射普通文件实现的共享内存通信可以指定何时将数据写入磁盘文件中。 注:前面讲到,system V共享内存机制实际是通过映射特殊文件系统shm中的文件实现的,文件系统shm的安装点在交换分区上,系统重新引导后,所有的内容都丢失。

  2. system V共享内存是随内核持续的,即使所有访问共享内存的进程都已经正常终止,共享内存区仍然存在(除非显式删除共享内存),在内核重新引导之前,对该共享内存区域的任何改写操作都将一直保留。

  3. 通过调用mmap()映射普通文件进行进程间通信时,一定要注意考虑进程何时终止对通信的影响。而通过system V共享内存实现通信的进程则不然。 注:这里没有给出shmctl的使用范例,原理与消息队列大同小异。

管道(PIPE)

  • 概念:
    管道是linux进程间通信的一种方式,其是利用管道“文件”作为不同进程之间的传输数据的媒介,而实现进程间的数据交换。而无名管道pipe则是利用内核虚拟出来的管道“文件”来作为不同进程间数据传输通道,而并非实际存在真正意义上的文件。
    管道实际是用于进程间通信的一段共享内存,创建管道的进程称为管道服务器,连接到一个管道的进程为管道客户机。一个进程在向管道写入数据后,另一进程就可以从管道的另一端将其读取出来。

  • 管道读写注意事项:

    • 必须在系统调用fork()中调用pipe(),否则子进程将不会继承文件描述符;
    • 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道;
    • 只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程)。比如fork或exec创建的新进程,在使用exec创建新进程时,需要将管道的文件描述符作为参数传递给exec创建的新进程。当父进程与使用fork创建的子进程直接通信时,发送数据的进程关闭读端,接受数据的进程关闭写端。
    • 单独构成一种独立的文件系统:管道对于管道两端的进程而言,就是一个文件,但它不是普通的文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统,并且只存在与内存中。
    • 数据的读出和写入:一个进程向管道中写的内容被管道另一端的进程读出。写入的内容每次都添加在管道缓冲区的末尾,并且每次都是从缓冲区的头部读出数据。
  • 匿名管道的用法:

#include <unistd.h>  
int pipe(int filedes[2]);
  1. pipe()会建立管道,并将文件描述词由参数filedes数组返回。
  2. filedes[0]为管道里的读取端
    filedes[1]则为管道的写入端。
  3. 若成功则返回零,否则返回-1,错误原因存于errno中。
    错误代码:
EMFILE 进程已用完文件描述词最大量
ENFILE 系统已无文件描述词可用。
EFAULT 参数 filedes 数组地址不合法。
  1. 调用pipe函数时在内核中开辟一块缓冲区(称为管道)用于通信,它有一个读端一个写端,然后通过filedes参数传出给用户程序两个文件描述符,filedes[0]指向管道的读端,filedes[1]指向管道的写端(很好记,就像0是标准输入1是标准输出一样)。所以管道在用户程序看起来就像一个打开的文件,通过read(filedes[0]);或者write(filedes[1]);向这个文件读写数据其实是在读写内核缓冲区。pipe函数调用成功返回0,调用失败返回-1。
  • 管道的实现机制:
    管道是由内核管理的一个缓冲区,相当于我们放入内存中的一个纸条。管道的一端连接一个进程的输出。这个进程会向管道中放入信息。管道的另一端连接一个进程的输入,这个进程取出被放入管道的信息。一个缓冲区不需要很大,它被设计成为环形的数据结构,以便管道可以被循环利用。当管道中没有信息的话,从管道中读取的进程会等待,直到另一端的进程放入信息。当管道被放满信息的时候,尝试放入信息的进程会等待,直到另一端的进程取出信息。当两个进程都终结的时候,管道也自动消失。
    管道只能在本地计算机中使用,而不可用于网络间的通信。
  • pipe函数原型:
#include <unistd.h>
int pipe(int file_descriptor[2]);//建立管道,该函数在数组上填上两个新的文件描述符后返回0,失败返回-1。  
eg.int fd[2]  
int result = pipe(fd);

通过使用底层的read和write调用来访问数据。 向file_descriptor[1]写数据,从file_descriptor[0]中读数据。写入与读取的顺序原则是先进先出。

  • 管道读写规则
    当没有数据可读时:
    O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止。
    O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。

当管道满的时候:

    O_NONBLOCK disable: write调用阻塞,直到有进程读走数据
    O_NONBLOCK enable:调用返回-1,errno值为EAGAIN

如果所有管道写端对应的文件描述符被关闭,则read返回0
如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE
当要写入的数据量不大于PIPE_BUF(Posix.1要求PIPE_BUF至少512字节)时,linux将保证写入的原子性。
当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。

  • 实例:
  1. 父进程调用pipe开辟管道,得到两个文件描述符指向管道的两端。
  2. 父进程调用fork创建子进程,那么子进程也有两个文件描述符指向同一管道。
  3. 父进程关闭管道写端,子进程关闭管道读端。子进程可以往管道里写,父进程可以从管道里读,管道是用环形队列实现的,数据从写端流入从读端流出,这样就实现了进程间通信。
#include<sys/types.h>  
#include<sys/stat.h>  
#include<unistd.h>  
#include<fcntl.h>  
#include<stdio.h>  
#include<stdlib.h>  
#include<errno.h>  
#include<string.h>  
#include<signal.h>  
int main(int argc, char *argv[])  
{  
    int pipefd[2];  
    if (pipe(pipefd) == -1)  
        perror("pipe error");  

    pid_t pid;  
    pid = fork();  
    if (pid == -1)  
        perror("fork error");  

    if (pid == 0)  
    {  
        close(pipefd[0]);  
        write(pipefd[1], "hello", 5);  
        close(pipefd[1]);  
        exit(EXIT_SUCCESS);  
    }  

    close(pipefd[1]);  
    char buf[10] = {0};  
    read(pipefd[0], buf, 10);  
    printf("buf=%s
", buf);  

    return 0;  
}  

命名管道(FIFO)

  • 概念
    命名管道(FIFO)是一种特殊类型的文件,它在系统中以文件形式存在。这样克服了管道的弊端,他可以允许没有亲缘关系的进程间通信。
  • 两个系统调用原型:
#include <sys/types.h>   
#include <sys/stat.h>   
int mkfifo(const char *filename,mode_t mode); //建立一个名字为filename的命名管道,参数mode为该文件的权限(mode%~umask),若成功则返回0,否则返回-1,错误原因存于errno中。  
eg.mkfifo( "/tmp/cmd_pipe", S_IFIFO | 0666 );  
  • 特点:
  1. 有名管道fifo可以用于任意不同进程间的通信,不仅仅局限于具有亲缘关系的进程之间。
    具体操作方法只要创建了一个命名管道然后就可以使用open、read、write等系统调用来操作。创建可以手工创建或者程序中创建。
  2. 其存储路径是用户定义的,有名管道文件是真正意义上的文件。
  • 操作
  1. 可以直接在命令行上创建:
    mkfifo filename
  2. 在程序中可以这样:
    • 创建有名管道: int mkfifo(const char *pathname, mode_t mode);
    • 打开管道: int open(const char *pathname, int flags);
    • 读写、关闭有名管道与操作无名管道一致。
  • 打开规则:
  1. 如果当前打开操作是为读而打开FIFO时
    O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO
    O_NONBLOCK enable:立刻返回成功
  2. 如果当前打开操作是为写而打开FIFO时
    O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO
    O_NONBLOCK enable:立刻返回失败,错误码为ENXIO
  3. 需要注意的是打开的文件描述符默认是阻塞的.
  • 注意:
  1. OPEN_MAX 一个进程在任意时刻打开的最大描述符数。可以通过调用sysconf函数查询。
  2. PIPE_BUF 可原子地写往一个管道或FIFO的最大数据量。Posix任务它是一个路径名变量,它的值可以随指定的路径名而变化,因为不同的路径名可以落在不同文件系统上,而这些文件系统可能有不同的特征。所以PIPE_BUF可通过pathconf函数取得。
  • 实例:
    启动server程序,再运行client,输入些字符,server端将在屏幕上显示转换为大写后的输入字符。
    server
#include <stdio.h>    
#include <sys/types.h>    
#include <sys/stat.h>    
#include <fcntl.h>    
#define FIFO_PATH "/tmp/myfifo"    
int main()    
{    
    int ret;    
    int fd;    
    char buffer;    
    int nread;    
    int i;    
    /*建立FIFO*/    
    ret = mkfifo(FIFO_PATH, 0777);    
    /*打开FIFO*/    
    fd = open(FIFO_PATH, O_RDONLY);    
    if(-1 == fd)    
    {    
        printf("error/n");    
        return -1;    
    }    
    while(1)    
    {    
        nread = read(fd, &buffer, 1);    
        if(nread > 0)     
        {    
            buffer = toupper(buffer); //将字符c转换为大写英文字母.如果c为小写英文字母.  
                                      //则返回对应的大写字母;否则返回原来的值。  
            printf("%c", buffer);    
        }    
    }    
}    

client

#include <stdio.h>    
#include <sys/types.h>    
#include <sys/stat.h>    
#include <fcntl.h>    
#define FIFO_PATH "/tmp/myfifo"    
int main()    
{    
    int fd;    
    int ret;    
    char c;    
    fd = open(FIFO_PATH, O_WRONLY);    
    if(-1 == fd)    
    {    
        printf("error/n");    
        return -1;    
    }    
    while(c = getchar())    
    {    
        write(fd, &c, 1);    
    }    
}    

信号

信号机制是linux系统中最为古老的进程之间的通信机制,用于一个或几个进程之间传递异步信号。信号可以有各种异步事件产生,比如键盘中断等。shell也可以使用信号将作业控制命令传递给它的子进程。
在此列出几个简单使用方法定义:

#include <sys/types.h>   
#include <signal.h>   
void (*signal(int sig,void (*func)(int)))(int); //用于截取系统信号,第一个参数为信号,第二个参数为对此信号挂接用户自己的处理函数指针。返回值为以前信号处理程序的指针。  
eg.int ret = signal(SIGSTOP, sig_handle);  

由于signal不够健壮,推荐使用sigaction函数。

int kill(pid_t pid,int sig); //kill函数向进程号为pid的进程发送信号,信号值为sig。当pid为0时,向当前系统的所有进程发送信号sig。  
int raise(int sig);//向当前进程中自举一个信号sig, 即向当前进程发送信号。  
#include <unistd.h>   
unsigned int alarm(unsigned int seconds); //alarm()用来设置信号SIGALRM在经过参数seconds指定的秒数后传送给目前的进程。如果参数seconds为0,则之前设置的闹钟会被取消,并将剩下的时间返回。使用alarm函数的时候要注意alarm函数的覆盖性,即在一个进程中采用一次alarm函数则该进程之前的alarm函数将失效。  
int pause(void); //使调用进程(或线程)睡眠状态,直到接收到信号,要么终止,或导致它调用一个信号捕获函数。

实例:
实现一个信号接收程序sigreceive(其中信号安装由sigaction())

#include <signal.h>

#include <sys/types.h>

#include <unistd.h>

void new_op(int,siginfo_t*,void*);

int main(int argc,char**argv)

{

        struct sigaction act;  

        int sig;

        sig=atoi(argv[1]);



        sigemptyset(&act.sa_mask);

        act.sa_flags=SA_SIGINFO;

        act.sa_sigaction=new_op;



        if(sigaction(sig,&act,NULL) < 0)

        {

                printf("install sigal error
");

        }



        while(1)

        {

                sleep(2);

                printf("wait for the signal
");

        }

}



void new_op(int signum,siginfo_t *info,void *myact)

{

        printf("receive signal %d", signum);

        sleep(5);

}

消息队列

  • 概念
    消息队列是内核地址空间中的内部链表,通过linux内核在各个进程直接传递内容,消息顺序地发送到消息队列中,并以几种不同的方式从队列中获得,每个消息队列可以用IPC标识符唯一地进行识别。内核中的消息队列是通过IPC的标识符来区别,不同的消息队列直接是相互独立的。每个消息队列中的消息,又构成一个独立的链表。
    消息队列克服了信号承载信息量少,管道只能承载无格式字符流。
  • 消息队列头文件:
#include <sys/types.h>   
#include <sys/stat.h>   
#include <sys/msg.h>  
  • 消息缓冲区结构:
struct msgbuf{  
    long mtype;  
    char mtext[1];//柔性数组  
}  

在结构中有两个成员,mtype为消息类型,用户可以给某个消息设定一个类型,可以在消息队列中正确地发送和接受自己的消息。mtext为消息数据,采用柔性数组,用户可以重新定义msgbuf结构。例如:

struct msgbuf{  
    long mtype;  
    char mtext[1];//柔性数组  
}  

当然用户不可随意定义msgbuf结构,因为在linux中消息的大小是有限制的,在linux/msg.h中定义如下:
#define MSGMAX 8192
消息总的大小不能超过8192个字节,包括mtype成员(4个字节)。

  • msqid_ds内核数据结构
struct msgid_ds{
struct ipc_perm msg_perm{
    time_t msg_stime;
    time_t msg_rtime;
    time_t msg_ctime;
    unsigned long _msg_cbuyes;
   ..........
 };

Linux内核中,每个消息队列都维护一个结构体,此结构体保存着消息队列当前状态信息,该结构体在头文件linux/msg.h中定义。

  • ipc_perm内核数据结构
struct ipc_perm{  
  key_t key;  
  uid_t uid;  
  gid_t gid;  
  .......  
};  

结构体ipc_perm保存着消息队列的一些重要的信息,比如说消息队列关联的键值,消息队列的用户id组id等。它定义在头文件linux/ipc.h中。
常用函数:
系统建立IPC通讯 (消息队列、信号量和共享内存) 时必须指定一个ID值。通常情况下,该id值通过ftok函数得到。

key_t ftok( const char * fname, int id );//参数一为目录名称, 参数二为id。如指定文件的索引节点号为65538,换算成16进制为0x010002,而你指定的ID值为38,换算成16进制为0x26,则最后的key_t返回值为0x26010002。  
eg.key_t key = key =ftok(".", 1);  
int msgget(key_t key,int msgflag); //msgget用来创建和访问一个消息队列。程序必须提供一个键值来命名特定的消息队列。  
eg.int msg_id = msgget(key, IPC_CREATE | IPC_EXCL | 0x0666);//根据关键字创建一个新的队列(IPC_CREATE),如果队列存在则出错(IPC_EXCL),拥有对文件的读写执行权限(0666)。  
int msgsnd(int msgid,const void *msgptr,size_t msg_sz,int msgflg); //msgsnd函数允许我们把一条消息添加到消息队列中。msgptr只想准备发送消息的指针,指针结构体必须以一个长整型变量开始。   
eg.struct msgmbuf{  
    int mtype;  
    char mtext[10];  
};  
struct msgmbuf msg_mbuf;  
msg_mbuf.mtype = 10;//消息大小10字节  
memcpy(msg_mbuf.mtext, "测试消息", sizeof("测试消息"));  
int ret = msgsnd(msg_id, &msg_mbuf, sizeof("测试消息"), IPC_NOWAIT);  
int msgrcv(int msgid, void *msgptr, size_t msg_sz, long int msgtype, int msgflg);

msgrcv可以通过msqid对指定消息队列进行接收操作。第二个参数为消息缓冲区变量地址,第三个参数为消息缓冲区结构大小,但是不包括mtype成员长度,第四个参数为mtype指定从队列中获取的消息类型。

eg.int ret = msgrcv(msg_id, &msg_mbuf, 10, 10, IPC_NOWAIT | MSG_NOERROR);  
int msgctl(int msqid,int cmd,struct msqid_ds *buf);

msgctl函数主要是一些控制如删除消息队列等操作。 cmd值如下:
- IPC_STAT:获取队列的msgid_ds结构,并把它存到buf指向的地址。
- IPC_SET:将队列的msgid_ds设置为buf指向的msgid_ds。
- IPC_RMID:内核删除消息队列,最后一项填NULL, 执行操作后,内核会把消息队列从系统中删除。

  • 消息队列的本质
    Linux的消息队列(queue)实质上是一个链表,它有消息队列标识符(queue ID)。 msgget创建一个新队列或打开一个存在的队列;msgsnd向队列末端添加一条新消息;msgrcv从队列中取消息, 取消息是不一定遵循先进先出的, 也可以按消息的类型字段取消息。
  • 实例
    发送端:
/*send.c*/  
#include <stdio.h>   
#include <sys/types.h>   
#include <sys/ipc.h>   
#include <sys/msg.h>   
#include <errno.h>   

#define MSGKEY 1024   

struct msgstru  
{  
   long msgtype;  
   char msgtext[2048];   
};  

main()  
{  
  struct msgstru msgs;  
  int msg_type;  
  char str[256];  
  int ret_value;  
  int msqid;  

  msqid=msgget(MSGKEY,IPC_EXCL);  /*检查消息队列是否存在*/  
  if(msqid < 0){  
    msqid = msgget(MSGKEY,IPC_CREAT|0666);/*创建消息队列*/  
    if(msqid <0){  
    printf("failed to create msq | errno=%d [%s]
",errno,strerror(errno));  
    exit(-1);  
    }  
  }   

  while (1){  
    printf("input message type(end:0):");  
    scanf("%d",&msg_type);  
    if (msg_type == 0)  
       break;  
    printf("input message to be sent:");  
    scanf ("%s",str);  
    msgs.msgtype = msg_type;  
    strcpy(msgs.msgtext, str);  
    /* 发送消息队列 */  
    ret_value = msgsnd(msqid,&msgs,sizeof(struct msgstru),IPC_NOWAIT);  
    if ( ret_value < 0 ) {  
       printf("msgsnd() write msg failed,errno=%d[%s]
",errno,strerror(errno));  
       exit(-1);  
    }  
  }  
  msgctl(msqid,IPC_RMID,0); //删除消息队列   
}

接受端:

/*receive.c */  
#include <stdio.h>   
#include <sys/types.h>   
#include <sys/ipc.h>   
#include <sys/msg.h>   
#include <errno.h>   

#define MSGKEY 1024   

struct msgstru  
{  
   long msgtype;  
   char msgtext[2048];  
};  

/*子进程,监听消息队列*/  
void childproc(){  
  struct msgstru msgs;  
  int msgid,ret_value;  
  char str[512];  

  while(1){  
     msgid = msgget(MSGKEY,IPC_EXCL );/*检查消息队列是否存在 */  
     if(msgid < 0){  
        printf("msq not existed! errno=%d [%s]
",errno,strerror(errno));  
        sleep(2);  
        continue;  
     }  
     /*接收消息队列*/  
     ret_value = msgrcv(msgid,&msgs,sizeof(struct msgstru),0,0);  
     printf("text=[%s] pid=[%d]
",msgs.msgtext,getpid());  
  }  
  return;  
}  

void main()  
{  
  int i,cpid;  

  /* create 5 child process */  
  for (i=0;i<5;i++){  
     cpid = fork();  
     if (cpid < 0)  
        printf("fork failed
");  
     else if (cpid ==0) /*child process*/  
        childproc();  
  }  
}

学习遇到的问题

  • 问题1:管道和命名管道的区别?

  • 思考:对于命名管道FIFO来说,IO操作和普通管道IO操作基本一样,但是两者有一个主要的区别,在命名管道中,管道可以是事先已经创建好的,比如我们在命令行下执行mkfifo myfifo,就是创建一个命名通道,我们必须用open函数来显示地建立连接到管道的通道,而在管道中,管道已经在主进程里创建好了,然后在fork时直接复制相关数据或者是用exec创建的新进程时把管道的文件描述符当参数传递进去。
    一般来说FIFO和PIPE一样总是处于阻塞状态。也就是说如果命名管道FIFO打开时设置了读权限,则读进程将一直阻塞,一直到其他进程打开该FIFO并向管道写入数据。这个阻塞动作反过来也是成立的。如果不希望命名管道操作的时候发生阻塞,可以在open的时候使用O_NONBLOCK标志,以关闭默认的阻塞操作。

  • 问题2:消息队列与命名管道有什么关系?

  • 思考:消息队列跟命名管道有不少的相同之处,通过与命名管道一样,消息队列进行通信的进程可以是不相关的进程,同时它们都是通过发送和接收的方式来传递数据的。在命名管道中,发送数据用write,接收数据用read,则在消息队列中,发送数据用msgsnd,接收数据用msgrcv。而且它们对每个数据都有一个最大长度的限制。
    与命名管道相比,消息队列的优势在于:

  1. 消息队列也可以独立于发送和接收进程而存在,从而消除了在同步命名管道的打开和关闭时可能产生的困难。
  2. 同时通过发送消息还可以避免命名管道的同步和阻塞问题,不需要由进程自己来提供同步方法。
  3. 接收程序可以通过消息类型有选择地接收数据,而不是像命名管道中那样,只能默认地接收。
原文地址:https://www.cnblogs.com/blackay03/p/7900808.html