Linux IO

时间:2015-11-29 13:24:07   收藏:0   阅读:433

 

1. I/O概述

1.1 文件类型

    UNIX将系统所有的内容都视为文件,其中文件类型包括如下几种:

1.2 I/O模型

    所有的操作系统都提供多种服务的入口点,由此程序向内核请求服务。各种版本的UNIX实现都提供良好定义、数量有限、直接进入内核的入口点,这些入口点被称为系统调用(system call)库函数是指为了迎合程序员使用,而编制的通用库函数,虽然这些函数可能会调用一个或多个内核的系统滴啊用,但是它们并不是内核的入口点。例如,printf函数会调用write(系统调用)输出一个字符串。

    从实现者的角度来看,系统调用和库函数之间有根本的区别,但从用户角度来看,其区别并不重要。但是需要注意的是:我们可以替换库函数,而通常不能替换系统调用。

    在UNIX下可用如下的几种I/O模型:

2. 阻塞式I/O模型

 

    阻塞式模型I/O是指当调用此类函数时,就会发生阻塞,直至函数返回如下图所示:

技术分享

    阻塞式模型又可以分为两种模型:不带缓冲的函数和带缓冲的函数(标准IO)。

2.1 不带缓冲的函数

 

    不带缓冲是指每个read和write都调用内核中的一个系统调用。其中有:open、read、write、lseek以及close。这些不带缓冲的I/O函数不是ISO C的组成部分,而是POSIX.1和Single UNIX Specification的组成部分。

2.2 带缓冲的函数

    标准I/O库是一类带缓冲的函数,其是ISO C的标准,标准I/O库是对上述的不带缓冲的系统调用函数进行封装和调用,其不属于UNIX中的系统函数,但标准I/O不仅在UNIX中能得到使用,在其它系统也能使用。

(一)缓冲

    标准I/O提供了3种类型的缓冲:

     1) 全缓冲:    即只在填满缓冲区后才进行实际I/O操作。

    对于驻留在磁盘上的文件通常是全缓冲的

     2) 行缓冲:是指当在输入和输出中遇到换行符时,标准I/O库执行I/O操作。

    这允许我们一次输出一个字符,但只有在写了一行之后才进行实际I/O操作。当涉及一个 终端时(如标准输入和标准输出),通常使用行缓冲

     3) 不带缓冲:标准I/O库不对字符进行缓冲存储,即马上进行I/O操作。

    例如标准I/O函数fputs写15个字符到不带缓冲的流中,我们就期望这15个字符立即输出。其中标准错误流stderr通常是不带缓冲的。

小结:标准错误时不带缓冲的,打开至终端设备的流是行缓冲的,其他流是全缓冲的。

(二)标准I/O函数

    标准I/O函数也是先打开(open)对流文件,然后进行读或写,其中一旦打开了流,则可进行3中不同类型的非格式化I/O操作:

  1. 每次一个字符的I/O:一次读或写一个字符,如果流是带缓冲的,则标准I/O函数处理了所有缓冲。
  2. 每次一行的I/O:使用fgetsfputs可以一次读写一行,每行都以一个换行符终止。
  3. 直接I/Ofreadfwrite是每次读写某种数量的对象。 

3. 非阻塞式I/O模型

3.1 阻塞的原因

    系统调用分成两类:"低速"系统调用和其它。但低速的系统调用可能会使进程永远阻塞的一类系统调用,包括:

3.2 定义

    非阻塞I/O是指我们发出的open、read和write等I/O操作,并使这些操作不会永远阻塞。如果这样的操作不能完成,则调用立即出错返回,表示该操作如继续执行将阻塞。如下图所示:

技术分享

    前三次调用recvfrom时没有数据可返回,因此内核立即返回一个EWOLDBLOCK错误,第四次调用recvfrom时已有一个数据报准备好,它被复制到应用进程缓冲区,于是recvfrom成功返回。

3.3 使用方式

    对于给定的描述符(文件描述符),有两种为其指定费阻塞I/O的方法:

  1. 如果调用open获得描述符,则可指定O_NONBLOCK标志。
  2. 对于已经打开的一个描述符,则可调用fcntl,由该函数打开O_NONBLOCK文件状态标志

    非阻塞式I/O可以应用到管道、FIFO、套接字、终端、伪终端以及其他一些类型的设备上。其中的文件描述符可以是由open函数的返回值,也可以是网络编程的socket值,如《UNP》343对TCP设置为非阻塞的方式:

