文件描述符:在对文件进行读写操作之前,需要先打开文件。内核会为每个进程维护一个打开文件的列表,该列表称为文件表。文件表由一些非负整数进行索引,这些索引称之为文件描述符。列表的每一项是一个打开文件的信息,包括指向该文件索引节点内存拷贝的指针以及关联的元数据,如文件位置指针和访问模式。
3个通用的进程描述符(LinuxC标准库提供的三个宏,值分别为0,1,2),STDIN_FILENO(标准输入,通常指的是用户键盘),STDOUT_FILENO(标准输出),STDERR_FILENO(标准错误)。
1. 打开文件
1.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);
参数:name代表所创建文件的名称,flags表示文件的打开方式,mode代表权限位。
返回值:执行成功,返回文件描述符,失败则返回-1,并设置errno。
flags参数支持三种访问模式:O_RDONLY(只读)、O_WRONLY(只写)、O_RDWR(读写)。该参数还能和一些别的值进行位或运算,修改打开文件的行为。接下来只列举一部分比较常见的值。
-
O_APPEND
文件以追加模式打开。在每次写操作之前,讲文件位置指针指向文件末尾。 -
O_CLOEXEC
在执行新的进程时,会关闭对应的文件描述符,避免出现竞争。 -
O_CREAT
当参数name指定的文件不存在时,内核自动创建。如果文件已存在,指定了O_EXCL时会返回-1。 -
O_EXCL
当和O_CREAT一起使用时,如果参数name指定的文件已经存在,会导致open()调用失败,防止创建文件时出现竞争。 -
O_TRUNC
如果文件存在,并且为普通文件,具有写权限,该标志位会把文件长度阶段为0.对于FIFO或终端设备,该标志位无效,对于其他文件类型,行为未定义。如果普通文件不具有写权限,指定该标志位的行为也是未定义的。
参数mode在创建文件时,提供了新建文件的权限。但是,实际上文件最后的权限是由文件创建掩码取反和参数mode按位与操作决定的(mode & ~umask(文件创建掩码))。用umask命令可以看到当前的文件创建掩码。
举个例子:
int fd = open(name, O_WRONLY | O_CREAT | O_TRUNC, 0664);
如果创建文件成功,该文件的权限会是0644。(0664 & (~0022) == 0644)
2 通过read()读文件
2.1 read函数介绍
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t len);
参数:fd代表要读取数据文件的文件描述符,buf保存读取的这些数据,len表示最多读取的字节数。
返回值:>0表示读取的字节数。
0表示EOF,没有更多可读的数据。
-1,读取失败,判断errno。errno为EINTR,表示读取字节之前收到信号,调用可以重新执行。
为EAGAIN,表示当前没有数据可用,这种情况只有在非阻塞模式下发生。为其他的值,表示更
严重的错误,重新执行读操作也不会成功。
由于调用read()会出现多种情况,要想能够处理所有错误,我们需要加个循环和一些条件语句
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;
}
2.2 非阻塞读
支持非阻塞模式执行I/O操作,因此需要额外检查errno是否为EAGAIN。当文件描述符以非阻塞模式打开时(oepn()调用指定参数为O_NONBLOCK),并且没有数据可读时,read()调用会返回-1,并设置errno值为EAGAIN,而不是阻塞模式。当以非阻塞模式读文件时,必须检查EAGAIN,不然可能因为丢失数据导致严重错误。你可能会需要下面的判断代码。
char buf[BUFSIZ] = "";
ssize_t nr;
start:
nr = read(fd, buf, BUFSIZ);
if (nr == -1)
{
if (errno == EINTR)
{
goto start;
}
if (errno == EAGAIN)
{
/* resubmit later*/
}
else
{
/*error*/
}
}
2.3 read()调用的大小限制
size_t和ssize_t是由POSIX决定的。前者保存字节大小,后者是有富豪的size_t。在32位操作系统中,对应的C类型通常是unsigned int 和 int。size_t的最大值是SIZE_MAX,ssize_t的最大值是SSIZE_MAX。如果len值大于SSIZE_MAX,read()调用的结果则是未定义的。在大多数操作系统上,SSIZE_MAX的值是LONG_MAX,在32位系统上的这个值是2147483647。这个数虽然已经很大了,但还是需要留心它。我们有时可能就需要加下面的代码判断一下了。
if (len > SSIZE_MAX)
{
len = SSIZE_MAX;
}
最后提一句,调用read()时,如果len参数为0,会返回0。
3 调用write()写
3.1 了解write()的用法
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
将buf中之多count个字节写入到文件中。
返回值:成功时返回写入的字节数,并更新文件位置。出错时返回-1,并设置errno值。
返回0表示写入了零个自己,并没有其他的意义。
const char *buf = "hello Linux";
ssize_t nr;
nr = write(fd, buf, strlen(buf));
inf (nr == -1)
{
/* error */
}
3.2 部分写
对于普通文件来说,一般不需要执行循环写操作,一次就能写入成功。但是对于有的类型文件来说,比如socket,需要循环保证写了所有请求的字节。一下是write()调用示例代码。
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;
}
3.3 Append模式
当以Append模式(参数设置为O_APPEND)打开文件描述符时,文件位置指针总是指向文件末尾,保证了多个写进程操作时,依然是追加写。
3.4 一些值得注意的错误码
-
EBADF
给定的文件描述符非法或不是以写方式打开。 -
EFAULT
buf指针指向的位置不在进程的地址空间内。 -
EFBIG
写操作将使文件大小超过进程的最大文件限制或内部设置的限制。 -
EINVAL
给定文件描述符指向的对象不支持写操作。 -
EIO
底层I/O错误。 -
ENOSPC
给定文件描述符所在的文件系统没有足够的空间。
3.5 write()大小限制
如果count值大于SSIZE_MAX,调用write()的结果是未定义的。调用write()时,如果count值为0,会立即返回,且返回值为0。
4 lseek()函数
有些应用需要跳跃式的读取文件,此时采取的是随机访问而不是先行访问。该函数能够把文件位置指针设置成指定值。lseek()只更新文件位置,并不执行其它操作。
#include <sys/stat.h>
#include <unistd.h>
off_t lseek(int fd, off_t pos, int origin);
调用成功返回从头开始的偏移值,失败返回-1,并设置相应的errno
该函数的行为主要取决于origin这个参数。
-
SEEK_CUR
将文件位置设置成当前值再加上pos个偏移值。 -
SEEK_END
将文件位置设置成文件长度再加上pos个偏移值。 -
SEEK_SET
将文件位置设置成pos值。
lseek()调用错误返回errno值
- EBADF: 给定的文件描述符没有指向任何打开的文件描述符
- EINVAL:origin不是设置成SEEK_SET、SEEK_CUR、SEEK_END,或者结果文件是负值。
- EOVERFLOW:结果文件偏移不能通过off_t表示。只有在32位的体系结构上才会发生这种错误,当前文件位置已经更新,但是该错误表示无法返回更新的值。
- ESPIPE:给出的文件描述符和不支持查找操作的对象关联,比如管道、FIFO或者socket等。
5 定位读写
Linux提供了两种可以替lseek()改变文件位置之后再进行读写操作的函数。分别为pread()和pwrite(),但是这两个函数调用完成后,它们不会更新文件位置指针(注意这点)。
#define _XOPEN_SOURCE 500
#include <unistd.h>
ssize_t pread(int fd, void *buf, size_t count, off_t pos);
该函数会从文件描述符fd的pos位置开始读取,共读去count个字节到buf中。失败返回-1,并设置相应的
errno值。成功返回从0开始的偏移位置。
ssize_t pwrite(int fd, const void *buf, size_t count, off_t pos);
该函数共文件描述符fd的pos位置开始,从buf中写到count字节到文件中。失败返回-1,并设置相应的
errno值。成功返回从0开始的偏移位置。
pread()和pwrite()调用和在read()或者write()调用之前执行lseek()调用的区别:
- pread()和pwrite()调用更易于使用,尤其是对于一些复杂的操作,比如在文件中反向或者随即查找定位。
- pread()和pwrite()在结束时不会修改文件位置指针。
- pread()和pwrite()调用避免了在使用lseek()时会出现的竞争。由于线程共享文件表,而当前文件位置保存在共享文件表中,可能会导致在多个线程情况下,有个进程调用lseek()之后,在执行读写操作之前,另外一个线程更新了文件位置,也就是说会引起文件位置指针的竞争。
6 文件截短
#include <unistd.h>
#include <sys/types.h>
int ftruncate(int fd, off_t len);
int truncate(const char *path, off_t len);
两个系统调用都将给定文件截短为参数len指定的长度。前者是在以可写方式打开的文件描述符上面操作。
后者是在路径指定的文件下进行操作。
返回时:成功为0,失败为-1并且设置相应errno值。