Linux进程间通信

进程间通信方式比较

  • 管道:速度慢,容量有限

  • 消息队列:容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题。

  • 信号量:不能传递复杂消息,只能用来同步

  • 共享内存区:能够很容易控制容量,速度快,但要保持同步,比如一个进程在写的时候,另一个进程要注意读写的问题,相当于线程中的线程安全,当然,共享内存区同样可以用作线程间通讯,不过没这个必要,线程间本来就已经共享了一块内存的。

管道(pipe)

  • 仅用于父子进程间的通信

  • 每个管道包含两个文件描述符, 一个用于读取一个用于写入

  • 父子进程通常持有两个管道, 对于任一进程来说: 持有一个管道的写入描述符和另外一个管道的读取描述符

函数原型

#include <unistd.h>
 
int pipe (int fd[2]);
  • f[0]用于读取, f[1]用于写入

代码示例

#include <sys/file.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <cstring>

void server(int readfd, int writefd)
{
    char buffer[128] = { 0 };
    int len = read(readfd, buffer, 128);
    printf("server recv: %s\n", buffer);

    strcpy(buffer, "this is server!");
    write(writefd, buffer, strlen(buffer));
}

void client(int readfd, int writefd)
{
    char buffer[128] = { 0 };
    strcpy(buffer, "this is client!");
    write(writefd, buffer, strlen(buffer));

    int len = read(readfd, buffer, 128);
    printf("client recv: %s\n", buffer);
}


int main()
{
    int pipe1[2] = { 0 };
    int pipe2[2] = { 0 };

    pipe(pipe1);
    pipe(pipe2);

    pid_t pid = fork();

    if(pid == 0)
    {
        close(pipe1[1]);
        close(pipe2[0]);

        //持有第一个管道用于读取的, 和第二个管道用于写入的
        client(pipe1[0], pipe2[1]);
    }
    else
    {
        close(pipe1[0]);
        close(pipe2[1]);

        server(pipe2[0], pipe1[1]);
        waitpid(pid, NULL, 0);
    }
    return 0;
}

资源消耗

  • 每个文件需要两个文件描述符

  • 如果没有数据的话可能会导致阻塞等待

有名管道(FIFO)

  • 这是一个设备文件, 提供一个路径名与FIFO对应

  • 不需要亲缘关系, 只要可以访问该路径名即可

  • 与无名管道相比需要预先使用open打开

接口

#include <sys/types.h>
#include <sys/stat.h>

int mkfifo(const char *pathname, mode_t mode);
  • pathname指的是路径名

  • mode指明权限, 文件权限是: (mode & ~umask); 比如: 0666

  • 使用unlink删除

示例(在两个文件中分别运行客户端和服务器)

#include <sys/file.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <cstring>
#include <sys/stat.h>
 #include <fcntl.h>

#define FIFO_NAME "/tmp/test_fifo"

void server()
{
    mkfifo(FIFO_NAME, 0666);        //创建FIFO文件

    int fd = open(FIFO_NAME, O_WRONLY);

    printf("fd: %d\n", fd);

    write(fd, "this is server!", 15);

    close(fd);

    unlink(FIFO_NAME);
}

void client()
{
    int fd = open(FIFO_NAME, O_RDONLY);

    printf("fd: %d\n", fd);

    char buffer[128] = { 0 };
    read(fd, buffer, 128);
    printf("client recv: %s\n", buffer);
    close(fd);
    
    return;
}


int main()
{
    server();
    return 0;
}
  • 如果采用: O_RDONLY 打开会阻塞, 直到有其他进程使用: O_WRONLY 打开; 反之亦然

信号

  • 用户进程可以通过: signal/signalaction指定对信号的操作方式

  • 可以对进程本身发送信号, 也可以对其他进程发送信号

常用接口

int kill(pid_t pid, int sig);       //给指定进程发送信号

int raise(int sig);                 //向进程自己发送信号

unsigned int alarm(unsigned int seconds);       //设置定时器

int pause(void);            //挂起直到收到信号

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);  //指定信号处理方式

void abort(void);       //发送异常终止信号

简单示例

#include <sys/file.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <cstring>
#include <signal.h>


void handle(int sig)
{
    printf("recv sig: %d\n", sig);
    raise(SIGKILL);
}

int main()
{
    pid_t pid = fork();

    if(pid == 0)
    {
        printf("%s\n", "ss");
        signal(SIGTERM, handle);

        while(1) {};
        //无限循环
    }
    else
    {
        sleep(2);
        kill(pid, SIGTERM);

        waitpid(pid, NULL, 0);
    }
    return 0;
}

