unix环境高级编程第3章----文件I/O

本文介绍了文件I/O的基础知识,包括文件描述符的概念、文件打开与关闭的函数open、openat、creat、close,以及文件读写函数read和write。此外,还详细讲解了文件偏移量设置函数lseek,文件描述符复制函数dup和dup2,文件同步函数sync、fsync和fdatasync,以及文件属性修改函数fcntl和ioctl。文章深入探讨了文件共享、原子操作的重要性,并介绍了/dev/fd目录的使用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

https://blog.youkuaiyun.com/qq_41543888/article/details/91043922

目录

3.1引言

3.2文件描述符

3.3函数open 和 openat——打开或创建一个文件

3.4函数 creat——创建一个新文件

3.5函数 close——关闭一个打开文件

3.6函数 lseek——显式的为一个打开文件设置偏移量

3.7函数read——从打开文件中读数据

3.8函数write——向打开文件写数据

3.10文件共享

3.11原子操作

3.12函数dup 和 dup2——都可用来复制一个现有的文件描述符

3.13函数sync fsync 和 fdatasync

3.14函数 fcntl——改变已经打开文件的属性

3.15函数ioctl

3.16 /dev/fd

3.17小结


3.1引言

大多数文件I/O只需要5个函数:open、read、write、lseek以及close

不带缓冲的I/O(unbuffered i/o):每个read和write都调用内核中的一个系统调用。

在描述了这些特征后,将说明dup、fcntl、sync、fsync和ioctl函数。

3.2文件描述符

对于内核而言,所有打开的文件都通过文件描述符引用。文件描述符是一个非负整数。

当打开一个现有文件或创建一个新文件时,内核向进程返回一个文件描述符。当读、写一个文件时,使用open或creat返回的文件描述符标识该文件,将其作为参数传送给read或write。

按照惯例,unix系统shell把文件操作符0与进程的标准输入关联,1与标准输出关联,2与标准错误关联。

幻数0、1、2虽然已被标准化,但应当把它们替换成符号常量STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO以提高可读性。这些常量都在头文件<unistd.h>中定义。

3.3函数open 和 openat——打开或创建一个文件

#include<fcntl.h>
int open  (const char *path, int oflag , .../*mode_t mode*/);
int openat(int fd,const char *path,int oflag , .../*mode_tmode*/);
//返回值:成功返回文件描述符,出错返回-1

path参数是要打开或创建文件的名字。

oflag参数可用来说明此函数的多个选项。用下列一个或多个常量进行“或”运算构成oflag参数(这些常量在头文件<fcnt1.h>中定义)。

将最后一个参数写为...,表明余下的参数的数量及类型是可变的,对于open而言,仅当创建新文件时才使用最后这个参数

O_RDONLY 只读打开    大多数定义为 0

O_WRONLY 只写打开                          1

O_RDWR 读、写打开                           2

O_EXEC    只执行打开。

O_SEARCH   只搜索打开(应用于目录)。有的操作系统不支持,熟悉前三个就行。

在这5个常量中必须指定一个且只能指定一个。下列常量则是可选的。

O_APPEND   每次写时都追加到文件的尾端。

O_CLOEXEC  把FD_CLOEXEC常量设置为文件描述符标志。

O_CREAT 若文件不存在,则创建它。使用此选项时,open函数需同时说明第3个参数mode(openat函数需说明第4个参数mode),用mode指定该新文件的访问权限位。

O_DIRECTORY 如果path引用的不是目录,则出错。

O_EXCL 如果同时指定了O_CREAT,而文件已经存在,则出错。用此可以测试一个文件是否存在,如果不存在,则创建此文件,这使测试和创建两者成为一个原子操作。

O_NOCTTY  如果path引用的是终端设备,则不将该设备分配作为此进程的控制终端。

O_NOFOLLOW 如果path引用的是一个符号链接,则出错。

O_NONBLOCK 如果path引用的是一个FIFO、一个块特殊文件或一个字符特殊文件,则此选项为文件的本次打开操作和后续的I/O操作设置非阻塞方式。

O_SYNC 使每次write等待物理I/O操作完成,包括由该write操作引起的文件属性更新所需的I/O。

O_TRUNC 如果此文件存在,而且为只写或读-写成功打开,则将其长度截断为0。

O_TTY_INIT 如果打开一个还未打开的终端设备,设置非标准termios参数值,使其符合SingleUNIXSpecification。

O_DSYNC使每次write要等待物理I/O操作完成,但是如果该写操作并不影响读取刚写入的数据,则不需等待文件属性被更新。

O_RSYNC 使每一个以文件描述符作为参数进行的read操作等待,直至所有对文件同一部分挂起的写操作都完成。

由open和openat函数返回的文件描述符一定是最小的未用描述符数值。这一点被某些应用程序用来在标准输入、标准输出或标准错误上打开新的文件。

fd参数把open和openat函数区分开,共有3种可能性。

