进程间通信

进程间通信

在前面我们学习了如何创建进程,但是如何让我们创建的这些进程相互联系起来,那么就需要利用进程间通信来完成。

概述

进程间通信就是在不同进程之间传播或交换信息,但是之前我们学过,每个进程在创建的时候都会分配自己独有的 4G 虚拟地址空间,进程之间都是相互独立的,所以一般而言是不能互相访问的,但是也有例外,那就是共享存储映射区。并且系统空间也是公共的,每个进程都可以访问,所以内核也可以提供进程间通信的条件。

目的

进程间通信的目的 :
- 数据传输 :一个进程需要将它的数据发送给另一个进程
- 资源共享 :多个进程之间共享相同的资源
- 通知事件 :一个进程需要向另一个进程或进程组发送消息,通知他们发生了某种事件
4. 进程控制 :有些进程希望完全控制另一个进程,此时控制进程希望能够拦截另一个进程的所有陷入和异常。

分类

  • 管道
    • 匿名管道
    • 命名管道
  • System V 进程间通信
    • 消息队列
    • 共享内存
    • 信号量
  • POSIX 进程间通信
    • 消息队列
    • 共享内存
    • 信号量
    • 互斥量
    • 条件变量
    • 读写锁

管道

我们把一个进程连接到另一个进程的一个数据流称为一个 ’ 管道 ‘。

匿名管道

#include <unistd.h>
int pipe(int pipefd[2]);
其中 pipefd[0] 表示读端
    pipefd[1] 表示写端

管道是单向的,先进先出的,无结构的,固定大小的字节流。
它把一个进程的标准输出端和另一个进程的标准输入端连接在一起。
写进程在管道的尾端写入数据,读进程在管道的首部读取数据。数据读出后将从管道中移除,其他读进程不能再读到这些数据。

管道提供简单的流控制机制,进程在试图读空管道时,在有数据写入管道之前 ,读进程将一直阻塞。当管道已经满时,进程再试图写入数据,在其他进程将数据从管道中移除前,写进程将一直阻塞。

管道可存放数据上限 512 bytes * 8 = 4096 bytes
这里写图片描述

例子1

从键盘读取数据,写入管道,读取管道,输出到屏幕上。
相当于我们将本进程的标准输入写入到管道里 ,然后从管道中读取,再将读取到的数据输出到标准输出中。
这里写图片描述
实现 :

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

int main( void ){
    int ret = 0;
    int len = 0;
    int fd[2];
    char buf[1024] = {0};

    ret = pipe(fd );
    if(ret < 0)
        perror("pipe"),exit(1);
        // read from stdin
    while(fgets(buf, 1024, stdin)){
        len = strlen(buf);
        // write into pipe
        if( write(fd[1] ,buf, len ) != len){
            perror("write into pipe");break;
        }   

        memset(buf, 0x00, 1024);
        // read fron pipe
        if((len = read(fd[0], buf, len)) < 0){
            perror("read");break;
        }

        // write into stdout
        if(write(1, buf, len) < 0){
            perror("write");break;
        }
    }
}

测试 :
这里写图片描述

例子2

两个进程共享一个管道,其中一个管道负责写入数据,一个管道负责读取数据。
需要两个进程所以我们需要调用 fork,由于子进程会继承父进程的文件描述符表,所以此时两个进程公用同一个管道。
所以会出现同时有两个输入端,同时有两个输出端的情况
这里写图片描述
此时我们需要关闭输入进程的读取端和输出进行的输入端。
在这里我们使用父进程写入,子进程读取并输出到屏幕上。
这里写图片描述

实现 :

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

int main( void ){
    int fd[2];
    if(pipe(fd) < 0)
        perror("pipe"),exit(1);

    int pid = fork();
    if(pid < 0)
        perror("fork"),exit(1);
    else if(pid > 0){ 
    //parent
        close(fd[0]);  // 关闭读取端
        int ret = write(fd[1], "new", 3); 
        if(ret < 0)
            perror("write"),exit(1);
    }else{
    //child
        close(fd[1]);  // 关闭输入端
        char buf[10] = {0};
        int ret = read(fd[0], buf, 10);
         if(ret < 0)
            perror("read"),exit(1);
        printf("%s\n", buf);
    }
}