内存映射

  • 通过将某个设备映射到应用进程的内存空间, 通过直接的内存操作就可以完成对设备或文件的的读写

接口说明

  • 1 头文件
#include <sys/mman.h>
  • 2 创建内存映射
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
+ addr: 将文件映射到内存空间指定地址, NULL即可
+ length: 映射到内存空间的内存块大小
+ prot: 访问权限, PROT_EXEC| PROT_READ | PROT_WRITE | PROT_NONE
+ flags: 程序对内存块的改变有什么影响
    + MAP_SHARED, 共享的, 内存块修改会保存到文件, 默认这个就可以
    + MAP_PRIVATE, 私有的, 修改只在局部范围有效
    + MAP_FIXED, 使用指定的映射起始地址
    + MAP_ANONYMOUS/MAP_ANON, 父子进程可以使用匿名映射, 文件描述符-1即可
+ fd: 文件描述符
+ offset: 从文件的哪里开始, 默认0即可

+ 返回映射的指针地址
  • 3 解除内存映射
int munmap(void *addr, size_t length);
+ addr: mmap的返回值
+ length: 长度

实例

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
 
void server()
{
    int fd;
    char buffer[128] = { 0 };
    strcpy(buffer, "this is server!");
 
    fd = open("/tmp/mmap_temp_file", O_RDWR|O_CREAT|O_TRUNC, 0644);
    ftruncate(fd, 64);  //64的大小
 
    // 使用fd创建内存映射区
    void* addr = mmap(NULL, 64, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
    close(fd); // 映射完后文件就可以关闭了
 
    memcpy(addr, buffer, strlen(buffer)); // 往映射区写数据

    munmap(addr, 64); // 释放映射区
}

void client()
{
    int fd;
 
    fd = open("/tmp/mmap_temp_file", O_RDONLY);
 
    // 使用fd创建内存映射区
    void* addr = mmap(NULL, 64, PROT_READ, MAP_SHARED, fd, 0);
    close(fd); 

    char *buffer = (char*)addr;
    printf("%s\n", buffer);

    munmap(addr, 64); // 释放映射区
}
 

 
int main() {
    server();
    return 0;
}
  • MAP_SHARED也会更新文件

  • MAP_PRIVATE 只能此进程访问此数据

消息队列

  • 生命周期随内核,消息队列会一直存在,需要我们显示的调用接口删除或使用命令删除

  • 消息队列可以双向通信

  • 克服了管道只能承载无格式字节流的缺点;

  • 从队列读出后会被删除

接口介绍

创建和访问一个消息队列

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflag);
  • key, 消息队列的名称, 通过ftok产生

  • msgflag: IPC_CREAT和IPC_EXCL, 单独使用IPC_CREAT,如果消息队列不存在则创建之,如果存在则打开返回;单独使用IPC_EXCL是没有意义的;两个同时使用,如果消息队列不存在则创建之,如果存在则出错返回。

  • 返回一个消息队列的标识码

ftok

#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
  • pathname: 路径名

  • proj_id: 项目ID,非 0 整数(只有低 8 位有效)

消息格式

typedef struct _msg
{
    long mtype;      // 消息类型
    char mtext[100]; // 消息正文
    //…… ……          // 消息的正文可以有多个成员
}MSG;

添加信息

#include <sys/msg.h>
int msgsnd(  int msqid, const void *msgp, size_t msgsz, int msgflg);
  • msqid: 即msgget的返回值

  • msgp 待发送消息结构体的地址。

  • msgsz 消息正文的字节数。

  • msgflag: 默认0即可

获取信息

#include <sys/msg.h>
ssize_t msgrcv( int msqid, void *msgp,  size_t msgsz, long msgtyp, int msgflg );
  • msqid:消息队列的标识符,代表要从哪个消息列中获取消息。

  • msgp: 存放消息结构体的地址。

  • msgsz:消息正文的字节数。

  • msgtyp:消息的类型。可以有以下几种类型:

    • msgtyp = 0:返回队列中的第一个消息。
      +msgtyp > 0:返回队列中消息类型为 msgtyp 的消息(常用)。
      +msgtyp < 0:返回队列中消息类型值小于或等于 msgtyp 绝对值的消息,如果这种消息有若干个,则取类型值最小的消息。
  • 在获取某类型消息的时候,若队列中有多条此类型的消息,则获取最先添加的消息,即先进先出原则。

消息队列的控制

