五种IO模型与存储映射IO。

IO模型

  • 阻塞IO
  • 非阻塞IO
  • I/O复用
  • 信号驱动IO(SIGIO)
  • 异步IO(POSIX的aio_系列函数)

一个输入输出通常包含两个不同阶段:

  • 等待数据准备好。
  • 从内核向进程复制数据。

对于一个套接字上的输入输出操作:

  • 第一步通常涉及等待数据从网络中到达。当所等待分组到达时,它被复制到内核中的某个缓冲区。
  • 第二步就是把数据从内核缓冲区复制到应用进程缓冲区。

五种IO模型

阻塞式IO模型

如图所示,进程调用recvfrom,其系统调用直到数据报到达且被复制到应用进程的缓冲区中或者发生错误时才返回。(从调用recvfrom开始到它返回的整段时间内是被阻塞的)

非阻塞式IO模型

进程把一个套接字设置成非阻塞的是在通知内核:当所请求的I/O操作非得把本进程投入睡眠才能完成时,不要把本进程投入睡眠,而是返回一个错误

当一个进程像上图这样对一个非阻塞描述符循环调用时,称这种行为为:轮询(polling)。

I/O复用

IO复用模型指,可以调用IO复用函数,进程阻塞在IO复用函数上,而不是阻塞在I/O系统调用上,当等待事件发生后,在调用对应的IO函数进行处理。(一般I/O函数可以等待多个文件描述符)

信号驱动式IO模型

可以注册信号处理函数,系统调用将立即返回,当数据准备好读取时,内核为该进程产生一个SIGIO信号通知进程。

异步IO模型

异步IO函数的工作机制是:告知内核启动某个操作,并让内核在整个操作(包括将数据从内核复制到进程缓冲区)完成后通知我们。

同步IO的第一阶段处理不同,第二阶段相同,异步IO处理了两个阶段,不导致请求进程阻塞。

select函数

1
#include<sys/select.h>
2
#include<sys/time.h>
3
int select(int maxfdpl, fd_set *readset, fd_set *writeset, 
4
    fd_set *exceptset, const struct timeval *timeout);
5
//若有就绪描述符则为其数目,超时则为0,出错则为-1。
6
struct timeval {
7
    long tv_sec;    /* seconds */
8
    long tv_usec;   /* microseconds */
9
};

timeval参数:

  • 永远等待下去:仅在有一个描述符准备好I/O时才返回。为此,把参数设置为空指针。
  • 等待一段固定时间:在有一个描述符准备好I/O时返回,但是不超过由该参数所指向的timeval结构中指定的秒数和微秒数。
  • 更不不等待:检查描述符后立即返回,这称为轮询(polling)。该参数必须指向一个timeval结构,而且其中的定时器必须为0。

前两种情形的等待通常会被进程在等待期间捕获的信号中断,并从信号处理函数返回。

readset,writeset,exceptset指定要让内核测试读,写,异常条件的描述符集。通过如下宏对该fd_set描述符集进行操作。

1
void FD_ZERO(fd_set *fdset);
2
void FD_SET(int fd, fd_set *fdset);
3
void FD_CLR(int fd, fd_set *fdset);
4
int FD_ISSET(int fd, fd_set *fdset);

maxfdpl参数指定待测试描述符个数,它的值是待测试的最大描述符加1

select函数返回后,描述符集内任何与未就绪描述符对应的位返回时均清为0。为此每次重新调用select函数时,需要再次把所有描述符集合内所关心的位均置为1。

poll函数

1
#include<poll.h>
2
int poll(struct pollfd *fdarray, unsigned long nfds, int timeout);
3
//返回就绪描述符数目,若超时则为0,出错则为1
4
struct pollfd{
5
    int     fd;      /* descriptor to check */
6
    short   events;  /* events of interest on fd */
7
    short   revents; /* events that occurred on fd */
8
};
常量 作为events的输入 作为revents的结果 说明
POLLIN * * 普通或优先级带数据可读
POLLRDNORM * * 普通数据可读
POLLRDBAND * * 优先级带数据可读
POLLPRI * * 高优先级数据可读
POLLOUT * * 普通数据可写
POLLWRNORM * * 普通数据可写
POLLWRBAND * * 优先级带数据可写
POLLERR * 发生错误
POLLHUP * 发生挂起
POLLNVAL * 描述符不是一个打开的文件

对于TCP和UDP套接字,一下条件引发poll返回特定的revent:

  • 所有正规TCP数据和所有UDP数据都被认为是普通数据。
  • TCP的带外数据被认为是优先级带数据。
  • 当TCP连接的读半部关闭时(例如收到一个来自对端的FIN),也被认为是普通数据,随后的读操作将返回0。
  • TCP连接存在错误即可认为是普通数据,也可认为是错误(POLLERR)。随后的读操作返回-1,并把errno设置为合适的值。可以用于处理诸如接收到RST或发生超时等待条件。
  • 在监听套接字上有新的连接可用即可认为是普通数据,也可认为是优先级数据。大部分视为普通数据。
  • 非阻塞式connect的完成被认为是使相应套接字可写。