这里写图片描述

特点
  • 当没有数据可读时,read 调用阻塞,暂停进程,等待数据到达。
  • 当管道已满时,wirte 调用阻塞,暂停进程,直到有进程读走数据。
  • 如果所读取的管道对应的文件描述符被关闭,则 read 返回 0。
  • 如果所写入的管道对应的文件描述符被关闭,则 write 触发异常,发出 SIGPIPE 信号,随后write 退出。
  • 如果写入的数据小于管道的容量,则保证写入操作的原子性,否则不保证。
  • 只能用于具有血缘关系的进程之间通信。
  • 流式服务
  • 进程退出,管道释放,管道的生命周期与进程同步。
  • 半双工,只能单向流动,如果想要双向通信,只能创建两个管道。

匿名管道

#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);

实现 :

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

int main( void  ){  
    mkfifo("test",0644);
}

运行后会创建一个名为 test 的管道文件。
这里写图片描述

匿名管道与命名管道的区别

匿名管道由函数 pipe 创建并打开
命名管道由函数 mkfifo 创建,由 open 打开
它们之间的区别只有打开和创建的方式不同,一旦这些工作完成后,它们将具有相同的语义。

缺点

管道发送的消息无法控制,是一次性全部发送过去的。

消息队列

概述

消息队列提供从一个进程向另一个进程发送数据块的方法。
每个数据块都被认为是有类型的,接收进程接收的数据可以有不同的类型。
消息队列缺点 :每条消息的最大长度是有上限的 ,每个消息队列可存放消息的总字节数是有上限的,系统上可以创建的消息队列的总数也是有上限的。
我们可以通过指令 ipcs -ql 进行查看消息队列的限制
这里写图片描述

消息队列可以将消息进行分片,保证获得的消息是完整的 。

内核为每个进程间通信对象维护一个数据结构

struct ipc_perm
{
    __kernel_key_t  key;  //xxxget 函数提供
    __kernel_uid_t  uid;  // 拥有者 ID
    __kernel_gid_t  gid;  // 拥有者 组ID
    __kernel_uid_t  cuid; // 创建者 ID 
    __kernel_gid_t  cgid; // 创建者组 ID
    __kernel_mode_t mode; // 权限 
    unsigned short  seq;  // 序列号
};

消息队列结构

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 */
    // 最后一个发送消息的队列的 pid
    __kernel_ipc_pid_t msg_lrpid;   /* last receive pid */
    // 最后一个接收消息的队列的 pid
};

消息队列控制

创建

msgget() 函数

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);

参数:
key
- 写死 1234 , a
- IPC_PRIVATE
- ftok() 函数

如果 key 是 IPC_PRIVATE ,创建一个新的消息队列。
如果 key 不是IPC_PRIVATE,同时 msgflg 设定为 IPC_CREAT ,如果所要创建的对象已经存在则检查相应的访问权限,若允许访问则返回该对象的标识符,如果不存在则创建新的消息队列。

函数 ftok()

key_t ftok ( const char *path ,path 是一个路径, 一般使用当前目录(必须存在)
             int  pro_id )    pro_id 是子序号 低 8 位不能位 0 

我们默认 ftok 函数的返回值是唯一的
返回值
这里写图片描述
次设备号查看
这里写图片描述
文件索引查看
这里写图片描述
将 920587转换为十六进制 0x000e 0c0b

现在我们使用 ftok() 函数创建一个消息队列

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

int main( void ){
    int id = msgget(ftok(".",'a'),IPC_CREAT);
    if( id == -1) 
        perror("msgget"),exit(1);
    printf("msgget ok\n");
}

在这里我们给出的 pro_id 是 ‘a’, —–> 97 —-16进制 —- 61
次设备号为 2 —-16进制 —- 02
之前的文件描述符的低16位为 0c0b
组合起来我们应该产生会产生一个key值为 0x 6102 0c0b 的消息队列。
通过指令 ipcs -q 查看我们创建的消息队列
这里写图片描述

删除

我们可以通过 ipcrm -Q + key值手动删除一个消息队列。

msgctl() 函数 删除一个消息队列