(1)path参数指定的是绝对路径名,在这种情况下,fd参数被忽略,openat函数就相当于open函数。

(2)path参数指定的是相对路径名,fd参数指出了相对路径名在文件系统中的开始地址。fd参数是通过打开相对路径名所在的目录来获取。

(3)path参数指定了相对路径名,fd参数具有特殊值AT_FDCWD。在这种情况下,路径名在当前工作目录中获取,openat函

数在操作上与open函数类似。

openat函数是POSIX.1最新版本中新增的一类函数之一,希望解决两个问题。第一,让线程可以使用相对路径名打开目录中的文件,而不再只能打开当前工作目录。在第11章我们会看到,同一进程中的所有线程共享相同的当前工作目录,因此很难让同一进程的多个不同线程在同一时间工作在不同的目录中。第二,可以避免time-of-check-to-time-of-use(TOCTTOU)错误。

TOCTTOU错误的基本思想是:如果有两个基于文件的函数调用,其中第二个调用依赖于第一个调用的结果,那么程序是脆弱的。因为两个调用并不是原子操作,在两个函数调用之间文件可能改变了,这样也就造成了第一个调用的结果就不再有效,使得程序最终的结果是错误 的。文件系统命名空间中的TOCTTOU错误通常处理的就是那些颠覆文件系统权限的小把戏,这些小把戏通过骗取特权程序降低特权文件的权限控制或者让特权文件打开一个安全漏洞等方式进行。

3.4函数 creat——创建一个新文件

#include<fcntl.h>
int creat(const char *path,mode_t mode);//返回值:成功返回为只写打开的文件描述符,出错返回-1
  • 等效于 open( path, O_WRONLY | O_CREAT | O_TRUNC, mode );
  • creat函数有一个不足,就是对于新创建的文件只能够进行写操作,若是你想对这个刚创建的文件进行读操作,就只能先close掉,然后再open进行读操作,非常繁琐,但是如果我们使用最新的open函数(对于不存在的文件,可以先创建然后再打开),则可以很简单的实现这一功能:open(pathname, O_RDWR | O_CREAT | O_TRUNC, mode_t) 

    所以,基本上我们可以使用open函数来替代creat函数了 

3.5函数 close——关闭一个打开文件

#include <unistd.h>
int close(int file);//成功返回0,出错-1

关闭一个文件时还会释放该进程加在该文件上的所有记录锁。

当一个进程终止时,内核自动关闭它所有的打开文件。很多程序都利用了这一功能而不显式地用close关闭打开文件。

3.6函数 lseek——显式的为一个打开文件设置偏移量

"当前文件偏移量”(current file offset)。它通常是一个非负整数,用以度量从文件开始处计算的字节数。

按系统默认的情况,当打开一个文件时,除非指定O_APPEND选项,否则该偏移量被设置为0。

//可以调用lseek显式地为一个打开文件设置偏移量。
#include <unistd.h>
off_t lseek(int fd,off_t offset,int whence);
//返回值:若成功,返回新的文件偏移量;若出错,返回为−1 

对参数offset的解释与参数whence的值有关,若whence是:

SEEK_SET,则将该文件的偏移量设置为距文件开始处offset个字节。

SEEK_CUR,则将该文件的偏移量设置为其当前值加offset,offset可为正或负。

SEEK_END,则将该文件的偏移量设置为文件长度加offset,offset可正可负。

若lseek成功执行,则返回新的文件偏移量,为此可以用下列方式确定打开文件的当前偏移量:

off_t currpos;

currpos = lseek(fd, 0, SEEK_CUR);

这种方法也可用来确定所涉及的文件是否可以设置偏移量,如果文件描述符指向一个管道、FIFO或网络套接字,则lseek返回−1,并 将errno设置为ESPIPE。

名字中的l是在引入off_t类型之前,offset参数和返回值都是long

3.7函数read——从打开文件中读数据


#include <unistd.h>
ssize_t read(int fd, void *buf, size_t nbytes);
//返回值:读到的字节数,若已到文件尾,返回0;若出错,返回−1

3.8函数write——向打开文件写数据

#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t nbytes);
//返回值:若成功,返回已写的字节数;若出错,返回−1

若返回值等于nbytes表示写操作成功; 否则表示出错
对于普通文件,写操作从文件的当前偏移量处开始,但是要注意的是,当文件打开标记含有O_APPEND时,每次写操作(调用write函数)之前,将文件偏移量设置在文件的当前结尾处。在一次成功写之后,该文件偏移量增加实际写的字节数。

 如果文件描述符不是socket的话,该函数是非阻塞的,否则该函数是阻塞的。 

3.10文件共享

内核使用3种数据结构表示打开文件,它们之间的关系决定了在文 件共享方面一个进程对另一个进程可能产生的影响。

(1)每个进程在进程表中都有一个记录项,记录项中包含一张打开文件描述符表,可将其视为一个矢量,每个描述符占用一项。

