文件I/O
在系统调用这个级别上,文件用一个非负整数来表示,被称为文件描述符。 文件描述符用来标识一个打开状态的“文件”,这里的文件是广义的,包含了: 管道、FIFO、socket、终端、设备、以及通常意义上的文件。
shell为每个进程打开了3个文件描述符:0、1、2 默认代表了终端。
上面这些广义文件的共同操作有这样四个: open、write、read、close
对于某些特定的设备的特殊操作:可以用万能的 ioctl()
open()
int open(const char* pathname, int flags, mode_t mode)
open() 返回的文件描述符必须是最小可用的。(比如0号描述符是可用的,则将0号分配出去)
open()有三个参数,其中第三个参数为视情况可选的。 第一个参数为文件路径,当路径代表符号链接时,会自动将其指向的真实文件打开,而不是符号链接本身。 第二个参数flags描述了使用此文件的方式, 第三个参数是在第二个参数中包含O_CREAT 时负责限制所新建文件的权限的,是一个以0开头的八进制数,通常设为0600、0666等
flags的介绍
-
关于读写方式 共有三个O_WRONLY、O_RDONLY、O_RDWR 这三个分别代表 只写、只读、可读可写。 并且这三个只能使用一个 O_TRUNC: 若文件已有内容则清空 O_APPEND :总是在文件末尾写入 -
和新建文件相关 O_CREAT 用于表示创建一个新文件,并用第三个参数mode_t限制新文件的权限 O_EXCL :和O_CREAT结合使用,只要文件存在就不打开,并使得open调用失败( 防止了用符号链接打开时在别处创建新文件 ) -
与缓冲相关的设置 O_SYNC O_DSYNC O_DIRECT -
避免打开时阻塞 O_NONBLOCK -
准备好IO后给进程发送信号 O_ASYNC:在Linux下不能用open设置该标记,要使用fcntl -
禁止open一符号链接 O_NOFOLLOW -
必须打开一个目录 O_DIRECTORY -
读取后不更新最后访问时间 st_atime O_NOATIME
read()
最多从文件中读取count个字节到缓冲区buffer中。
int read(int fd,void* buffer,size_t count);
正常读取时,返回实际读取到的字节数 读取结束:返回0
读取到的字节数可能会小于count指示的字节数,比如: 快读到文件末尾时、socket、管道、终端等
当从终端处read时,只要一遇到换行符,read()就会立即终止读取
write()
最多将缓冲区buffer中的count个字节写入到文件fd
int write(int fd,void* buffer,size_t count);
调用成功时,返回实际写入的字节数 write()成功返回并不代表数据百分百写入磁盘,因为在内核中有缓冲区负责暂存写出的数据
close()
int close(int fd);
与大部分系统调用相同,失败时会返回-1; 一般来说无需关心是否调用成功,但以防万一程序重复关闭同一文件描述符或关闭一个未打开的文件描述符
lseek() 移动文件偏移量
何为文件偏移量? 对当前打开文件进行write() read()时文件的位置,每次write、read调用后都会改变文件偏移量,二者是共用同一个文件偏移量的。
off_t lseek(int fd ,off_t offset,int whence);
whence:起始地址 SEEK_SET SEEK_CUR SEEK_END offset:距离起始地址的偏移位置
返回:返回所设置的位置(绝对偏移量)
sun@salted:~/src/c$ ./testlseek
0
5
10
sun@salted:~/src/c$ cat test.
test.c test.md test.txt
sun@salted:~/src/c$ cat test.txt
hellohello
#define BUF_SIZE 5
void printoffset(int fd){
printf("%lld \n",(long long) lseek(fd,0,SEEK_CUR));
}
int main(int argc,char** argv){
int fd ,ret;
char* buf = alloca(BUF_SIZE);
fd = open("./test.txt",O_RDWR|O_CREAT,0600);
printoffset(fd);
ret = read(fd,buf,BUF_SIZE);
if(ret ==-1){
perror("read()");
exit(1);
}
printoffset(fd);
ret = write(fd,buf,BUF_SIZE);
if(ret==-1){
perror("write()");
exit(1);
}
printoffset(fd);
close(fd);
}
文件空洞
当前打开文件的末尾后 到 后面真正写入数据的位置 之间的这块地方
文件空洞不占有实际大小磁盘空间,也就是说它很小。只有当真正向文件空洞处写入数据时才会为其分配真正大小的磁盘空间。 因此空洞文件表面上显示的大小 < < 实际占据的磁盘空间大小 核心转储文件就是典型的空洞文件。
制造文件空洞
lseek(fd,HOLE_SIZE,SEEK_END);
write(fd,"\0",1);
//这样就制造出了大小为HOLE_SIZE的空洞
原子操作
在执行过程中不会被其他线程或进程打断的操作 所有系统调用都是原子操作
竞争状态
将操作做成原子操作的重要理由是为了避免竞争状态 竞争状态指的是两个线程对同一共享资源进行操作,并且其运行结果取决于哪个先被调度运行(调度顺序)
open()操作的flags参数所提供的某些功能并不仅仅是为了简化操作而是为了将多个操作合并为一个具有原子性的系统调用:
例1:以独占方式打开文件(有则失败,无则创建)
比如:O_CREAT 无则创建 O_EXCL 有则报错
一种方式是:通过只写方式open文件,看其是否失败,失败则代表了文件不存在;而后再用open第二次打开文件,只是这次使用的是O_CREAT 但这种方案的弊端在于:在两次open()调用之间完全可能出现当前进程时间片耗尽,控制权被交给另一个尝试独占该文件的程序,当该进程第一次open后发现文件不存在,进而准备创建此文件来独占他,但这显然与第一个进程是矛盾的。
例2:多个进程向同一文件追加写
非原子的做法是先调用lseek移动到文件末尾,再调用write进行写入。其中的问题显然在于两次操作之间显然可能被打断,从而让另一进程改变了文件长度。 正确的做法是 加入 O _ APPEND使得文件位置指针移到末尾与写入成为一个原子操作。
fcntl() 对一个已经打开的文件增加某些设置选项
int fcntl(int fd,int cmd,
fcntl()获取文件的状态选项&文件的访问模式
int flags = fcntl(fd,F_GETFL);
对于访问方式之外的标志:可以用&操作直接判断: if(flags& O_SYNC){ … }
对于文件访问模式,需要先从flags提取出访问模式字段: flags & O_ACCMODE 再判断该字段是否等于 O_WRONLY、O_RDONLY、O_RDWR eg: if( flags&O_ACCMODE ==O_RDWR)
fcntl设置某些flags
通过
fcntl(fd,F_SETFL,flags);
仅能修改: O_APPEND、O_NONBLOCK、O_ATIME、O_ASYNC、O_DIRECT
何时需要修改文件描述符的flags?
- 文件fd不是由当前线程、函数…打开的,即不是当前程序调用的open
- 除了open之外的系统调用打开的文件描述符
文件描述符与已打开文件之间的关系
显而易见,多个文件描述符可以指代同一已打开文件。 更具体的来讲,需要说明这三个数据结构之间的关系:
进程级的文件描述符表 文件描述符 |close_on_exec| 文件指针
系统级的已打开文件表(每个表项为一句柄,对应着一次open() ) 文件偏移量|状态flags| iNode指针(inode表索引)
文件系统中的iNode表 被句柄引用数 类型 文件锁 属性 权限 …
- 多个文件描述符指向同一句柄:通过dup() fork() … 共享状态flags和文件偏移指针
- 多个句柄指向同一iNode表项,通过多次open()同一文件
- 文件描述符私有close_on_exec属性
通过复制dup描述符表项的方式分配新fd
int dup(int old);
通过复制已有fd的文件描述符表项来分配一新的fd。 新的fd的分配原则:总是将最小可用的fd分配出去。
有时候我们希望将已经有的某fd复制到一个新的fd上:
int dup2(int oldfd,int newfd); 返回 新设置的fd
其行为是 若newfd已经打开,则会将其先关闭,然后将oldfd对应的文件描述符表项复制到newfd对应的位置
另外,在进程的文件描述符表中提到过,每个fd都附带一个属性 close on exec (调用exec时关闭文件) 默认情况下不设置此标记。
复制出一个新描述符时也可以借助dup3()来指定该属性:
int dup3(int oldfd,int newfd,int flags);
flags可为O_CLOEXEC
最后,使用万能的fcntl()也能实现 通过复制已有fd的方式创建新的fd
newfd = fcntl(oldfd, F_DUPFD, startfd);
表示将从startfd开始寻找最小的可用fd
并且也可以设置close on exec属性:
newfd = fcntl(oldfd, F_DUPFD_CLOEXEC ,newfd);
pread()&pwrite() :不改变文件偏移的写入和读取
offset指定了要读取或写入的位置
ssize_t pread(int fd, void* buf,size_t count ,off_t offset);
返回实际读取或写入字节数(结尾时返回0)
这一系统调用实际上是将lseek与read、write合并成了一个原子操作,适用于多线程场景。
分散读写
缓冲区对象
struct iovec{
void* iov_base; //缓冲区起始地址
size_t iov_len; //缓冲区长度
}
readv()&write()
iov为缓冲区数组,iovcnt为缓冲区个数
ssize_t readv(int fd , const struct iovec* iov,int iovcnt);
按缓冲区顺序将文件中的数据读入到各个缓冲区中。并且修改文件偏移指针
ssize_t writev(int fd, const struct iovec* iov, int iovcnt);
按顺序将各个缓冲区中的数据写入到文件,并且修改文件偏移指针。
返回实际读取或写入的字节数。
preadv() pwritev()
不改变文件偏移量的分散读写
ssize_t preadv(int fd,const struct* iovec,int iovent, off_t offset);
ssize_t pwritev(int fd ,const struct* iovect ,int iovent, off_t offset);
改变文件长度
int truncate(const char* pathname,off_t length);
int ftruncate(int fd, off_t length);
若当前文件长度> 新长度:将文件截断,丢弃多出的部分。 若当前文件长度 < 新长度:新增的部分将会被\0填充,即在末尾形成空洞
非阻塞IO :要么成功 要么立即报错
对于能用open()打开的文件类型,可以用O_NONBLOCK标记来启用非阻塞IO 对于像socket、管道这样的不是由open()打开的文件,可以用fcntl F_SETFL 来附加非阻塞标志。
非阻塞性质体现在两个方面:
- open打开文件时,只要没立即打开成功就会报错,而不是陷入阻塞
- 对带非阻塞标记的文件进行IO时,要么只读写部分数据,要么直接报错。绝对不会被卡住
长文件的打开与IO -D_FILE_OFFSET_BITS 加上编译选项后可自动将 open off_t lseek 等替换成64位版本 输出 off_t 要先用(long long)进行强转,然后用%lld进行输出
/dev/fd
每个进程都有一个虚拟的目录,/dev/fd/,这里显示了该进程所打开的文件描述符
创建临时文件
返回文件描述符fd
int mkstemp(char* tempname);
tempname 必须以6个X结尾,会被系统自动填充为随机字符串
使用方式: mkstemp后立即进行unlink,使得该文件名“消失”,最后close(fd)实现文件的真正删除
返回文件流 FILE*
FILE* tmpfile( void );
记得调用fclose() 来实现临时文件的真正删除。
|