《Linux系统编程(第2版)》——第2章 文件I/O 2.1打开文件

第2章 文件I/O

本章以及后续的3个章节将介绍文件相关的内容。UNIX系统主要是通过文件表示的,因此这些章节的探讨会涉及UNIX系统的核心。本章介绍了文件I/O的基本要素,详细阐述了最简单也是最常见的文件交互方式——系统调用。第3章基于标准C库描述标准I/O,第4章继续探讨更高级和专业的文件I/O接口。第8章以文件和目录操作为主题结束了文件相关的探讨。

在对文件进行读写操作之前,首先需要打开文件。内核会为每个进程维护一个打开文件的列表,该列表称为文件表(file table)。文件表是由一些非负整数进行索引,这些非负整数称为文件描述符(file descriptors,简称fds)。列表的每一项是一个打开文件的信息,包括指向该文件索引节点(inode)内存拷贝的指针以及关联的元数据,如文件位置指针和访问模式。用户空间和内核空间都使用文件描述符作为唯一cookies:打开文件会返回文件描述符,而后续操作(读写等)都把文件描述符作为基本参数。

文件描述符使用C语言的int类型表示。没有使用特殊类型来表示文件描述符显得有些怪异,但实际上,这种表示方式正是继承了UNIX传统。每个Linux进程可打开的文件数是有上限的。文件描述符的范围从0开始,到上限值减1。默认情况下,上限值为1 024,但是可以对它进行配置,最大为1 048 576。因为负数不是合法的文件描述符,所以当函数出错不能返回有效的文件描述符时,通常会返回-1。

按照惯例,每个进程都至少包含三个文件描述符:0、1和2,除非显式关闭这些描述符。文件描述符0表示标准输入(sdtin),1表示标准输出(stdout),2表示标准错误(stderr)。Linux C标准库没有直接引用这些整数,而是提供了三个宏,分别是:STDIN_FILENO, STDOUT_FILENO和STDERR_FILENO。一般而言,stdin是连接到终端的输入设备(通常是用户键盘),而stdout和stderr是终端的屏幕。用户可以重定向这些文件描述符,甚至可以通过管道把一个程序的输出作为另一个程序的输入。shell正是通过这种方式实现重定向和管道的。

值得注意的是,文件描述符并非局限于访问普通文件。实际上,文件描述符也可以访问设备文件、管道、快速用户空间互斥(futexes)[1]、先进先出缓冲区(FIFOs)和套接字(socket)。遵循一切皆文件的理念,几乎任何能够读写的东西都可以通过文件描述符来访问。

默认情况下,子进程会维护一份父进程的文件表副本。在该副本中,打开文件列表及其访问模式、当前文件位置以及其他元数据,都和父进程维护的文件表相同,但是存在一点区别:即当子进程关闭一个文件时,不会影响到父进程的文件表。虽然一般情况下子进程会自己持有一份文件表,但是子进程和父进程也可以共享文件表(类似于线程间共享),在第5章将对此进行更详细的介绍。

2.1 打开文件

最基本的文件访问方法是系统调用read()和write()。但是,在访问文件之前,必须先通过open()或creat()打开该文件。一旦完成文件读写,还应该调用系统调用close()关闭该文件。

2.1.1 系统调用open()
通过系统调用open(),可以打开文件并获取其文件描述符:

如果系统调用open()执行成功,会返回文件描述符,指向路径名name所指定的文件。文件位置即文件的起始位置(0),文件打开方式是根据参数flags值来确定的。

open()的flags参数
flags参数是由一个或多个标志位的按位或组合。它支持三种访问模式:O_RDONLY、O_WRONLY或O_RDWR,这三种模式分别表示以只读、只写或读写模式打开文件。

举个例子,以下代码以只读模式打开文件/home/kidd/madagascar:

不能对以只读模式打开的文件执行写操作,反之亦然。进程必须有足够的权限才能调用系统调用来打开文件。举个例子,假设用户对某个文件只有只读权限,该用户的进程只能以O_RDONLY模式打开文件,而不能以O_WRONLY或O_RDWR模式打开。

flags参数还可以和下面列出的这些值进行按位或运算,修改打开文件的行为:

O_APPEND

文件将以追加模式打开。也就是说,在每次写操作之前,将会更新文件位置指针,指向文件末尾。即使有另一个进程也在向该文件写数据,以追加模式打开的进程在最后一次写操作时,还是会更新文件位置指针,指向文件末尾(参见2.3.2小节)。

O_ASYNC

