Linux系统编程学习笔记(三)高级文件I/O

Linux系统编程学习笔记(三)高级文件I/O

高级文件I/O

1、Linux提供的高级I/O操作:

  • Scatter/gatter I/O:可以通过单个系统调用从多个buffer写到文件或者读到多个buffer中。
  • Epoll:是poll()和select()的改进版。
  • Memory-mapped I/O:将一个文件映像到内存,通过内存的操作来操作文件,操作更高效简单。
  • File advice:允许进程向内核提供使用的场景来优化I/O效率。
  • 异步I/O:允许进程发出I/O请求后不等待操作完成返回。

2、Scatter/gatter I/O:

通过单个系统调用从单个流读取到或写入多个buffer数据。

优点:对于有些操作更自然、更高效、具有原子性。

1)readv:从文件描述符fd中读去count个段到多个buffers iov中:

#include <sys/uio.h>  
  
ssize_t readv(int fd,const struct iovec *iov, int count);  

2)writev:从多个buffers中写入入最多count个段到由fd描述符标示的文件中。

#include <sys/uio.h>  
  
ssize_t writev(int fd,const struct iovec *iov, int count); 

readv、writev与read、write的行为一致,除了读到多个buffers中或者从多个buffers写

iovec结构:

#include <sys/uio.h>  
  
struct iovec{  
    void *iov_base; /* pointer to the start of buffer */  
    size_t iov_len; /* size of buffer in bytes */  
}  

按照iovec[0],iovec[1],…,iovec[count-1]的次序依次读写。
返回值:成功,返回读写的字节数,这个字节数是所有iovec结构中iov_len的总和。
失败返回-1,并设置好errno。
iov_len的总和不能超过SSIZE_MAX,count需要大于0,小于IOV_MAX(<limits.h>中),在linux为1024,
否则返回-1,errno设置为EINVAL,

例子:writev:

#include <stdio.h>  
#include <sys/types.h>  
#include <sys/stat.h>  
#include <fcntl.h>  
#include <string.h>  
#include <sys/uio.h>  
  
int main(){  
    struct iovec iov[3];  
    ssize_t nr;  
    int fd,i;  
      
    char *buf[] = {  
        "The term buccaneer comes from the word boucan.\n",  
        "A boucan is a wooden frame used for cooking meat.\n",  
        "Buccaneer is the West Indies name for a pirate.\n"   
    };  
      
    fd = open("buccaneer.txt",O_WRONLY | O_CREAT | O_TRUNC);  
    if( fd == -1 ){  
        perror("open");  
        return 1;  
    }  
  
    for(i = 0; i < 3; i++){  
        iov[i].iov_base = buf[i];  
        iov[i].iov_len = strlen(buf[i]);  
    }  
      
    nr = writev(fd,iov,3);  
    if(nr == -1){  
        perror("writev");  
        return 1;  
    }  
    printf("wrote %d bytes\n",nr);  
  
    if(close(fd)){  
        perror("close");  
        return 1;  
    }  
    return 0;  
}  

readv例子:

#include <stdio.h>  
#include <sys/types.h>  
#include <sys/stat.h>  
#include <fcntl.h>  
#include <sys/uio.h>  
  
int main(){  
    char foo[48],bar[51],baz[49];  
    struct iovec iov[3];  
    ssize_t nr;  
    int fd,i;  
  
    fd = open("buccaneer.txt",O_RDONLY);  
    if(fd == -1){  
        perror("open");  
        return 1;  
    }  
  
    iov[0].iov_base = foo;  
    iov[0].iov_len = sizeof(foo);  
    iov[1].iov_base = bar;  
    iov[1].iov_len = sizeof(bar);  
    iov[2].iov_base = baz;  
    iov[2].iov_len = sizeof(baz);  
  
    nr = readv(fd,iov,3);  
    if(nr == -1){  
        perror("readv");  
        return 1;  
    }  
  
    for(i = 0; i < 3; i++){  
        printf("%d: %s",i,(char *)iov[i].iov_base);  
    }  
  
    if(close(fd)){  
        perror("close");  
        return 1;  
    }  
    return 0;  
}  