int msgctl( int msqid, 相应的消息队列号
            int cmd, IPC_STAT 
                     IPC_RMID 删除该消息队列
            struct msqid_ds *buf )  每一个消息队列都有一个 msqid_ds 来描述队列当前状态
                                     没事就填 NULL

实现 :我们利用之前创建的消息队进行删除

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

int main( void )
{
    int id = msgget(ftok(".",'a'), 0); 
    if( id == -1) 
        perror("ftok"),exit(1);

    msgctl(id, IPC_RMID, 0); 
}

这里写图片描述

接收/发送消息
msgsnd() 函数 —- 接收消息
int msgsnd( int msqid,  要写入的消息队列标识符
            const void *msgp,   指向消息缓冲区的指针 msgbuf,用户可定义的通用结构
            size_t msgsz,  消息大小,不包含 channel 字段
            int msgflg )  IPC_NOWAIT  当消息队列写满时马上返回  并置错误码 errno = EAGAIN

如果我们第一次发送消息是向 3 号管道发送,则系统同时会打开 12号管道。
    管道实际标号为 管道号 -1
msgrcv() 函数 —- 发送消息
ssize_t  msgrcv ( int msqid ,       要读取的消息队列标识符
                  void *msgp ,    指向消息缓冲区的指针 msgbuf
                  size_t msgsz ,   消息大小
                  long msgtyp ,   > 0 从指定的通道收取数据
                  = 0 返回消息队列中的第一条
                  < 0 读取 <= |tpye|,并且从最小的开始读取
                  int  msgflg )     一般写 0  / IPC_NOWAIT

现在我们用前面的函数来制作一个环形消息队列,一个进程负责向消息队列中发送消息,另一个进程负责从消息队列中读取消息。

发送 :

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

struct msgbuf{
    long channel;
    char text[1024];
};

int main( int argc, char* argv[] ){
    if(argc != 3)
        printf("usage :%s  channel msg\n",argv[0]);

    int id = msgget(1234, 0); 
    if(id == -1) 
        perror("msgget"),exit(1);

    struct msgbuf mb; 
    mb.channel = atol(argv[1]);
    strcpy(mb.text, argv[2]);

    if(msgsnd(id, &mb, strlen(mb.text), 0) == -1) 
        perror("msgsnd"),exit(1);
}

接收 :

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

struct msgbuf{
    long int channel;
    char text[1024];
};

int main(int argc, char* argv[]){
    long type;
    struct msgbuf mb = { };
    if(argc != 2)
        printf("usage :%s channel msg%\n", argv[0]);

    int id = msgget(1234, 0); 
    if(id == -1) 
        perror("msgget"),exit(1);

    type = atol(argv[1]);
    if(msgrcv(id, &mb, 1024, type, 0) <  0)  
        perror("msgrcv"),exit(1);
    printf("%s\n",mb.text);
}

现在我们向 1 3 5号消息队列发送消息
这里写图片描述

我们对所创建的消息队列进行查看
这里写图片描述
可以看到,我们发送数据的总大小为 13个字节,总共有三条消息在等待接收

接收消息
这里写图片描述
但是这里要注意,如果我们接收的消息队列中没有消息,那么接收端将阻塞等待,直到有消息被写入
这里写图片描述
这里写图片描述

缺点

系统限制了消息队列的大小,没有共享内存更高效。

共享内存

概述

共享内存是进程间通信中最快的一种,当我们创建的共享内存映射到不同的进程之间后,当这些进程之间需要进行数据交换时,就不再需要内核的参与,效率变得更高。
这里写图片描述
而我们所创建的共享内存就位于栈和堆之间的共享存储映射区,在同一位置的还有相应的内存映射和共享库。

函数

创建

shmget()函数

 int shmget(key_t key, size_t size, int shmflg);
 key 要获取/创建的共享内存标识符
 size 创建时输入大于0 的整数,所要创建的共享内存的大小
 shmflg IPC_CREAT|0644

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

struct buf{
    int nu; 
    char name[20];
};
int main(){
    int ret = shmget(1234, sizeof(struct buf), IPC_CREAT|0644);
    if(ret == -1) 
        perror("shmget"),exit(1);
    printf("shmget ok\n");
}

通过 ipcs -m 查看创建的共享内存
这里写图片描述
这里写图片描述

