Loading

C语言--常用进程间通信方式

进程间通信(Inter-process Comminication)

每个进程各自有不同的用户地址空间,任何一个进程的变量在另一个进程中都看不到,所以进程之间交换数据必须要通过内核,在内核中开辟一块缓冲区,进程A把数据从用户空间拷到内核缓冲区,进程B再从内核缓冲区把数据读走,内核提供的这种机制称之为进程间通信。

进程间通信的本质:进程之间可以看到一份公共资源;而这份资源的的提供方式或是提供者不同,造就了不同的进程间通信方式。


管道

管道是一种队列型的数据结构,它的数据从一端输入,另一端输出。管道最常见应用就是连接两个进程的输入和输出,将一个进程的输入作为另一个进程的输出。

shell中有专门的管道运算符|,常用于检索内容,如:find | grep str

管道如何实现通信

image

无名管道(匿名管道)Pipe

无名管道一般直接称为管道,占用两个文件描述符,只能被具有亲缘关系的进程共享,也就是只能用于具有亲缘关系的进程间通信,一般是指父子进程;单工的通信模式,具有固定的读端和写端;

无名管道的建立

UNIX中一切皆是文件,管道也是文件中的一种。当系统创建一个管道时,它返回两个文件描述符,分别用作输出端(读)和输入端(写);
在UNIX中,使用pipe函数来创建管道:
头文件:#include <unistd.h>
声明:int pipe(int __pipedes[2])
功能:创建单向通信通道(管道)。如果成功,两个文件描述符将存储在__pipedes[2]中。一般写在__pipedes[1]上的字节可以从__pipedes[0]读取。
返回值:成功返回0;否则返回-1;

单向管道模型

通常管道的两端分别被两个不同的进程控制,这样两个进程就能够进行通信。控制输入端的进程向管道发送信息,控制输出端的进程从管道中读取信息。

在父进程创建管道并产生子进程之后,父子进程就都拥有管道两端的访问权。此时通过控制父子进程中管道两端开闭,就能够实现父子进程之间的单向通信;例:

#include <stdio.h>
#include <unistd.h>

int main() {
    int fd[2];
    pid_t fpid;
    char buff[20];
    // 创建管道,实现父进程向子进程发送信息
    if (pipe(fd) < 0) {
        printf("create Pipe error !!\n");
    }
    // 创建子进程
    fpid = fork();
    if (fpid < 0) {
        printf("fork error !!\n");
    } else if (fpid == 0) {
        // 子进程关闭管道输入端
        close(fd[1]);
        // 子进程从管道输出端读取
        read(fd[0], buff, 20);
        printf("%s\n", buff);
    } else {
        // 父进程关闭管道输出端
        close(fd[0]);
        // 父进程从输入端写入
        write(fd[1], "hello world\n", 12);
    }
    return 0;
}

若想要实现子进程向父进程发送信息,将父子进程中代码内容交换即可(即更改输入输出端);

管道读取的四种情况:
  1. 读端不读,写端一直写
  2. 写端不写,读端一直读
  3. 读端一直读fd[0]保持打开,写端写一部分就不写了关闭fd[1]
  4. 读端读了一部分不读了关闭fd[0],写端一直写fd[1]保持打开
  • 总结
    如果一个管道的写端一直在写,而读端的引⽤计数是否⼤于0决定管道是否会堵塞,引用计数大于0,只写不读再次调用write会导致管道堵塞;
    如果一个管道的读端一直在读,而写端的引⽤计数是否⼤于0决定管道是否会堵塞,引用计数大于0,只读不写再次调用read会导致管道堵塞;
    而当他们的引用计数等于0时,只写不读会导致写端的进程收到一个SIGPIPE信号,导致进程终止,只写不读会导致read返回0,就像读到⽂件末尾⼀样

双向管道模型

管道是进程间的单向交流方式,如果要实现双向通信就需要使用两个管道。
具体使用方式如下:

#include <stdio.h>
#include <unistd.h>

