介绍文件读写的基本要素。
在对文件读写操作钱,需要先打开文件,
内核为每一个进程维护一个打开文件的列表,该表称为文件表 -file table。由一些文件描述符(fds)的非负整数进行索引。
文件描述符 int 类型。
每个进程都会打开3个文件描述符:0,1,2,除非进程显式的关闭。
0=标准输入
1=标准输出
2=标准错误
文件描述符可以访问任何可以读写的东西。
文件操作:使用 read() 和write() 系统调用。文件被访问之前,必须打开获取文件描述符。
1. open() 系统调用
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *name , int flags);
int open(const char *name, int flags, mode_t mode);
例:只读方式打开 /home/dir1/dir2/name1 文件
int fd;
fd=open("/home/dir1/dir2/name1", O_RDONLY);
if(fd == -1)
/*error*/
2.creat() 函数 创建文件:
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
int creat(const char *name, mode_t mode);
例:
int fd ;
fd=creat(file,0644);
fd=creat(file, O_WRONLY|O_CREAT|O_TRUNC,0644);
if(fd == -1)
/*error*/
在大多是Linux架构上 creat 是一个系统调用。
3.read() 读取文件
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t len);
例:读入unsigned long 个字节
unsigned long word;
ssize_t nr;
nr = read(file ,&word, sizeof(unsigned long));
if(fd == -1)
/*error*/
读入所有字节:
ssize_t ret;
while(len !=0 &&(ret = read(fd,buf,len))!=0){
if(ret == -1){
if(errno == EINTR)
continue;
perror("read");
break;
}
len -= ret;
buf+= ret;
}
非阻塞读:
char buf[BUFSIZE];
ssize_t nr;
start:
nr = read(fd, buf, BUFSIZE);
if(nr == -1) {
if(errno == EINTR)
goto start;
if(errno == EAGAIN)
/*resubmit later*/
else
/*error*/
}
}
4.write() 写文件
#include<unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
例:
const char *buf=" hello ,hello, hei";
ssize_t nr;
nr = write(fd, buf, strlen(buf));
if(nr == -1)
/*error*/
常用的形式:
unsigned long word=1720;
size_t count;
ssize_t nr;
count =sizeof(word);
nr = write(fd, &word, count);
if(nr == -1)
/*error*/
else if (nr != count)
/*do not write all*/
部分写:如针对套接字,使用循环保证写入了所有请求的数据
ssize_t ret, nr;
while(len != 0; && (ret = write(fd, buf, len)) !=0){
if(ret == -1){
if(error == EINTR)
continue;
perror("wirte");
break;
}
len -=ret;
buf+=ret;
}
更新日志类文件:使用追加模式: O_APPEND 写。
5.同步I/O: 确认数据写入磁盘
#include<unitstd.h>
int fsync(int fd);
int fdatasync(int fd);
void sync(void);
例:
int ret ;
ret=fsync(fd);
if(ret == -1)
/*error*/
为保证任何对目录项的更新也同步到磁盘上,必须对目录本身也调用fsync() 进行同步。
当fsync 错误时,应该尝试 fdatasync
if(fsync (fd) == -1){
if(error == EINVAL){
if(fdatasync(fd) == -1)
perror("fdatasync");
}
else
perror("fsync");
}
可以在打开文件 open() 时,增加 O_SYNC 标志,使所有在文件上的I/O操作同步。
int fd ;
fd =open(file, O_WRONLY | O_SYNC);
if(fd == -1){
perror("open");
return -1;
}
Linux 的O_SYNC 比 fsync() 更有效。
O_DSYNC:每次指定操作后,只有普通数据同步,元数据不同步。
O_RSYNC:要求读请求向写那样进行同步。
6.直接I/O:Linux内核实现复杂的缓存、缓冲 以及设备和应用之间的I/O管理的层次结构。
open() 中使用O_DIRECT标志会使内核最小化I/O 管理的影响:忽略页缓存机制,直接对用户空间缓冲和设备进行初始化,所有的I/O都是同步的:操作在完成之前不能返回。
7.关闭文件: close()
#include <unistd.h>
int close(int fd);
例:
if(close (fd) == -1)
perror("close");
想保证文件在关闭之间写道磁盘,需要使用一个同步操作 fsync。
8.文件重定位: lseek() 更新文件指针的位置。
#include <sys/type.h>
#include <unistd.h>
off_t lseek (int fd, off_t pos, int origin);
例:更新文件位置为123;
off_t ret;
ret = lseek(fd, (off_t)123), SEEK_SET);
if(ret == (off_t)-1)
/*error*/
例:更新文件位置到文件末尾。
off_t ret;
ret = lseek(fd, 0, SEEK_END);
if(ret == (off_t)-1)
/*error*/
更新文件位置之后,一般都需要获取文件的当前位置。
例:
int pos;
pos=lseek(fd, 0, SEEK_END);
if(pos == (off_t)-1)
/*error*/
else
/*pos is current position of fd*/
空洞:对文件系统末尾之后的区域进行填充0的空间,不占有物理磁盘空间。
稀疏文件
9.定位读写: pread, pwite: 调用以需要读写的文件位置作为参数,完成时,不修改文件位置。
#define _XOPEN_SOURCE 500
#include <unistd.h>
ssize_t pread(int fd, void *buf, size_t count, off_t pos); //从fd文件的pos位置获取count个字节到buf中
ssize_t pwrite(int fd, const char *buf, size_t count, off_t pos); //从文件fd 的pos 位置写count 个字节到buf中。
只适用于可以进行定位操作的文件描述符fd。
10.截短文件 ftruncate(),truncate(),不修改文件当前的位置。
#include <unistd.h>
#include <sys/types.h>
int ftruncate(int fd, off_t len); //将指定文件描述符fd 的文件截短到len长度
int truncate(const char *path, off_t len); //将文件路径path指向的文件截短到len 长度。
例:testfile.txt的内容
abcdefghijklmnopqrstwuvxyz
hello, hai,oh, yes, none.
truncate1.c 文件内容
/*********************************************/
#include <unistd.h>
#include <stdio.h>
#include <stlib.h>
int main(int ac, char *av[])
{
int ret;
ret = truncate("./testfile.txt",off_t(atoi(av[1])));
if(ret == -1){
perror("truncate");
return -1;
}
return 0;
}
/*********************************************/
./truncate1 19
testfile.txt的内容:abcdefghijklmnopqrs
11.I/O多路复用:应用程序常常需要在于一个文件描述符上阻塞。
非阻塞I/O:应用可以发起请求并返回一个特别的错误。从而避免阻塞。
I/O多路复用:允许应用在多个文件描述符上同时阻塞,并在其中某个可以读写时受到通知。
I/O多路设计原则:
1.I/O多路复用:当任何文件描述符准备好I/O时告诉我
2.在一个或更多文件描述符就绪前始终处于睡眠状态
3.唤醒:哪个准备好了
4.在不阻塞情况下处理所有I/O就绪的文件描述符
5.返回1。
select(),poll(),epoll()。
select() 调用:实现同步I/O多路复用的机制:
#include <sys/time.h>
#include <sys/typesh>
#include <unistd.h>
int select(int n, fs_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
FD_CLR(int fd, fd_set *set);
FD_ISSET(int fd, fd_set *set);
FD_SET(int fd, fd_set *set);
FD_ZERO(fd_set *set);
在指定的文件描述符准备好I/O之前或者超过一定的时间限制,select()调用就会阻塞。
检测的文件描述符分为3类:
1.检测readfds集合中的文件描述符,确认其中是否有可读数据。
2.检测writefds集合中的文件描述符,确认其中是否有可写数据。
3.检测exceptfds集合中的文件描述符,确实其中述符有出现异常发生或者出现带外数据
集合为空时,select不对这些数据进行检测。
timeval 结构体
#include <sys/time.h>
struct timeval{
long tv_sec; //秒
long tv_usec; //微秒
};
检测集合中的文件操作符,通过辅助宏来进行管理。在select 之前,需要调用该宏
fd_set writefds;
FD_ZERO(&writefds); //初始化一个宏
FD_SET(fd, &writefds); //向指定合集添加一个文件描述符。
FD_CLR(fd,&writefds); //从指定合计移除一个文件描述符
if(FD_ISSET(fd, &writefds) == 1) //检测文件描述符是否在合集中
例:监听标准输入设备5秒(非阻塞),5秒内获取到标准输入时,继续处理。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/time.h>
#define TIMEOUT 5
#define BUF_LEN 1024
int main(int ac, char *av[])
{
struct timeval tv;
fd_set readfds;
int ret;
FD_ZERO(&readfds);
FD_SET(STDIN_FILENO,&readfds);
tv.tv_sec=TIMEOUT ; //等待5秒
tv.tv_usec=0;
ret=select(STDIN_FILENO+1, &readfds, NULL, NULL, &tv);
if(ret == -1){
perror("select");
return 1;
} else if(!ret){
printf("%d seconds elapsed.\n",TIMEOUT);
return 0;
}
if(FD_ISSET(STDIN_FILENO, &readfds)){
char buf[BUF_LEN+1];
int len;
len =read(STDIN_FILENO, buf, BUF_LEN);
if(len == -1){
perror("read");
return 1;
}
if(len ){
buf[len] = '\0';
printf("read:%s\n",buf);
}
return 0;
}
fprintf(stderr,"this should not happen!\n");
return 1;
}
自定义sleep(): 采用微秒级别select 作为睡眠机制
struct timeval tv;
tv.tv_sec=0;
tv.tv_usec=500;
select(0,NULL,NULL,NULL,&tv);
pselect():POSIX 自定义。
#define X_OPEN_SOURCE 600
#include <sys/select.h>
int pselect(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, const struct timespec *timeout,
const sigset_t *sigmask);
void FD_CLR(int fd, fd_set *set);
int FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);
pselect() 和select() 的不同:
1.pselect 的timeout 采用timespec 结构,而不是timeval 结构。更精确。
2.pselect() 调用不修改timeout 参数。
3.select()调用没有 sigmask 参数,当sigmask =0 时,pselect=select.
#include <sys/time.h>
struct timespec{
long tv_sec;
long tv_nsec;
}
poll():
#include <sys/poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
每个pollfd结构体监视单一的文件描述符。可以传递多个文件描述符,使得poll 监视多个文件描述符。
每个结构体的 events 字段是要监视的文件描述符事件的一组位掩码。
revents字段则是发生在该文件描述符上的事件的位掩码。
例:
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define TIMEOUT 5
int main(int ac, char *av[])
{
struct pollfd fds[2];
int ret;
fds[0].fd=STDIN_FILENO;
fds[0].events=POLLIN;
fds[1].fd=STDOUT_FILENO;
fds[1].events=POLLOUT;
ret=poll(fds,2,TIMEOUT*1000);
if(ret == -1){
perror("poll");
return 1;
}
if(!ret){
printf("%d seconds elapsed",TIMEOUT);
return 0;
}
if(fds[0].revents & POLLIN)
printf("stdin is readable\n");
if(fds[1].revents & POLLOUT)
printf("stdout is writeable\n");
return 0;
}
在一个应用中使用了poll,无需在每次调用时重新构建pollfd结构。相同的结构可能会被反复传递;必要时内核会把revents 字段清空。
ppoll(): Linux 专门的调用
#define _GNU_SOURCE
#include <poll.h>
int ppoll(struct pollfd *fds, nfds_t nfds,
const struct timespec *timeout, const sigset_t *sigmask);
poll和select的性能比较:poll 性能较为优越
1.poll() 无需使用计算最大的文件描述符+1和 传递该参数
2.poll() 在应对较大值的文件描述符时更具有效率。select 监视多个文件描述符,内核需要检查每个集合中的每个比特位。
3.select() 的文件描述符集合是竟态的, *poll() 可以创建合适大小的数组,仅需要监视一项或仅仅传递一个结构体。
4.select() 文件描述符集合会在返回时重新创建,这样之后每个调用都必须重新初始化它们。poll() 分离了输入和输出,数组无需改变即可使用
5.select() 的timeout 参数在返回时是未定义的,可移植代码需要重新初始化。poll() 没有这个问题。
select() 的优点:
1.select() 的使用范围更广,部分linux 不支持poll()。
2.select() 提供更好的超时方案。
12.内核内幕:如何实现I/O的,集中关注三个内核子系统:
1.虚拟文件系统(VFS)。
2.页缓存
3.页回写。
虚拟文件系统(VFS):Linux内核的文件操作的抽象机制。内核无需了解文件系统类型的情况下,使用文件系统函数和操作文件系统数据
通用文件模型:基于函数指针和各种面向对象方法*
页缓存:一种在内存中保存最近使用在磁盘文件系统上访问过的数据的方式。
时间局部性,空间局部性。
内核寻找文件系统数据的第一目的地。
内核管理预读。
页回写:内核使用缓冲区来延迟写操作。
触发回写条件:
1.当空闲内存小于设定的阈值,脏的缓冲区就会回写到磁盘上
2.当脏的缓冲区寿命超过设定的阈值时,缓冲区被回写到磁盘上。
pidflush的内核线程操作。
缓冲区在内核中使用buffer_head 结构表示。