Advanced I/O Functions
1. Introduction
本章主要包含以下几种I/O operations:
- 三种方法实现带倒计时的I/O operations
read()
和write()
的替代:recv()
和send()
,readv()
和writev()
,recvmsg()
和sendmsg()
- 如何判断socket receive buffer中的数据量
- 如何使用C standard I/O library操作socket
2. Socket Timeouts
以下是三种实现带倒计时功能I/O operations的方法:
- 调用
alarm()
, 倒计时结束后向进程发送SIGALRM signal - 使用
select()
自带的计时器 - 使用
SO_RCVTIMEO
和SO_SNDTIMEO
socket option, 但不是所有系统都支持这两个socket options
上述三种方法可用于input和ouput操作, 但如果想为connect()
设置倒计时, 则不能使用socket option; 对于select()
中自带的倒计时, 必须将socket切换为nonblocking mode.
2.1 connect with a Timeout Using SIGALRM
static void connect_alarm(int signo) |
上述方法存在三个问题:
- 使用
alarm()
可以减少connect()
的等待时间, 但不能延长等待时间. 以Berkeley-derived kernel为例, 其connect()
默认等待时间为75秒, 假设alarm()
设置为80秒, 则connect()
在等待75秒后自动返回. alarm()
利用system call的可打断性, 实现connect()
函数提前返回. 但某些library默认忽略接收到的EINTER
signal, 导致alarm()
无法打断system call.- 对于多线程项目,
alarm()
发出的SIGALRM
signal会被进程的某个线程接收, 因此该方法只适合单线程项目.
2.2 recvfrom with a Timeout Using SIGALRM
static void sig_alrm(int signo) |
2.3 recvfrom with a Timeout Using select
int readable_timeo(int fd, int sec) |
2.4 recvfrom with a Timeout Using the SO_RCVTIMEO Socket Option
void dg_cli(FILE *fp, int sockfd, const SA *pservaddr, |
3. recv and send Functions
|
相比于read()
和write()
, recv()
和send()
多了一个参数: flags
. 通过flags, 可更改input和output operation的行为.
flags | recv | send | Description |
---|---|---|---|
MSG_DONTROUTE | ✓ | Bypass routing table lookup | |
MSG_DONTWAIT | ✓ | ✓ | Only this operation is nonblocking |
MSG_OOB | ✓ | ✓ | Send or receive out-of-band data |
MSG_PEEK | ✓ | Peek at incoming message | |
MSG_WAITALL | ✓ | Wait for all the data |
- MSG_DONTROUTE: 若destination address为本地网络, 可使用该flag通知kernel无需进行routing table查询. 也可使用
SO_DONTROUTE
socket option让socket发出的所有datagram都通过routing table查询. - MSG_DONTWAIT: 让单次I/O operation变为nonblocking. 与
fcntl()
的O_NONBLOCK
功能相同, 但MSG_DONTWAIT只会影响单次I/O operation; O_NONBLOCK则永久改为nonblocking. - MSG_OOB: 若
send()
使用该flag, 可将数据作为out-of-band data发送; 若recv()
使用该flag, 会读取out-of-band data. - MSG_PEEK: 只能被
recv()
使用, 用于查看socket receive buffer中的数据, 但不将数据从buffer中删除 - MSG_WAITALL: 只能被
recv()
使用. 只有buffer中数据大于或等于nbytes
时才返回. 但以下三种特殊情况会让recv()
立即返回:recv()
被signal打断- 连接中断
- 出现错误
recv()
和send()
存在一个缺陷: flags
参数只能由进程传递给kernel, 而kernel无法传递给进程任何信息. 对于TCP/IP, 这并不算缺陷; 但对于OSI Protocol, 则需要从kernel中获取信息.
4. readv and writev Functions
readv()
和writev()
解决了读取或写入多个buffer的问题, 其中readv()
称为scatter read(数据被读取到多个buffer中), writev()
被称为gather write(一次output operation发送多个buffer数据).
|
readv()
和writev()
都是atomic operations, writev()
会将所有iov数据作为一个UDP datagram发送. POSIX规定IOV_MAX
常量为iovcnt
的上限值, 不同UNIX系统拥有不同的IOV_MAX
值.
5. recvmsg and sendmsg Functions
|
补充:
msg_name
和msg_namelen
用于无需建立连接的socket, 如UDP socket.recvmsg()
中的msg_name
表示sender's source address,sendmsg()
中msg_name
表示receiver's destination address. 对于TCP socket或connected UDP socket,sendmsg()
可将msg_name
置为NULL.msg_iov
和msg_iovlen
表示input/output buffer及大小. 其中,msg_iov
为一个iovec struct组成的链表,msg_iovlen
表示链表的长度.msg_control
和msg_controllen
表示optional ancillary data的位置和长度
recvmsg()
和sendmsg()
包含两类flags:
- msg_flags: 只用于
recvmsg()
, kernel会通过msg_flags
将flag值传递给进程; 会被sendmsg()
忽略 - flags: 进程传递给kernel的参数, 用于修改input和output的行为
以下是recvmsg()
可收到的6种msg_flags
:
- MSG_BCAST: 当datagram为link-layer broadcast或destination IP address为broadcast address时, 返回该flag
- MSG_MCAST: 当datagram为link-layer multicast时, 返回该flag
- MSG_TRUNC: 当进程的buffer(所有iovec空间)不足以接收所有data时, 返回该flag
- MSG_CTRUNC: 当进程的buffer(msg_controllen)不足以接收所有ancillary data, 返回该flag
- MSG_EOR: 当
send()
未设置MSG_EOR时,recvmsg()
不返回该flag; 当send()
设置MSG_EOR时,recvmsg()
返回该flag - MSG_OOB: 对于TCP out-iof-band data, 该flag不会返回; 其他protocol suites会返回该flag
假设UDP socket调用recvmsg()
前, msghdr structure如下:
其中:
- protcol address: 16 bytes
- ancillary data: 20 bytes
- iovec 1: 100 bytes
- iovec 2: 60 bytes
- iovec 3: 80 bytes
- UDP socket设置IP_RECVDSTADDR socket option用于获取UDP datagram的destination IP address
假设收到来自192.6.38.100
, port为2000的70-bytes UDP datagram, 其destination IP address为206.168.112.96
, 则recvmsg()
返回的msghdr structure如下:
以下是recvmsg()
调用前后的变化:
- 向
msg_name
所指向的buffer添加一个internet socket address structure, 其中包括source IP address和source UDP port msg_namelen
用于表示msg_name
的长度, 为16 bytes- 前100 bytes存放在第一个iovec 1中, 接下来的60 bytes存放在iovec 2中, 最后的10 bytes存放在iovec 3中.
recvmsg()
返回170, 表示接收到的所有字节数 msg_control
指向cmsghdr structure, 其中cmsg_len
为16,cmsg_level
为IPPROTO_IP,cmsg_type
为IP_RECVDSTADDR, 接下来4-bytes用于存放destination IP addressmsg_controllen
表示ancilly data, 更新为16 bytes- 由于无flag返回, 所有
msg_flags
无变化
以下是不同I/O functions的对比:
6. Ancillary Data
sendmsg()
和recvmsg()
可通过msg_control
和msg_controllen
传递和接收ancillary data. Ancillary data也称为control information, 以下是总结:
一个Ancillary data可包含多个ancillary data objects, 每个object都以cmsghdr struct开头, 如下:
struct cmsghdr { |
假设control buffer中有两个ancillary data object, msg_control
指向第一个ancillary data object, msg_controllen
表示ancillary data的总长度. 每个ancillary data object指向一个cmsghdr structure, cmsg_type
和data
之间会存在padding, 可使用CMSG_xxx
macro可获取所有padding:
以下是用于简化ancillary data的marcos:
|
7. How Much Data Is Queued?
有时进程需在不读取数据的情况下, 知道多少数据阻塞在socket buffer:
- 若buffer没有可读数据, 且进程不想被kernel阻塞, 可使用nonblocking I/O
- 若进程想要读取数据, 又不想让数据从buffer中移除, 可使用
MSG_PEEK
flag; 若不确定是否有数据, 可使用nonblocking I/O和MSG_DONTWAIT
flag. 对于TCP socket, 两次recv()
可能获得长度不同的数据, 因为可能有数据在中途接收; 但对于UDP socket, 两次recv()
获取的结果相同, 即使中途接收到新的数据. - 部分UNIX系统支持
ioctl()
中使用FIONREAD
, 该参数会返回socket receive buffer的字节数. Berkeley-derived系统返回的字节数还包括sender IP address和port number (IPv4 16-bytes, IPv6 24-bytes)
8. Sockets and Standard I/O
read()
和write()
等I/O functions都属于UNIX I/O. 这些函数直接作用于file descritpor, 并作为system call由UNIX kernel实现. 除此之外还可使用standard I/O, 该library可用于非UNIX系统, 支持ANSI C. 除了兼容性, standard I/O还为input/output stream提供buffering, 可提高input/output operation效率. 但伴随着stream buffering, 使用standard I/O需注意以下问题:
fdopen()
可将任何file descriptor变为standard I/O stream, 也可通过fileno()
获取对应的file descriptor.- TCP/UDP socket为full-duplex. 当使用
r+
模式打开stream时, 该stream也是full-duplex(可读可写). 但对于full-duplex stream, 若调用output function后调用input function, 两个操作之间需调用fflush()
,fseek()
,fsetpos()
, 或rewind()
; 若input function后调用output function, 除非input function读取到EOF, 否则需调用fseek()
,fsetpos()
, 或rewind()
. - full-duplex stream最简单的使用方式: 为一个file descriptor创造两个stream, 一个用于读取, 一个用于写入
以下是使用standard I/O替代UNIX I/O后的str_echo()
:
void str_echo(int sockfd) |
运行client和server后, 结果如下:
% tcpcli02 206.168.112.96 |
以下是client/server的整个流程:
- 用户在client输入第一行并传输至server
- server调用
fgets()
获取数据, 并由fputs()
输出给fpout stream - 由于standard I/O stream为fully buffered, 当buffer没有装满时, stream会将数据保存在buffer中, 而不是将数据写入descriptor
- 用户在client输入第二行并传输至server
- server调用
fgets()
,fputs()
后, 由于buffer依然没有装满, 因此无输出 - 用户在client输入第三行, 情况如上
- 用户在client输入EOF,
str_cli()
调用shutdown()
并向server发送FIN - server的
fgets()
收到FIN, 并返回null str_echo()
返回, child process调用exit()
完成终止exit()
调用cleanup function, 输出buffer的所有数据到fpout- server的fpout将数据传递给client, client的
str_cli(
)输出数据 - server的child process结束终止, 向client发送FIN完成TCP four-way termination
- client的
str_cli()
接收到EOF并返回
以下是standard I/O Library的三种buffering类型:
- Fully buffered: 只有buffer没有剩余空间, 进程调用
exit()
, 或进程调用fflush()
时, 才发生I/O operation - Line buffered: 只有输入newline, 进程调用
fflush()
, 或进程调用exit()
时, 才发生I/O operation - Unbuffered: 每当调用standard I/O output function时都发生I/O operation
对于大部分UNIX系统, standard I/O library遵循以下规则:
- Standard error采用unbuffered
- terminal dervice采用line buffered
- 除去terminal dervice, 其他stream都采用fully buffered
由于socket不是terminal device, 所以str_echo()
中的stream采用fully buffered. 可调用setvbuf()
将stream变为line buffered, 也可在每次调用fputs()
后调用fflush()
. 但无论怎么解决, 都可能导致socket出错, 且与Nagle algorithm冲突. 最好的解决方法就是避免在socket programming中使用standard I/O library.
9. Advanced Polling
虽然大多数系统支持select()
和poll()
, 但这两个函数都未被收录在POSIX中, 且每个系统对于select()
和poll()
的实现各不相同, 导致兼容性问题. 以下是替代方案:
9.1 /dev/poll Interface
Solaris提供了一个特殊文件: /dev/poll
, 该文件提供了一种可扩展的方式来轮询多个file descriptor. 对于select()
和poll()
, 每次循环都需要将file descriptor再添加一遍, poll device则不需要.
打开/dev/poll
后, polling program会初始化一个pollfd structure. 该array会被kernel调用write()
写入/dev/poll, 然后调用ioctl()
, DO_POLL来等待事件, 以下是ioctl()
传入的structure:
struct dvpoll { |
以下是/dev/poll
的例子:
void str_cli(FILE *fp, int sockfd) |
9.2 kqueue Interface
FreeBSD 4.1引入kqueue interface, 让进程可以注册一个event filter, 其中event包括file I/O, asychronous I/O, file modification notification, process tracking, 和signal handling.
|
以下是kevent的所有flags:
以下是kevent的所有filters:
以下是kqueue的例子:
void str_cli(FILE *fp, int sockfd) |