标签:
文件概念对于Linux系统的重要性不言而喻,本章主要介绍了内核为文件的创建、读、写、定位等系统调用以及高效的I/O机制。Linux系统为文件操作提供了通用的系列系统调用,使开发人员能够对所有“文件”做相同的操作,同时还提供了ioctl()
系统调用对非通用文件操作,fcntl()
系统调用对文件描述符做操作。
此外Linux内核为了弥补CPU运算速度和磁盘I/O速度的巨大差异,引入了页缓存、页回写机制。简单来说就是读取文件时多读取一部分并保存在内核缓冲区中,在下次收到读取请求时直接将内存中的数据返回;写入文件时只是在内核缓冲区中保存要写入的数据,在合适的时机提交给磁盘(这样在写入之后的读取请求是从内核缓冲区中获取数据的,多个写入请求合并为一次磁盘操作能够提高效率)。当然,内核也提供了同步I/O机制来确保数据立即被写入磁盘;直接I/O机制来确保数据不经由内核缓存,直接提交给磁盘。
文件描述符与偏移量
Linux系统调用一般通过文件描述符来指定对哪个文件进行操作,文件描述符与文件直接的关系在系统中由三个不同层次的表来维护。
* 进程级的文件描述符表(open file descriptor)
系统为每个进程维护了一个表,该表中每条记录保存了控制文件描述符操作的一组标志(目前仅有一个close-on-exec标志——使用exec()系列函数加载程序时自动关闭)和打开的文件句柄的引用
* 系统级的打开文件表(open file table/open file descriptor)
每个打开的文件都在该表中有一条对应数据,表中每条记录称为文件句柄(open file handle),一个文件句柄对应了一个打开的文件的全部信息,包括:当前文件偏移量;打开文件时使用的标志(open()的flags参数);文件访问模式(读写模式);与信号驱动I/O相关的设置;inode节点的引用。
* 文件系统级的inode表
inode表被文件系统保存,任何文件系统中的文件都拥有inode信息,一个inode节点包括:文件类型和访问权限;指向文件持有的锁的指针;文件的各种类型例如大小、时间戳等。
由此可见,文件描述符与打开的文件之间有如下关系:
1 文件的偏移量和文件句柄相关,和文件描述符无关,同一个文件句柄可能被不同的文件描述符或进程共享。每次调用open()
时内核都会分配一个文件句柄。
2 同一进程的不同文件描述符指向同一个文件句柄。这种情况可能是dup()
、dup()2
、fcntl()
等系统调用或重定向>
、>>
导致的,尽管文件描述符值不同,但其指向的句柄是同一个。此时不同文件描述符的文件偏移量是通用的,因为文件句柄是同一个。
3 不同进程的相同文件描述符指向同一个文件句柄。这种情况可能是打开文件描述符后调用了fork()
函数,此时文件描述符的文件偏移量是通用的,因为文件句柄是同一个(见2.1的例子)。
4 不同进程的不同文件描述符指向同一个文件句柄,这种情况是2和1的顺序组合的结果,此时文件描述符的文件偏移量也是通用的。
5 同一个进程的文件描述符指向同一个文件(例如多次open()),这种情况下尽管一个进程多次打开同一个文件,资源描述符不相同,但是文件句柄也不相同,其文件偏移量必然不同:
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
#include <iostream>
using namespace std;
int main ()
{
//test.txt存在,内容为
//1234567890
int fd1 = open("test.txt", O_RDWR);
int fd2 = open("test.txt", O_RDWR);
cout << "fd1: " << fld1 << " fld2: " << fld2 << endl;//fld1:3 fld2:4
char arr1[2] = {0};
char arr2[2] = {0};
read(fld1, arr1, 1);//简单起见,不再错误处理
read(fld2, arr2, 1);
cout << "arr1: " << arr1 << endl;//arr1:1,说明是从文件头开始读取的
cout << "arr2: " << arr2 << endl;//arr2:1,说明fd2的偏移量不与fd1共享
return 0;
}
/dev/fd目录
内核为每个进程提供了一个/dev/fd/n
的虚拟路径,链接到对应该进程打开的文件描述符,例如对每个进程来说,/dev/fd/0
代表标准输入。在进程中打开该目录,相当于复制了一个文件描述符,其指向同一个文件句柄。fd=open("/dev/fd/1")
等于fd=dup(1)
(dup()的作用见【重定向与复制文件描述符】)。在这种情况下open()传递的标准应该与对应的文件描述符一样。
此外系统还提供了/dev/stdin
、/dev/stdout
、/dev/stderr
三个文件来方便引用。
一个例子来演示如何操作该目录下的文件:
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
#include <iostream>
using namespace std;
int main ()
{
//从标准输入读取信息并输出到文件test.txt中
//简单起见不做错误处理
int fd1 = open("/dev/stdin", O_RDONLY);
int fd2 = open("test.txt", O_WRONLY|O_CREAT);
char buf[1024];
int iCount = read(fd1, buf, 1024);
if(iCount != -1)
write(fd2, buf, iCount);
return 0;
}
临时文件
临时文件是进程关闭时自动删除的文件,glibc提供了类似
#include <stdlib.h>
int mkstemp(char* template);
FILE* tempfile();
的函数来帮助建立临时文件。
* mkstemp
template参数是一个字符数组,表示文件的模板,后六个字符必须是”X”,库函数会修改该值并在成功时返回文件描述符。例如char tempname[]="/temp/fileXXXXXX"
,在成功后会创建tempname的值会被修改并创建同名文件,进程所有者对其有读写权限。一般程序不再使用该临时文件时可以使用unlink()
系统调用解除对该文件的引用,稍后在close()
时该文件由于引用数为0,会被自动删除。当然,如果不显示close()的话,在进程结束时系统自动调用close()。
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
int main ()
{
char tempFile[]="/root/cpproot/test/tempfileXXXXXX";
int fd = mkstemp(tempFile);
if(fd == -1)
{
perror("mkstemp");
return -1;
}
unlink(tempFile);
char temp[32] = "hi?";
write(fd, temp, sizeof(temp));
lseek(fd, 0, SEEK_SET);
char temp2[32];
read(fd, temp2, sizeof(temp2));
printf("read : %s\n", temp2);
return 0;
}
unlink()系统调用作用是解除进程对文件描述符的引用(记得吗,引用计数记录在inode节点中),若在close(fd)时fd对应的文件引用数为0,系统会删除该文件。在对文件进行操作时,unlink()行为与remove()行为一样。
在对文件操作前需要打开文件,内核为每个进程维护了一个打开文件的列表。在打开文件后创建子进程,子进程会共享父进程打开的文件、当前文件位置等信息,子进程关闭文件不会对父进程有影响。
以下代码验证不同的进程中文件描述符是独立的:
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
int main ()
{
pid_t fpid;
fpid=fork();
if (fpid < 0)
printf("error in fork!");
//父进程返回子进程pid,子进程返回0
else if (fpid == 0) {
int fd1 = open("test.txt", O_RDWR|O_CREAT );
printf("child process fd %d\n",fd1 ); //3
}
else {
int fd1 = open("test.txt", O_RDWR|O_CREAT );
printf("parent process fd %d\n",fd1 ); //3
}
return 0;
}
可以看出对于不同的进程而言,每个进程的文件描述符都是私有的,内核会为每个进程重新分配文件描述符。
以下代码验证子进程共享父进程文件描述符和文件当前位置:
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
int main ()
{
pid_t fpid;
int fd1 = open("test.txt", O_RDWR|O_CREAT );
fpid=fork();
if (fpid < 0)
printf("error in fork!");
//父进程返回子进程pid,子进程返回0
else if (fpid == 0)
{
write(fd1, "123", 3);
}
else
{
write(fd1, "abc", 3);
}
return 0; //文件最后是6个字符,如果父子进程分别向0位置写的话,会有数据被覆盖
}
可以看出在fork()之后,子进程会共享父进程的文件描述符和文件位置(即共享同一个文件句柄),相对的测试如下:
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
int main ()
{
pid_t fpid;
fpid=fork();
if (fpid < 0)
printf("error in fork!");
//父进程返回子进程pid,子进程返回0
else if (fpid == 0)
{
int fd1 = open("test.txt", O_RDWR|O_CREAT );
write(fd1, "123", 3);
}
else
{
int fd1 = open("test.txt", O_RDWR|O_CREAT );
write(fd1, "abc", 3);
}
return 0; //文件最后是3个字符,因为父子进程都是从文件头开始写的,有一个进程写的内容会被另一个覆盖
}
系统调用open()打开文件并返回系统分配的文件描述符,错误时返回-1并设置errno,错误码取值可以通过man 2 open
查看。
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode );
pathname-文件路径
flags-读写模式,有O_RDONLY、O_WRONLY、O_RDWR三种,同时可以和以下至少一个选项做或运算:
取值 | 说明 |
---|---|
O_APPEND | 每次写操作都写入文件的末尾,该标志保证了写文件的原子性,避免了多个进程写入同一个文件时内容被覆盖 |
O_ASYNC | 文件可读或可写时产生一个信号(默认SIGIO),只能用于终端和套接字文件上,普通文件无法使用 |
O_CREAT | 如果指定文件不存在,则创建这个文件 |
O_DIRECT | 打开的文件用于直接I/O |
O_DIRECTORY | 如果打开的文件不是目录则返回错误,用于opendir()内部使用 |
O_LARGEFILE | 打开文件时使用64位偏移量,这样大于2G的文件也能打开,64位架构默认就有该标志 |
O_EXCL | 如果要创建的文件已存在,则返回 -1,并且修改 errno 的值。该标识保证了创建文件的原子性,避免多个进程同时创建文件后都认为文件是自己创建的 |
O_TRUNC | 如果文件存在,并且以只写/读写方式打开,则清空文件全部内容 |
O_NOCTTY | 如果路径名指向终端设备,不要把这个设备用作控制终端 |
O_NOFOLLOW | 如果文件是一个符号链接,则open()会失败 |
O_NONBLOCK | 如果路径名指向 FIFO/块文件/字符文件,则把文件的打开和后继 I/O设置为非阻塞模式(nonblocking mode) |
O_DSYNC | 等待物理 I/O 结束后再 write。在不影响读取新写入的数据的前提下,不等待文件属性更新 |
O_RSYNC | read 等待所有写入同一区域的写操作完成后再进行 |
O_SYNC | 等待物理 I/O 结束后再 write,包括更新文件属性的 I/O |
O_CLOEXEC | 内核2.6.23开始支持,指定在子进程执行exec()系列函数时自动关闭文件描述符,该参数出现之前使用fcntl()实现相同功能 |
O_NOATIME | 内核2.6.8开始支持,使用该标志时,读取文件不会更新inode节点中的最新访问时间,减少读写的数据量 |
mode-新创建文件时用于指定权限,在创建文件时不指定权限会产生未定义的行为。权限在第一次创建文件时不会检查,因此第一次打开文件时权限是无效的。POSIX规定了以下参数可以用或运算同时指定:
取值 | 说明 |
---|---|
S_IRWX(U/G/O) | 所有者、所有组、其他用户有读写执行权限 |
S_IX(USR/GRP/OTH) | 所有者、所有组、其他用户有执行权限 |
S_IR(USR/GRP/OTH) | 所有者、所有组、其他用户有可读权限 |
S_IW(USR/GRP/OTH) | 所有者、所有组、其他用户有可写权限 |
由于O_WRONLY|O_CREAT|O_TRUNC组合非常常见,有专门的函数来实现相同功能(某些架构上可能没有该系统调用,使用的是库函数):
#include <fcntl.h>
int creat(const char* name, mode_t mode);
返回值与open()一样。
read()是系统调用,用于从fd代表的文件中读取最多len个字节到buf中,正确时返回读取的字节数,失败时返回-1并设置errno,只读到一个文件结束符(EOF)时返回0。
#include <unistd.h>
ssize_t read (int fd, void *buf, size_t len);
每次read()成功后内核保存的文件位置指针会向前移动len个字节长度。这里的位置是内核记录的位置,与库调用时的位置不一样——库函数例如fgetc()
,可能每次从内核读取1024个字符,但是每次只返回一个字符,此时内核的位置应该是1024而不是1。
以下代码验证对于一个文件描述符,不同进程共享同一个文件位置:
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
int main ()
{
pid_t fpid;
//假设test.txt存在且内容为"abcd",父子进程读取出来的是a和b而不是a和a
int fd1 = open("test.txt", O_RDWR );
fpid=fork();
if (fpid < 0)
printf("error in fork!");
//父进程返回子进程pid,子进程返回0
else if (fpid == 0)
{
char temp[1024] = {0};
read(fd1, temp, 1);
printf("child read:%s", temp);
}
else
{
char temp[1024] = {0};
read(fd1, temp, 1);
printf("parent read:%s", temp);
}
return 0;
}
当文件没有数据可读或未读完len个字节时(并且没读取到EOF,例如网络通信时)read()调用会阻塞,直到读满。
read()可能会有如下返回值对应的场景:
返回len,说明需要的数据长度全部读取到内存中
返回小于len,说明读取过程中有信号大端、读取时发生了错误、读取到EOF,再次读取时将剩余数据读到内存中
返回0,读到EOF
调用阻塞,等待可读的数据到来
调用返回-1,errno为EINTR,表示读取之前收到信号,可以再次调用
调用返回-1,errno为EAGAIN,表示没有可用数据,在非阻塞模式下发生
调用返回-1,errno为其他值,表示发生的其他严重错误
一个比较全面的代码是这样的:
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;
}
函数在读取到len个字节或遇到EOF之前会一直尝试从fd中读取,在出现中断时再次读取,在出现错误时打印错误信息并退出循环。
read()系统调用在等待数据时会阻塞,在有些情况下我们不希望等待数据时阻塞,而是做一些其他操作——例如有多个文件要读,阻塞在一个上面不如看看其他文件有没有数据(当然有更好的办法,见I/O多路复用);或者有大量数据要在后面的代码中处理,而且数据不依赖于从文件中读取的内容,此时可以使用非阻塞I/O模式。在没有数据可读时read()也立即返回-1,并将errno设置为EAGAIN。进入非阻塞I/O模式需要在open()时指定O_NONBLOCK参数。
POSIX规定了size_t和ssize_t类型表示占用内存的大小,ssize_t是有符号版的size_t,32位操作系统上是int,64位系统是long int。size_t最大值是SIZE_MAX,ssize_t则为SSIZE_MAX。若len大于SSZIE_MAX,read()调用结果是未定义的。
write()也是系统调用,表示从fd代表的当前位置读取最多count个字符到buf中。读取成功时返回真实的读取字符数,否则返回-1且设置errno。当count比SSIZE_MAX还大,调用的结果未定义。
#include <unistd.h>
ssize_t write (int fd, const void *buf, size_t count);
write()不太可能返回一个小于count的数,对于普通文件,发生部分写时很可能出现了错误。因此对于普通文件,出现部分写时不需要通过循环保证所有字符都写入文件。但对于socket等特殊文件来说,最好使用循环来保证全部写入文件。
ssize_t ret, nr;
while (len != 0 && (ret = write (fd, buf, len)) != 0)
{
if (ret == -1)
{
if (errno == EINTR)
continue;
perror (”write”);
break;
}
len -= ret;
buf += ret;
}
文件在open()时如果指定O_APPEND参数,则写操作每次都从文件末尾开始。例如有多个进程向同一个文件写入,追加模式保证了每个进程都不会覆盖其他进程写入的数据,因为系统能够保证每次写入都从尾部开始,对日志功能非常有用。
文件在open()时如果指定了O_NONBLOCK参数,则write()会正常阻塞时,调用会立即返回-1并设置errno为EAGAIN,普通文件不会出现这种情况。
write()操作会产生的错误码还有:
错误码 | 描述 |
---|---|
EBADF | 给定的fd非法或不是以写方式打开的 |
EFAULT | 给定的buf不在进程地址空间内 |
EFBIG | 写操作使文件超出进程最大文件限制 |
EINVAL | fd对应的对象无法进行写操作 |
EIO | 发生了一个底层I/O错误 |
ENOSPC | 文件系统没有足够空间 |
EPIPE | fd对应的管道或socket的读端被关闭,同时进程会受到SIGPIPE信号 |
由于硬盘I/O速度与CPU处理速度相差较大,在程序执行时等待数据真正写入硬盘对程序性能有较大影响,因此内核提供了内核缓冲区用于存放要写入硬盘的数据。write()操作在数据从用户空间拷贝到内核空间时即返回,内核会确保在某个合适的时机将数据写入硬盘。
这种延迟写入的方式不会改变交替读写的结果:当需要read()刚刚写入硬盘的数据时,若此时数据在内核缓冲区中还未写入硬盘,读取的将是缓冲区的数据,减少了读取硬盘的次数。
为了保证及时将输入写入硬盘,在/proc/sys/vm/dirty_expire_centiseconds
中配置了最大时效,内核会在超出最大时效之前将数据写入硬盘。内核缓冲区
系统变量类型是指一些系统实现细节相关的变量类型typedef成不暴露实现细节的变量类型,从而保证跨平台时源码级别的兼容性。例如对于进程id,其系统变量类型是pid_t
,常见的还有size_t
、socklen_t
等,在sys/types.h
中定义。
32位Linux上,文件偏移量类型off_t
类型为int,32位,即能够寻址的文件最大为2G。若想实现32位系统上对超过2G的文件寻址,需要将_FILE_OEFFSET_BISTS
宏设置为64,可以在包含其他头文件之前#define _FILE_OEFFSET_BISTS 64
或在makefile中添加-D_FILE_OEFFSET_BISTS=64
,编译时根据该条件会再次将off_t类型typedef成__off64_t
,这个变量是64位长度的。通过这个宏,在不修改源码的前提下将只支持最大2G操作的源码扩展成支持最大2^63-1大小。
除此之外还能使用过度型API来指明对大文件读写,包括open64()
、lseek64()
等。更推荐的是在32位Linux上使用_FILE_OEFFSET_BISTS宏。此外使用宏定义的方式对大文件做支持时要注意,所有与文件读写相关的模块在编译时都要使用该宏,避免类型不一致导致的问题。
64位Linux的off_t长度默认就是64位的,因此不需要定义上述的宏。
标签:
原文地址:http://blog.csdn.net/wylblq/article/details/51668946