int main() {
    int fd1[2], fd2[2];
    pid_t fpid;
    char buff[20];
    // 创建管道1和管道2
    if (pipe(fd1) < 0 || pipe(fd2) < 0) {
        printf("create Pipe error !!\n");
    }
    // 创建子进程
    fpid = fork();
    if (fpid < 0) {
        printf("fork error !!\n");
    } else if (fpid == 0) {
        // 子进程关闭管道1输入端,从管道1输出端读取并打印
        close(fd1[1]);
        read(fd1[0], buff, 20);
        printf("[child] %s\n", buff);
        // 子进程关闭管道2输出端,从管道2输入端写入
        close(fd2[0]);
        write(fd2[1], "child information!\n", 20);
    } else {
        // 父进程关闭管道1输出端,从管道1输入端写入
        close(fd1[0]);
        write(fd1[1], "parent information!\n", 20);
        // 父进程关闭管道2输入端,从管道2输出端读取并打印
        close(fd2[1]);
        read(fd2[0], buff, 20);
        printf("[parent] %s\n", buff);
    }
    return 0;
}

运行结果:

[child] parent information!

[parent] child information!

连接I/O管道模型

通过使用dup2函数可以对标准输入、输出、错误进行重定向,例如:

dup2(fd1,0)//复制文件描述符fd1到文件描述符0即可重定向标准输入
dup2(fd2,1)//复制文件描述符fd2到文件描述符1即可重定向标准输出
dup2(fd3,2)//复制文件描述符fd3到文件描述符2即可重定向标准错误

通过将标准输入,输出,错误重定向到管道两端的文件描述符,就可以实现两个进程间标准输入输出的传递;

以下程序中,父进程向stdout输出hello,直接转移到子进程的stdin,由子进程中的fgets(buff)接收

#include <stdio.h>
#include <unistd.h>

int main() {
    int fd[2];
    pid_t fpid;
    char buff[256];
    // 创建管道
    if (pipe(fd) < 0) {
        printf("create Pipe error!!");
        return -1;
    }
    // 创建子进程
    fpid = fork();
    if (fpid < 0) {
        printf("fork error!!");
        return -1;
    }
    // 子进程关闭管道输入端;将标准输入(stdin,文件描述符为0)重定向为文件描述符fd[0];
    else if (fpid == 0) {
        close(fd[1]);
        dup2(fd[0], 0);
        close(fd[0]);
        fgets(buff, 256, stdin);
        printf("[child] %s\n", buff);
        return 0;
    }
    // 父进程关闭管道输出端;将标准输出(stdout,文件描述符为1)重定向为文件描述符fd[1];
    else {
        close(fd[0]);
        dup2(fd[1], 1);
        close(fd[1]);
        puts("hello");
        return 0;
    }
}

运行结果

[child] hello

高级管道popen模型

创建连接标准I/O的管道需要多个步骤,unix提供了一组函数来简化这个复杂的过程:
头文件:#include <stdio.h>
声明:FILE *popen(const char *__command, const char *__modes),int pclose(FILE *__stream)
功能:popen()创建一个连接到运行给定命令的管道的新流:调用fork()产生子进程来执行__command命令。pclose()关闭popen打开的流并返回其子流的状态。(将另一个程序当做新的进程在当前程序中启动,算是当前程序的子进程)
参数:

  • __command:子进程所执行的命令,如ls,pwd或是自己写的程序./test
  • __modes
    • r时,创建与子进程的标准输出连接的管道(管道数据由子进程流向父进程)
    • w时,创建与子进程的标准输入连接的管道(管道数据由父进程流向子进程)

返回值:

  • __modesr时:返回子进程的标准输出stdout
  • __modesw时:返回子进程的标准输入stdin

原理:使用popen运行一个程序时,它会首先启动一个shell,即使用sh命令,然后将参数__command字符串传递给他。

示例1:子进程执行ls命令,父进程从管道读取并打印输出

