unix环境高级编程(下)-高级IO和进程间通信篇

时间:2022-07-26
本文章向大家介绍unix环境高级编程(下)-高级IO和进程间通信篇,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

前言

笔者将《unix环境高级编程》主要内容总结为三篇:文件篇进程篇高级io和进程间通信三大板块。本文是unix环境高级编程系列文章第三篇:高级IO和进程间通信篇。该篇主要包括:

高级io

先介绍记录锁的概念和记录锁的数据结构。然后介绍阻塞io,非阻塞IO,异步io,IO多路转接等概念,后者都是针对前者更优的技术。IO多路转接技术包括:select,peslect,poll。最后介绍存储映射IO。

进程间通信

介绍了基本进程间通信机制,包括两大类:

  • 进程间数据共享:管道,FIFO,消息队列和共享存储
  • 进程间数据同步:信号量

网络进程间通信

介绍网络间的进程通信机制:套接字。首先是如何寻址。然后介绍socket编程的连接建立,数据传输等。

高级进程间通信

高级进程间通信提供一种可以在进程间传递文件描述符的机制,包括STREAMS管道和unix域套接字

一. 高级IO

1. 非阻塞IO

1.1 概念

  • 非阻塞io使得与磁盘io有关的系统调用永远不会被阻塞
  • 这些io相关的系统调用有:open,read,write
  • 如果这种操作不能完成,则调用立即出错返回

1.2 如何指定非阻塞io

  • 如果调用open获得文件描述符,可指定O_NONBLOCK标识
  • 对于已经打开的文件描述符,可调用fcntl,由该函数打开O_NONBLOCK标识

2. 记录锁

2.1 概述

  • 概念:当一个进程正在读或修改文件的某个部分时,可以阻止其他进程修改同一文件区
  • flock:文件锁,早期的unix只支持锁整个文件,使用该函数
  • fcntl:记录锁,允许锁文件中的任意字节数的区域