当指定的文件可读或可写时,会生成一个信号(默认是SIGIO)。O_ASYNC标志位只适用于FIFO、管道、socket和终端,不适用于普通文件。

O_CLOEXEC

在打开的文件上设置“执行时关闭”标志位。在执行新的进程时,文件会自动关闭。设置O_CLOEXEC标志位可以省去调用fcntl()来设置标志位,且避免出现竞争。Linux内核2.6.23以上的版本才提供该标志位。

O_CREAT

当参数name指定的文件不存在时,内核自动创建。如果文件已存在,除非指定了标志位O_EXCL,否则该标志位无效。

O_DIRECT

打开文件用于直接I/O(参见2.5节)。

O_DIRECTORY

如果参数name不是目录,open()调用会失败。该标志位被置位时,内部会调用opendir()。

O_EXCL

当和标志位O_CREAT一起使用时,如果参数name指定的文件已经存在,会导致open()调用失败。用于防止创建文件时出现竞争。如何没有和标志位O_CREAT一起使用,该标志位就没有任何含义。

O_LARGEFILE

文件偏移使用64位整数表示,可以支持大于2GB的文件。64位操作系统中打开文件时,默认使用该参数。

O_NOATIME+

在读文件时,不会更新该文件的最后访问时间。该标志位适用于备份、索引以及类似的读取系统上所有文件的程序,它可以避免为了更新每个文件的索引节点而导致的大量写操作。Linux内核2.6.8以上的版本才提供该标志位。

O_NOCTTY

如果给定的参数name指向终端设备(比如/dev/tty),它不会成为这个进程的控制终端,即使该进程当前没有控制终端。该标志位很少使用。

O_NOFOLLOW

如果参数name指向一个符号链接,open()调用会失败。正常情况下,指定该标志位会解析链接并打开目标文件。如果给定路径的子目录也是链接,open()调用还是会成功。举个例子,假设name值为/etc/ship/plank.txt,如果plank.txt是个符号链接, open()会失败;然而,如果etc或ship是符号链接,只要plank.txt不是符号链接,调用就会成功。

O_NONBLOCK

文件以非阻塞模式打开。不管是open()调用还是其他操作,都不会导致进程在I/O中阻塞(sleep)。这种情况只适用于FIFO。

O_SYNC

打开文件用于同步I/O。在数据从物理上写到磁盘之前,写操作都不会完成;普通的读操作已经是同步的,因此该标志位对读操作无效。POSIX还另外定义了两个标志位O_DSYNC和O_RSYNC,在Linux系统中,这些标志位和O_SYNC含义相同(参见2.4.3节)。

O_TRUNC

如果文件存在,且是普通文件,并且有写权限,该标志位会把文件长度截断为0。对于FIFO或终端设备,该标志位无效,对于其他文件类型,其行为是未定义。因为对文件执行截断操作,需要有写权限,所以如果文件是只读,指定标志位O_TRUNC,行为也是未定义的。

举个例子,以下代码会打开文件/home/teach/pearl,用于写操作。如果文件已经存在,该文件长度会被截断为0;由于没有指定标志位O_CREAT,如果文件不存在,该open调用会失败:

2.1.2 新建文件的所有者
确定新建文件的所有者很简单:文件所有者的uid即创建该文件的进程的有效uid。

确定新建文件的用户组则相对复杂些。默认情况下,使用创建进程的有效gid。System V是通过这种方式确定,Linux的很多行为都是以System V为模型,因此标准Linux也采用这种处理方式。

但是问题在于,BSD定义了自己的行为方式:新建文件的用户组会被设置成其父目录的gid。在Linux上可以通过挂载选项[2]实现这一点——在Linux上,如果文件的父目录设置了组ID(setgid)位,默认也是这种行为。虽然大多数Linux系统会采用System V行为(新建的文件使用创建进程的gid),但BSD行为(新建文件接收父目录的gid)也有存在的可能,这意味着对于那些对新建文件的所属组非常关注的代码,需要通过系统调用fchown()手动设置所属组(参见第8章)。

幸运的是,大部分时候不需要关心文件的所属组。

2.1.3 新建文件的权限
前面给出的两种open()系统调用方式都是合法的。除非创建了新文件,否则会忽略参数mode;如果给定O_CREAT参数,则需要该参数。在使用O_CREAT参数时如果没有提供参数mode,结果是未定义的,而且通常会很糟糕——所以千万不要忘记!

当创建文件时,参数mode提供了新建文件的权限。对于新建的文件,打开文件时不会检查权限,因此可以执行与权限相反的操作,比如以只读模式打开文件,却在打开后执行写操作。