#include <stdio.h>
#include <string.h>

int main() {
    FILE *in;
    char buf[256];
    memset(buf, 0, sizeof(buf));
    // 子进程执行ls命令,在父进程显示
    if ((in = popen("ls", "r")) == NULL) {
        printf("popen error!");
        return -1;
    }
    fread(buf, sizeof(char), sizeof(buf), in);
    printf("[parent] %s", buf);
    pclose(in);
    return 0;
}

运行结果

[parent] a.out
popen.c
test
test.c

示例2:子程序执行命令./test,父进程从管道中读取内容
test.c测试程序内容,编译后文件名为test

#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[]) {
    char buf[256];
    memset(buf, 0, sizeof(buf));
    printf("this is other process information\n");
    return 0;
}

popen.c文件内容,编译执行

#include <stdio.h>
#include <string.h>

int main() {
    FILE *in;
    char buf[256];
    memset(buf, 0, sizeof(buf));
    // 子进程执行./test命令,在父进程显示
    if ((in = popen("./test", "r")) == NULL) {
        printf("popen error!");
        return -1;
    }
    fread(buf, sizeof(char), sizeof(buf), in);
    printf("[parent] %s", buf);
    pclose(in);
    return 0;
}

运行结果

[parent] this is other process information

上面示例都是父进程读取子进程的内容,父进程向子进程写入内容示例如下:

#include <stdio.h>
#include <string.h>

int main() {
    FILE *out;
    char buf[256];
    memset(buf, 0, sizeof(buf));
    // 子进程执行od命令读取父进程的内容并显示出来
    if ((out = popen("od -c", "w")) == NULL) {
        printf("popen error!");
        return -1;
    }
    sprintf(buf, "hello world!");
    fwrite(buf, sizeof(buf), 1, out);
    pclose(out);
    return 0;
}

运行结果

0000000   h   e   l   l   o       w   o   r   l   d   !  \0  \0  \0  \0
0000020  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0
*
0000400

命名管道(有名管道)named Pipe

有名管道也是半双工的通信方式,不过它允许无亲缘关系的进程间通信。
有名管道是一个伪文件,占用磁盘大小永远为0,数据存储在内核里,有一个对应的缓冲区。
在磁盘上会有这么一个文件,当使用ls -l查看该文件时,它的类型为p

有名管道的创建

使用mkfifo函数
头文件:#include <stat.h>
声明:int mkfifo (const char *__path, __mode_t __mode)
功能:创建一个名为__path的新FIFO,权限位为__mode
说明:其中参数__path为管道文件的路径和名称,__mode类似于open()函数的第三个参数,并且自带了O_CREATO_EXCL选项,所以此函数只能创建一个不存在的管道文件,否则会返回文件已存在的错误。
返回值:成功调用返回0,否则返回-1

注意:此函数只适用于linux的文件系统,如果__path为与windows系统的共享文件夹内,使用mkfifo函数会报错:mkfifo error: Operation not supported,只需要管道文件路径设置为linux的本地文件夹即可。

示例:**分别写两个程序,开启两个进程,一个程序创建管道文件并向其中写入信息,另一个程序从管道文件中读取

namedPipe.c

#include <fcntl.h> //open
#include <stdio.h>
#include <string.h>   //memset
#include <sys/stat.h> //mkfifo
#include <unistd.h>

int main(int argc, char *argv[]) {
    // 创建管道文件
    int m = mkfifo("/home/liyugui/test/namedPipe", 0777);
    if (m == -1) {
        perror("mkfifo error");
        return -1;
    }

    int count = 0;
    char buf[128];
    memset(buf, 0, sizeof(buf));
    // 以只写方式打开管道文件
    int fd = open("/home/liyugui/test/namedPipe", O_WRONLY);
    if (fd == -1) {
        perror("file open faild!");
    }
    // 不断向管道文件内写入信息
    while (1) {
        snprintf(buf, sizeof(buf), "[%d] this is namedPipe information!!",
                 count);
        write(fd, buf, sizeof(buf));
        count++;
        sleep(2);
    }
    close(fd);
    return 0;
}

