管道(匿名管道, 命名管道)和共享内存

管道

每个进程都拥有自己的独立的虚拟地址空间和页表结构, 所以每个进程是独立的, 进程间必须借助媒介来进行通信, 这些媒介包括管道, 共享内存 消息队列 和信号量
把从一个进程连接到另一个进程的一个数据流称为一个“管道” ; 通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道 ; 进程退出,管道释放,所以管道的生命周期随进程; ,内核会对管道操作进行同步与互斥
管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。

1. 匿名管道

匿名管道(在内核中创建的这块缓冲区并没有一个标识), 但是内核会返回给我们用户两个文件描述符(fd[0]缓冲区的读端, fd[1]缓冲区的写端),
       创建匿名管道的接口 int pipe(int fd[2])
        fd[2] : 文件描述符数组,其中fd[0]表示读端, fd[1]表示写端;
        接口返回值有两 返回0 表示创建成功; 返回-1(也就是小于0)表示失败
        
        由于这段缓冲区没有标识, 而对于没有关系的两个进程 想要通过这个缓冲区来进行数据交换时, 不同的进程是找不到这片缓冲区的, 
    无法去修改缓冲区中的内容, 所以也就无法进行数据交换, -------> 所以匿名管道只适合具有亲缘关系的进程之间, 
        父进程先去创建一个管道, 然后在父进程的文件描述表files_struct中就有fd[0]和fd[1], 分别对应3号和4号文件描述符, 
        然后再创建出来子进程, 由于子进程会拷贝父进程的PCB, 所以文件描述表也被拷了, 所以在子进程中也有了fd[0], fd[1],也对应他的3,4文件描述符,   这时候父子进程就可以通过操作文件描述符来操作缓冲区, 
        同时匿名管道又是一个半双工的, 数据流向只能是从写端流到读端(like水管), 数据只能向一个方向流动;需要双方通信时,需要建立起两个管道;
        加上每次将字节写入到缓冲区后, 数据之间时没有明确的数据边界的, 所以在读端读的时候, 他可以按自己的需求读任意个数的字节,也就是任意大小的数据, 且管道大小(PIPE_SIZE)为64K, 所以会出现两种情况
        	1. 当读端不读, 写端一直写的时候, 而当管道被写满时, 就会造成写端的阻塞, 
       		2. 当写端不写, 读端却一直读时, 而当管道为空的时候, 就会造成读端的阻塞, 
        在创建匿名管道时 返回的文件描述符的属性 默认是阻塞属性, 
        而fcntl这个接口可以修改阻塞属性, 将其变为非阻塞属性 int fcntl(int fd, int cmd, ...)
            这里的fd是我们想要更改属性的文件描述符, cmd是操作(获取属性和设置属性), 可变参数列表(传递最终要变成的属性数据)
            可变参数只有在设置(F_SETFL)的时候才写, 而获取(F_GETFL)属性数据时不用写
            要想设置为非阻塞属性, 得先获取当前属性, 然后将当前属性按位或上O_NONBLOCK,然后将结果作为可变参数列表, 最终将当前的文件描述符修改为可变参数列表的数据

        针对上面的两种情况, 现在把文件属性设置为非阻塞属性 也有两种情况(将写端设置为非阻塞和将读端设置为非阻塞)
             1. 不进行读, 但是一直去写, 设置写端的文件描述符为非阻塞属性, 不用设置读端文件描述符, 因为没有用到, 
              读端不读分读端关闭和读端不关闭两种情况          
                1.1 读端不关闭, 写端一直写, write会返回-1, 报错当前资源不可用,
                1.2 读端直接关闭掉, 写端还一直写, 当前进程收到了SIGPIPE信号(进程退出), 写端程序被杀死, 管道破裂
            2. 不写, 一直读; 只需将读端设置为非阻塞, 写端可以不用关心, 因为没有用到写端
              写端不写也分写端关闭和写端不关闭两种情况
                2.1 写端不关闭, 读端进行读, read调用返回-1, 返回资源不可用
                2.2 所有写端关闭, 读端进行读, read是正常调用的, read返回的是读到的字节数量
   

