linux 进程间通信详解

前言

        进程间通信简称 IPC(Interprocess communication),进程间通信就是在不同进程之间信息的交换。每个进程都有独立的用户空间,是互不干扰的,所以要实现进程间的通信就需要第三方的介入,从而达到进程间信息交换的功能,实际上系统会分配一段物理内存作为第三方介质。

通信方式分类

UNIX IPC包括无名管道、FIFO(有名管道)、信号;System V IPC包括:信号量、消息队列、共享内存;POSIX IPC包括:信号量、消息队列、共享内存;Socket IPC:基于Socket进程间通信。

POSIX IPC是基于System V IPC改进优化的。

以下内容以System V IPC记录。

1. 管道

1.1 无名管道

1.1  pipe函数原型

头文件:

#include <unistd.h>

函数原型:

int pipe(int pipefd[2])

参数:

pipefd[2]:表示管道的文件描述符,pipefd[0]为读pipefd[1]为写

功能:

创建并打开无名管道;

返回值:

成功:返回 0

失败:返回 -1

1.2  无名管道特性

只能在有亲缘关系的进程间通信,例如父子进程、兄弟进程、子孙进程等;

半双工通信,只能一个进程读,一个进程写,不能同时读写。

创建好管道后,将一个进程的读端关闭、写端打开,将另一个进程的写端关闭、读端打开。写端写入的数据存放到内存缓冲区,等待读端的读取。

 管道中的资源就好比临界资源,对临界资源的操作就要有同步或互斥机制,不能多个进程同时对临界资源操作,管道内部就自带有同步与互斥机制,不需要我们另外添加,管道提供的是流服务,它的容量是有限的,生命周期跟随进程的生死。

如果管道中没数据,那么读进程就会等待数据的到来,再去读取;如果管道容量达到后就无法再往管道写数据了,需要先把管道数据读走后再写入。

1.3 例程

原进程调用pipe函数创建无名管道,然后调用fork函数创建子进程,子进程中关闭读通道,向管道中写入数据,然后退出;在父进程中,关闭写通道,等待管道中的数据并读取,读完后回收子进程,并退出父进程。

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

int main()
{
    int pipe_fd[2];
    pid_t pid = -1;
    char buff[16];

    if(pipe(pipe_fd) < 0){
        perror("pipe create");
        exit(0);
    }

    pid = fork();

    if(pid < 0){
        perror("fork");
        exit(0);
    }
    else {
        if(pid == 0) {
            close(pipe_fd[0]);
            write(pipe_fd[1],"hello qurry.",13);
            _exit(0);
        }
        if(pid > 0){
            close(pipe_fd[1]);
            read(pipe_fd[0],buff,sizeof(buff));
            printf("pipe read buff : %s\n",buff);
            wait(NULL);
            exit(0);
        }
    }
    return 0;
}

1.2 有名管道

1.2.1 函数原型

头文件:

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

函数原型:

int mkfifo(const char *pathname, mode_t mode)

参数:

pathname:有名管道的路径及名称;

mode:权限;

功能:

创建有名管道;

返回值:

成功:返回 0

失败:返回 -1

1.2.2  有名管道特性

半双工通信

无关联的进程可以通过有名管道文件描述符通信

1.2.3 测试例程

写两个进程,read_fifo进程用于创建、打开管道,并读取管道,管道名通过shell终端命令传入;

write_fifo进程用来打开管道和写数据到管道。

创建管道、并读管道(read_fifo.c)

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

int main(int argc,char *argv[])
{
    int fd = -1;
    char buff[64];
    size_t ret;
    if(argc < 2)
        exit(0);

    if(mkfifo(argv[1],0666) < 0){
        perror("mkfifo:");
        exit(0);
    }

    fd = open(argv[1],O_RDONLY);
    if(fd < 0){
        perror("open:");
        exit(0);
    }

    while(1)
    {
        ret = read(fd,buff,sizeof(buff));
        if(ret == 0){
            close(fd);
            exit(0);
        }
        printf("read buff : %s\n",buff);
        memset(buff,0,strlen(buff));
    }
    return 0;
}

 写管道(write_fifo.c)

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