namedPipe2.c

#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
    char buf[128];
    memset(buf, 0, sizeof(buf));
    // 以只读方式打开文件
    int fd = open("/home/liyugui/test/namedPipe", O_RDONLY);
    if (fd == -1) {
        perror("file open faild!!");
    }
    // 不断从管道文件内读取信息
    int r = 0;
    while (1) {
        r = read(fd, buf, sizeof(buf));
        // write(1, buf, r);
        printf("[namedPipe2] %s\n", buf);
        sleep(2);
    }
    close(fd);
    return 0;
}

执行

Agui@localhost:$ gcc namedPipe.c -o a.out ; gcc namedPipe2.c -o b.out
Agui@localhost:$ ./a.out &
[1] 11941
Agui@localhost:$ ./b.out
[namedPipe2] [0] this is namedPipe information!!
[namedPipe2] [1] this is namedPipe information!!
[namedPipe2] [2] this is namedPipe information!!
[namedPipe2] [3] this is namedPipe information!!
^C
Agui@localhost:$ 
[1]+  Broken pipe             ./a.out

说明:namedPipe.cnamedPipe2.c编译成a.outb.out后,把a.out挂在后台运行,然后运行b.out,可以看到程序b.out获取到了a.out写入管道内的内容并打印出来。将b.out程序关闭后,a.out程序也会随之关闭,并返回Broken pipe的消息。需要注意的是,在打开管道文件,使用open函数时,最好使用阻塞打开,也就是不添加O_NONBLOCK选项,否则会立即返回进程。

信号量Semaphore

信号量,有时候被称为信号灯;在本质上是一种数据操作锁,也可以说是一个计数器,用来记录临时资源的数目的。它本身并不具备数据交互的功能,主要是用来保护共享资源不被多个进程调用,也就是使资源在一个时刻只有一个进程独享。

信号量是可以用来保证两个或多个关键代码段不被并发调用。在进入一个关键代码段之前,线程必须获取一个信号量;一旦该关键代码段完成了,那么该线程必须释放信号量。其它想进入该关键代码段的线程必须等待直到第一个线程释放信号量。

以一个停车场的运作为例。简单起见,假设停车场只有三个车位,一开始三个车位都是空的。这时如果同时来了五辆车,看门人允许其中三辆直接进入,然后放下车拦,剩下的车则必须在入口等待,此后来的车也都不得不在入口处等待。这时,有一辆车离开停车场,看门人得知后,打开车拦,放入外面的一辆进去,如果又离开两辆,则又可以放入两辆,如此往复。
在这个停车场系统中,车位是公共资源,每辆车好比一个线程,看门人起的就是信号量的作用。

信号量的工作原理

由于信号量(sv)只能进行两种操作,等待和发送信号,即P(sv)V(sv),它们的行为是这样的:

  • P(sv):如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行;
  • V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加1;

在信号量进行PV操作时都为原子操作[1],因为它需要保护临时资源。

信号量的生命周期并不随进程的结束而结束,而是随着内核

信号量的相关命令

ipcs -s //查看信号量
ipcrm -s [semid] //删除信号量

信号量的相关函数

ftok

头文件:#include <sys/sem.h>
声明:key_t ftok(const char *__pathname, int __proj_id)
功能:把一个已经存在的路径名和一个整数标识得转换成一个key_t值,称为IPC键
参数:__pathname :路径名,必须存在且可取。
__proj_id:IPC序列号,可以根据自己的约定,随意设置。这个数字,有的称之为project ID; 在UNIX系统上,它的取值是1到255;
返回值:成功返回键值,失败返回-1;
说明:两进程如在pathname和proj_id上达成一致(或约定好),双方就都能够通过调用ftok函数得到同一个IPC键。
pathname的实现是组合了三个键,分别是:
(1)pathname所在文件系统的信息(stat结构的st_dev成员)。
(2)pathname在文件系统内的索引节点号(stat结构的st_ino成员)。
(3)id的低序8位(不能为0)。
ftok调用返回的整数IPC键由proj_id的低序8位,st_dev成员的低序8位,st_info的低序16位组合而成。
不能保证两个不同的路径名与同一个proj_id的组合产生不同的键,因为上面所列的三个条目(文件系统、标识符、索引节点、proj_id)中的信息位数可能大于一个整数的信息位数。