乱序结果。待理解。

3、The Event Poll Interface

poll()和select()需要一些描述符列表进行监控,内核需要遍历每一个描述符进行监控,这样如果描述符列表比较大,就会产生性能问题。

epoll解决了这个问题,它将监控的注册和实际的监控进行了解耦,一个系统调用用来初始化epoll的环境,另一个用来添加或者删除描述符,第三个进行实际的事件等待。

1)创建一个新的Epoll实例:epoll_create:

#include <sys/epoll.h>  
  
int epoll_create(int size);  

创建了一个epoll实例,返回一个文件描述符,一般称为epollfd,这个文件描述符不和一个实际的文件相关,只是被后面的系统调用所用到。size是提供给内核要监控多少个文件描述符的一个线索,并不是最大值,提供一个大约的值可以提高性能。
失败了返回-1,并设置errno。EINVAL表示size参数不是正数,ENFILE表示达到了打开文件的最大值,ENOMEM内存不足。

例子:

int epfd;  
  
epfd = epoll_create(100);  
if(epfd < 0)  
    perror("epoll_create");  
  1. 控制epoll:

epoll_ctl可以从epoll的环境中添加和删除文件描述符。

#include <sys/epoll.h>  
  
int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event);  

epoll_event结构:

struct epoll_event{  
    __u32 events;/* events */  
    union{  
        void *ptr;  
        int fd;  
        __u32 u32;  
        __u64 u64;  
    }data;  
};  

op指定了与fd相关要执行的操作,event参数进一步描述了操作的行为。

op值:
EPOLL_CTL_ADD:添加一个监控的文件描述符到有epfd关联的epoll中。
EPOLL_CTL_DEL:从epfd关联的epoll中删除一个监控的文件描述符。
EPOLL_CTL_MOD: 修改有fd表示的文件描述符。

epoll_event结构中:events可以是以下值的OR:
EPOLLERR: 文件发生错误,这个事件始终监控,即使没有指定。
EPOLLET:使得监控文件的edge-triggered行为可用,和Level-triggered events对应。默认是Level-triggered
EPOLLHUP:文件hangup发生。这个事件始终监控,即使没有指定。
EPOLLIN:文件可以无需阻塞的进行读操作。
EPOLLONESHOT:当一个事件被生成和读取,这个文件自动不再被监控,需要重新通过EPOLL_CTL_MOD设置监控。
EPOLLOUT:文件可以无需阻塞的进行写操作。
EPOLLPRI:存在紧急的out-of-band数据需要读。\

成功返回0,失败返回-1,并设置好errno,
EBADF:epfd不是一个合法的epoll实例。
EEXIST:EPOLL_CTL_ADD操作,fd已经存在。
ENOENT:EPOLL_CTL_ADD,EPOLL_CTL_DEL操作,不存在相关的fd。
EINVAL:epfd不是epoll的一个实例。
ENOMEM:没有足够的内存。
EPERM:fd不支持epoll

例子:添加

struct epoll_event event;  
  
int ret;  
  
event.data.fd = fd;  
event.events = EPOLLIN | EPOLLOUT;  
  
ret = epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&event);  
if(ret)  
    perror("epoll_ctl");  

修改:

struct epoll event event;  
int ret;  
  
event.data.fd = fd;  
event.events = EPOLLIN;  
  
ret = epoll_ctl(epfd,EPOLL_CTL_MOD,fd,&event);  
if(ret)  
    perror("epoll_ctl");  

删除:

struct epoll_event event;  
int ret;  
  
ret = epoll_ctl(epfd,EPOLL_CTL_DEL,fd,&event);  
if(ret)  
    perror("epoll_ctl");  

3)使用epoll等待事件:

epoll_wait:

#include <sys/epoll.h>  
  