timeout参数:

  • INFTIM 用于等待。
  • 0 立即返回,不阻塞进程。
  • >0 等待指定书目的毫秒数

epoll函数

记录锁

记录锁(record locking)的功能是:当第一个进程正在读或修改文件的某个部分时,使用记录锁可以阻止其他进程修改同一文件区。

1
#include<fcntl.h>
2
int fcntl(int fd, int cmd, .../* struct flock *flockptr */);
3
struct flock{
4
    short l_type;   /* F_RDLCK, F_WRLCK, or F_UNLCK */
5
    off_t l_start;  /* offset in bytes, relative to l_whence */
6
    short l_whence; /* SEEK_SET, SEEK_CUR, or SEEK_END */
7
    off_t l_len;    /* length, in bytes; 0 means lock to EOF */
8
    pid_t l_pid;    /* returned with F_GETLK */
9
};

任意多个进程在一个给定的字节上可以有一把共享读锁(l_type为F_RDLCK),但是在一个给定字节上只能有一个进程有一把独占写锁(l_type为F_WRLCK)。

上述说明适用于不同进程提出的锁请求,如果一个进程对一个文件区间已经有了一把锁,该进程再次企图在同一文件区间再加一把锁时,新锁会替换已有锁。

加读锁时,描述符必须是读打开,写锁时,描述符必须是写打开。

cmd参数 说明
F_GETLK 判断由flockptr所描述的锁是否被另一个锁所排斥(阻塞)。
F_SETLK 设置由flockptr所描述的锁,如果出错,errno设置为EACCES或EAGAIN。
F_SETLKW 此命令是F_SETLK的阻塞版本。

记录锁的隐含继承和释放:

  • 锁与进程和文件两者相关联。当一个进程终止时,它所建立的锁全部释放;无论一个文件描述符何时关闭,该进程通过这一描述符引用的文件上的任何一把锁都会释放。
  • 由fork产生的子进程不继承父进程所设置的锁。
  • 执行exec后,新程序可以继承原执行程序的锁。
1
//...
2
fd1 = open(pathname, ...);
3
write_lock(fd1, 0, SEEK_SET, 1);    /* parent write locks byte 0 */
4
if ((pid = fork()) > 0) {           /* parent */
5
    fd2 = dup(fd1);
6
    fd3 = open(pathname, ...);
7
} else if (pid == 0) {
8
    read_lock(fd1, 1, SEEK_SET, 1); /* child read locks byte 1 */
9
}
10
pause();
11
//....

lockf结构由i结点结构开始互相连接起来。每个lockf结构描述了一个给定进程的一个加锁区域(由偏移量和长度定义)。

守护进程使用文件锁来保证副本唯一的模板。

1
#include <unistd.h>
2
#include <fcntl.h>
3
4
int
5
lockfile(int fd)
6
{
7
    struct flock fl;
8
9
    fl.l_type = F_WRLCK;
10
    fl.l_start = 0;
11
    fl.l_whence = SEEK_SET;
12
    fl.l_len = 0;
13
    return(fcntl(fd, F_SETLK, &fl));
14
}

POSIX异步I/O

咕咕咕…鸽(

存储映射IO

存储映射I/O(memory-mapped I/O)能将一个磁盘文件映射到存储空间上的一个缓冲区上,于是当从缓冲区中取数据时,就相当于读文件中相应的字节。将数据存入缓冲区时,相应字节就自动写入文件。

1
#include <sys/mman.h>
2
void *mmap(void *addr, size_t len, int prot,
3
    int flag, int filedes, off_t off );

addr参数用于指定映射存储区的起始地址。通常将其设置为0,表示由系统选择该映射区的起始地址。此函数的返回值是该映射区的起始地址。

fd参数是指定要被映射文件的描述符。在文件映射到地址空间之前,必须先打开该文件。len参数是映射的字节数,off是要映射字节在文件中的起始偏移量。

prot参数 说明
PROT_READ 映射区可读
PROT_WRITE 映射区可写
PROT_EXEC 映射区可执行
PROT_NONE 映射区不可访问
flag参数 说明
MAP_FIXED 返回值必须等于addr。不利于可移植性,不鼓励使用此标志。
MAP_SHARED 描述了本进程对映射区所进行的存储操作的配置。
MAP_PRIVATE 本标志说明,对映射区的存储操作导致创建该映射文件的一个私有副本。

1
#include<sys/mman.h>
2
int mprotect(void *addr, size_t len, int prot);
3
int msync(void *addr, size_t len, int flags);
4
int munmap(caddr_t addr, size_t len);