int main(int argc,char *argv[])
{
    int fd = -1;
    char write_buff[] = "hello qurry fifo.";

    fd = open(argv[1],O_WRONLY);
    if(fd < 0){
        perror("open:");
        exit(0);
    }

    while(1)
    {
        write(fd,write_buff,sizeof(write_buff));
        sleep(5);
    }
    close(fd);
    return 0;
}
读进程
写进程

2. 消息队列

2.1 消息队列通信原理

消息队列提供了一个从一个进程向另一个进程发送数据块的方法,一个数据块包含一个长整型类型和数据,接收端通过这个类型筛选数据信息。这些IPC对象存在于内核空间中,由内核维护,生命周期跟随内核,终端命令行中输入“ipcs -q”查看内核维护的消息队列对象,删除指定消息队列命令“ipcrm -q msqid”。

2.2 消息队列通信流程

① 获取或自定义key键值,内核会通过key键值映射成IPC标识符;

② 通过①中获取的key键值,调用msgget函数获取msgid;

③ 通过②中获得的msqid,访问、控制相应的IPC对象。

2.3  消息队列API

2.3.1  获取key键值

一般有两种方法来设置键值:

自定义方式,例如:key_t key = 1234;

通过调用ftok函数获取。通过ftok生成key键值,可能会造成冲突,生成的key键值相同才能访问共一个消息队列。

这里说下返回值,Linux中文件或目录都有一个文件索引值,例如pathname文件索引为0x123456,proj_id设置为0x21,则产生的key键值为0x21123456,大概就是这个意思。

头文件:

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

函数原型:

key_t ftok(const char *pathname, int proj_id)

参数:

pathname:指定一个可以访问的路径或者文件名;

proj_id:可以设置为0~255,用来区别同一个路径或者文件名的不同序列号;

返回值:

成功:返回一个key键值;

失败:返回 -1 。

2.3.2  创建消息队列

通过msgget函数获取的msqid值对于进程访问控制IPC对象很重要。

头文件:

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

函数原型:

int msgget(key_t key, int msgflg)

参数:

key:填入获取的key键值;

msgflg:

        IPC_PRIVATE:创建一个该进程独占的消息队列,其它进程不能访问该消息队列;

        IPC_CREAT:如果对应key的消息队列不存在,创建一个新的消息队列

        IPC_EXCL:与IPC_CREAT搭配使用,如果对应key的消息队列存在,则出错返回;

        IPC_NOWAIT:以非阻塞的方式获取;

返回值:

成功:返回对应的msqid值;

失败:返回 -1。

2.3.3  发送函数

头文件:

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

函数原型:

int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg)

参数:

msqid:msgget函数获取的IPC标识码;

msgp:指向数据块的结构体指针;

msgsz:数据块中发送数据的大小;

msgflg:

        0:以阻塞的方式将数据块发送到消息队列;

        IPC_NOWAIT:以非阻塞的方式发送消息,若消息队列已满,函数立即返回;

返回值:

成功:返回 0;

失败:返回 -1

2.3.4 接收函数

参数msgtyp为发送的数据块结构体中的长整型类型,msgflg设置接收数据的方式。

头文件:

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

函数原型:

ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg)

参数:

msqid:msgget函数获取的IPC标识码;

msgp:指向接收到的消息存放的结构体内存;

msgsz:接收消息的数据大小;

msgtyp:接收消息的类型;

msgflg:

        0:以阻塞的方式接收消息队列的数据;

        IPC_NOWAIT:如果消息队列为空,不阻塞等待;

返回值:

成功:返回 0;

失败:返回 -1

2.3.5 控制函数

msgctl(msqid,IPC_EMID,0)表示将对应消息队列删除掉。

头文件:

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

函数原型:

int msgctl(int msqid, int cmd, struct msqid_ds *buf)

参数:

msqid:msgget函数获取的IPC标识码;

cmd:

        IPC_STAT:取出消息队列的msqid_ds结构体并将参数存入buf所指向的msqid_ds结构对象中;

        IPC_SET:设定消息队列的msqid_ds 数据中的msg_perm 成员。设定的值由buf 指向的msqid_ds结构给出;

        IPC_EMID:将队列从系统内核中删除;

buf:消息队列msqid_ds结构体指针;

返回值:

成功:返回0;

失败:返回-1。

2.4 测试例程