semget

头文件:#include <sys/sem.h>
声明:int semget(key_t __key, int __nsems, int __semflg)
功能:创建一个信号量或访问一个已经存在的信号量集
参数:__key:key是长整型(唯一非零),系统建立IPC通讯 ( 消息队列、 信号量和 共享内存) 时必须指定一个ID值。通常情况下,该id值通过ftok函数得到,由内核变成标识符,要想让两个进程看到同一个信号集,只需设置key值不变就可以.
__nsems:nsem指定信号量集中需要的信号量数目,它的值几乎总是1
__semflg:一组标志,当想要当信号量不存在时创建一个新的信号量,可以将flag设置为IPC_CREAT与文件权限做按位或操作.
返回值:成功返回一个相应信号量标识符(非零),失败返回-1
说明:__semflg设置了IPC_CREAT标志后,即使给出的key是一个已有信号量的key,也不会产生错误。而IPC_CREAT | IPC_EXCL则可以创建一个新的,唯一的信号量,如果信号量已存在,返回一个错误。一般我们会还或上一个文件权限

semctl

头文件:#include <sys/sem.h>
声明:int semctl(int __semid, int __semnum, int __cmd, ...)
功能:初始化或移除信号量集
返回值:成功返回正数,失败返回-1
参数:__semid:信号量集IPC标识符
__semnum:表示信号量集中的哪个信号量,第一个为0
__cmd:在semid指定的信号量集上指向此命令,cmd的选择如下,通常使用以下加粗部分

参数 说明
IPC_STAT 读取一个信号量集的数据结构semid_ds,并将其存储在semun中的buf参数中
IPC_SET 设置信号量集的数据结构semid_ds中的元素ipc_perm,其值取自semun中的buf参数
IPC_RMID 将信号量集从内存中删除。
GETALL 用于读取信号量集中的所有信号量的值。
GETNCNT 返回正在等待资源的进程数目。
GETPID 返回最后一个执行semop操作的进程的PID。
GETVAL 返回信号量集中的一个单个的信号量的值。
GETZCNT 返回这在等待完全空闲的资源的进程数目。
SETALL 设置信号量集中的所有的信号量的值。
SETVAL 设置信号量集中的一个单独的信号量的值。

semop

头文件:#include <sys/sem.h>
声明:int semop(int __semid, struct sembuf *__sops, size_t __nsops)
功能:用来创建和访问一个信号量集
返回值:成功返回0,失败返回-1
参数:__semid:是该信号量的标识码,也就是semget函数的返回值
__sops:是个指向一个结构数值的指针
__nsops:进行操作信号量的个数,即sops结构变量的个数,需大于或等于1。最常见设置此值等于1,只完成对一个信号量的操作
说明:信号量操作由sembuf结构体表示

/* 用于参数“semop”以描述操作的结构.  */
struct sembuf
{
  unsigned short int sem_num;	/* semaphore number 在信号集中的编码0,1,2...*/
  short int sem_op;		/* semaphore operation 信号量在一次操作中需要改变的数据,通常是两个数,  
// 一个是-1,即P(等待)操作,一个是+1,即V(发送信号)操作*/
  short int sem_flg;		/* operation flag 通常为SEM_UNDO,使操作系统跟踪信号,并在进程没有释放该信号量而终止时, 操作系统释放信号量*/
};

信号量使用示例

首先对信号量相关操作进行封装,方便使用;测试程序中开启一个子进程,通过信号量来控制打印顺序。
comm.h