2. 匿名管道的特性:

    1. 管道的大小(PIPE_SIZE)为64K
    2. PIPE_BUF: 大小为4K, 当我们读写的数据小于PIPE_BUF时, 保证了我们读写的原子性; 以追加方式写 后面写的数据不会覆盖前面写的数据; 
	    当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。
    若两个进程同时想去修改匿名管段中的buf,想到并行(在多核CPU中可行)和并发;
        并行: 不同的进程拿着不同的CPU同时进行运算
        并发: 不同的进程在不同的时刻拿着同样的CPU进行运算
    假设我们这两个进程现在并发去修改BUF的内容, 若进程A写的资源是大于PIPE_BUF(4k)的,还没写完, 进程A就被强制退出CPU, 此时进程B就要去占用CPU往buf中写,写完之后进程B退出, 进程A继续将未写完的剩余数据往buf中写, 这样所看到的数据就是不连续的, 由于进程A中途被打断了
    而原子性: 就是指当前操作不能被打断, 不论是读还是写操作都不能被打断, 引申出来的含义就是, 当前操作要么是完成了, 要么就是没完成, 不可能有完成一半的情况. 被打断就意味着不能保证原子性. (要么正在读, 要么压根就没开始)
    临界资源: 同一时间, 当前的资源只能被一个进程所访问, 前提是写入或读取的数据必须是小于PIPE_BUF(4k); 如果多个进程同时去修改临界资源, 可能会导致数据二义性(造成数据不连续 不是想要的结果)
问题: 如何保证对临界资源访问的合理性, 不会造成数据二义性呢?
    1. 多人都想去上同一个厕所, 如果同时进去就会导致数据二义性, 我们希望的结果是同一时刻, 只能有一个人在厕所里,所以给厕所装了门, 保证了同一时间只能有一个人在厕所;
    互斥: 同一时间, 保证只能有一个进程访问临界资源
    2. 但是现在又出现了另一种现象, 在厕所的人故意不出来, 导致其他的人永远都上不了厕所,也就意味着其他进程永远访问不了这块内存,这时其他人在厕所门口写上联系方式, 等里面的人出来了call他, 保证了所有人都能上厕所
    同步: 保证了进程对临界资源访问的合理性

3. 命名管道:

 - 命名管道具有标识符, 内核创建的内存时有标识的, 不同的进程可以通过标识访问到命名管道; 如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。
 - 如何创建衣柜命名管道呢?
    使用命令创建: 
        mkfifo [命名管道的文件名称]    (名称其实是一个标识名称), 文件类型是p,p为管道文件
    使用函数创建
        int mkfifo(const char* pathname, mode_t mode);       
        包含在头文件 <sys/stat.h>
        pathname: 管道路径, mode: 权限
 - 用户可以通过操作命名管道文件来对内核当中的命名管道的内存区域进行读写操作
 - 特性
    具有标识符, 可以满足不同进程之间的进程间通信
    其他特性和匿名管道相同

注意: 不论是我们的 匿名管道还是命名管道, 生命周期都是跟随进程的

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

  • 匿名管道由pipe函数创建并打开。
  • 命名管道由mkfifo函数创建,打开用open
  • FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完 成之后,它们具有相同的语义。

4. 共享内存:

1. 共享内存的原理:
    在物理内存中开辟了一段空间, 不同的进程通过页表结构将物理内存映射到虚拟地址空间的共享区域, 另一个进程也通过同样的结构将物理内存映射到他的虚拟地址空间, 若想进行数据交换, 则只需通过PCB1将内容写入共享区,通过页表结构映射到物理内存, PCB2通过特殊的页表结构从物理内存中读取内容到PCB2的虚拟地址空间中的共享区, 实质上并未改变物理内存里的数据,而管道当中若读取数据后就没有了, 物理内存中读取数据后数据还在, 采取的是覆盖式写, 
    原理:
    1. 先在物理内存中开辟一段空间
    2. 各个进程通过页表结构将物理内存映射到自己的虚拟地址空间当中的共享区域
    3. 各个进程之间的通信是通过修改自己的虚拟地址空间当中的共享区的地址完成
    特性:
    不同进程对共享内存区域进行读的时候,并不会抹除物理内存中的值; 进程不再通过执行进入内核的系统调用来传递彼此的数据