分别写两个测试例程,send.c用于数据块的发送,recv.c用于数据块的接收。

定义了一个struct msg_queue结构体,用于数据块的发送和接收,type表示消息类型;

通过当前路径和id = 97获取key值,这个怎么配置不重要,能获取唯一键值就行;

发送端我配置了十条不同类型的消息,需要接收端根据对应的消息类型去获取数据,接收端没有数据接收就删除该消息队列。

send.c代码如下

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

typedef struct msg_queue {
    long int type;
    char data[128];
}msg_queue_t;

int main()
{
    key_t key;
    int msg_id;
    msg_queue_t msg_q;

    key = ftok("./",97);
    if(key < -1){
        perror("ftok:");
        exit(0);
    }
    printf("key = %d\n",key);

    msg_id = msgget(key,0666 | IPC_CREAT);
    msg_q.type = 1;
    sprintf(msg_q.data,"qurry send data.");
    while(1)
    {
        if(msgsnd(msg_id,&msg_q,strlen(msg_q.data)+1,0) != -1) {
            printf("type:%ld;data:%s\n",msg_q.type,msg_q.data);
        }
        if(msg_q.type < 10)
            msg_q.type++;
        else
            exit(0);
    }
    return 0;
}

recv.c代码如下

#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_queue {
    long int type;
    char data[128];
}msg_queue_t;

int main()
{
    key_t key;
    static int i = 1;
    int msg_id;
    msg_queue_t msg_q;

    key = ftok("./",97);
    msg_id = msgget(key,0666 | IPC_CREAT);

    while(1)
    {
        if(msgrcv(msg_id, (void *)&msg_q,128, i++, IPC_NOWAIT)!= -1)
        {
            printf("type = %ld;data is %s\n",msg_q.type,msg_q.data);
            sleep(1);
        }
        else
        {
            perror("rcv");
            msgctl(msg_id,IPC_RMID,0);
            exit(0);
        }
    }
    return 0;
}

3. 共享内存

3.1 共享内存通信原理

共享内存是不同的进程通过访问同一块物理内存来达到信息交换的目的的。在物理内存中申请一块内存空间,然后将这块内存空间分别与各个进程各自的页表之间建立映射关系,从而建立起进程虚拟地址与物理共享内存块的映射。

共享内存的通信过程中没有系统调用,所以通信速度会相对消息队列和信号量快一点,它本身没有同步机制,但凡涉及到多个进程访问临界资源的情景,都是需要搭配同步机制的,否则会造成资源的混乱乃至崩溃。Linux系统中,共享内存查看命令“ipcs -m”。

3.2 共享内存通信流程

① 获取key键值,获取方式消息队列部分已经介绍;

② 调用shmget()创建指定大小的共享内存段在应用层的标识id;

③ 调用shmat()将共享内存段映射到进程的虚拟地址空间中;

④ 上面步骤完成后,才可以访问共享内存段资源。

3.3 共享内存API

3.3.1 创建共享内存

返回的标识符id后面操作共享内存都需要使用到。

头文件:

#include <sys/ipc.h>
#include <sys/shm.h>

函数原型:

int shmget(key_t key, size_t size, int shmflg)

参数:

key:获取的key键值;0(IPC_PRIVATE)会建立新共享内存对象,大于0由shmflg确定

size:申请的内存大小,为了避免内存碎片,一般设置为页(4kbyte)的整数倍;

shmflg:

        IPC_CREAT:如果已经创建了,则忽略返回;

        IPC_EXCL:搭配IPC_CREAT一起使用,如果已经创建了,则错误退出;

返回值:

成功:返回共享内存标识符id;

失败:返回 -1。

3.3.2 映射共享内存

头文件:

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

函数原型:

void *shmat(int shmid, const void *shmaddr, int shmflg)

参数:

shmid:共享内存的标识符;

shmaddr:挂接的进程虚拟内存地址,配置为NULL 则系统自动分配虚拟地址完成映射;

shmflg:

        0:表示共享内存可读可写;

        SHM_RDONLY:表示共享内存可读模式;

返回值:

成功:返回共享内存映射到进程中的虚拟地址;

失败:返回 -1

3.3.3  解除共享内存映射