#ifndef _COMM_H
#define _COMM_H

#include <sys/sem.h>
#define PATHNAME "."
#define PROJ_ID 0

union semun {
    int val;
    struct semid_ds *buf;
    unsigned short *arry;
    struct seminfo *__buf;
};

int CreatSemid(int nums);
int GetSemid(int nums);
int InitSem(int semid, int which, int _val);
int P(int semid, int which, int _op);
int V(int semid, int which, int _op);
int Destory(int semid);

#endif

comm.c

#include "comm.h"
#include <stdio.h>

// 信号量是创建还是获取在于semget函数参数flag的设置
static int CommSemid(int nums, int flags) {
    key_t _key = ftok(PATHNAME, PROJ_ID);
    if (_key > 0) {
        return semget(_key, nums, flags);
    } else {
        perror("CommSemid");
        return -1;
    }
}

// 创建信号量
int CreatSemid(int nums) {
    return CommSemid(nums, IPC_CREAT | IPC_EXCL | 0666);
}

// 获取已创建的信号量
int GetSemid(int nums) { return CommSemid(nums, IPC_CREAT); }

// 销毁信号量,信号量不会随进程结束而结束,所以需要主动销毁
int Destory(int semid) {
    if (semctl(semid, 0, IPC_RMID) > 0) {
        return 0;
    } else {
        perror("Destory");
        return -1;
    }
}

// 初始化信号量
int InitSem(int semid, int which, int _val) {
    union semun _semun;
    _semun.val = _val;
    if (semctl(semid, which, SETVAL, _semun) < 0) {
        perror("InitSem");
        return -1;
    }
    return 0;
}

// PV操作在于_op值
static int SemPV(int semid, int which, int _op) {
    struct sembuf _sf;
    _sf.sem_flg = 0;
    _sf.sem_num = which;
    _sf.sem_op = _op;
    return semop(semid, &_sf, 1);
}

// P操作
int P(int semid, int which, int _op) {
    if (SemPV(semid, which, _op) < 0) {
        perror("p");
        return -1;
    }
    return 0;
}

// V操作
int V(int semid, int which, int _op) {
    if (SemPV(semid, which, _op) < 0) {
        perror("V");
        return -1;
    }
    return 0;
}

semaphoreTest.c

#include "comm.c"
#include <stdio.h>
#include <sys/wait.h> //waitpid
#include <unistd.h>

int main(int argc, char *argv[]) {
    // 创建信号量,查看信号量的semid
    int semid = CreatSemid(1);
    printf("%d\n", semid);
    InitSem(semid, 0, 1);
    pid_t fpid;

    if ((fpid = fork()) < 0) {
        perror("fork error");
        return -1;
    }
    if (fpid == 0) {
        int semid = GetSemid(0);
        while (1) {
            P(semid, 0, -1);
            printf("A");
            fflush(stdout);
            sleep(1);
            printf("A\n");
            fflush(stdout);
            sleep(1);
            V(semid, 0, 1);
        }
    } else {
        while (1) {
            P(semid, 0, -1);
            sleep(1);
            printf("B");
            fflush(stdout);
            sleep(1);
            printf("B\n");
            fflush(stdout);
            sleep(1);
            V(semid, 0, 1);
        }
        if (waitpid(fpid, NULL, 0) < 0) {
            perror("waitpid");
            return -1;
        }
    }
    Destory(semid);
    return 0;
}

执行

Agui@localhost:~$ gcc semaphoreTest.c
Agui@localhost:~$ ./a.out
0
BB
AA
BB
AA
BB
AA
Agui@localhost:~$ 

从执行结果可以看到,只有父进程打印了两个B后,子进程才能打印,这就是通过信号量来实现进程资源独享。如果不使用信号量控制,结果打印的顺序是不可预测的。

消息队列

什么是消息队列