(2)内核为所有打开文件维持一张文件表。

(3)每个打开文件(或设备)都有一个 v 节点(v-node)结构。v 节点包含了文件类型和对此文件进行各种操作函数的指针。对于大多数文件,v节点还包含了该文件的i节点(i-node,索引节点)。

对于多个进程读取同一文件都能正常工作,每个进程有自己的文件表项,其中也有它自己的当前文件偏移量,但是,当多个进程写同一文件时,则可能产生预想不到的结果。

3.11原子操作

1.追加到一个文件:

在打开文件时设置O_APPEND标志,这样使得内核每次写操作前,都将进程的当前偏移量设置到该文件的尾端处,实现原子操作

2.函数pread和pwrite

这两个函数可以原子性地定位并执行I/O.

#include <unistd.h>
ssize_t pread(int fd, void *buf, size_t nbytes, off_t offset);
返回值:读到的字节数,若已到文件尾,返回0;若出错,返回−1
ssize_t pwrite(int fd, const void *buf, size_t nbytes,off_t offset);
返回值:若成功,返回已写的字节数;若出错,返回−1
  • 调用pread时,无法中断其定位和读操作。
  • 不更新当前文件偏移量。

原子操作(atomic operation)指的是由多步组成的一个操作。如果该操作原子地执行,则要么执行完所有步骤,要么一步也不

执行,不可能只执行所有步骤的一个子集。

3.12函数dup 和 dup2——都可用来复制一个现有的文件描述符

#include <unistd.h>
int dup(int fd);
int dup2(int fd, int fd2);
两函数的返回值:若成功,返回新的文件描述符;若出错,返回−1

由dup返回的新文件描述符一定是当前可用文件描述符中的最小数值。 

对于 dup2,可以用fd2参数指定新描述符的值。

如果fd2已经打开, 则先将其关闭。

如若fd等于fd2,则dup2返回fd2,而不关闭它。

否则, fd2的FD_CLOEXEC文件描述符标志就被清除,这样fd2在进程调用exec时是打开状态。

实际上,调用dup(oldfd)等效于,fcntl(oldfd, F_DUPFD, 0) 
           而调用dup2(oldfd, newfd)等效于,close(oldfd);fcntl(oldfd, F_DUPFD, newfd);不完全等效,dup2是原子操作

3.13函数sync fsync 和 fdatasync

在内核中设有缓冲区高速缓存或页高速缓存,大多数磁盘I/O都通过缓冲区进行。当我们向文件写入数据时,内核通常先将数据复制到缓冲区中,然后排入队列,晚些时候再写入磁盘。这种方式被称为延迟写(delayed write)。

#include <unistd.h>
int fsync(int fd);
int fdatasync(int fd);
返回值:若成功,返回0;若出错,返回−1
void sync(void);
//sync只是将所有修改过的块缓冲区排入写队列,然后就返回,它并不等待实际写磁盘操作结束。

称为update的系统守护进程周期性地调用(一般每隔30秒) sync函数。这就保证了定期冲洗(flush)内核的块缓冲区。命令sync(1) 也调用sync函数。

fsync函数只对由文件描述符fd指定的一个文件起作用,并且等待写磁盘操作结束才返回。fsync可用于数据库这样的应用程序,这种应用程序需要确保修改过的块立即写到磁盘上。

fdatasync函数类似于fsync,但它只影响文件的数据部分。而除数据外,fsync还会同步更新文件的属性。

3.14函数 fcntl——改变已经打开文件的属性

使用fcntl,我们只需要知道打开文件的描述符,就可以 修改描述符的属性。

#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* int arg */);
//返回值:若成功,则依赖于cmd(见下);若出错,返回−1

3.15函数ioctl

ioctl函数一直是I/O操作的杂物箱。不能用本章中其他函数表示的 I/O操作通常都能用ioctl表示。终端I/O是使用ioctl最多的地方。

#include <unistd.h> /* System V */
#include <sys/ioctl.h> /* BSD and Linux */
int ioctl(int fd, int request, ...);
//返回值:若出错,返回−1;若成功,返回其他值

磁带操作使我们可以在磁带上写一个文件结束标志、倒带、越过指定个数的文件或记录等,用本章中的其他函数(read、write、lseek 等)都难于表示这些操作,所以,对这些设备进行操作最容易的方法就是使用ioctl。

3.16 /dev/fd

较新的系统都提供名为/dev/fd 的目录,其目录项是名为 0、1、2 等的文件。打开文件/dev/fd/n等效于复制描述符n(假定描述符n是打开的)。

fd=open("/dev/fd/0",mode); 等效于fd=dup(0);

/dev/fd文件主要由shell使用,它允许使用路径名作为调用参数的程序,能用处理其他路径名的相同方式处理标准输入和输出。

3.17小结

因为read和write都在内 核执行,所以称这些函数为不带缓冲的I/O函数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值