`

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

阅读更多
高级文件I/O
1、Linux提供的高级I/O操作:
1)Scatter/gatter I/O:可以通过单个系统调用从多个buffer写到文件或者读到多个buffer中。
2)Epoll:是poll()和select()的改进版。
3)Memory-mapped I/O:将一个文件映像到内存,通过内存的操作来操作文件,操作更高效简单。
4)File advice:允许进程向内核提供使用的场景来优化I/O效率。
4)异步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实例,返回一个文件描述符,这个文件描述符不和一个实际的文件相关,只是被后面的系统调用
所用到。size是提供给内核要监控多少个文件描述符的一个线索,并不是最大值,提供一个大约的值可以提高性能。
失败了返回-1,并设置errno。EINVAL表示size参数不是正数,ENFILE表示达到了打开文件的最大值,ENOMEM内存不足。
例子:
int epfd;

epfd = epoll_create(100);
if(epfd < 0)
	perror("epoll_create");

2) 控制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对齐的,连续的lenzijie的映像,一旦被删除,内存中的区域
就不在有效,访问会产生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[len]);
	}

	if(munmap(p,sb.st_size) == -1){
		perror("mummap");
		return 1;
	}
	return 0;
}

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》
2
0
分享到:
评论
1 楼 yanqingluo 2013-06-03  
好文章,转走了啊.

相关推荐

    Linux系统编程学习笔记

    ### Linux系统编程学习笔记 #### 一、IO **1.1 标准I/O (stdio)** - **fopen/fclose**: `fopen` 用于打开或创建一个文件,并返回一个指向该文件的 `FILE *` 类型的指针。`fclose` 用于关闭由 `FILE *` 指向的文件...

    linux 系统编程 尚观 linux内核驱动开发 笔记

    尚观Linux内核驱动开发笔记不仅涵盖了这些基础知识,还可能包含实践案例、常见问题解析以及高级技术探讨,例如异步I/O、内存管理优化、多线程同步等。通过学习和实践这些内容,开发者可以提升在Linux平台上的系统...

    学习《高级Linux环境编程》读书笔记(APUE读书笔记)

    《高级Linux环境编程》或称为APUE(Advanced Programming in the UNIX ...这本书不仅涵盖了UNIX编程的核心概念,还包括了许多实用的编程技巧和解决方案,是一本非常适合在Linux环境下从事系统编程学习的参考书籍。

    linux编程学习笔记PDF资料下载.txt

    根据提供的文件信息,我们可以推断出这是一份关于Linux编程学习笔记的PDF资料。下面将对这份资料可能涉及的关键知识点进行详细的阐述。 ### Linux编程基础知识 #### 1. Linux操作系统概述 - **定义与特点**:Linux...

    LINUX环境高级编程整理

    本笔记主要涵盖了三个关键领域:文件系统操作、进程管理和高级IO(输入/输出)。 首先,让我们深入探讨文件系统操作。在Linux中,一切皆为文件,包括硬件设备。文件系统是组织这些文件的方式,它定义了如何存储、...

    UNIX系统编程学习笔记

    在进入UNIX系统编程的学习之前,我们首先需要理解UNIX的基本概念。UNIX是一种多用户、多任务的操作系统,由贝尔实验室在1960年代末开发。它以其简洁、强大的命令行接口和丰富的工具集而闻名,是许多现代操作系统设计...

    Linux学习笔记(搬运).zip

    Linux学习笔记是一个包含丰富内容的资源包,专为那些希望深入了解和掌握Linux操作系统的人们设计。Linux是一种开源、自由的类UNIX操作系统,被广泛应用于服务器、嵌入式设备以及个人计算机上。本笔记将带你踏上Linux...

    C++和Linux学习笔记.zip

    5. **网络和I/O**:掌握网络编程的基本概念,如套接字编程,以及如何通过标准输入/输出和文件描述符进行I/O操作。 6. **系统调用与内核接口**:深入理解Linux内核,学习如何使用系统调用编写高效的程序。 压缩包中...

    Linux 初级学习的基本概念和资料

    提供的压缩包文件包含一系列关于Linux系统编程的学习笔记,如线程、信号、文件I/O、进程管理、内存管理、进程间通信等主题,这些都是Linux初级学习的关键点。通过阅读这些笔记,可以深入理解Linux底层机制,为后续的...

    201808达内大数据Linux阶段学习课后笔记

    这份“201808达内大数据Linux阶段学习课后笔记”涵盖了Linux基础到高级应用的诸多方面,旨在帮助学习者深入理解Linux系统及其在大数据环境中的应用。 笔记首先可能会介绍Linux的基础知识,包括Linux的历史、哲学...

    UNix系统编程笔记

    通过以上内容,我们可以了解到Unix系统编程涉及到多个方面的知识,从操作系统的基本概念到具体的文件操作、进程管理等高级主题。掌握这些知识点对于深入理解Unix系统的工作原理以及编写高质量的应用程序至关重要。

    linux学习笔记 命令 文件

    在Linux操作系统中,掌握基本的命令和文件操作是至关重要的,因为它们构成了日常使用...随着经验的积累,你还可以探索更多高级功能,如文件权限的更细粒度控制、文件的权限变更、脚本编程等,进一步提升你的Linux技能。

    jz2440学习笔记 本仓库内容 jz2440学习笔记 思维导图 ARM体系架构学习 Linux内核驱动学习

    标题"jz2440学习笔记"表明这是一个关于jz2440处理器的学习资源,通常jz2440是基于ARM体系架构的微控制器,常用于嵌入式系统的学习和开发。描述中提到了"思维导图"、"ARM体系架构学习"和"Linux内核驱动学习",这意味...

    linux 学习笔记 .

    这只是Linux学习笔记的冰山一角。Linux系统还包括包管理、网络配置、进程管理、权限控制、脚本编程等多个方面。熟练掌握这些基本命令和概念是成为Linux高手的第一步。随着经验的积累,你还可以学习更高级的主题,如...

    Linux学习笔记.pdf,linux详解,LINUX源码.zip

    "Linux学习笔记.pdf":这可能是一份详尽的文档,包含了Linux的基础知识、命令行操作、系统管理、网络配置、软件安装、文件系统管理等多个方面。这份笔记可能涵盖了从初学者到高级用户所需的各种知识,帮助读者快速...

    Unix-Linux编程实践教程

    3. **文件系统和I/O操作**:讨论文件描述符、打开/关闭文件、读写文件、标准输入输出以及文件权限等。 4. **网络编程**:讲解TCP/IP协议栈和套接字API,如何创建服务器和客户端应用程序。 5. **系统调用和库函数**...

Global site tag (gtag.js) - Google Analytics