消息队列就是一个消息的列表。可以把消息看做一个记录,具有特定格式以及特定的优先级。对消息队列具有写权限的进程可以向消息队列中按照一定的规则添加新消息;对消息队列有读权限的进程则可以从消息队列中读走消息。消息队列是随内核持续的(不会随着进程结束而删除)

具体表现

  • 消息队列提供了一个从一个进程向另一个进程发送一块数据的方法;
  • 每个数据块都被认为有一个类型,接收进程接收的数据块可以有不同的类型;
  • 消息队列的存储也是有上限的,即每个消息的最大长度(MSGMAX)、每个消息队列总的字节数(MSGMBN)、系统消息队列的总数(MSGMNI)都是有上限的;

linux相关命令

ipcs -q:显示所有的消息队列
ipcs -qt:显示消息队列的创建时间,发送和接收最后一条消息的时间
ipcs -qp:显示消息队列中放消息和从消息队列中取消息的进程ID
ipcs -q -i msgid :消失该消息队列结构体中的消息信息
ipcs -ql :显示消息队列的限制信息

ipcrm -q msgid:删除消息队列

相关函数

msgget

头文件:#include <sys/msg.h>
声明:int msgget(key_t __key, int __msgflg)
功能:创建一个消息队列或是得到消息队列标识符
返回值:成功返回消息队列的标识符,失败返回-1,错误代码保存在error中
参数:
__key:
(1)0(IPC_PRIVATE):会建立新的消息队列;
(2)大于0的32位整数:视参数__msgflg来确定操作。通常要求此值来源于ftok返回的IPC键值
__msgflg
(1)0:取消息队列标识符,若不存在则函数会报错
(2)IPC_CREAT:当msgflg & IPC_CREAT为真时,如果内核不存在键值与key相等的消息队列,则新建一个消息队列;如果存在这样的消息队列,返回此消息队列的标识符
(3)IPC_CREAT | IPC_EXCL:如果内核中不存在键值与key相等的消息队列,则新建一个消息队列;如果存在这样的消息队列则报错

返回错误

错误代码 说明
EACCES 指定的消息队列已存在,但调用进程没有权限访问它
EEXIST __key指定的消息队列已存在,而__msgflg同时指定IPC_CREATIPC_EXCL标志
ENOENT __key指定的消息队列不存在,同时__msgflg中没有指定IPC_CREAT标志
ENOMEM 需要建立消息队列,但内存不足
ENOSPC 需要建立消息队列,但已达到系统的限制

msgctl

头文件:#include <sys/msg.h>
声明:int msgctl(int __msqid, int __cmd, struct msqid_ds *__buf)
功能:获取和设置消息队列的属性
返回值:成功返回0,失败返回-1,错误原因存储在error中
参数:
__msqid:消息队列标识符
__cmd
(1)IPC_SATA:获得msgid的消息队列头数据到__buf中;
(2)IPC_SET:设置消息队列属性,要设置的属性先存在__buf中,可设置的属性包括:msg_perm.uidmsg_perm.gidmsg_perm.mode以及msg_qbytes
__buf:消息队列管理结构体

返回错误

错误代码 说明
EACCESS 参数__cmdIPC_STAT,确无权限读取该消息队列
EFAULT 参数__buf指向无效的内存地址
EIDRM 标识符为msqid的消息队列已被删除
EINVAL 无效的参数cmdmsqid
EPERM 参数cmdIPC_SETIPC_RMID,却无足够权限执行

msgsnd

头文件:#include <sys/msg.h>
声明:int msgsnd(int __msqid, const void *__msgp, size_t __msgsz, int __msgflg)
功能:__msgp中的消息写入到标识符为__msqid的消息队列中
返回值:成功返回0,失败返回-1,错误原因存于error中
参数:
__msqid:消息队列标识符
__msgp:发送给队列的消息。__msgp可以使任意类型的结构体,但第一个字段必须为long类型,即表明发送消息的类型,msgrcv函数根据此接收消息。
_-msgsz:要发送消息的的大小,不含消息类型占用的long字节长度,也就是__msgp结构体字节数减去long字节数
msgflg
(1)0:当消息队列满时,msgsnd函数将会阻塞,直到消息能写进消息队列
(2)IPC_NOWAIT:当消息队列已满时,msgsnd函数不等待立即返回
(3)IPC_NOERROR:若发送的消息大于__msgsz字节,则把该消息截断,截断部分将被丢弃,且不通知发送进程