删除

shmctl()函数

int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmid shmget返回的标识符
cmd   IPC_RMID 删除共享内存
buf   指向保存着共享内存的数据结构       

同时我们也可以使用 ipcrm -M + key 删除共享内存

挂载

shmat()函数

void *shmat(int shmid, const void *shmaddr, int shmflg);
shmid shget返回的标识符
shmaddr 指定链接的地址 填 NULL 由系统自动分配
shmflg  暂时填 NULL
取消挂载
int shmdt(const void *shmaddr);
shmat 返回的地址

例 :
我们使用共享内存进行简单的数据交换
输入

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

struct buf{
    int nu; 
    char name[20];
};

int main(){
    int ret = shmget(1234, 0, 0); 
    if(ret == -1) 
        perror("shmget"),exit(1);

    struct buf* mybuf = (struct buf*)shmat(ret, NULL,0);
    mybuf->nu = 10; 
    strcpy(mybuf->name, "hekzl");

    shmdt(mybuf);
}

获取

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

struct buf{
    int nu; 
    char name[20];
};

int main(){
    int id = shmget(1234, 0 ,0);
    if(id == -1) 
        perror("shmget"),exit(1);

    struct buf * p = (struct buf *)shmat(id, NULL, 0); 
    printf("nu = %d name = %s\n", p->nu, p->name);
}

这里写图片描述

信号量

信号量在进程间通信主要用于进程间的同步与互斥
互斥 :
某一资源在同一时间只允许一个访问者进行访问,具有唯一性和排他性,但互斥无法限制访问者对资源的访问顺序,即访问是无效的。
同步 :
通过其他机制实现访问者对资源的有序访问,在大多数情况下,同步已经实现了互斥,特别是所有写入操作都必须互斥,少数情况可以允许多个访问者同时访问资源。

在进程中,涉及互斥资源的程序段称为临界区。

函数

semget()函数

int semget(key_t key, int nsems, int semflg)
key 要获取/创建时对应的标识符
nsemes 信号量集中的信号量个数
semflg 创建 IPC_CREAT|0644
       获取 填 0

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

int main( void )
{
    int id = semget(1234, 1, IPC_CREAT|0644);
    if( id == -1) 
        perror("semget"),exit(1);
    printf("semget ok\n");
}

这里写图片描述

控制

semctl()函数

int semctl(int semid, int semnum, int cmd, ...);
semid semget返回的标识符
semnum 所要操作的信号量编号
cmd 操作 : SETVAL 设置信号量的初值
           GETVAL 返回信号量的当前值
           IPC_RMID 删除信号量
           ....
这个函数是否有第四个参数取决于 cmd 
当有第四个时,第四个参数是一个联合类型
union semun {
    int              val;    /* Value for SETVAL */
    // val 的初值 
    struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
    // IPC_STAT,IPC_SET对应的结构体
    unsigned short  *array;  /* Array for GETALL, SETALL */
    // GETALL SETLL 对应的数组 
    struct seminfo  *__buf;  /* Buffer for IPC_INFO
    // IPC_INFO 对应的结构体    (Linux-specific) */
};

PV操作

semop()函数
在信号量的数据结构 semun中有一个val值和一个指针,指针指向下一个等待该信号量的进程。信号量的值与相应资源的使用相关。当他大于 0 时,表示当前可用资源数,当小于零时,则表示当前等待资源的进程数。
而信号量的值只能通过 PV 操作来更改。
执行一个 P 操作,意味着请求分配一个资源,信号量的值 -1。
执行一个 V 操作,意味着请求释放一个资源,信号量的值 +1。
要注意,P V操作必须成对使用,从而不会造成死循环。

int semop(int semid, struct sembuf *sops, unsigned nsops)
semid semget返回的标识符
sops  指向存储信号操作结构的数组指针
nsops 信号操作结构的数量
sembuf 结构体
struct sembuf{
unsigned short sem_num;  /* semaphore number */
// 信号量编号
short          sem_op;   /* semaphore operation */
// -1  p 操作  +1 v 操作
short          sem_flg;  /* operation flags */
// IPC_NOWAIT 不阻塞 SEM_UNDO 保证信号值会被重设,避免异常情况
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值