有映射共享内存,相应的当不使用的时候就需要解除映射。此函数调用只是解除该进程的映射,并不会删除内核中的共享内存对象。

头文件:

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

函数原型:

int shmdt(const void *shmaddr)

参数:

shmaddr:需要解除的映射到进程的虚拟地址;

返回值:

成功:返回0;

失败:返回 -1

3.3.4 操作共享内存函数

头文件:

#include <sys/ipc.h>
#include <sys/shm.h>

函数原型:

int shmctl(int shmid, int cmd, struct shmid_ds *buf)

参数:

shmid:共享内存标识符id;

cmd:

        IPC_STAT:得到共享内存的状态,把共享内存的shmid_ds结构复制到buf中;

        IPC_SET:改变共享内存的状态,把buf复制到共享内存的shmid_ds结构内;

        IPC_RMID:删除共享内存;

buf:共享内存管理结构体;

返回值:

成功:返回 0 ;

失败:返回 -1

3.4 测试例程

编写了两个测试程序,write.c用于向共享内存中写入数据,read.c将共享内存中的数据读出后,将共享内存删除。

write.c代码如下

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

typedef struct shm_data {
    char data[64];
}shm_data_t;

int main()
{
    int shmid;
    key_t key;
    char *addr = NULL;
    shm_data_t Data;

    key = ftok("./",28);
    if(key != -1){
        printf("key = %d\n",key);
    }
    else{
        perror("ftok");
        exit(0);
    }
    shmid = shmget(key,4096, 0666 | IPC_CREAT | IPC_EXCL);
    if(shmid != -1){
        printf("shmid = %d\n",shmid);
    }
    else {
        perror("shmget");
        exit(0);
    }

    addr = shmat(shmid,NULL,0);
    printf("shm addr = 0x%x\n",addr);
    sprintf(Data.data,"%s","qurry shm data.");
    sprintf(addr,"%s",(char *)&Data);

    shmdt(addr);
    return 0;
}

read.c代码如下

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

typedef struct shm_data {
    char data[64];
}shm_data_t;

int main()
{
    int shmid;
    key_t key;
    char *addr = NULL;
    shm_data_t Data;

    key = ftok("./",28);
    if(key != -1){
        printf("key = %d\n",key);
    }
    else {
        perror("ftok");
        exit(0);
    }
    shmid = shmget(key,0,0);
    if(shmid != -1){
        printf("shmid = %d\n",shmid);
    }
    else {
        perror("shmget");
        exit(0);
    }
    addr = shmat(shmid ,NULL,0);
    sprintf(Data.data,"%s",addr);
    printf("read shm data :%s\n",Data.data);
    shmdt(addr);
    shmctl(shmid,IPC_RMID,0);
    return 0;
}

4. 信号量

4.1 信号量概述

信号量是一个特殊的变量,它被用于进程的同步与互斥,而不是交换数据信息,它的值与相应资源的使用情况有关,值大于 0 时,表示当前可用的资源数的数量;值小于 0 时,其绝对值表示等待使用该资源的进程个数;只允许等待操作(P操作)和发送操作(V操作)。linux终端下输入命令“ipcs -s”可查看信号量信息。

二值信号量是我们用的最多的,只有“0”和“1”两个值。以下仅根据二值信号量说明。

p操作:如果信号量的值大于0,执行p操作就给信号量减1;

             如果信号量等于0,执行p操作进程挂起等待。

v操作:如果有其他进程因等待信号量而被挂起,就让它恢复运行;

             如果没有进程因等待信号量而挂起,信号量就加1。

4.2 信号量API

4.2.1 创建或获取信号量集

头文件:

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

函数原型:

int semget(key_t key, int nsems, int semflg)

参数:

nsems:信号量数量;

返回值:

成功:返回信号量ID;

失败:返回 -1

4.2.2 信号量控制

头文件:

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

函数原型:

int semctl(int semid, int semnum, int cmd, union semun arg)

参数:

semnum:需要控制的信号量编码;0为第一个信号量;

cmd:

        IPC_STAT:获取信号量的属性;

        IPC_SET:设置信号量的属性;

        IPC_RMID :删除信号量;

        SETVAL:设置信号量的值;

arg:sem.h中定义的共用体变量;也可以自定义一个;