共享内存的接口:
    1. 创建共享内存
        int shmget(key_t key, size_t size, int shmflg);
            key: 共享内存的标识符
            size: 共享内存的大小
            shmflg: 
                IPC_CREAT: 如果想要获取的共享内存不存在, 则创建共享内存; 如果共享内存存在, 则返回共享内存的操作句柄;(用遥控器操作电视机)
                IPC_EXCL | IPC_CREAT: 如果想要获取的共享内存存在, 则报错; (想要获取自己新创建的共享内存 不想要之前的共享内存 就加上这一个参数, )
                按位或上权限, 权限可以使用8进制数字来进行传参;
    2. 将进程附加到共享内存上
        void* shmat(int shmid, const void* shmaddr, int shmflg);
            shmid: 共享内存的操作句柄, shmget的返回值;
            shmaddr: 需要将物理内存映射到虚拟地址当中的哪一个地址; 一般情况下, 我们传递NULL值, 让操作系统默认为我们分配地址(因为我们并不知道到哪一块地址是空的, 所以让操作系统分配)
            shmflg: 指定共享内存的读写权限;
                0: 可读可写
                IPC_RDONLY: 只读
            返回值: 返回的是映射到共享区的哪一个地址了,程序员可以通过操作这个地址来操作物理内存(共享内存);
    3. 从共享内存中分离进程
        int shmdt(const void* shmaddr)
            shmaddr: 共享区当中映射的虚拟地址的首地址 ---> shmat的返回值
    4. 共享内存的销毁
        int shmctl(int shmid, int cmd, struct shmid_ds* buf)
            shmid: 共享内存的操作句柄
            cmd: shmctl函数执行的动作
                销毁:
                    IPC_RMID: 删除共享内存, 标记共享内存为删除状态
                获取共享内存:
                    ICP_STAT: 获取共享内存的状态信息, 需要搭配shmid_ds* buf使用, 信息放到buf中
            buf: 是一个出参, 用来返回共享内存的状态信息, 一般在使用的时候, 传入struct shmid_ds结构体对象的地址

    共享内存的生命周期是跟随我们操作系统内核的, 意味着只有不删除这样的共享内存,操作系统不关闭, 那共享内存就一直存在 并不会随着进程的退出而释放

    使用ipcs可以查看的信息有消息队列, 共享内存, 信号量
    使用ipcs -m 过滤并查看共享内存的信息, 

key(共享内存标识) shmid(共享内存操作句柄) owner(所属者) perms(权限) bytes(共享内存大小) nattch(附加的进程数量) status(共享内存的状态)
    

如何删除一个有进程附加的共享内存, 操作系统内核的做法是:
    1. 将该共享内存的状态标识为dest(destory), 将我们的共享内存的标识设置为0x00000000, 标识当前的共享内存不能再被其他进程附加,同时会释放共享内存
    2. 会带来风险, 如果还有进程在被删除的共享内存上, 有可能访问到非法的内存, 从而导致程序越界, 崩溃掉
    3. 当附加的程序退出掉, 操作系统内核也会随之将描述共享内存的结构体释放掉
                     这里的两个进程是独立的, 不是父子进程

注意:共享内存没有进行同步与互斥!

消息队列: (底层不一定是队列, 也可以是链表 只要实现了先进先出的特性就可以了)

	消息队列提供了一个从一个进程向另外一个进程发送一块数据的方法 ; 每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同的类型值
	队列的特性: 先进先出, 底层实现就是内核当中创建的链表, 链表保证先进先出的特性;
	在队列当中的每一个元素都有自己的类型, 类型之间有一个优先级的概念;

	int msgget(key_t key, int msgflg);
			key: 消息队列的标识符
			msgflg:
				IPC_CREAT
				IPC_CREAT | IPCEXCL
				按位或上权限
	int msgsnd(int msqid, const void* msgp, size_t msgsz, int msgflg);
		msqid: 消息队列的操作句柄
		msgp: 发送的数据
		msgsz: 发送数据的大小
		msgflg: 
			0: 阻塞发送, 当队列满的时候, 则阻塞
			IPC_NOWAIT: 非阻塞
	size_t msgrcv(int msqid, void* msgp, size_t msgsz, long msgtyp, int msgflg);
		msgtyp: 接收什么样类型的数据
			0: 任意数据类型都是可以接收的
			小于0: 则返回队列当中消息类型为msgtype的第一个消息;
			大于0: 则返回对垒当中消息类型小于等于msgtype绝对值的消息,
					如果说这种消息的类型比较多, 则返回最小的那个消息
	int msgctl(int msqid, int cmd, struct msqid_ds* buf);
		cmd:
			IPC_STAT
			IPC_RMID
特性:
	消息队列的声明周期是跟随内核的
	消息队列可以进行双工通信, 由于数据有明显的数据边界了; 克服了管道当中无格式的字节流的缺点;
©️2020 CSDN 皮肤主题: 游动-白 设计师:上身试试 返回首页