int val,sockfd; 
    sockfd = socket(…)//打开socket 
    val = fcntl(sockfd, F_GETFL, 0); //获取文件状态标志 
    fcntl(sockfd, F_SETFL, val | O_NONBLOCK); //在原来的状态标志中添加非阻塞模型 

 

4. I/O多路复用

 

    I/O多路复用是指:通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作主要由select,poll和epoll来实现这种I/O多路复用的机制。但select,poll,epoll本质上都是同步I/O即会阻塞等待,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。

 

    其中需先了解两种通知模式

    如下表总结了I/O多路复用、信号驱动I/O以及epoll所采用的通知模型:

I/O模式

水平触发

边缘触发

select(),poll()

l

 

信号驱动I/O

 

l

epoll()

l

l

 

4.1 select函数

 

    select函数关注的问题是:在指定的时间内所关心的描述符集是否准备就绪

4.1.1 函数原型

int select(int maxfd, fd_set *readfds, fd_set * writefds, fd_set * exceptfds, struct timeval * tvptr)
                                                      返回值:准备就绪的描述符数目;若超时,返回0;若出错,返回-1
?    maxfd:最大文件描述符编号值加1;
?    readfds 、writefds、exceptfds:分别指向所关心的描述符集的指针;
?    tvptr:指定愿意等待的时间长度。

4.1.2 注意问题

4.1.3 select的缺点

4.2 poll函数

    poll的功能和select类似,只是修改了select的函数原型,poll不是为每个条件(可读性、可写性和异常条件)构造一个描述符集,而是构造一个pollfd结构的数组,每个数组元素指定一个描述符编号以及我们对该描述符感兴趣的条件。

4.2.1 函数原型

    pollfd结构

struct pollfd { 
    int fd;     /* file descriptor to check, or <0 to ignore */ 
    short events;     /* events of interest on fd */ 
    short revents; /* events that occurred on fd */ 
}; 

 

    poll函数原型为

int poll(struct pollfd fdarrays[], nfds_t nfds, int timeout); 
                                                                 返回值:准备就绪的描述符数目;若超时,返回0;若出错,返回-1

 

4.2.2 注意问题

4.2.3 和select相比,poll的优点是

4.2.4 poll缺点

    poll和select共同的问题是性能较差:遍历所有的文件描述符,当监听描述符个数增加时,监听效率降低,并且select和poll每次都要在用户态和内核态拷贝监听的描述符参数。

4.3 epoll函数

    epoll是Linux所独有的,所以epoll的移植性没有select和poll好,但epoll既支持水平模式,又支持触发模式

epoll API的核心数据结构称作epoll实例,它和一个打开的文件描述符相关联。通过这个描述符实现如下的目的:

4.3.1 epoll操作函数

epoll API由一下3个系统调用组成。

1) epoll_create函数创建epoll实例

 

int epoll-create(int size);                                               
                 成功返回描述符,错误返回-1

          参数size指定了内部数据结构的划分初始大小,而不是最大上限(从Linux2.6.8版就忽略该值)。

 

2) epoll_ctl函数修改epoll兴趣列表

int epoll_ctl(int epfd, int op, struct epoll_event* ev) 
                                                            成功返回0,错误返回-1

    参数fd指定感兴趣列表中的哪一个文件描述符的设定。op值可以是EPOLL_CTL_ADDEPOLL_CTL_MODEPOLL_CTL_DEL分表对epoll兴趣列表进行增加、修改和删除操作。ev是指向epoll_event的指针,结构体的定义如下:

 struct epoll_event{ 
        unit32_t events; 
        epoll_data_t data; 
    } 

 

    其中的data是如下的一个联合体类型:

typdef union epoll_data { 
        void *ptr; 
        int fd; 
        unit32_t u32; 
        unit64_t u64; 
}epoll_data_t; 

 

    其中ev为文件描述符fd所做的设置如下:

  1. events是一个位掩码,它指定了我们为待检查的描述符fd上所感兴趣的事件集合。
  2.     data是一个联合体,当描述符fd稍后成为就绪态时,联合体的成员可用来指定传回给调用进程的信息

 

