文件IO

文件描述符:在对文件进行读写操作之前,需要先打开文件。内核会为每个进程维护一个打开文件的列表,该列表称为文件表。文件表由一些非负整数进行索引,这些索引称之为文件描述符。列表的每一项是一个打开文件的信息,包括指向该文件索引节点内存拷贝的指针以及关联的元数据,如文件位置指针和访问模式。

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值。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值