【Linux】进程间通信

版权声明:本文为博主原创文章,未经博主允许不得转载。Copyright (c) 2018, code farmer from sust. All rights reserved. https://blog.csdn.net/sustzc/article/details/82734433

> test.c 相当于把一个空文件输入重定向到test.c中,实际上就是清空test.c的内容。

dup(fd);    查找第一个未被使用的文件描述符
    修改文件描述符的指向。
    dup函数对传入的文件描述符进行复制,返回一个新的文件描述符,
    新的文件描述符一定是系统可用文件描述符的最小数值。
输入重定向
    close(0);
    dup(fd);
输出重定向
    close(1);
    dup(fd);
    
    int dup2(int oldfd, int newfd);
    newfd重定向到oldfd
    
scanf是基于read实现的
printf是基于write实现的    
    
命令之mkfifo   必须保证在相同目录下操作
    mkfifo my.p  // 创建一个管道
    echo "hello" > my.p // 输出内容到管道缓存中
    cat my.p // 从管道中读取写入的数据

//创建管道文件
int mkfifo(const char *name, mode_t mode)

int main(int argc, char *argv[])
{
    mkfifo("p2", 0644);
    return 0;
}

//打开管道文件
int fd = open(name, O_RDONLY); //读
int fd = open(name, O_WRONLY); //写

//read write  语义和匿名管道一样

管道的最大缓存是65536字节
使用write的时候不要一次使用超过4096字节,也就是PIPE_BUF的大小,这时候可能不再是原子操作。

原子操作:一次不存在任何中断或者失败的操作。
    要么操作成功完成;要么操作没有执行;不会出现部分执行的状态。
OS需要利用同步机制在并发执行的同时,保证一些操作是原子操作。

消息队列(间接通信)
    比较鸡肋,如果传输的数据量太大的话,就会崩溃。
    由于处在用户态的进程之间无法通信,OS提供了一种消息队列的机制,比如队列1指向一个结点,内核空间内的链表是连续的,
    而队列2从该结点读取数据,这样就实现了进程间的通信。
    重点:OS创建队列(***get()) 、 读取数据(***rcv()) 、 写入数据(***snd()) 、 销毁内核链表(***ctl())
    
    由OS维护的以字节序列为基本单位的间接通信机制,其中 每个消息就是一个字节序列,相同标识的消息按照
    先进先出的顺序组成一个消息队列。
    双向链表

创建消息队列    
    #include <sys/ipc.h>     SystemV IPC
    #include <sys/msg.h>
    int msgget(key_t key, int msgflag);
    返回值:消息队列的id,相当于open的返回值(文件描述符)。
    注意:不需要close
    key   key来代表内核对象,由于不能写成文件名,因此用数字来代替。
    flag  创建IPC_CREAT | 0644
          0表示打开
    ipcs命令:查看所有IPC对象的状态    SystemV IPC中不支持文件,所有的都缓存在内核中
        messages记录存放了多少条消息   used-bytes消息占用的字节数
        -q 查看创建的消息队列当前的状态
    IPC关闭:手动删除;重启。
    ipcrm:手动删除IPC对象
        ipcrm -Q key 删除消息队列
    消息队列是有限制的资源
        cat /proc/sys/kernel/msgmni   系统上能够创建的消息队列的总数MSGMNI
        cat /proc/sys/kernel/msgmax   每个消息最多能装多少个字节MSGMAX
        cat /proc/sys/kernel/msgmnb   每个消息队列中所有消息的总字节数MSGMNB

往消息队列中发送数据
    int msgsnd(int id, const void *msgp, size_t msgsz, int msgflag);
            1.msgget的返回值  2.要发送的消息在哪里 3.消息的字节数(不包括channel的大小,实际发送消息的大小)   4.0
    返回值:成功返回0,失败返回-1
    参数msgflag:发送标志一般填0(消息队列会阻塞在这里并等待)
    如果消息队列已存在,那么标志可以写为0。(也可以写为之前消息队列设置的权限,如果权限不同,root用户可以创建,但是普通用户不可以。)
    struct msgbuf
    {
        long mtype;        //long channel;  //消息类型(通道号),必须大于等于1  最多两亿
        //随便写
        char mtext[10];  //写上自己的消息内容
    };
    
    ./msgrcv mtype
    
往消息队列中接收数据    
    int msgrcv(int id, const void *msgp, size_t msgsz, long msgtype, int msgflag);
                msgget返回值  取出来的消息放的地方  装消息的地方的大小(mtext大小),不包括类型  取哪个类型的消息  0(阻塞等待)
    返回值:成功返回实际读取的字节数,失败返回-1

删除消息队列
    msgctl
缺陷:装的内容有限,使用时需要切换用户态到内核态,系统开销大。