#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
  • 对消息队列进行各种控制,如修改消息队列的属性,或删除消息消息队列。

  • cmd:函数功能的控制。其取值如下:

    • IPC_RMID:删除由 msqid 指示的消息队列,将它从系统中删除并破坏相关数据结构。
    • IPC_STAT:将 msqid 相关的数据结构中各个元素的当前值存入到由 buf 指向的结构中。相对于,把消息队列的属性备份到 buf 里。
    • IPC_SET:将 msqid 相关的数据结构中的元素设置为由 buf 指向的结构中的对应值。相当于,消息队列原来的属性值清空,再由 buf 来替换。
  • buf:msqid_ds 数据类型的地址,用来存放或更改消息队列的属性。

代码


#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
 
typedef struct _msg
{
    long mtype;
    char mtext[50];
}MSG;

void server()
{
    struct _msg m = {100, "hihihih"};

    key_t key;
    int msgqid;


    key = ftok("./", 2015);
    msgqid = msgget(key, IPC_CREAT|0666);       //额外指定权限


    msgsnd(msgqid, &m, sizeof(m) - sizeof(long), 0);
    //正文大小
}

void client()
{
    key_t key = ftok("./", 2015);
    int msgqid = msgqid = msgget(key, IPC_CREAT|0666); 

    struct _msg m;
    msgrcv(msgqid, &m, sizeof(m) - sizeof(long), 100, 0);

    msgctl(msgqid, IPC_RMID, NULL);
    printf("%s\n", m.mtext);
    return ;
}
 

 
int main() {
    client();
    return 0;
}

信号量

  • 信号量是描述资源可用性的计数器; 信号量可以通过创建一个值为1的信号量来专门锁定某个对象, 如果信号量的值大于零,则资源可用, 进程分配“资源的一个单元”,信号量减少一个

  • 传输的数据较少, 通常用于进程间同步; 可以和共享内存合作

接口

头文件

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h> 

创建信号量集

int semget(key_t key,int nsems,int flags)
  • key: 和消息队列一样使用ftok获得即可

  • 第二个参数nsem指定信号量集中需要的信号量数目,它的值几乎总是1

  • 第三个参数flag是一组标志,当想要当信号量不存在时创建一个新的信号量,可以将flag设置为IPC_CREAT与文件权限做按位或操作(如果再配合IPC_EXEC则已存在会报错)

删除信号量

int semctl(int semid, int semnum, int cmd, ...);
  • semid: 信号量标识符

  • semnum: 当前信号量集的哪一个信号量

  • cmd通常是下面两个值中的其中一个

    • SETVAL:用来把信号量初始化为一个已知的值, 这个值通过union semun中的val成员设置,其作用是在信号量第一次使用前对它进行设置。
    • IPC_RMID:删除信号量标识符,删除的话就不需要缺省参数
  • 第四个变量通常是: semun

union semun
{ 
    int val;  //使用的值
    struct semid_ds *buf;  //IPC_STAT、IPC_SET 使用的缓存区
    unsigned short *arry;  //GETALL,、SETALL 使用的数组
    struct seminfo *__buf; // IPC_INFO(Linux特有) 使用的缓存区
};

改变信号量的值

int semop(int semid, struct sembuf *sops, size_t nops);
  • nsops: 进行操作信号量的个数,即sops结构变量的个数,需大于或等于1。最常见设置此值等于1,只完成对一个信号量的操作

  • sembuf的定义如下:

struct sembuf{ 
    short sem_num;   //除非使用一组信号量,否则它为0 
    short sem_op;   //信号量在一次操作中需要改变的数据,通常是两个数,                                        
                    //一个是-1,即P(等待)操作, 
                    //一个是+1,即V(发送信号)操作。 
    short sem_flg; //通常为SEM_UNDO,使操作系统跟踪信号量, 
                  //并在进程没有释放该信号量而终止时,操作系统释放信号量 
}; 

代码实例


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

void server()
{
    key_t key;
    int semid;
    struct sembuf sb = { 0 };
    sb.sem_op = -1;
    sb.sem_flg = SEM_UNDO;

    key = ftok("./", 2015);

    semid = semget(key, 1, IPC_CREAT | 0666);   //创建信号量

    printf("%s\n", "Get Sem!");
    semop(semid, &sb, 1);

    semctl(semid, 0, IPC_RMID);     //删除

}

void client()
{
    key_t key = ftok("./", 2015);
    int semid = semget(key, 1, IPC_CREAT|0666); 

    struct sembuf sb = { 0 };
    sb.sem_op = 1;
    sb.sem_flg = SEM_UNDO;

    semop(semid, &sb, 1);
    printf("%s\n", "Release Sem!");

    return ;
}
 

 
int main() {
    server();
    return 0;
}