int epoll_wait(int epfd,struct epoll_event *events,int maxevents,int timeout);  

epoll_wait等待与epfd相关联的文件描述符的事件直到timeout微妙。
timeout是0,则立即返回,timeout是-1等待直到一个事件发生。
成功,events指针指向了epoll_event结构,描述了每一个事件,最大到maxevents个事件。

失败返回-1,并设置errno:
EBADF:epfd不合法的文件描述符。
EFAULT:进程没有对有events指向的内存的写访问权限。
EINTR:进程被信号中断。
EINVAL:epfd是个不合法的epoll实例。

例子:

#define MAX_EVENTS 64  
  
struct epoll_event *events;  
int nr_events,i,epfd;  
  
events = malloc(sizeof(struct epoll_event) * MAX_EVENTS);  
if(! events ){  
    perror("malloc");  
    return 1;  
}  
  
nr_events = epoll_wait(epfd,events,MAX_EVENTS,-1);  
if(nr_events < 0){  
    perror("epoll_wait");  
    free(events);  
    return 1;  
}  
  
for(i = 0; i < nr_events;i++){  
    printf("event=%ld on fd=%d\n",events[i].events,events[i].data.fd);  
    /* 
     *现在可以使用events[i].data.fd进行无阻塞的操作了。 
     */  
}  
  
free(events);  

4)边缘触发(Edge-Triggered Events) VS 条件触发(Level-Triggered Events):
在epoll_ctl中,如果epoll_event结构中的events参数中设置为EPOLLET,则监控为Edge-Triggered,否则是Level-Triggered,默认是Level-Triggered。Level-Triggered和poll、select的行为一致,这是很多开发者期望的方式。使用非阻塞编程可以使用Edge-Triggered。

不同点:
考虑下面使用pipe通讯的生产者消费者例子:
1)生产者写1kb到管道中。
2)消费者使用epoll_wait,等待管道数据到来。
如果使用Level-Triggered,epoll_wait会立即返回,因为已经有数据到来。
如果使用Edge-Triggered,epoll_wait可能会阻塞,如果没有写操作继续的话,因为Edge-Triggered只有在监控的文件的事件发生的时候分发事件。
而2)步是在生产者写之后去epoll_wait的。

4、将文件映像到内存:

内核提供了一个接口,可以将文件映像到内存的buffer中,这样当我们从buffer中获取数据时,相应的文件数据就会被读取,类似,当我们存储数据到内存中时,相应的数据自动写入文件。

1)mmap:

#include <sys/mmap.h>  
  
void *mmap(void *addr,size_t len,int prot, int flags,int fd,off_t offset);  

addr:建议内核将文件映射到的地址,这只是一个hint,为了可移植性,一般设置为0,调用返回mapping开始的实际地址。
fd:要映像文件的描述符。

prot描述了内存映像的保护权限,可以使用OR连接以下选项:
PROT_READ:区域可读。
PROT_WRITE:区域可写
PROT_EXEC:区域可执行
PROT_NONE:区域不可访问,很少有用。

prot不能和打开文件的模式冲突。比如打开了一个只读文件,prot不可以指定为PROT_WRITE。

flags:描述了内存映像的方式,可以使用OR连接以下选项:
MAP_FIXED: addr是必须的,并且不是一个hint,如果内核无法在该地址映像,则失败。
具有不可移植性,不推荐使用。
MAP_PRIVATE:映像不共享,文件被映像为copy-on-write,任何在内存中的改变,都不会反映在文件或者其他进程的mapping中。
MAP_SHARED:映像和其他进程共享映射的同一个文件,写入buffer等效于写入文件,读取映像写操作同时反映在其他进程中。
MAP_SHARE和MAP_PRIVATE其中之一必须被指定。
addr和off的值需要被指定为系统虚拟页的整数倍,可以通过使用_SC_PAGESIZE_SC_PAGE_SIZE作为sysconf参数获得页的大小。
如果文件的大小12字节,但系统页的大小是512字节,那么系统调用会映射512字节,其他的部分被填充为0,我们可以修改另外的500个字节,但是并不反应在文件中,所以我们不能使用mmap来append文件,我们需要首先增大文件到指定的值。