3) epoll_wait函数事件等待

    系统调用epoll_wait()返回epoll实例中处于就绪状态的文件描述符信息。单个epoll_wait()调用能返回多个就绪态文件描述符的信息。

  int epoll_wait(int epfd, struct epoll_event *evlist, int maxevents, int timeout) 
                                                               返回准备就绪的文件描述符,0超时,-1出错

    evlist所指向的结构体数组中返回的就绪文件描述符信息,maxevents是指定的evlist数组的大小。

    在数组evlist中,每个元素返回的都是单个就绪态文件描述符的信息。events字段返回了在该描述符上已经发生的事件掩码。data字段返回的是我们在描述符上使用epoll_ctl()注册感兴趣的事件时在ev.data中所指定的值。

    timeout用来确定epoll_wait()的阻塞行为,有如下几种。

4.3.2 epoll的优点

epoll解决了select和poll的几个性能上的缺陷:

5. 信号驱动I/O

5.1 定义

    信号驱动I/O是指请求数据的进程可以利用信号,向内核声明一个信号处理例程,让内核在描述符就绪时发送SIGIO信号(默认情况下)通知进程;而进程在向内核声明后,能够处理其他的任务,当I/O操作可执行时通过接受信号来获得通知。如下图所示:

技术分享

5.2 步骤

要使用信号驱动I/O,程序需要按照如下步骤来执行:

  1. 为内核发送的通知信号安装(通过sigaction函数)一个信号处理例程。默认情况下,这个通知信号为SIGIO.
  2. 设定文件描述符的属主,即当文件描述符上可执行I/O时,接收到通知信号的进程或进程组。通常让调用进程成为属主,并通过fcntl()F_SETOWN操作完成属主的设定:

    fcntl(fd, F_SETOWN, pid);

  3. 通过设定O_NONBLOCK标志使能非阻塞I/O
  4. 通过打开O_ASYNC标志使能信号驱动I/O。可以与上一步合并为一个操作。

    flags= fcntl(fd, F_GETFL);

    fcntl(fd, F_SETFL, flags |ASYNC | NONBLOCK);

  5. 调用进程现在可以执行其他的任务了。当I/O操作就绪时,内核为进程发送一个信号,然后调用在第1步中安装好的信号处理例程。
  6. 信号驱动I/O提供的是边缘触发通知。这表示一旦进程被通知I/O就绪,它就应该尽可能多地执行I/O(如尽可能多地读取字节)。

6. 异步IO

    从历史上信号驱动I/O有时也称为异步I/O,但是,如今的异步I/O(POSIX AIO规范)机制是进程请求内核执行一次I/O操作,内核启动该操作之后立刻将控制权还给调用进程,并且当内核在整个操作完成(包括将数据从内核复制到进程的缓冲区)或错误发生时,该进程得到通知如下图所示:

技术分享

 

    其中信号驱动I/O与异步I/O的区别是:信号驱动I/O是由内核通知我们何时可以启动一个I/O操作(即何时就绪);而异步I/O模型是通知我们I/O操作何时完成

7. 存储映射I/O

7.1 定义

    存储映射I/O    能将一个磁盘文件映射到存储空间中的一个缓冲区上,于是,当从缓冲区中读取数据时,就相当于读文件中的相应字节。于此类似,将数据存入缓冲区时,相应字节就自动写入文件。这样,就可以在不适用read和write的情况下执行I/O。

    映射分为两种类型:

    并且进程之间可以共享存储映射区域,所以又可以将映射的区域分为私有映射共享映射

 

7.2 使用

 

    为了使用这种功能,应首先告诉内核将一个给定的文件映射到一个存储区域中。这是由mmap函数实现的

void *mmap(void* addr, size_t len, int prot, int floag, int fd, off_t off); 
                                                        返回值:若成功,返回映射区的起始地址;若出错,返回MAP_FAILED 

当进程终止时,会自动解除存储映射区的映射,货值直接调用munmap函数也可以解除映射区

int munmap(void* addr, size_t len); 
                                                                   返回值:若成功,返回0;若出错,返回-1

 

原文:http://www.cnblogs.com/hlwfirst/p/5004611.html

评论(0
© 2014 bubuko.com 版权所有 - 联系我们:wmxa8@hotmail.com
打开技术之扣,分享程序人生!