共享内存

  • 使得多个进程可以访问同一块内存区域, 是最快可用的IPC形式, 往往和信号量结合使用, 达到进程间的同步与互斥

接口

创建共享内存

#include<sys/ipc.h>
#include<sys/shm.h>
int shmget(key_t key,size_t size,int shmflg);
  • key: 类似于semget/msget, 通过ftok获得的标识符

  • size: 指定共享内存大小, 它的值一般为一页大小的整数倍(如果不到一页会对齐到一页)

  • shmflag: 和semflag/msgflag一样, 指定权限; 此外通过IPC_CREAT | IPC_EXCL创建新的

将共享内存映射到虚拟地址空间

#include<sys/types.h>
#include<sys/shm.h>
void * shmat (int shmid, const void * shmaddr, int shmflg);
  • shmid: 标识符

  • shmaddr: 映射地址, NULL即可

  • shm_flg: 一组标志位,通常为0

操作共享内存

int shmctl(int shm_id, int cmd, struct shmid_ds *buf);
  • shm_id: 标识符

  • cmd: 三个值

    • IPC_STAT: 把shmid_ds结构中的数据设置为共享内存的当前关联值, 即用共享内存的当前关联值覆盖shmid_ds的值。
    • IPC_SET:如果进程有足够的权限,就把共享内存的当前关联值设置为shmid_ds结构中给出的值
    • IPC_RMID:删除共享内存段(通常用这个就可以)

分离操作

int shmdt(const void *shmaddr);
  • 注意, 并没有删除标识符和数据结构

代码实例(sem+shm)


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

void server()
{
   key_t sem_key = ftok("./", 2016);        //信号量的key
   int sem_id = semget(sem_key, 1, IPC_CREAT | 0666);

    struct sembuf sb = { 0 };
    sb.sem_op = -1;
    sb.sem_flg = SEM_UNDO;

    semop(sem_id, &sb, 1);
    //如果获取了信号量, 表示另外以测已经写入了

    key_t shm_key = ftok("./", 2015);
    int shmid = shmget(shm_key, 64, IPC_CREAT | 0666);
    void *addr = shmat(shmid, NULL, 0);

    printf("Get Sem & Get [%s]\n", (char*)addr);
   
    semctl(sem_id, 0, IPC_RMID);     //删除信号量

    shmdt(addr);
    shmctl(shmid, IPC_RMID, NULL);      //删除共享内存

}

void client()
{
    key_t key = ftok("./", 2015);
    int shmid = shmget(key, 64, IPC_CREAT | 0666);

    key_t sem_key = ftok("./", 2016);
    int semid = semget(sem_key, 1, IPC_CREAT|0666);

    struct sembuf sb = { 0 };
    sb.sem_op = 1;
    sb.sem_flg = SEM_UNDO;

    void *addr = shmat(shmid, NULL, 0);

    strcpy((char*)addr, "this is client!");

    semop(semid, &sb, 1);
    printf("%s\n", "Write Data & Release Sem!");

    shmdt(addr);
    return ;
}
 

 
int main() {
    server();
    return 0;
}

几种通信方式比较

  • 管道文件步占用磁盘空间, 管道读取完毕后会自动进入阻塞; 管道是半双工的, PIPE_SIZE限制为64k

  • 管道是没有边界的, 只能传递无格式字节流

  • 消息队列独立于进程外, 进程退出后数据依然存在; 要考虑上一次没有读完数据的问题

  • Linux下一个消息队列的最大字节数为 16k,系统中最多存在 16 个消息队列

  • 消息队列在使用完后需要手动删除

  • 信号/信号量: 不能用于传递复杂消息

  • 共享内存: 是最快的, 因为少了将数据从用户态复制到内核态的拷贝过程; 缺乏同步安全

接口

很多接口都是类似的, 做一下总结

  • 无名管道: pipe+write/read

  • 有名管道: mkfifo + open(注意阻塞) + read/write + unlink

  • 信号: kill+signal

  • 内存映射(文件映射到内存): open+mmap+munmap

  • 消息队列(通过key作为唯一标识): ftok+msgget+msgsnd+msgrcv+msgctl

  • 信号量: ftok+semget+semop+semctl

  • 共享内存: ftok+shmget+shmat+shmdt+shmctl

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,372评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,368评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,415评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,157评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,171评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,125评论 1 297
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,028评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,887评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,310评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,533评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,690评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,411评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,004评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,659评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,812评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,693评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,577评论 2 353

推荐阅读更多精彩内容