sysconf:

#include <unistd.h>  
  
long sysconf(int name);  

获得页的大小:

long page_size = sysconf (_SC_PAGESIZE);  

linux提供了getpagesize():

#include <unistd.h>  
  
int getpagesize(void); 

并不是所有的Unix系统都提供这个方法,POSIX标准已经将它抛弃掉了。

mmap()成功返回mapping的地址,失败返回MAP_FAILED,并设置errno:
EACESS:fd不是普通文件或者fd的模式和prot、flags冲突。
EAGAIN:文件被文件锁锁定。
EBADF:文件描述符不合法。
EINVAL:addr、len或者off不合法
ENFILE:达到了系统最大打开文件数
ENODEV:文件所在的文件系统不支持mmap。
ENOMEM:进程没有足够的内存。
EOVERFLOW:addr+len大于地址空间
EPERM:PROT_EXEC被指定,但是文件系统被mount成noexec。\

两个相关的信号:
SIGBUS:进程访问一个已经不合法的映射,例如,文件映射之后被truncated
SIGSEGV:试图往被映射成只读的区域写。

2)munmap:删除由mmap()创建的映像:

munmap:

#include <sys/mman.h>  
  
int mummap(void *addr, size_t len);  

mummap删除了从addr地址开始,是page对齐的,连续的len字节的映像,一旦被删除,内存中的区域就不再有效,访问会产生SIGSEGV信号。
munmap通常传入由mmap返回的addr以及设置的len长度。
成功返回0,失败返回-1,并设置errno:EINVAL,表示有一个或者更多的参数不正确。

if(munmap(addr,len) == -1)  
    perror("munmap");  

内存映射例子:

#include <stdio.h>  
#include <sys/types.h>  
#include <sys/stat.h>  
#include <fcntl.h>  
#include <unistd.h>  
#include <sys/mman.h>  
  
int main(int argc,char *argv[]){  
    struct stat sb;  
    off_t i;  
    char *p;  
    int fd;  
  
    if(argc < 2){  
        fprintf(stderr,"usage: %s <file>\n",argv[0]);  
        return 1;  
    }  
  
    fd = open(argv[1],O_RDONLY);  
    if(fd == -1){  
        perror("open");  
        return 1;  
    }  
  
    if(fstat(fd,&sb) == -1){  
        perror("fstat");  
        return 1;  
    }  
  
    if(! S_ISREG(sb.st_mode)){  
        fprintf(stderr,"%s is not a regular file\n",argv[1]);  
        return 1;  
    }  
  
    p = mmap(0,sb.st_size, PROT_READ,MAP_SHARED,fd,0);  
    if(p == MAP_FAILED){  
        perror("mmap");  
        return 1;  
    }  
  
    if(close(fd) == -1){  
        perror("close");  
        return 1;  
    }  
  
    for(i = 0;i < sb.st_size; i++){  
        putchar(p[i]);  
    }  
  
    if(munmap(p,sb.st_size) == -1){  
        perror("mummap");  
        return 1;  
    }  
    return 0;  
}  

上述函数将fd里面的文件输出到标准输出里面。

3)mmap的优缺点:

优点:

  1. 从内存映像文件中读写,避免了read、write多余的拷贝。
  2. 从内存映像文件中读写,避免了多余的系统调用和用户-内核模式的切换
  3. 可以多个进程共享内存映像文件。
  4. seeking内存映像只需要指针操作,避免系统调用lseek。

缺点:

  1. 内存映像需要时整数倍页大小,如果文件较小,会浪费内存。
  2. 内存映像需要在放在进程地址空间,大的内存映像可能导致地址空间碎片,找不到足够大的空余连续区域供其它用。
  3. 内核需要维护更多的和内存映像相关的数据结构。