参数mode是常见的UNIX权限位集合,比如八进制数0644(文件所有者可以读写,其他人只能读)。从技术层面看,POSIX是根据具体实现确定值,支持不同的UNIX系统设置自己想要的权限位。但是,每个UNIX系统对权限位的实现都采用了相同的方式。因此,虽然技术上不可移植,但在任何系统上指定0644或0700效果都是一样的。

为了弥补mode中比特位的不可移植性,POSIX引入了一组可以按位操作的常数,按位结果提供给参数mode:

S_IRWXU

文件所有者有读、写和执行的权限。

S_IRUSR

文件所有者有读权限。

S_IWUSR

文件所有者有写权限。

S_IXUSR

文件所有者有执行权限。

S_IRWXG

组用户有读、写和执行权限。

S_IRGRP

组用户有读权限。

S_IWGRP

组用户有写权限。

S_IXGRP

组用户有执行权限。

S_IRWXO

任何人都有读、写和执行的权限。

S_IROTH

任何人都有读权限。

S_IWOTH

任何人都有写权限。

S_IXOTH

任何人都有执行权限。

实际上,最终写入磁盘的权限位是由mode参数和用户的文件创建掩码(umask)执行按位与操作而得到。umask是进程级属性,通常是由login shell设置,通过调用umask()来修改,支持用户修改新创建的文件和目录的权限。在系统调用open()中,umask位要和参数mode取反。因此,umask 022和mode参数0666取反后,结果是0644。对于系统程序员,在设置权限时通常不需要考虑umask——umask是为了支持用户限制程序对于新建文件的权限设置。

举个例子,以下代码会对文件file执行写操作。如果文件不存在,假定umask值为022,文件在创建时指定权限为0644(虽然参数mode值为0664)。如果文件已存在,其长度会被截断为0:

为了代码可读性(以可移植性为代价,至少理论上如此),这段代码可以改写成如下,其效果完全相同:

2.1.4 creat()函数
OWRONLY | O_CREAT | O**TRUNC 的组合经常被使用,因而专门有个系统调用提供这个功能:

诚如你所看到的,函数名creat的确少了个e。UNIX之父Ken Thompson曾开玩笑说他在UNIX设计中感到最遗憾的就是漏掉了这个字母。
典型的creat()调用如下:

这段代码等效于:

在绝大多数Linux架构[3]中,creat()是个系统调用,虽然在用户空间也很容易实现:

这是一个历史遗留问题,因为之前open()函数只有两个参数,所以也设计了creat()函数。当前,为了向后兼容,仍然保留creat()这个系统调用。在新的体系结构中,creat()可以实现成调用open()的库函数调用,如上所示。

2.1.5 返回值和错误码
系统调用open()和creat()在成功时都会返回文件描述符。出错时,返回-1,并把errno设置成相应的错误值(第1章讨论了errno并列出了可能的错误值)。处理文件打开的错误并不复杂,一般来说,在打开文件之前没有什么操作,因此不太需要执行撤销。典型的处理方式是提示用户换个文件名或直接终止程序。

时间: 2024-11-04 23:03:18

《Linux系统编程(第2版)》——第2章 文件I/O 2.1打开文件的相关文章

《Linux系统编程(第2版)》——第1章 入门和基本概念 1.1 系统编程

第1章 入门和基本概念 摆在你面前的是一本关于系统编程的书,你将在本书中学习到编写系统软件的相关技术和技巧.系统软件运行在系统的底层,与内核和系统核心库进行交互.常见的系统软件包括Shell.文本编辑器.编译器.调试器.核心工具(GNU Core Utilities)以及系统守护进程.此外,网络服务.Web服务和数据库也属于系统软件的范畴.这些程序都是基于内核和C库实现的,可以称为"纯"系统软件.相对地,其他软件(如高级GUI应用),很少和底层直接交互.有些程序员一直在编写系统软件,而

《Linux系统编程(第2版)》——导读

前言 这本书是关于Linux上的系统编程."系统编程"是指编写系统软件,其代码在底层运行,直接跟内核和核心系统库对话.换句话说,本书的主题是Linux系统调用和底层函数说明,如C库定义的函数. 虽然已经有很多书探讨UNIX上的系统编程,却很少有专注于探讨Linux方面的书籍,而探讨最新版本的Linux以及Linux特有的高级接口的书籍更是凤毛麟角.此外,本书还有一个优势:我为Linux贡献了很多代码,包括内核及其上面的系统软件.实际上,本书中提到的一些系统调用和系统软件就是我实现的.因

