3.4 底层文件访问
每个运行中的程序被称为进程(process),它有一些与之关联的文件描述符。这是一些小值整数,你可以通过它们访问打开的文件或设备。有多少文件描述符可用取决于系统的配置情况。当一个程序开始运行时,它一般会有3个已经打开的文件描述符:
❑ 0:标准输入
❑ 1:标准输出
❑ 2:标准错误
你可以通过系统调用open把其他文件描述符与文件和设备相关联,稍后讲解。其实使用自动打开的文件描述符就已经可以通过write系统调用来创建一些简单的程序了。
3.4.1 write系统调用
系统调用write的作用是把缓冲区buf的前nbytes个字节写入与文件描述符fildes关联的文件中。它返回实际写入的字节数。如果文件描述符有错或者底层的设备驱动程序对数据块长度比较敏感,该返回值可能会小于nbytes。如果这个函数返回0,就表示未写入任何数据;如果它返回的是-1,就表示在write调用中出现了错误,错误代码保存在全局变量errno里。
下面是write系统调用的原型:
有了这些知识,你就可以编写第一个程序simple_write.c了:
这个程序只是在标准输出上显示一条消息。当程序退出运行时,所有已经打开的文件描述符都会自动关闭,所以你不需要明确地关闭它们。但处理被缓冲的输出时,情况就不一样了。
需要再次提醒的是,write可能会报告写入的字节比你要求的少。这并不一定是个错误。在程序中,你需要检查errno以发现错误,然后再次调用write写入剩余的数据。
3.4.2 read系统调用
系统调用read的作用是:从与文件描述符fildes相关联的文件里读入nbytes个字节的数据,并把它们放到数据区buf中。它返回实际读入的字节数,这可能会小于请求的字节数。如果read调用返回0,就表示未读入任何数据,已到达了文件尾。同样,如果返回的是-1,就表示read调用出现了错误。
下面这个程序simple_read.c把标准输入的前128个字节复制到标准输出。如果输入少于128个字节,就把它们全体复制过去。
运行这个程序,你会看到:
第一次运行程序时,你使用echo通过管道为程序提供输入。在第二次运行时,你通过文件重定向输入。此时,你可以看到文件draft1.txt的第一部分出现在了标准输出上。
请注意,下一个shell提示符出现在输出数据最后一行的尾部,因为在这个例子中,128个字节的数据并没有构成一个完整的行。
3.4.3 open系统调用
为了创建一个新的文件描述符,你需要使用系统调用open。
严格来说,在遵循POSIX规范的系统上,使用open系统调用并不需要包括头文件sys/types.h和sys/stat.h,但在某些UNIX系统上,它们可能是必不可少的。
简单地说,open建立了一条到文件或设备的访问路径。如果调用成功,它将返回一个可以被read、write和其他系统调用使用的文件描述符。这个文件描述符是唯一的,它不会与任何其他运行中的进程共享。如果两个程序同时打开同一个文件,它们会分别得到两个不同的文件描述符。如果它们都对文件进行写操作,那么它们会各写各的,它们分别接着上次离开的位置继续往下写。它们的数据不会交织在一起,而是彼此互相覆盖。两个程序对文件的读写位置(偏移值)不同。你可以通过使用文件锁功能来防止出现冲突,我们将在第7章里介绍该功能。
准备打开的文件或设备的名字作为参数path传递给函数,oflags参数用于指定打开文件所采取的动作。
oflags参数是通过命令文件访问模式与其他可选模式相结合的方式来指定的。open调用必须指定表3-1中所示的文件访问模式之一。
表3-1
open调用还可以在oflags参数中包括下列可选模式的组合(用“按位或”操作)。
❑ O_APPEND:把写入数据追加在文件的末尾。
❑ O_TRUNC:把文件长度设置为零,丢弃已有的内容。
❑ O_CREAT:如果需要,就按参数mode中给出的访问模式创建文件。
❑ O_EXCL:与O_CREAT一起使用,确保调用者创建出文件。Open调用是一个原子操作,也就是说,它只执行一个函数调用。使用这个可选模式可以防止两个程序同时创建同一个文件。如果文件已经存在,open调用将失败。
其他可以使用的oflag值请参考open调用的手册页,它们出现在该手册页的第二节(使用man 2 open命令查看)。
open调用在成功时返回一个新的文件描述符(它总是一个非负整数),在失败时返回-1并设置全局变量errno来指明失败的原因。我们将在本章后面对errno做进一步讨论。新文件描述符总是使用未用描述符的最小值,这个特征在某些情况下非常有用。例如,如果一个程序关闭了它的标准输出,然后再次调用open,文件描述符l就会被重新使用,并且标准输出将被有效地重定向到另一个文件或设备。
POSIX规范还标准化了一个creat调用,但它并不常用。这个调用不仅会像我们预期的那样创建文件,还会打开文件。它的作用相当于以oflags标志O_CREAT|O_WRONLY|O_TRUNC来调用open。
任何一个运行中的程序能够同时打开的文件数是有限制的。这个限制通常是由limits.h头文件中的常量OPEN_MAX定义的,它的值随系统的不同而不同,但POSIX要求它至少为16。这个限制本身还受到本地系统全局性限制的影响,所以一个程序未必总是能够打开这么多文件。在Linux系统中,这个限制可以在系统运行时调整,所以OPEN_MAX并不是一个常量。它通常一开始被设置为256。
3.4.4 访问权限的初始值
当你使用带有O_CREAT标志的open调用来创建文件时,你必须使用有3个参数格式的open调用。第三个参数mode是几个标志按位或后得到的,这些标志在头文件sys/stat.h.中定义,如下所示。
❑ S_IRUSR:读权限,文件属主。
❑ S_IWUSR:写权限,文件属主。
❑ S_IXUSR:执行权限,文件属主。
❑ S_IRGRP:读权限,文件所属组。
❑ S_IWGRP:写权限,文件所属组。
❑ S_IXGRP:执行权限,文件所属组。
❑ S_IROTH:读权限,其他用户。
❑ S_IWOTH:写权限,其他用户。
❑ S_IXOTH:执行权限,其他用户。
请看下面的例子:
它的作用是创建一个名为myfile的文件,文件属主拥有读权限,其他用户拥有执行权限,且只设置了这些权限。
有几个因素会对文件的访问权限产生影响。首先,指定的访问权限只有在创建文件时才会使用。其次,用户掩码(由shell的umask命令设定)会影响到被创建文件的访问权限。open调用里给出的mode值将与当时的用户掩码的反值做AND操作。举例来说,如果用户掩码被设置为001,并且指定了S_IXOTH模式标志,那么其他用户对创建的文件不会拥有执行权限,因为用户掩码中指定了不允许向其他用户提供执行权限。因此,open和creat调用中的标志实际上是发出设置文件访问权限的请求,所请求的权限是否会被设置取决于当时umask的值。
1.umask
umask是一个系统变量,它的作用是:当文件被创建时,为文件的访问权限设定一个掩码。执行umask命令可以修改这个变量的值。它是一个由3个八进制数字组成的值。每个数字都是八进制值1、2、4的OR操作结果。它们的具体含义见表3-2,这3个数字分别对应着用户(user)、组(group)和其他用户(other)的访问权限。
表3-2
例如,如果要禁止组的写和执行权限,同时禁止其他用户的写权限,那么umask值应该如表3-3所示。
表3-3
每个数字的取值OR在一起,因此第2个数字的值是2 | 1,结果为3。最终的umask值为032。
当你通过open或creat调用创建文件时,mode参数将与当前的umask值进行比较。在mode参数中被设置的位如果在umask值中也被设置了,那么它就会从文件的访问权限中删除。因此,用户完全可以设置自己的环境,比如“不准创建允许其他用户有写权限的文件,即使创建该文件的程序要求该权限也不行。”这样做虽然并不能阻止程序或用户在随后使用chmod命令(或者在程序中使用chmod系统调用)来添加其他用户的写权限,但它确实能够帮助用户,使他们不必对每个新文件都去检查和设置其访问权限。
2.close系统调用
你可以使用close调用终止文件描述符fildes与其对应文件之间的关联。文件描述符被释放并能够重新使用。close调用成功时返回0,出错时返回-1。
注意,检查close调用的返回结果非常重要。有的文件系统,特别是网络文件系统,可能不会在关闭文件之前报告文件写操作中出现的错误,这是因为在执行写操作时,数据可能未被确认写入。
3.ioctl系统调用
ioctl调用有点像是个大杂烩。它提供了一个用于控制设备及其描述符行为和配置底层服务的接口。终端、文件描述符、套接字甚至磁带机都可以有为它们定义的ioctl,具体细节可以参考特定设备的手册页。POSIX规范只为流(stream)定义了ioctl调用,但它超出了本书讨论的范围。下面是ioctl的原型:
ioctl对描述符fildes引用的对象执行cmd参数中给出的操作。根据特定设备所支持操作的不同,它还可能会有一个可选的第三参数。
例如,在Linux系统上对ioctl的如下调用将打开键盘上的LED灯:
实验 一个文件复制程序
在学习了关于open、read和write系统调用的知识以后,我们来编写一个底层程序copy_system.c,用来逐个字符地把一个文件复制到另外一个文件。
在本章中,我们将采用多种方法来完成这一工作,以比较各种方法的执行效率。为简单起见,我们将假设输入文件已经存在,输出文件不存在,并且所有的读写操作都成功。当然,在实际程序里,我们必须检验这些假设是否成立!
(1)首先,你需要有一个用于测试的输入文件,长度为1MB,取名为file.in。
(2)然后编译copy_system.c:
注意,#include <unistd.h>行必须首先出现,因为它定义的与POSIX规范有关的标志可能会影响到其他的头文件。
(3)运行这个程序,将得到如下的输出结果:
实验解析
我们在这里使用time工具对这个程序的运行时间进行了测算。Linux使用TIMEFORMAT变量来重置默认的POSIX时间输出格式,POSIX时间格式不包括CPU使用率。你可以看到在这台相当老的系统上,1MB的输入文件file.in被成功复制到file.out,后者只允许属主拥有读写权限。但这次复制花费了大约两分半钟,并且几乎消耗了所有的CPU时间。之所以这么慢,是因为它必须完成超过两百万次的系统调用。
近些年来,Linux在系统调用和文件系统性能方面有了很大改善。一个类似的测试在Linux 2.6内核下只需不到14秒就完成了。
实验 另一个文件复制程序
你可以通过复制大一些的数据块来改善效率较低的问题,请看下面这个改进后的程序copy_block.c,它每次复制长度为IK的数据块,用的还是系统调用:
先删除旧的输出文件,然后运行这个程序:
实验解析
改进后的程序只花费了百分之几秒的时间,因为它只需做大约2000次系统调用。当然,这些时间与系统本身的性能有很大的关系,但它们确实显示了系统调用需要巨大的开支,因此值得对其使用进行优化。
3.4.5 其他与文件管理有关的系统调用
还有许多其他的系统调用能够操作这些底层文件描述符。通过它们,程序可以控制文件的使用方式和返回文件的状态信息。
1.lseek系统调用
lseek系统调用对文件描述符fildes的读写指针进行设置。也就是说,你可以用它来设置文件的下一个读写位置。读写指针既可被设置为文件中的某个绝对位置,也可以把它设置为相对于当前位置或文件尾的某个相对位置。
offset参数用来指定位置,而whence参数定义该偏移值的用法。whence可以取下列值之一。
❑ SEEK_SET:offset是一个绝对位置。
❑ SEEK_CUR:offset是相对于当前位置的一个相对位置。
❑ SEEK_END:offset是相对于文件尾的一个相对位置。
lseek返回从文件头到文件指针被设置处的字节偏移值,失败时返回-1。参数offset的类型off_t是一个与具体实现有关的整数类型,它定义在头文件sys/types.h中。
2.fstat、stat和lstat系统调用
fstat系统调用返回与打开的文件描述符相关的文件的状态信息,该信息将会写到一个buf结构中,buf的地址以参数形式传递给fstat。
下面是它们的原型:
注意:包含头文件sys/types.h是可选的,但由于一些系统调用的定义针对那些某天可能会做出调整的标准类型使用了别名,所以要在程序中使用系统调用时,我们还是推荐将这个头文件包含进去。
相关函数stat和lstat返回的是通过文件名查到的状态信息。它们产生相同的结果,但当文件是一个符号链接时,lstat返回的是该符号链接本身的信息,而stat返回的是该链接指向的文件的信息。
stat结构的成员在不同的类UNIX系统上会有所变化,但一般会包括表3-4中所示的内容。
表3-4
stat结构中返回的st_mode标志还有一些与之关联的宏,它们定义在头文件sys/stat.h中。这些宏包括对访问权限、文件类型标志以及一些用于帮助测试特定类型和权限的掩码的定义。
访问权限标志与前面介绍的open系统调用中的内容是一样的。文件类型标志如下所示。
❑ S_IFBLK:文件是一个特殊的块设备。
❑ S_IFDIR:文件是一个目录。
❑ S_IFCHR:文件是一个特殊的字符设备。
❑ S_IFIFO:文件是一个FIFO(命名管道)。
❑ S_IFREG:文件是一个普通文件。
❑ S_IFLNK:文件是一个符号链接。
以下是其他模式标志。
❑ S_ISUID:文件设置了SUID位。
❑ S_ISGID:文件设置了SGID位。
下面列出了用于解释st_mode标志的掩码。
❑ S_IFMT:文件类型。
❑ S_IRWXU:属主的读/写/执行权限。
❑ S_IRWXG:属组的读/写/执行权限。
❑ S_IRWXO:其他用户的读/写/执行权限。
下面是一些用来帮助确定文件类型的宏定义。它们只是对经过掩码处理的模式标志和相应的设备类型标志进行比较。
❑ S_ISBLK:测试是否是特殊的块设备文件。
❑ S_ISCHR:测试是否是特殊的字符设备文件。
❑ S_ISDIR:测试是否是目录。
❑ S_ISFIFO:测试是否是FIFO。
❑ S_ISREG:测试是否是普通文件。
❑ S_ISLNK:测试是否是符号链接。
例如,如果想测试一个文件代表的不是一个目录,设置了属主的执行权限,并且不再有其他权限,你可以使用如下的代码进行测试:
3.dup和dup2系统调用
dup系统调用提供了一种复制文件描述符的方法,使我们能够通过两个或者更多个不同的描述符来访问同一个文件。这可以用于在文件的不同位置对数据进行读写。dup系统调用复制文件描述符fildes,返回一个新的描述符。dup2系统调用则是通过明确指定目标描述符来把一个文件描述符复制为另外一个。
它们的原型如下:
当你通过管道在多个进程间进行通信时,这些调用也很有用。我们将在第13章对dup系统调用进行深入讨论。