返回值:

成功:返回正数;

失败:返回 -1


/* arg for semctl system calls. */
union semun {
    int val;            /* value for SETVAL */
    struct semid_ds __user *buf;    /* buffer for IPC_STAT & IPC_SET */
    unsigned short __user *array;   /* array for GETALL & SETALL */
    struct seminfo __user *__buf;   /* buffer for IPC_INFO */
    void __user *__pad;
};

4.2.3 P、V操作函数

struct sembuf结构体中,成员变量

sem_num表示操作信号在信号集中的编号,第一个信号的编号是0;

sem_op表示p、v的操作值;

        1 为释放资源(V 操作),

        -1 为获得资源(P 操作),

        0 为等待,如果没有设置IPC_NOWAIT,则调用该操作的进程或者线程将暂时睡眠,直到信号量的值为0;否则,进程或者线程不会睡眠,函数返回错误;

sem_flg为信号操作标志;0 表示阻塞,IPC_NOWAIT 表示非阻塞。

头文件:

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

函数原型:

int semop(int semid, struct sembuf *sops, size_t nsops)

参数:

sops:指向存储信号操作结构体的数组指针;

nsops:要操作信号量的数量;

返回值:

成功:返回 0;

失败:返回 -1

/* semop system calls takes an array of these. */
struct sembuf {
    unsigned short  sem_num;    /* semaphore index in array */
    short       sem_op;     /* semaphore operation */
    short       sem_flg;    /* operation flags */
};

4.3  测试例程

编写两个测试程序,proc1.c创建并初始化信号量,初始化完成后,执行p操作获取信号量控制权,延迟10s再去执行v操作释放信号量控制权,在这10s期间,proc2.c进程是无法获取到信号量控制权的,等待proc1进程释放控制权后,立马获取到信号量控制权。

打开两个shell控制终端,先执行proc1,再去执行proc2。

proc1.c代码如下

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

int semid;
struct sembuf sem;
union semun {    //自定义共用体
    int val;     //信号量值
};

void sem_p()    //信号量p操作
{
    sem.sem_num = 0;
    sem.sem_op = -1;
    sem.sem_flg = 0;
    semop(semid,&sem,1);
}

void sem_v()    //信号量v操作
{
    sem.sem_num = 0;
    sem.sem_op = 1;
    sem.sem_flg = 0;
    semop(semid,&sem,1);
}

int main()
{
    key_t key;
    union semun arg;

    key = ftok("./",5);
    if(key != -1){
        printf("sem key: 0x%x\n",key);
    }
    else{
        perror("ftok");
        exit(0);
    }

    semid = semget(key, 1, 0666|IPC_CREAT);
    if(semid == -1){
        perror("semget");
        exit(0);
    }
    printf("semid : %d\n",semid);

    arg.val = 1;
    semctl(semid,0,SETVAL,arg);

    sem_p();
    sleep(10);    //10s后输出打印信息,并释放信号量
    printf("proc1 release sem.\n");
    sem_v();
    return 0;
}

proc2.c代码如下

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

int semid;
struct sembuf sem;

void sem_p()    //信号量p操作函数
{
    sem.sem_num = 0;    //信号量编号为0
    sem.sem_op = -1;    //减1
    sem.sem_flg = 0;
    semop(semid,&sem,1);
}

void sem_v()    //信号量v操作函数
{
    sem.sem_num = 0;
    sem.sem_op = 1;    //加1
    sem.sem_flg = 0;
    semop(semid,&sem,1);
}

int main()
{
    key_t key;

    key = ftok("./",5);
    if(key != -1){
        printf("sem key: 0x%x\n",key);
    }
    else{
        perror("ftok");
        exit(0);
    }
    semid = semget(key, 1, 0666|IPC_CREAT);
    if(semid == -1){
        perror("semget");
        exit(0);
    }
    sem_p();
    printf("get sem now.\n");
    sem_v();
    semctl(semid,IPC_RMID,0);    //删除信号量
    return 0;
}

5. 信号

信号记录在其它篇章,链接附上linux 信号

6.socket通信

socket通信知识点多,记录在其它篇章。链接如下Linux 网络编程socket

猜你喜欢

转载自blog.csdn.net/weixin_49576307/article/details/128578960