2.2 fcntl

  • cmd:
    • F_GETLK:获取锁信息
    • F_SETLK:设置锁信息
    • F_SETLKW:阻塞版本的F_SETLK
  • flockptr:指向flock的指针struct flock{ short l_type;//F_RDLCK共享读锁,F_WRLCK独占写锁,F_UNLCK解锁 off_t l_start;//加锁区域其实位置 short l_whence;//和start一起确定加锁位置 off_t l_len;//加锁长度 pid_t l_pid;//进程id } 复制代码
  • 不同锁的兼容性:针对同一把锁。如果不同锁,新锁总是覆盖旧锁

2.3 锁的隐含继承和释放

  • 进程终止时,所建立的锁全部释放
  • 关闭文件描述符时,文件描述符引用的文件上的任何一把锁都被释放
  • fork产生的子进程不继承父类设置的锁
  • 执行exec后,新进程可以继承原程序的锁

2.4 FreeBSD中记录锁的数据结构

  • v节点表的i节点结构串联起所有的lockf结构
  • 每个lockf结构说明了一个给定进程的一个加锁区域
  • 在父进程中,关闭任意一个文件描述符,内核都会遍历i节点各项lockf,并释放持有的锁

3. 系统v流机制

3.1 基本概念

  • STREAMS是系统V提供的构造内核设备驱动程序和网络协议包的一种通用方法。不同于标准io中的stream
  • 流在用户进程和设备驱动程序之间提供一条全双工通路,流无需和实际硬件设备之间会话
  • 简单流的基本结构:

3.2 STREAMS消息

  • STREAMS的所有输入输出都基于消息
  • 流首和用户进程进行消息交换的函数:read,write,ioctl,getmsg,getpmsg,putmsg,putpmsg
  • 消息可以顺流而下,也可以逆流而上
  • 消息的组成:消息类型,控制信息,数据。控制信息和数据由strbuf指定:
  • 消息约有25种,但一般使用的只涉及三种:
    • M_DATA:用户数据
    • M_PROTO:协议控制信息
    • M_PCPROTO:高优先级协议控制信息
  • 每个输入STREAMS模块有两个输入队列,一个来自上面模块的消息,另一个来自下面模块的消息
  • 流中的消息都有一个排队优先级,通过优先级波段指定

3.3 putmsg和putpmsg

  • 用于将STREAMS消息写入流中
  • 后者允许指定优先级波段

3.4 getmsg和getpmsg

  • 从流首读STREAMS消息

4. IO多路转接

4.1 阻塞io

  • 读取一个文件描述符对数据,如果没有数据就一直阻塞住
  • 缺点:长时间阻塞在同一个文件描述符,另一个文件描述符虽然有很多数据却得不到及时处理

4.2 非阻塞io

  • 将两个文件描述符都设置为非阻塞的
  • 对第一个文件描述符发送read,如果该输入上有数据,则读取并处理。如无数据则立即返回。
  • 第二个描述符重复上一步操作
  • 若干秒后,重复执行以上步骤,即轮询
  • 缺点:浪费cpu时间,大多数时间实际上上无数据可读的。轮询的时间间隔也很难确定

4.3 异步io

  • 当一个文件描述符已准备好可以进行io时,用一个信号通知它
  • 缺点:并发所有的系统都支持,其次这种信号对每个进程而言只有一个

4.4 IO多路转接

  • 一种比异步IO更好的处理IO的技术
  • 先构造一张有关描述符的图表,然后调用一个函数,直到这些描述符中至少一个准备好io时,该函数才返回。返回时,告诉哪些文件描述符已准备好可以io
  • 支持IO多路转接的函数:poll,pselect,select

4.5 select

  • readfds:可读描述符集,每一个文件描述符占一位
    • 内部结构视图
    • 描述符集的设置函数
  • maxfdp1:最大描述符+1,可设置为FD_SETSIZE(1024)
  • writefds:可写描述符集
  • exceptfds:异常描述符集
  • tvptr:愿意等待的时间
    • NULL:永远等待,捕捉到信号则中断等待
    • 时间每个字段为0:完全不等待,测试指定的文件描述符并立即返回
    • 不为0:实际等待的时间
  • 返回值:
    • 返回-1:表示出错,文件描述符没有准备好时收到信号,此时不修改文件描述符
    • 返回0:已经超时了,指定都文件描述符都没有准备好
    • 正数:已经准备好的文件描述符数量(每个文件描述符读写单独各算一次)

4.6 pselect

pselect与select类似,仅仅少部分有差异,如下:

  • 超时值的数据结构不同
  • pselect超时值为const,不可改变
  • 可使用信号屏蔽字

4.7 poll

  • poll类似与select,不过接口有所不同
  • 不是为每个状态构造文件描述符集,而是构造一个pollfd的数组,数组每个元素指定文件描述符编号和关心的状态
  • 参数:
    • events:用户设置关心的事件
    • reevents:内核返回文件描述符事件

5. 异步IO

5.1 概述

异步io并不像select和poll对所有文件描述符都生效

  • SystemV系统:只对STREAMS设备和STREAMS管道起作用,发送SIGPOLL信号
  • BSD系统:只对终端和网络起作用,发送SIGIO信号

5.2 SystemV异步IO

  • 启动异步IO,需要调用ioctl,第二个参数为I_SETSIG
  • 同时,在调用ioctl之前建立信号处理程序

5.3 BSD异步IO

异步IO是SIGIO(通用异步io)和SIGURG(通知网络进程数据到达)两个信号的组合

  • 调用signal或signalaction为SIGIO建立信号处理程序
  • 以命令F_SETOWN调用fcntl设置进程id和进程组id,将接收对于该描述符的信号
  • 以命令F_SETFL调用fcntl设置O_ASYNC文件状态标识,使文件描述符上可以进行异步IO

6. readv和writev

  • 用于在一次函数调用中读写多个非连续的缓冲区

7. readn和writen

  • 按需多此调用read和write,直至读写了N各字节数据
  • 使用与读写管道,网络设备或终端数据

8. 存储映射IO

  • 使一个磁盘空间与一个存储空间中的缓冲区映射。当从缓冲区取数据,就相当于读文件中的相应字节。写数据到缓冲区相当于自动写入文件。这样就可以不用read和write的情况下执行io
  • 文件映射到存储区:
  • addr:存储映射起始地址,通常设置为0,表示由系统选择地址然后作为返回值返回
  • port:说明对存储映射区的保护要求,权限不能超过文件本身权限
    • PORT_READ:映射区可读
    • PORT_WRITE:映射区可写
    • PORT_EXEC:映射区可执行
    • PORT_NONE :映射区不可访问
  • flag:
    • MAP_FIXED:返回值必须等于addr,不利于移值
    • MAP_SHARED:存储操作的配置
    • MAP_PRIVATE:创建私有副本
  • 更改存储映射区权限:mprotect
  • 刷新映射存储区:msync
  • 解除存储映射区:munmap

二. 进程间通信

进程间通信机制包括:

  • 经典IPC:管道,FIFO,消息队列,信号量,共享存储
  • 网络IPC:套接字

1. 管道

1.1 概述

  • 最古老的ipc机制
  • 管道有两个局限性:
    • 历史上,它是半双工的,即数据只能在一个方向流动。虽然现在某些系统提供全双工,但是为了移植性,不假定它有此特性
    • 他们只能在具有公共祖先的进程之间使用
  • 尽管有局限性,半双工管道仍然是最常用的ipc
  • 若write写一个尚无进程为读而打开的管道,产生SIGPIPE信号
  • 若管道的最后一个写进程关闭该管道,则为管道的读进程产生文件结束标识

1.2 管道的创建

  • 参数fields传入两个文件描述符,field[0]为读而打开,field[1]为写而打开,field[1]的输出是field[0]的输入
  • 管道模型:

1.3 popen和pclose

  • popen先执行fork,然后调用exec以执行cmdstring,并返回标准io文件指针。如果type=“r“,文件指针连接到cmdstring的标准输出。如果type=“w”,文件指针连接到cmdstring的标准输入
  • pclose关闭标准io流

1.4 FIFO

  • FIFO也成为命名管道,通过FIFO,不相关的进程也能交换数据
  • 创建FIFO:
  • mode参数与open函数一致
  • 非阻塞标准O_NONBLOCK:
    • 没有指定该参数:只读open要阻塞到某个其他进程为写而打开此FIFO
    • 指定该参数:只读open立即返回。没有进程打开FIFO,将出错返回-1
  • 类似与管道,若write写一个尚无进程为读而打开的FIFO,产生SIGPIPE信号。若FIFO的最后一个写进程关闭该FIFO,则为FIFO的读进程产生文件结束标识
  • PIPE_BUF说明了可被原子写到FIFO的最大数据量
  • FIFO的用途
    • 由shell命令使用,以便将数据从一条管道线传到另一条,无需创建中间临时文件
    • 用于客户-服务器进程中,以在客户进程和服务器进程间传递数据

2. XSI IPC

消息队列,信号量和共享存储,这三种IPC称做XSI IPC,他们之间有很多共性,包括:

2.1 标识符和键

  • 标识符:唯一标识IPC对象的内部名,非负整数
  • 键:IPC对象的外部名,使多个合作进程能在同一个IPC对象上会合。键基本数据类型为key_t
  • 客户进程和服务器进程在同一IPC上会合的方法:
    • 服务器进程指定键IPC_PRIVATE创建一个新的IPC结构,将返回的标识符放到某处(文件)给客户进程使用。缺点:要分别读写文件
    • 在公共头文件中定义一个键,服务器进程指定该键创建IPC结构。缺点:可能IPC已经存在,获取时会出错
    • 客户进程和服务器进程认同一个路径名和项目id,接着调用ftok将两个值变换为键,再调用方法2

2.2 权限结构

  • XSI IPC为每个IPC结构设置了一个ipc_perm结构,规定了权限和所有者。

2.3 结构限制

  • 三种形式的IPC都有内置限制

2.4 优点和缺点

缺点

  • IPC结构是在系统范围内起作用的,没有访问计数
  • IPC结构在文件系统中没有名字,不能修改属性,不能ls查看IPC对象,不能用rm删除,也不能用chmod修改权限。不能用文件描述符,也就不能使用select,poll模型

优点

  • 可靠
  • 流是受控的:缓冲区资源紧张,进程就休眠
  • 面向记录
  • 可以用非先进先出方式处理

特征对比

3. 消息队列

3.1 概述

  • 消息的链接表,存放在内核中,由消息队列标识符标识
  • 最开始出现的为了提供比一般IPC更高速度的通讯方式,但现在速度上没有优势,已经不再使用了
  • 创建或打开队列:msgget
  • 发送消息:msgsend
  • 获取消息:msgrcv,不一定先进先出,可按消息的类型字段取

3.2 数据结构

  • 每个队列相关的数据结构
  • 消息队列在各个系统中的参数限制

3.3 msgctl函数

msgctl函数对队列执行多装操作(类似于ioctl,垃圾桶函数)

  • cmd:要执行的命令
    • IPC_STATE:获取msgid_ds结构,并放入buf参数
    • IPC_SET:按buf值,设置数据
    • IPC_RMID:删除队列和数据

3.4 msgsend函数

  • ptr:指向消息内容指针,消息的组成:
    • 类型:正长整型类型
    • 长度
    • 实际数据
  • flag:标志
    • IPC_NOWAIT:非阻塞io

3.5 msgrcv函数

  • ptr:获取的数据地址,包括类型和实际数据
  • nbytes:数据缓冲区长度
  • type:获取哪种消息。
    • type=0:返回队列中第一条消息
    • type>0:返回消息类型为type的第一个消息
    • type<0:返回消息类型小于等于type绝对值的消息
  • flag:
    • IPC_NOWAIT:非阻塞

4. 信号量

4.1 概述

  • 信号量不同于管道和消息队列,它是一个计数器,用于多进程堆共享数据对象的访问
  • 信号量计数操作必须是原子的,通常在内核中实现
  • 使用信号量获取共享资源的操作
    • 测试该资源的信号量N
    • 若N为正,则进程可以使用该资源。然后N=N-1,表示使用了一个资源单位
    • 若N=0,则进程休眠,直到N>0才唤醒,然后第一步
    • 当进程不使用共享资源时,N=N+1,如果有进程在休眠等待则唤醒
  • XSI信号量相对复杂一些
    • 信号量并发单个非负值,而是一个或多个信号量值的集合
    • 创建信号量和赋值是分开的,不能原子的创建信号集合
    • 即使没有进程在使用信号量,他仍然存在
  • 获得一个信号量ID:semget

4.2 数据结构

  • 内核为每个信号量集合设置了一个semid_ds结构
  • 每个信号量的结构
  • 信号量的系统限制

4.3 semctl函数

  • 包含多种信号量操作
  • cmd:
    • IPC_STAT:取semid_ds结构
    • IPC_SET:设置数据
    • IPC_RMID:删除信号量集合

4.4 信号量与记录锁在liunx的对比

  • 记录锁比信号量耗时
  • 但如果只锁一个资源,宁可用记录锁。因为他使用简单,进程终止时会自动清理锁

5. 共享存储

5.1 概述

  • 共享存储允许两个或更多进程共享给定的存储区
  • 数据不需要在进程间复制,是最快的IPC
  • 多进程对于同一个存储区,要注意同步访问,通常使用信号量来进行同步
  • 获取共享存储区域id:shmget
  • 共享存储的位置:栈下面

4.2 数据结构

  • 内核为每个共享存储段设置了shmid_ds结构
  • 共享存储的系统限制

4.3 shmctl函数

  • 包含堆共享存储的多种操作
  • 参数同前面

4.4 共享存储的使用

  • shmat函数:进程用于连接共享存储到其他的地址空间中
  • addr参数:
    • 为0:连接到由内核选择的可以地址上,推荐方式
    • 非0:且没有指定SHM_RND,连接到该地址
    • 非0:指定SHM_RND,将地址向下取最低边界地址倍数
  • flag:
    • SHM_RDONLY:只读
    • 其他:读写

4.5 共享存储的释放

  • shmdt:脱离该段,但并不删除数据,标识符还在,直到调用shmctl删除

三. 网络进程间通信:套接字

1. 套接字描述符

  • 套接字是通信端点的抽象,是用文件描述符实现的
  • 创建套接字描述符:
  • domain:套接字域
  • type:套接字类型
  • protocol:协议,通常为0。表示根据套接字类型默认选择协议
  • 关闭套接字:close
  • shutdown:禁止套接字上的输入/输出,可只关闭一个方向

2. 寻址

2.1 字节序

  • 大端字节序:最大字节地址对应于数字最低有效字节
  • 小段字节需:最小字节地址对应于数字最低有效字节
  • 各个平台的字节序如下:
  • 网络传输中:tcp/ip使用大端字节序

2.2 地址格式

  • 地址标识了套接字端点,通用地址格式为:
    struct sockaddr{
        sa_famliy_t sa_famliy;
        char        sa_data[];
    }
复制代码
  • 套接字实现可以自由添加aa_data字段以及长度
    //linux实现
    struct sockaddr{
        sa_famliy_t sa_famliy;
        char        sa_data[14];
    }
    //freeBSD实现
     struct sockaddr{
        unsigned char sa_len;
        sa_famliy_t sa_famliy;
        char        sa_data[14];
    }
复制代码
  • ipv4套接字通用地址:,实现者可以自由添加额外字段
  • ipv6套接字通用地址:实现者可以自由添加额外字段
  • sockaddr_int和sockaddr_int6都会被转化为sockaddr结构传入套接字例程中
  • 二进制地址与文本格式地址转化:inet_ntop,inet_pton

2.3 地址查询

  • 查找给定计算机主机信息:gethostent
  • 返回的主机信息数据结构:
  • 获取网络名字和网络号
  • 获取协议名字和协议号
  • 服务名字和端口号映射关系查询
  • 将主机名和服务名映射到一个地址
  • 地址信息包含的成员

2.4 将套接字与地址绑定

  • 客户端套接字关联地址没有太大意义,可以让系统选一个默认地址
  • 服务端需要给一个客户端请求的套接字绑定一个众所周知的地址
  • 客户端绑定服务端地址的方法:

3. 建立连接

3.1 connect

  • connect为客户端调用,用于连接请求
  • addr为服务器地址
  • 如果sockfd没有绑定地址,connect会给调用者绑定一个默认地址
  • 连接可能失败,应用程序必须能处理connect返回的错误

3.2 listen

  • listen为服务端调用
  • 服务器用listen宣告可以接受连接请求
  • backlog:连接请求数量

3.3 accept

  • accept获得连接请求,并建立连接
  • 返回的文件描述符是套接字描述符,描述符连接到调用connect到客户端
  • 新的套接字描述符和原始套接字sockfd具有相同的套接字类型和地址族
  • 传给accept的原始套接字没有关联到这个连接,而是继续保存可以状态并接受其他连接请求
  • 如果没有连接请求等待处理,accept会阻塞直到有请求到来

4. 数据传输

4.1 send

  • 发送数据,类似与write函数
  • send比write多了第四个参数flags,用于改变处理数据到传输方式
    • MSG_DONTROUTE:勿将数据路由出本地网络
    • MSG_DONTWAIT:允许非阻塞操作
    • MSG_EOR:记录结束
    • MSG_OOB:外带数据
  • sendto函数:类似send。但是sendto允许在勿连接到套接字上指定一个目标地址

4.2 recv

  • 获取数据,类似于read函数
  • recv比read多了第四个产生flags,用于控制如何接收数据
    • MSG_OOB:接受外带数据
    • MSG_PEEK:返回报文内容而不真正取走报文
    • MSG_TRUNC:即使报文被截短,也返回实际的长度
    • MSG_WAITALL:等待直到所以数据可用

5. 套接字选项

5.1 套接字选项包括

  • 通用选项,工作在所有套接字类型上
  • 在套接字层次管理的选项,但是依赖底层协议的支持
  • 特定与某种协议的选项,为某个协议独有

5.2 设置套接字的函数

6. 带外数据

  • 带外数据是一些通信协议支持的可选特征,允许高优先级的数据比普通数据优先传输
  • TCP将外带数据成为“紧急数据”

四. 高级进程间通信

1. 概述

  • Streams管道和unix套接字,这两种高级IPC,可以在进程间传递文件描述符
  • 服务进程可以使他们的打开文件描述符与特定的名字相关联
  • 客户进程可以使用这些名字与服务器通信
  • 操作系统会为每个客户进程提供一个独自的IPC通道

2. STREAMS管道

  • Streams pipe是一个全双工(双向)通道
  • 内部结构如下

3. UNIX域套接字

  • 用于在同一台机器上运行的进程之间通讯