4)重新设置映像大小:

#define _GNU_SOURCE  
  
#include <unistd.h>  
#include <sys/mman.h>  
  
void *mremap(void *addr, size_t old_size, size_t new_size, unsigned long flags);  

mremap扩展或者缩小内存映像,从区域[addr,add+old_size]变成一个新的大小new_size

flags:
0:不可以移动来改变内存映像大小。
MREMAP_MAYMOVE:如果需要可以移动地址改变内存映像大小。

返回值:成功返回重新设置大小之后的内存映像的大小,失败返回MAP_FAILED,并设置errno:
EAGAIN:内存区域被锁定,无法改变大小。
EFAULT:给定区域的一些也有不合法的页或者重新映像存在问题。
EINAL:参数不合法。
ENOMEM:不移动则无法扩展,如果MREMAP_MAYMOVE没有被设置。\

glibc经常使用mremap来实现高效的realloc():

void * realloc(void *addr, size_t len){  
    size_t old_size = look_up_mapping_size(addr);  
    void *p;  
    p = mremp(addr,old_size, len,MREMAP_MAYMOVE);  
    if(p == MAP_FAILED)  
        return NULL;  
    return p;  
}  

5)改变映像的protection:

mprotect可以允许程序改变已经存在内存区域的permissions:

#include <sys/mman.h>  
  
int mprotect(const void *addr, size_t len, int prot);  

一些系统只能改变由mmap得到的内存映像的protection,Linux可以操作任何一个内存区域。

成功返回0,失败返回-1,并设置errno为:
EACCESS:不可以被设置成prot的permissions,可能是文件打开时只读的,设置成可写
EINVAL:参数不合法。
ENOMEM:内核内存不足或者所给的内存区域不是进程的合法地址空间。

6)将文件和映像同步:

POSIX提供了和文件操作中fsync类似的将文件和内存映像同步的操作msync:

#include <sys/mman.h>  
  
int msync(void *addr, size_t len, int flags);  

将内存映像flush到磁盘。没有msync,没有能够保证将mapping的脏数据写回磁盘,除非被unmapped。
当修改内存映像,进程直接修改在内核页cache中的文件页,内核可能不会很快将内核的页cache同步到磁盘。

flags使用OR链接下面选项:
MS_ASYNC:异步的执行同步操作,msync立即返回,更新操作被调度。
MS_INVALIDATE:指定所有其他的内存映像cache副本无效。任何以后的操作都同步到磁盘中。
MS_SYNC:同步的执行同步操作,等将内容写入磁盘在返回。

if(msync(addr,len,MS_ASYNC) == -1)  
    perror("msync");  

成功返回0,失败返回-1,并设置errno:
EINVAL:MS_SYNC和MS_ASYNC同时被设置或者addr没有页对齐。
ENOMEM:所给的内存区域(或者部分)没有被映射。

7)给mapping建议:

Linux提供了madvice来让进程建议内核或者给内核提供线索来使用mapping,这样可以优化mapping的使用。

#include <sys/mman.h>  
  
int madvice(void *addr, size_t len, int advice);  

len如果为0,内核将建议施用于从addr开始的整个映像。

advice:可以是以下之一:
MADV_NORMAL:没有特别的建议。建议使用中等程度的预先读。
MADV_RANDOM:以随机访问的方式访问指定的区域。建议较少的预先读。
MADV_SEQUENTIAL:顺序访问指定区域。建议大量预先读
MADV_WILLNEED:将来要访问指定区域。初始化预先读,将指定的页读到内存。
MADV_DONTNEED:将来不再访问指定区域。内核释放与指定页关联的资源。后续的读会导致从文件中再度调入。

int ret;  
  
ret = madvise(addr,len,MADV_SEQUENTIAL);  
if(ret < 0)  
    perror("madvice");  

参考:

  1. 《Linux system programming》
  2. 《Unix system programming》
  3. 《Advanced Programming in the Unix Environment》
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值