错误消息

错误代码 说明
EAGAIN 参数__msgflg设为IPC_NOWAIT,而消息队列已满
EIDRM 标识符为__msgqid的消息队列已被删除
EACCESS 无权限写入消息队列
EFAULT 参数__msgp指向无效的地址内存
EINTR 队列已满而处于等待情况下被信号截断
EINVAL 无效的参数__msgqid__msgsz或参数消息类型type小于0

msgrcv

头文件:#include <sys/msg.h>
声明:ssize_t msgrcv(int __msqid, void *__msgp, size_t __msgsz, long __msgtyp, int __msgflg)
功能:从标识符为__msqid的消息队列中读取消息并保存在__msgp中,读取后把此消息从消息队列中删除
返回值:成功返回实际读取到的消息数据长度,失败返回-1,错误原因存在error中
参数:
__msqid:消息队列标识符
__msgp:存放消息的结构体,结构体类型需要与msgsnd函数发送的相同
__msgsz:要接收的消息大小,不含消息类型占用的字节
__msgtyp
(1)0:接收第一个消息
(2)大于0:接收类型等于__msgtyp的第一个消息
(3)小于0:接收类型等于或小于__msgtyp绝对值的第一个消息
__msgflg
(1)0:阻塞式接收消息
(2)IPC_NOWAIT:如果没有返回条件的消息调用立即返回,此时错误码为ENOMSG
(3)IPC_EXCEPT:与__msgtyp配合使用返回队列中第一个类型不为__msgtyp的消息
(4)IPC_NOERROR:如果队列中满足条件的消息内容大于所请求的__msgsz字节,则把该消息截断,截断部分将被丢弃

错误消息

错误代码 说明
E2BIG 消息数据长度大于msgsz而msgflg没有设置IPC_NOERROR
EIDRM 标识符为msqid的消息队列已被删除
EACCESS 无权限读取该消息队列
EFAULT 参数msgp指向无效的内存地址
ENOMSG 参数msgflg设为IPC_NOWAIT,而消息队列中无消息可读
EINTR 等待读取消息队列内的消息情况下被信号中断

示例一个进程创建消息队列并向其中写入“hello world!",另一个进程从消息队列中读取消息
msgsend.c

#include <stdio.h>
#include <sys/msg.h>

typedef struct _msgbuf {
    long type;
    int groupid;
    int appid;
    char buf[1024];
} msgbuf;

int main(int argc, char *argv[]) {
    int msgid;
    msgid = msgget(0x100, IPC_CREAT | 0777);
    msgbuf mb = {1, 1, 1, "hello world"};
    int ret;
    ret = msgsnd(msgid, &mb, sizeof(msgbuf) - sizeof(long), 0);
    return 0;
}

msgrcv.c

#include <stdio.h>
#include <sys/msg.h>

typedef struct _msgbuf {
    long type;
    int groupid;
    int appid;
    char buf[1024];
} msgbuf;

int main(int argc, char *argv[]) {
    int msgid;
    msgid = msgget(0x100, IPC_CREAT | 0777);
    msgbuf mb;
    msgrcv(msgid, &mb, sizeof(msgbuf) - sizeof(long), 1, 0);
    printf("type:[%ld]\tgroupid:[%d]\tappid:[%d]\n", mb.type, mb.groupid,
           mb.appid);
    puts(mb.buf);
}

  1. 不会被线程调度机制打断的操作;这种操作一旦开始就会一直运行到结束,中间不会切换到另一个进程 ↩︎

posted @ 2023-01-04 18:04  henryLyg  阅读(597)  评论(0编辑  收藏  举报