《Linux系统编程(第2版)》——2.12 结束语

2.12 结束语 本章讨论了Linux系统编程的基础:文件I/O.在Linux这样遵循一切皆文件的操作系统中,了解如何打开.读.写和关闭文件是非常重要的.所有这些操作都是传统的UNIX方式,很多标准都涵盖它们.

《Linux系统编程(第2版)》——2.2 通过read()读文件

2.2 通过read()读文件 前面讨论了如何打开文件,现在一起来看如何读文件.在接下来的一节中,我们将讨论写操作. 最基础.最常见的读取文件机制是调用read(),该系统调用在POSIX.1中定义如下: 每次调用read()函数,会从fd指向的文件的当前偏移开始读取len字节到buf所指向的内存中.执行成功时,返回写入buf中的字节数:出错时,返回-1,并设置errno值.fd的文件位置指针会向前移动,移动的长度由读取到的字节数决定.如果fd所指向的对象不支持seek操作(比如字符设备文件),

《Linux系统编程(第2版)》——1.2 API和ABI

1.2 API和ABI 程序员都希望自己实现的程序能够一直运行在其声明支持的所有系统上.他们希望能在自己的Linux版本上运行的程序也能够运行于其他Linux版本,同时还可以运行在其他支持Linux体系结构的更新(或更老)的Linux版本上. 在系统层,有两组独立的影响可移植性的定义和描述.一是应用程序编程接口(Application Programming Interface,API),二是应用程序二进制接口(Application Binary Interface,ABI),它们都是用来定义

《Linux系统编程(第2版)》——1.4 Linux编程的概念

1.4 Linux编程的概念 本节给出了Linux系统提供的服务的简要概述.所有的UNIX系统,包括Linux,提供了共同的抽象和接口集合.实际上,UNIX本身就是由这些共性定义的,比如对文件和进程的抽象.管道和socket的管理接口等等,都构成了UNIX系统的核心. 本概述假定你对Linux环境很熟悉:会使用shell的基础命令.能够编译简单的C程序.它不是关于Linux或其编程环境的,而是关于Linux系统编程的基础. 1.4.1 文件和文件系统文件是Linux系统中最基础最重要的抽象.Li

《Linux系统编程(第2版)》——1.5 开始系统编程

1.5 开始系统编程 这一章着眼于Linux系统编程的基础概念并从程序员视角探索Linux系统.下一章将讨论基本的文件I/O,这当然包括读写文件,但是由于Linux把很多接口以文件形式实现,因此文件I/O的至关重要性不仅仅是对于文件而言,对于Linux系统的很多其他方面亦是如此. 了解了这些基础知识后,可以开始深入探索真正的系统编程了.我们一起动手吧.

linux系统编程之文件与I/O(六) fcntl函数与文件锁

一.fcntl函数 功能:操纵文件描述符,改变已打开的文件的属性 int fcntl(int fd, int cmd, ... /* arg */ ); cmd的取值可以如下: 复制文件描述符 F_DUPFD (long) 设置/获取文件描述符标志 F_GETFD (void) F_SETFD (long) 设置/获取文件状态标志 F_GETFL (void) F_SETFL (long) 获取/设置文件锁 F_GETLK F_SETLK,F_SETLKW 其中复制文件描述符可参见<linux系

LINUX系统编程 LINUX 虚拟内存

LINUX 虚拟内存 以32位操作系统为例子,因为64位系统虚拟地址过大为2^64,32位仅仅为2^32=4G更利于描述,但是原理东西都一样 这首先要从程序和进程之间的关系开始,我们一般写好一段C\C++代码编译后仅仅为可执行文件假设为a.out,我们 运行a.out的时候,这个才叫进程,进程是OS级别抽象的实体(PCB task_struct结构体),为程序运行进行各种检查和 系统资源分配,一个PCB包含部分信息如下: (摘至刑文鹏LINUX系统编程讲义) * 进程id.系统中每个进程有唯一的

《Linux系统编程(第2版)》——1.3 标准

1.3 标准 UNIX系统编程是门古老的艺术.UNIX编程的基础理念在几十年来一直根深蒂固.但是,对于UNIX系统,变化却是无处不在.各种行为不断变化,特性不断增加.为了使UNIX世界变得有序,标准化组织为系统接口定义了很多套官方标准.虽然存在很多这样的官方标准,但是Linux没有遵循任何一个标准.相反地,Linux致力于和两大主流标准兼容:POSIX和单一UNIX规范(Single UNIX Specification,SUS). 除了其他内容,POSIX和SUS为类UNIX操作系统定义了一套