C/S模型
    消息队列实现C/S收发消息
    都发送到某个给定的mtype   根据pid接收消息  发送消息的时候在mtext数组中添加pid的内容(前4个字节)(后面是发送的消息内容)
    以便于区分是哪个进程的消息。
    server.c
        mb.mtype=*(int *)(mb.mtext);
        msgsnd(id, &mb, strlen(mb.mtext+sizeof(int))+sizeof(int), 0);  //strlen括号中加上sizeof(int)是为了偏移保存pid号的一个整型所占的字节。
    client.c
        fgets(mb.mtext+sizeof(int), 1024-sizeof(int), stdin); //读取键盘内容
        msgsnd(id, &mb, strlen(mb.mtext+sizeof(int), 0);   //发送给服务器
        //读取服务器前先清空
        memset(&mb, 0x00, sizeof(mb));
        msgecv(id, &mb, 1024, getpid(), 0);
共享内存(直接通信)
    最快的进程间通信方式
    
    首先物理内存的数据通过三级映射分别指向虚拟内存中的共享存储映射区,
    那么一个进程通过共享存储映射区修改物理内存中存储的数据,只需要把该数据的存储地址交给共享存储映射区即可在虚拟内存中修改,
    跟该进程一起指向物理内存的另外一个进程就会收到消息(某个进程修改了物理内存)。
    重点:想方设法让物理内存可以映射到共享存储映射区。
    
    把同一个物理内存区域同时映射到多个进程的内存地址空间的通信机制。
    进程
        每个进程都有私有内存地址空间
        每个进程的内存地址空间需要明确设置共享内存段
    线程
        同一进程中的线程总是共享相同的内存地址空间
    优点:快速方便地共享数据
    不足:必须用额外的同步机制来协调数据访问
    需要信号量等机制协调共享内存的访问冲突
    
    创建或打开共享内存
    #include <sys/ipc.h>     SystemV IPC
    #include <sys/shm.h>
    int shmget(key_t key, size_t size, int flag);
        共享内存的id   共享内存段的大小(0或者真实大小)  创建IPC_CREAT|0644,
        0表示打开
    ipcs -m  查看共享内存信息
    ipcs -M key  删除创建的共享内存
        nattch  有几个映射到共享内存
    
    让共享内存和本进程建立关系(粘贴)
        OS可以通过页表查看到某个虚拟内存的地址
    void* shmat(int id, const char * shmaddr, int flag);
            1.共享内存的id   2.让OS挂载到该地址空间(NULL 让OS自己选择)   3.0
    返回值:实际挂载到虚拟地址的起始位置
    
    卸载掉共享内存段(只是之间的联系断开了)
    int shmdt(void *shmaddr);  
    成功返回0失败返回-1
    
    删除共享内存
    int shmctl(int id, int cmd, NULL); //cmd写IPC_RMID  删除共享内存   NULL在#include <stdlib.h> 中  最后一项也可以写0
    
信号量(灯)
    指示作用
    临界资源:具有排他性(只能一个人用)的资源
    临界区:进程中访问临界资源的一段需要互斥执行的代码
    进入区
        检查可否进入临界区的一段代码
        如可进入,设置相应的“正在访问临界区“标志
    退出区
        清除相应的“正在访问临界区“标志
    剩余区
        其他代码
    
    临界区的访问规则
        空闲则入 忙则等待 有限等待 让权等待(可选)
临界区的实现方法
    禁用中断
        没有并发,进程无法停止,会导致其他进程处于饥饿状态。
    更高级的抽象方法
        锁
    互斥访问数据(上厕所)
    上锁(P)  
        计数器--
            计数器小于0置成阻塞状态,在阻塞队列中等待被唤醒
    P尝试减少
        申请一个资源,那么当前可用资源数sem减1
        若sem < 0,进入等待,如果某个线程释放了资源,就可以使用了,否则继续。
    注意:P()可能阻塞,V()不会阻塞。
    通常假定信号量是"公平的"
        线程不会被无限期阻塞在P()操作
        假定信号量等待按照先进先出排队     (自旋锁不能实现先进先出)
    注意:PV操作必须成对出现。
    P(s)
    {
        s.value = s.value--;
        if (s.value < 0)
        {
            该进程状态置为等待状态
            将该进程的PCB插入相应的等待队列s.queue末尾
        }
    }
    
    解锁(V)
        计数器++
            唤醒阻塞队列
    V增加
    释放一个资源,那么当前可用资源数sem加1
    若sem <= 0,则唤醒一个等待进程。
    V(s)
    {
        s.value = s.value++;
        if (s.value < =0)
        {
            唤醒相应等待队列s.queue中等待的一个进程
            改变其状态为就绪态
            并将其插入就绪队列
        }
    }

信号量集结构
    进程同步:多个进程需要相互配合共同完成一项任务。
    同步:P V不在同一进程
    互斥:P V在同一进程中
    
    信号量值S含义
        S>0表示可用资源的个数
        S=0表示没有可用资源,没有可用进程
        S<0 ISI表示等待队列中的进程个数
    
    司机与售票员
    
    sem_sj = 0  // 司机能不能启动车辆
    sem_py = 0    // 售票员能不能开门
    
        司机
        while (1)
        {
            P(sem_sj)
            启动车辆
            正常行驶
            到站停车
            V(sem_py)
        }
        
        售票员
        while (1)
        {
            关门
            V(sem_sj)
            售票
            P(sem_py)
            开门
        }
        
家庭采购问题分析
    买面包买重了
    不好的解决方案:在冰箱上设置一个锁和钥匙,在买面包之前锁住冰箱并且拿走钥匙。
    事实上,加锁后会导致冰箱中的其他食品,别人取不到。
    
    方案一:使用便利贴来提示以避免购买太多面包。
        if (nobread)
        {
            if (noNote)
            {
                leave Note;
                buy bread;
                remove Note;
            }
        }
    不足:有可能会错过,没有看到便利贴导致买重。
    方案二:先留下便利贴,后检查面包和便利贴。
        leave Note;
        if (nobread)
        {
            if (noNote)
            {
                buy bread;
            }
        }
        remove Note;
    不足:会出现没人买面包
    方案三:标记便利贴,以区别不同人的便利贴。
    进程A                       进程B
    leave Note1;            leave Note2;
    if (noNote2)            if (noNote1)
    {                        {
        if (nobread)            if (nobread)
        {                        {
            buy bread;                buy bread;
        }                        }
    }                        }
    remove Note1;            remove Note2;
    不足:可能导致没有人去买面包(每个人都认为对方去买面包,实际上都没有去买。)
    方案四:两个人采用不同的处理流程
    进程A                      进程B
    leave Note1;            leave Note2;
    while (noNote2)            if (noNote1)
    {                        {
        ;                        if (nobread)
    }                            {
    if (nobread)                    buy bread;
    {                            }
        buy bread;            }
    }                        remove Note2;
    remove Note1;                
    不足:比较复杂
    方案五:利用两个原子操作实现一个锁
    Lock.Acquire()
        在锁被释放前一直等待,然后获得锁,
        如果两个线程都在等待同一个锁,并且同时发现锁被释放了,那么只能有一个能够获得锁。
    Lock.Release()
        解锁并且唤醒任何等待中的进程
    基于原子锁的解决方案
        breadlock.Acquire();  //进入临界区
        if (nobread)
        {
            buybread;          //临界区
        }
        breadlock.Release();  //退出临界区
    
    创建或打开信号量
    int semget(key_t key, int nsems, int semflag);
            信号量id   信号量集 中 信号量的个数(一般写1)  创建用IPC_CREAT|0644,打开写0
    ipcs -s查看信号量信息
    ipcrm -S key删除某个信号量
    ipcs -s选项中的nsems表示该信号集中有多少个信号量
    
    设置信号量初值sem
    原型:
    int semctl(int semid, int semnum, int cmd, ...);
    int semctl(int semid, int semnum, int cmd, su); //su为设置的信号量初值
              1.信号量id   2.信号量集中的第几个信号量  3.SETVAL  4.(使用联合体semun中的val,自己定义)
              创建好了信号集后,再次打开信号集时,可以填写0,由OS自动分配。
    查看信号量初值
        将SETVAL 改为GETVAL,最后的可变参数可以不用传参
        返回值:当前信号量的值

PV操作
    创建和访问一个信号量集
    int semop(int semid, struct sembuf sb[], int len);
    成功返回0,失败返回-1。
    
    struct sembuf
    {
        short sem_num; //信号量的下标  (一般情况下只有一个信号量,因此这里一般填0表示第一个元素)
        short sem_op; //1 v操作,-1 p操作
        short sem_flg; //一般写0
    };
    
    struct sembuf sb[1]; //信号量集中只有一个信号量
在共享内存段中使用信号量进行PV操作
    在读取数据前,已经V了多次,那么在进行P操作的时候,就会输出多个相同数字(P要先执行--),如果P减到0了,那么再减一次变成-1,
    就会阻塞在那里,直到V加1后才可以读取数据,这样就实现了一边写数据,一边读取数据。
作业:P操作V V也可以操作P
    设置两个信号量,一个表示写完了可以读sem1=0,一个表示读完了可以写sem2=1.
    写端:p(sem2)...v(sem1)   在写之前先发送请求写,由于sem2=1,此时p操作后值变为0,
        写入数据,最后分配给读资源sem1=1。
    读端:p(sem1)...v(sem2)   在读之前先发送请求读,由于sem1=1,此时p操作之后值变为0,
        读取数据,最后读取完毕后分配给写资源sem2=1,这样写端就可以继续写了。
   

猜你喜欢

转载自blog.csdn.net/sustzc/article/details/82734433