1 I/O
1.1 文件 I/O
通过文件描述符标识。对于内核而言,所有打开文件都通过文件描述符引用。 其也是有缓冲的,只是其缓冲区在内核空间,不再用户空间。体现在延迟写,只在适当的时候才调用写文件操作, 减少不必要的写操作,增加性能。 函数 open() 打开文件时,指定模式必须有且仅有 O _RDONLY / O _WRONLY / O _RDWR 三者中的一个。 选项 O _APPEND 指定为追加。原来的 UNIX 系统不支持追加,只能先使用 lseek() 先设置文件的偏移量到文件的 结尾,然后再写。但是这在多个进程同时写的时候会出问题,因为调用了两个函数来追加,所以不是一个原子操作。 而追加选项确保设置偏移和写操作为原子操作。
1.1.1 管道读写
写管道的时候不用使用追加选项,内核会自动按写的顺序写入管道。读取后自动将相应的内容从管道清除。 读取一个写端关闭的管道,在读取完全部数据后,read 函数返回 0 ; 写一个读端关闭的管道,产生 SIGPIPE 信号,设置该信号处理函数或者忽略该信号,write 返回 -1,且 errno 设置为 EPIPE。 读管道,如果管道为空,调用线程阻塞,同进程内的其他线程不受影响。
1.1.2 套接字描述符
虽然套接字描述符本质上是一个文件描述符,但不是所有参数为文件描述符的函数都可以接受套接字描述符。 套接字不支持文件偏移量概念,不能使用 lseek 函数,也不可以使用 mmap 函数,不可调用 fchdir 函数。
shutdown 函数可以直接关闭一个套接字的读端或者写端,不管该套接字描述符复制了多少分; close 函数只有在关闭最后一个套接字的时候才会释放该套接字。
1.1.3 改变文件偏移量
lseek
1.2 标准库 I/O
标准 I/O 库的操作围绕流(stream)进行。利用指向 FILE 对象维护,该结构体包含了标准 I/O 库为了维护该流所 需要的信息:文件描述符,指向缓冲区的指针,缓冲区的长度,缓冲区中当前的字符数,出错标志等。不需要关心 FILE 结构的具体形式。 相对于文件 I/O,标准库的 I/O 都是带缓冲的,标准库维护了一个缓冲区,在适当的时候才调用 read、write 函 数,从而减少系统调用的开销。
1.2.1 定位流
ftell、fseek、ftello、fseek、fgetpos、fsetpos
2 IPC
IPC 传统上是 UNIX 中一个杂乱不堪的领域,虽然有了各种各样的解决办法,但没有一个是完美的。可分为四个主 要领域:
- 消息传递 : 管道、FIFO、消息队列
- 同步 : 互斥锁、条件变量、读写锁、信号量
- 共享内存区 : 匿名共享内存区、命令共享内存区
- 过程调用 : Solaris 门、Sun RPC
包括单个进程内多个线程的 IPC 和多个进程间的 IPC 。
2.1 消息边界
2.1.1 无边界
管道和 FIFO 是字节流,没有消息边界; TCP 没有记录边界的字节流;
2.1.2 有边界
POSIX 消息和 System V 消息有从发送者向接收者维护的边界; UDP 提供具有边界记录的消息;
2.2 窥探能力
2.2.1 可以窥探
socket recv、recvfrom 函数可以使用标志 MSG _PEEK 从接收队列读取数据,且系统不在读取之后丢弃这些数据;
2.2.2 不可窥探
管道、FIFO、POSIX 消息、System V 消息 只有一个副本递交到一个线程,且消息不能广播或多播到多个接收者(UDP 广播、多播)
2.3 读取顺序
2.3.1 先进先出
管道、FIFO
2.3.2 优先级最高的最早消息
POSIX 消息
2.3.3 指定优先级的消息
System V 消息
2.4 对象持续性
2.4.1 随进程持续 process-persistent
IPC 对象一直存在到打开着该对象的最后一个进程关闭该对象为止。没有 unlink 函数。 管道、FIFO、POSIX 互斥锁、POSIX 条件变量、POSIX 读写锁、POSIX 基于内存的信号量、fcntl 记录锁、TCP 套 接字、UDP 套接字、Unix 域套接字
2.4.2 随内核持续 kernel-persistent
一直存在到内核重新自举或显示删除该 IPC 对象。存在对应的 unlink 函数。 POSIX 消息队列、POSIX 有名信号量、POSIX 共享内存区、System V 消息队列、System V 信号量、System V 共 享内存
2.4.3 随文件系统持续 filesystem-persistent
一直存在到显示删除该 IPC 对象为止。 IPC 的一个基本设计目标是高性能,而具备随文件系统的持续性可能会使其性能降级,而且进程不可能跨越自举继 续存活。
2.5 名字空间 – name space
一种给定的 IPC 类型,其可能名字的集合称为名字空间。名字空间非常重要,因为名字是客户与服务器彼此连接 以交换消息的手段。
IPC 类型 | 打开或创建 IPC 的名字空间 | IPC 打开后的标识 |
---|---|---|
管道 | NA | 描述符 |
FIFO | 路径名 | 描述符 |
POSIX 互斥锁 | NA | pthread _mutex _t 指针 |
POSIX 条件变量 | NA | pthread _cond _t 指针 |
POSIX 读写锁 | NA | pthread _rwlock _t 指针 |
fcntl 记录上锁 | 路径名 | 描述符 |
POSIX 消息队列 | POSIX IPC 名字 | mqd _t 值 |
POSIX 命名信号量 | POSIX IPC 名字 | sem _t 指针 |
POSIX 共享内存 | POSIX IPC 名字 | 描述符 |
POSIX 基于内存的信号量 | NA | sem _t 指针 |
TCP 套接字 | IP 地址与 TCP 端口 | 描述符 |
UDP 套接字 | IP 地址与 UDP 端口 | 描述符 |
Unix 域套接字 | 路径名 | 描述符 |
Sun RPC | 程序/版本 | RPC 句柄 |
门 | 路径名 | 描述符 |
System V 消息队列 | key _t 键 | System V IPC 标识符 |
System V 信号量 | key _t 键 | System V IPC 标识符 |
System V 共享内存 | key _t 键 | System V IPC 标识符 |
2.5.1 打开 IPC 名字空间
2.5.2 打开 IPC 后的标识
2.6 fork、exec、exit 对 IPC 的影响
- 无名同步变量(互斥锁、条件变量、读写锁、基于内存的信号量),从一个具有多线程的进程中调用 fork 将变得 混乱不堪;如果这些变量驻留在共享内存区中,而且创建时设置了进程共享属性,那么对于能访问该共享内存区的 任意进程来说,其任意线程能继续访问这些变量。
- System V IPC 的三种形式没有打开或关闭的说法,只需知道其标识符即可访问。
IPC 类型 | fork | exec | _exit |
---|---|---|---|
管道、FIFO | 子进程取得父进程的所有打开着的文件描述符的副本 | 所有打开着的文件描述符继续打开,除非设置了 FD _CLOEXEC 位 | 关闭所有打开着的描述符,最后一个关闭时删除管道或 FIFO 中残留的数据 |
POSIX 消息队列 | 子进程取得父进程的所有打开着的消息队列描述符的副本 | 关闭所有打开着消息队列描述符 | 关闭所有打开着的消息队列描述符 |
System V 消息队列 | -- | -- | -- |
POSIX 互斥锁和条件变量 | 若驻留在共享内存区中而且具有进程间共享属性,则共享 | 除非在继续打开着的共享内存区中而且具有进程间共享属性,否则消失 | 除非在继续打开着的共享内存区中而且具有进程间共享属性,否则消失 |
POSIX 读写锁 | 若驻留在共享内存区中而且具有进程间共享属性,则共享 | 除非在继续打开着的共享内存区中而且具有进程间共享属性,否则消失 | 除非在继续打开着的共享内存区中而且具有进程间共享属性,否则消失 |
POSIX 基于内存的信号量 | 若驻留在共享内存区中而且具有进程间共享属性,则共享 | 除非在继续打开着的共享内存区中而且具有进程间共享属性,否则消失 | 除非在继续打开着的共享内存区中而且具有进程间共享属性,否则消失 |
POSIX 命名信号量 | 父进程中所有打开着的命名信号量在子进程中继续打开着 | 关闭所有打开着的命名信号量 | 关闭所有打开着的命名信号量 |
fcntl 记录锁 | 子进程不继承父进程持有的锁 | 只要描述符继续打开着,锁就不变 | 解开由进程持有的所有未处理的锁 |
System V 信号量 | 子进程中所有 semadj 值都置位 0 | 所有 semadj 值都携入新进程 | 所有 semadj 值都加到相应的信号量值上 |
mmap 内存映射 | 父进程的内存映射存留到子进程中 | 去除内存映射 | 去除内存映射 |
POSIX 共享内存区 | 父进程中内存映射存留到子进程中 | 去除内存映射 | 去除内存映射 |
System V 共享内存区 | 附接着的共享内存区在子进程中继续附接着 | 断开所有附接着的共享内存区 | 断开所有附接着的共享内存区 |
门 | 子进程取得父进程的所有打开着的描述符,但是客户在门描述符上激活其过程时,只有父进程是服务器 | 所有门描述符都应关闭,因为它们创建时设置了 FD _CLOEXEC 位 | 关闭所有打开着的描述符 |
2.7 锁释放
2.7.1 内核自动释放
持有某个锁的进程没有释放就终止,内核自动释放该锁; fcntl 记录锁、System V 信号量(可选项)
2.7.2 无法释放锁
互斥锁、条件变量、读写锁、POSIX 信号量
2.8 进程、线程与共享信息
没有任何东西限制任何 IPC 技术只适用于两个进程。
- 两个进程共享存留于文件系统中某个文件上的某些信息。为了访问这些信息,每个进程都得穿越内核(例如 read、write、lssk 等);需要某种形式的同步,如记录锁。
- 两个进程共享驻留于内核的某些信息,访问共享信息的每次操作都涉及对内核的一次系统调用。管道、 System V 消息队列、System V 信号量均是;
- 两个进程有一个双方都能访问的共享存储区,需要某种形式的同步(信号量等)。每个进程一旦设置好该共享 内存区,就能根本不涉及内核而访问其中的数据。
3 进程 – 线程
3.1 设计线程的原因:
- fork 的开销很大。内存映射要从父进程复制到子进程,所有描述符要在子进程复制一份。尽管存在写时复制 (copy-on-write) 的技术,fork 的开销仍然很大。
- fork 子进程后,需要使用 IPC 在父子进程之间传递信息。
3.2 线程共享全局内存空间
一个进程内的所有线程共享同一个全局内存空间。使得线程间很容易共享信息,但这种易用性也带来了同步 (synchronization) 问题。
3.3 线程共享的资源
- 全局内存空间
- 进程指令
- 打开的文件
- 信号处理程序和信号处置
- 当前工作目录
- 用户 ID 和 组 ID
3.4 线程私有资源
- 线程 ID
- 寄存器集合,包括程序计数器和栈指针
- 栈,存放局部变量和返回地址
- errno
- 信号掩码
- 优先级
4 系统开销
执行一般命令 | 1ns = 1/1,000,000,000s |
从 L1 级缓存读 | 0.5ns |
分支误预测 | 5ns |
L2 级缓存读 | 7ns |
加锁、解锁 | 25ns |
主存中读 | 100ns |
1Gbps 网络中发 2k 数据 | 20,000ns |
主存中读 1MB 序列 | 250,000ns |
查找从硬盘中读 | 8,000,000ns |
硬盘读 1MB 序列 | 20,000,000ns |
报文从美国发到欧洲并返回 | 150ms = 150,000,000ns |
5 练习代码
5.1 基本函数
1 | #include <stdio.h> |
5.2 IPC
1 | #include <stdio.h> |
5.3 pthread
1 | #include <stdio.h> |
1 | #include <stdio.h> |