第3章文件及目录管理Linux系统中一切皆文件,不仅程序和数据是以文件的形式保存的,目录和各种设备也都被当作文件来使用。本章主要介绍POSIX标准的系统I/O操作,而对于标准I/O操作不是本章介绍的重点,读者如果具备C语言基础并且理解了本章介绍的系统I/O接口操作,那么理解标准I/O接口操作的使用方法将不会感到困难。系统I/O接口提供了不带缓冲区的I/O操作,而标准I/O接口提供了带缓冲区的I/O操作,两者在使用缓冲区方面有比较大的区别,因此本章对不带缓冲区和带缓冲区的文件操作原理进行介绍,同时描述了不带缓冲区的文件描述符和带缓冲区的流概念。本章加入了一些关于Linux文件系统的相关知识介绍,使读者可以对Linux系统下的I/O接口有更深入的理解。同时,在介绍相关I/O接口时,对涉及的一些Linux内核数据结构和Linux操作系统文件管理方法也将进行介绍。 3.1文件和I/O操作分类〖4/5〗3.1.1文件概念在大多数应用中,文件是一个核心成分。除了实时应用和一些特殊的应用外,应用程序的输入都是通过文件来实现的。实际上所有应用程序的输出都保存在文件中,这便于信息的长期存储及用户或应用程序将来的访问。 文件可以认为是具有标识符的一组相关联信息项的有序集合,一般体现为磁盘上的文件。对于文件的管理,从Linux内核的角度来看,处理文件的部分是文件系统,Linux内核通过文件系统对文件存储器空间进行组织和分配,负责文件的存储并对存入的文件进行保护和检索。具体地说,它负责为用户建立文件,存入、读出、修改、转储文件、控制文件的存取,当用户不再使用时撤销文件等。从用户的角度来看,文件的管理是通过Linux操作系统提供的一系列接口完成的,比如系统I/O、标准I/O、字符命令和图形接口来完成文件的管理。 3.1.2文件操作分类 根据应用程序对文件的访问方式,即是否使用缓冲区,对文件的访问可分为不带缓冲区的文件操作和带缓冲区的文件操作。这里所说的缓冲区有别于操作系统内核中的磁盘缓冲,磁盘缓冲是为提高磁盘访问速度而专门开辟的内存空间。〖1〗Linux环境高级程序设计第3章文件及目录管理[3]〖3〗 不带缓冲区的文件操作。属于低级文件操作,需要用户在自己的程序中为每个文件设置缓冲区,如图3.1所示,对文件进行读写操作时,数据从磁盘输入到用户程序的数据区之前或数据从用户程序的数据区输出到磁盘之前,都不会经过系统管理的输出缓冲区进行缓冲。遵循POSIX标准的系统I/O使用的就是不带缓冲区的文件操作,系统I/O接口也称为I/O系统调用,如open、read、write和lseek等。  带缓冲区的文件操作。属于高级文件操作,系统将在用户空间中自动为正在使用的文件开辟内存缓冲区,如图3.2所示,数据在从文件读到程序数据区和从程序数据区输出到文件的过程中,将会经过一个系统管理的输入或输出缓冲区进行缓冲。遵循ANSI标准的I/O操作使用的就是带缓冲区的文件操作,标准I/O接口也称为I/O库函数,如fopen、fread、fwrite、flush和fseek等。 图3.1不带缓冲区的文件操作图 图3.2带缓冲区的文件操作 在第1章中已经了解到系统调用和库函数的本质区别,调用系统调用会陷入Linux内核,而调用库函数不会陷入Linux内核。如果采用非缓冲区的文件访问方式,每次在对文件进行任何一次读或写操作时,分别使用读文件的系统调用read来处理读操作,或使用写文件的系统调用write来处理写操作。如果用户程序需要访问磁盘空间中的某个文件,read或write是以一个数据单元为单位进行读或写操作,因此,一个文件的读或写操作可能需要多次调用read或write系统调用完成。一个文件长度可能被划分成多个数据单元,每访问一个数据单元都要执行一次系统调用,执行系统调用会有一次用户态到内核态的切换,系统调用完成后还会有内核态到用户态的转换,这两次态式的转换将涉及CPU状态的切换、用户态进程和系统进程的上下文切换,这将消耗一定的CPU时间。对磁盘读或写操作是一个机械运动,相对CPU处理速度来讲,是一个缓慢的过程,频繁的磁盘访问对程序执行效率会造成较大的影响。从而可以得到一个结论,通过多次调用read或write系统调用完成大文件数据的读或写,从效率角度来讲比较低效。 ANSI标准I/O库函数建立在底层系统调用之上,在对文件函数库的实现中使用了低级文件I/O系统调用。ANSI标准C库中的文件处理函数为了减少使用系统调用的次数以及提高效率,根据应用程序的不同,采用缓冲区机制,这样,对磁盘文件进行读或写操作时,可以一次性地从文件中读取大量的数据到缓冲区中。以后对这部分数据的访问就不需要再使用系统调用了,因为数据可以直接从输入文件缓冲区或输出文件缓冲区中获得,即整个读或写操作只需要少量的CPU状态的切换和磁盘的读写机械访问次数。在对磁盘写文件进行操作时,可以先将内容存储在文件缓冲区中,将文件缓冲区充满后,或者确实需要更新的时候再调用系统调用,将该文件一次性写入到磁盘。 ANSI标准C库函数为了实现这一特性,采用了流的概念,因为数据的输入和输出就像流动的水一样。在流的实现中,缓冲区是最重要的单元。根据使用需求的不同,可以选择使用全缓冲区、行缓冲区和无缓冲区等3种缓冲区处理方式来处理文件的读写操作。 3.2Linux文件系统概述〖4/5〗3.2.1文件结构文件结构是文件存放在磁盘等存储设备上的组织方法,主要体现在对文件和目录的组织上。Linux操作系统使用标准的目录结构,它提供了一个方便有效地管理文件的途径。在安装Linux的时候,安装程序就会为用户创建文件系统和完整而固定的目录组成形式,并指定了每个目录的作用和其中的文件系统,如图3.3所示。 图3.3Linux目录树结构 文件主要包含两方面的内容: 一是文件本身所包含的数据;另一个是文件的属性,也称为元数据,一般包括文件访问权限、所有者、文件大小和创建日期等信息。 目录也是一种文件,称为目录文件,它的内容是该目录的目录项。目录项是该目录下各个文件和目录的相关信息。当创建一个新目录时,系统将自动创建两个目录项“.”和“..”,“.”代表当前目录,“..”代表当前目录的父目录,在Shell下输入ls a命令可以将其显示在终端上。对于根目录,两者是相同的。 在Shell环境下输入cd /可以切换到根目录,再输入ls命令可以查看到根目录下的目录情况,常见的Linux系统发行版本都包含有如下几个目录。  /bin: 用于存放普通用户可执行的命令,系统中的任何用户都可以执行该目录中的命令,如ls、cp、mkdir等命令。  /boot: Linux内核及启动系统时所需要的文件,为保证启动文件更加安全可靠,通常把该目录存放在独立的分区上。  /dev: 设备文件的存储目录,如磁盘、光盘等。  /etc: 用于存放系统的配置文件,比如用户账号及密码存放在配置文件/etc/password和/etc/shadow中。  /home: 普通用户的主目录,每个用户在该目录下都有一个与用户同名的目录。  /lib: 用于存放各种库文件。  /proc: 该目录是一个虚拟文件系统,只有在系统运行时才存在。通过访问该目录下的文件,可以获取系统状态信息并且修改某些系统的配置信息。可以使用cat、strings命令来查看这些信息,如在Shell下输入cat/proc/meminfo命令可以获取系统内存的使用情况,输入man proc命令获取关于proc的详细信息。  /root: 超级用户root的主目录。  /sbin: 存放的是用于管理系统的命令。  /tmp: 存放的是临时文件。  /usr: 用于存放系统应用程序及相关文件,如说明文档、帮助文件等。  /var: 用于存放系统中经常变化的文件,如日志文件、用户邮件等。 3.2.2文件系统模型 文件的本质就是长期存储在物理磁盘上的数据,操作系统通过文件系统功能可以方便地对磁盘上的文件进行管理。Linux的文件系统模型如图3.4所示。 图3.4文件系统模型 对物理磁盘的访问都是通过设备驱动程序进行的,而对设备驱动的访问则有两种途径: 一种是通过设备驱动本身提供的接口;另一种是通过虚拟文件系统(Virtual File System,VFS)提供给上层应用程序的接口。第一种方式能够让用户进程绕过文件系统直接读写磁盘上的内容,这给操作系统带来了不稳定性,因此大部分操作系统Linux都是使用虚拟文件系统来访问设备驱动的。只有在特殊情况下才允许用户进程通过设备驱动接口直接访问物理磁盘。 VFS是虚拟的、不存在的,它与前面提到的proc文件系统一样,都只存在于内存而不存在于磁盘中,即只有在系统运行后才存在。VFS提供一种机制,它将各种不同的文件系统整合在一起,并提供统一的应用程序接口(API)供上层的应用程序使用。VFS的使用体现了Linux的最大特点之一: 支持多种不同的文件系统。Linux不仅支持EXT2、EXT3,也支持Windows系统的文件系统。 从硬盘的构造可知,访问物理磁盘的最小单位是位于某个盘面上某个磁道的一个扇区,即使用户只需访问1个字节的数据,实际读写时都需要先将该字节所在的扇区读入到内存,然后再选择指定的数据进行访问。因此,文件系统是由一系列的块(block)构成的,每个块的大小因文件系统不同而不同,但文件系统一旦安装之后,其块的大小就固定了。通常一个块的大小是一个扇区的大小。 3.2.3目录、索引结点和文件描述符1. 目录和索引结点为了能对一个文件进行正确的存取,必须为文件设置用于描述和控制文件的数据结构,称为文件控制块(File Control Block,FCB),文件管理程序(内核)可借助文件控制块中的信息对文件施以各种操作。 每个文件都有一个FCB用于描述文件的各种属性信息,FCB的有序集称为文件目录。 一个FCB 也称为一个目录项,为实现对文件目录的管理,通常将文件目录以文件的形式保存在外存上,这个文件就称为目录文件。FCB一般包含三类信息: 第一类为基本信息,如文件名、文件物理位置、文件逻辑结构和文件物理结构;第二类为存取控制信息,如文件所有者存取权限、所有者所在组的存取权限、其他用户存取权限;第三类为信息类,如文件建立日期和时间、文件上一次修改日期和时间、当前使用信息项(已打开该文件的进程数,是否被其他进程锁住,文件在内存中是否已被修改但尚未复制到磁盘上)。 在现代操作系统中,一般为了加快文件的检索速度,将除文件名之外的一些属性信息,如文件创建/修改日期、文件访问权限、文件长度和文件在磁盘上的存放位置等信息存储在一个特殊的索引结点(inode)数据块中,因此每个文件都有一个inode。Linux的文件系统把所有的inode组织成一个数组,给每个inode分配一个号码,也就是该inode在数组中的索引号,称为索引结点编号。Linux将FCB(对应内核的dentry结构体)组织为“文件名,索引结点编号”这样的结构,如表3.1所示,表中每一行对应一个文件的FCB,通过索引结点编号,内核可以找到文件对应的索引结点inode,通过inode可以了解文件的诸多属性,包括文件数据在外设的物理位置。Linux中一切皆文件,每个文件或目录都对应一个索引结点,因此对应上层目录和本层目录“..”和“.”也有对应的索引结点编号。表3.1Linux的文件目录 文件名索引结点编号.2bin3407873文件系统就是靠这个索引结点编号来识别文件的,在Shell环境中,可以使用ls / ail来查看文件的索引结点编号,如下: root@ubuntu:~# ls / -ail 2 drwxr-xr-x 23 root root 4096 3月 24 16:07 . 2 drwxr-xr-x 23 root root 4096 3月 24 16:07 .. 3407873 drwxr-xr-x 2 root root 4096 10月 28 2015 bin 2028 lrwxrwxrwx 1 root root33 10月 28 2015 initrd.img -> boot/initrd.img-3.19.0-31-generic从以上运行结果可以看出,bin是一个目录,而initrd.img是一个文件,它们在目录文件中都有一个与其对应的目录项(dentry),有唯一的inode索引结点。bin对应的索引结点编号是3407873,而initrd.img对应的索引结点编号是2028。当前目录“.”和上一层目录“..”索引结点编号都为2,说明当前目录是根目录。 2. 文件描述符 说到文件描述符,不得不提task_struct结构体。操作系统中,一个处于运行状态的程序,称为进程,为了描述和控制进程的运行,内核定义了一个称为进程控制块PCB(Process Control Block)的数据结构,PCB中记录了内核所需的、用于描述进程当前情况以及控制进程运行的全部信息,在Linux中,这个数据结构为task_struct。 文件描述符是一个非负整数,对于内核而言,所有打开的文件都通过文件描述符引用。当用户程序需要访问文件时,内核通过open、create等系统调用向用户程序返回一个文件描述符,随后在用户程序中所有对该文件的访问都使用该文件描述符。 那么,文件描述符究竟是什么呢?在内核中打开的文件是用file结构体来表示的,每一个结构体都会有一个指针指向它们,这些指针被统一存放在一个称为fd_array的数组中,而这个数组被存放在一个称为files_struct的结构体中,该结构体是进程控制块task_struct的重要组成部分。它们的关系如图3.5所示。 图3.5文件描述符含义 在图3.5中,task_struct结构体中记录了内核所需的、用于描述进程当前情况以及控制进程运行的全部信息,当然其中也包含了进程在运行中所有打开文件的信息,这些信息被一个files指针统一管理。files所指向的结构体files_struct中的数组fd_array是一个指针数组,用户进程每一次调用open函数都会使得内核实例化一个file结构体,并将一个指向该结构体的指针依次存放在fd_array数组中,该指针所占据的数组下标将被作为“文件描述符”返回给用户进程,因此文件描述符是从0开始的非负整数值。 结构体file是内核管理文件操作的重要的数据之一,里面存放对该文件的访问模式、文件位置偏移量、打开模式、目录项dentry等信息。在操作文件之前,open系统调用参数中指定的打开模式mode和文件状态标志flags被记录在该结构体中,在操作文件过程中,文件相关的控制数据也一并在此管理。 通过file结构体读者会发现其成员f_dentry指向了该文件对应的唯一的目录项,即前面介绍的FCB,dentry结构体中存放了表示文件inode结点编号的成员i_inode,根据i_inode文件系统可以在磁盘上找到该文件对应的inode结点,通过inode结点中文件物理位置可以访问到文件在磁盘上的位置,这样文件系统就可以找到需要操作的文件。 下面是file结构体的类型定义: //from /usr/src/linux-headers-3.19.0-25/include/linux/fs.h struct file { union { struct llist_nodefu_llist; struct rcu_head fu_rcuhead; } f_u;   struct pathf_path; //包含dentry和mnt两个成员,用于确定文件路径 struct inode f_inode; //cached value const struct file_operations f_op;//文件操作函数集 /  Protects f_ep_links, f_flags.  Must not be taken from IRQ context. / spinlock_tf_lock; atomic_long_t f_count; unsigned int f_flags;//由open函数参数flags指定 fmode_t f_mode; //由open函数参数mode指定 struct mutex f_pos_lock; loff_t f_pos; //文件位置偏移量 struct fown_struct f_owner; const struct cred f_cred; struct file_ra_state f_ra; u64 f_version; #ifdef CONFIG_SECURITY void f_security; #endif /needed for tty driver, and maybe others / void private_data; #ifdef CONFIG_EPOLL /Used by fs/eventpoll.c to link all the hooks to this file / struct list_head f_ep_links; struct list_head f_tfile_llink; #endif /#ifdef CONFIG_EPOLL / struct address_space f_mapping; } __attribute__((aligned(4)));/lest something weird decides that 2 is OK /上述代码中用粗体标注出来的f_op、f_flags、f_mode和f_pos是其中重要的成员。f_op包含了该文件实际读/写的操作算法,这些算法由文件所在的设备驱动程序提供,设备类型不同,驱动程序也不尽相同,这些差异性都被封装在f_op里面,应用程序看到是f_op提供的统一接口,比如lseek、read、write、open、mmap等。 f_flags和f_mode的值通过用户调用open函数时的flags和mode参数传递过来,这样就规定了对文件访问的选项和新建时的初始权限,用户对文件使用的需求通过open记录到内核文件fs.h中。 f_pos指的是当前对文件操作的位置,文件的起始位置为0。打开文件时,f_pos的值通常为0,也就是从距离文件开始偏移量为0的字节开始读写,读写了n个字节后,内核自动将f_pos的值加n,使得下次读取该文件时从第n+1个字节开始。若文件以追加方式打开,则f_pos指向文件尾。这个值在应用层可以通过lseek或fseek函数来调整。 文件描述符的取值范围在0~NR_OPEN之间,Linux系统中NR_OPEN默认设置为255,也就是说每个进程最多只能打开256个文件。 当一个程序开始运行时,编号为0、1、2的三个文件描述符就已经默认打开了,因此用户使用的文件描述符最小从3开始。文件描述符0代表标准输入文件,一般就是键盘;文件描述符1代表标准输出文件,一般指显示器;文件描述符2代表标准错误输出,一般也指显示器。事实上,在代码中经常使用STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO来代替0、1和2。 3.2.4文件的分类 Linux中包含以下几种文件类型。 (1) 普通文件(regular file): 这是常见的文件类型,这种文件包含了某种形式的数据,至于这种数据是文本还是二进制数据,对内核而言并无区别。对普通文件内容的解释由处理该文件的应用程序完成。 (2) 目录文件(directory file): 目录文件就是目录,目录也有访问权限,目录文件的内容就是该目录下的文件和子目录的信息,对一个目录文件具有读许可权的任一进程都可以读该目录的内容,但只有内核可以写目录文件。 (3) 字符特殊文件(character special file): 用于表示系统字符类型的设备,比如键盘,鼠标等,这些硬件对操作系统来说只是一个文件。 (4) 块特殊文件(block special file): 用于表示系统中块类型的设备,如硬盘、光驱等。对这些设备上的数据的访问通常以块的方式进行,即一次至少读写一块。 (5) FIFO: 这种类型文件用于进程间的通信,也称为命名管道。 (6) 套接字(socket): 主要用于网络通信,套接字也可以用于一台主机上的进程之间的通信。 (7) 符号链接(symbolic link): 指向另一个文件,是另一个文件的引用。 在Shell下可通过输入ls l<文件名>来查看文件的类型,在程序中查看文件的类型则需要使用stat/fstat/lstat函数族,在3.4.1节将会介绍。例如,在某个目录下执行ls l的结果如下: root@ubuntu:~# ls -l -rw-r--r--1 root root 711月1111:43 test.c drwxr-xr-x 3 root root 40963月2008:34 zll结果中,第一行的第一项表示“文件的类型和访问权限”,第一个字母“”表示test.c是一个普通文件。第二行的第一个字母“d”取自directory的首字母,表示zll是一个目录。 执行下面的命令ls l/,结果如下:root@ubuntu:~# ls -l / lrwxrwxrwx 1 root root 33 10月 28 2015 initrd.img -> boot/initrd.img-3.19.0-31-generic结果中,第一项的第一个字母“l”表示initrd.img文件是一个链接文件,它是boot/initrd.img3.19.031generic文件的引用。 执行下面的命令ls /dev/sda l,结果如下:root@ubuntu:/# ls /dev/sda -l brw-rw----1 root disk 8, 04月52017 /dev/sda第一项的第一个字母“b”取自block的首字母,表示/dev/sda文件是一个块特殊文件。 在ls l<文件名>命令结果显示中,文件类型用最左边一栏第一字母表示,是文件类型的缩写,汇总说明如下:  (regular): 普通文件。  d(directory): 目录文件。  c(character): 字符设备文件。  b(block): 块设备文件。  p(pipe): 管道文件(命名管道)  s(socket): 套接字文件。  l(link): 链接文件(软链接即符号链接)。 3.2.5文件访问权限控制〖2〗3.2.5.1访问权限控制Linux是一个多用户、多任务的操作系统,因此可能常常会有多个用户同时使用一台主机工作。为了保证多用户对同一主机文件系统的安全访问,Linux对用户访问文件进行了访问控制。比如,文件的创建者不希望其他用户修改自己的文件,管理员不希望普通用户有运行系统中某些命令的权利。在Linux中,当前用户有可能是文件的所有者、与文件所有者同在一组或是其他用户,根据这三种不同的身份,系统分别对文件的读、写、执行权限进行控制,从而保证多用户对同一主机上文件访问的安全性。 在Shell环境中,可以通过ls l 命令来查看某一个文件的属性,例如以下输出是在某个目录下执行ls l命令的结果: -rw-r--r--1 root root711月 11 11:43 test.c drwxr-xr-x 3 root root40963月 20 08:34 zll输出结果中,从左至右依次是: 文件类型+访问权限、连接数、文件所有者、拥有该文件的用户所属的组、文件大小、文件的创建时间、文件名。 第一项“文件类型+访问权限”共由10位构成,第一位表示文件类型。剩下9位表示文件的访问权限。按照每3位为一组分为3组,从左到右: 第一组表示文件所有者对该文件的操作权限;第二组表示与文件所有者同组(group)的用户对该文件的操作权限;第三组表示其他用户对该文件的操作权限。通常每组会出现3种字母,r表示具有读权限,w表示具有写权限,x表示具有执行权限。 以test.c文件为例: 第一组为rw,表示test.c的所有者具有对该文件的读写权限,无可执行权限;第二组r,表示test.c 文件所有者所在的组对该文件具有读权限,无写、无执行的权限;第三组为r,表示其他用户对该文件具有读权限,无写、无执行的权限。 对文件访问权限的修改在Shell环境中可通过chmod命令来实现,如: root@ubuntu:~# chmod 666 test.c root@ubuntu:~# ls -l -rw-rw-rw-1 root root711月 11 11:43 test.cchmod命令中的数字6是通过计算所得,对于可读、可写、可执行3种权限,分别对应了一个值,读权限为4,写权限为2,执行权限为1,即r=4、w=2、x=1,4+2=6,表示拥有读写权限但不具有可执行权限。666表示test.c文件的访问权限修改为: 所有者、所属组和其他用户具有读、写、不可执行的3种权限,更详细的用法请参考man手册。 通过之后的ls l命令可以看到,test.c的所有者、所属组和其他用户都拥有对test.c的读、写、不可执行3种权限。 在进行程序设计时,可以通过chmod/fchmod函数对文件访问权限进行修改,可参考3.4.2节。 3.2.5.2访问权限在系统中的表示1. st_mode结构文件类型与访问权限被定义在一个名为st_mode的内核数据结构中,st_mode 实质上是一个无符号16位短整型数,文件类型和权限被编码在这个数中,如图3.6所示。 图3.6st_mode内核数据结构 其中低9位即st_mode\[0:8\]一一对应代表了文件的各种用户权限,分为3组,对应3种用户,它们是文件所有者、同组用户和其他用户, r为读权限,w为写权限,x为执行权限。 其中高4位即st_mode\[12:15\]用作文件类型,最多可以标识16种类型,目前已经使用了其中7个。 接下来的3位st_mode\[9:11\]即是文件的特殊属性,1代表具有某种属性,0代表没有,这3位分别是suid位,sgid位和sticky位。 st_mode\[10\]和st_mode\[11\]分别用来设置文件的suid(只对普通文件有效)和sgid(只对目录有效)。如果suid被设置为1,则任何用户在执行该文件时均会获得该文件所有者的临时授权,即其有效UID将等于文件所有者的UID。如果sgid被设置为1,则任何在该目录下执行的程序均会获得该目录所属组成员的临时授权,即其有效GID将等于该目录所属组成员的GID。 suid和sgid位能让普通用户以root用户的角色运行只有root账号才能运行的程序或命令,另外,这种机制对于某些只能由root用户启动,但启动后需要回到普通用户权限的程序很有帮助。 例如,普通用户运行passwd命令可以更改个人的账号密码,用户账号密码存放在/etc/passwd文件中,因此修改用户密码实际上最终更改的是/etc/passwd文件。通过ls l /etc/passwd命令可以查看/etc/passwd文件的属性。如下所示: root@ubuntu:etc# ls -l /etc/passwd -rw-r--r-- 1 root root 19204月52017 /etc/passwd运行结果显示,只有具有root权限的用户才能更改/ect/passwd文件的内容。 那么普通用户是如何通过passwd命令修改个人的账号密码呢?通过ls l /usr/bin/passwd命令可以查看到passwd命令程序的文件属性,如下所示: root@ubuntu:~# ls -l /usr/bin/passwd -rwsr-xr-x 1 root root 470327月162015 /usr/bin/passwd标志s出现在文件所有者执行权限位上,说明文件/usr/bin/passwd文件的suid位为1,意味着执行passwd命令的用户暂时具有该程序所有者(root)的权限,因此普通用户可以执行passwd命令修改个人账号密码。 标志s出现在文件所有者的执行权限位上,说明id位置1;标志s出现在用户组的执行权限位上,说明sgid位为1,如rwxsx;标志在其他组的x位上,说明sticky置1,如rwxrt。sticky(只对目录有效)在当前用户拥有该目录的写权限情况下,如果这一位被设置1,那么该用户只能删除在本目录下属于自己的文件。 suid、sgid和sticky权限设置,可以通过chmod命令来实现,在3.2.5.1节中已经了解chmod可以通过数字形态更改权限,那么在表示读、写、执行权限的三个数字之前所加的那个数就代表了这三个特殊权限,其中suid为4,sgid为2,sticky为1。如下所示: root@ubuntu:~# chmod 7666 test.c root@ubuntu:~# ls -l test.c -rwSrwSrwT 1 root root 711月 11 11:43 test.c -rwSrwSrwT表示test.c文件权限suid、sgid、sticky位置12. 掩码技术 通过按位与操作,掩码可以将二进制数中不需要的位置0,需要的位的值不发生改变。图3.7是为八进制数042664的st_mode值按位与八进制掩码170000的计算过程,可以看到,结果中保留了042664高4位的值,将低12位值置为0。 图3.7掩码技术运算示例 通过图3.7可以看出,042664与170000经过位与运算后,得到八进制040000,说明st_mode值对应的文件是一个目录文件。 3. st_mode中一些宏定义 为了对文件的st_mode值操作方便,Linux系统在/usr/include/sys/stat.h文件中定义了一些宏,这些宏中包括了文件类型和权限掩码的定义和应用。 下面是关于文件类型和特殊位的宏定义:#define S_IFMT00170000//文件类型掩码 #define S_IFSOCK0140000 //文件类型: 套接字 #define S_IFLNK0120000 //文件类型: 链接 #define S_IFREG0100000 //文件类型: 普通文件 #define S_IFBLK0060000 //文件类型: 块设备 #define S_IFDIR0040000 //文件类型: 目录 #define S_IFCHR0020000 //文件类型: 字符设备 #define S_IFIFO0010000 //文件类型: 管道 #define S_ISUID0004000 //文件的suid #define S_ISGID0002000 //文件的sgid #define S_ISVTX0001000 //文件的粘贴位下面是判断文件权限的宏定义:#define S_ISLNK(m)(((m) & S_IFMT) == S_IFLNK)//判断是否为链接文件 #define S_ISREG(m) (((m) & S_IFMT) == S_IFREG) //判断是否为普通文件 #define S_ISDIR(m) (((m) & S_IFMT) == S_IFDIR) //判断是否为目录文件 #define S_ISCHR(m) (((m) & S_IFMT) == S_IFCHR) //判断是否为字符设备文件 #define S_ISBLK(m) (((m) & S_IFMT) == S_IFBLK) //判断是否为块设备文件 #define S_ISFIFO(m) (((m) & S_IFMT) == S_IFIFO) //判断是否为管道文件 #define S_ISSOCK(m) (((m) & S_IFMT) == S_IFSOCK)//判断是否为套接字文件下面是文件权限的宏定义:#define S_IRWXU 00700//所有者权限掩码 #define S_IRUSR 00400 //所有者读权限 #define S_IWUSR 00200 //所有者写权限 #define S_IXUSR 00100 //所有者执行权限 #define S_IRWXG 00070 //所属组成员权限掩码 #define S_IRGRP 00040 //所属组成员读权限 #define S_IWGRP 00020 //所属组成员写权限 #define S_IXGRP 00010 //所属组成员执行权限 #define S_IRWXO 00007 //其他用户权限掩码 #define S_IROTH 00004 //其他用户读权限 #define S_IWOTH 00002 //其他用户写权限 #define S_IXOTH 00001 //其他用户执行权限 3.3文件的读写 在Linux操作系统中,提供了对文件I/O操作的两类接口,分别是I/O系统调用接口和标准I/O库函数接口,它们的关系如图3.8所示。直接I/O系统调用,遵守POSIX标准,是Linux操作系统自身提供的系统调用函数,如open、read、write和close等函数,这些函数的使用方法可以在Shell下输入“man 2 <函数名>”命令获取。直接进行I/O系统调用的可移植性差,只能在遵循POSIX标准的类UNIX环境中直接使用。标准I/O库函数是由ANSI标准提供的标准I/O库函数,如fopen、fread、fwrite和fclose等函数,这些函数的使用方法可以在Shell下输入“man 3 <函数名>”命令获取,这些库函数是对直接I/O系统调用的封装,其在访问文件时根据需要设置了不同类型的缓冲区,从而减少直接I/O系统调用的次数,提高访问效率。但这需要系统下有相应的库支持,另外,对于特殊操作只能使用直接I/O操作。 图3.8I/O系统调用和标准I/O库函数关系 无论是直接I/O系统调用(POSIX接口,系统I/O)还是标准I/O库函数(ANSI接口,标准I/O),都是为应用程序服务的I/O接口,只是工作在不同的层次,用户可以根据实际需要调用相应的接口完成应用的需求。 3.3.1文件打开、创建和关闭1. open系统调用对某一个文件进行操作之前,首先必须“打开”它,系统调用open用来打开或创建一个文件,并返回一个文件描述符,其他的函数可以通过文件描述符指定文件进行读取与写入操作。在Shell 下输入“man 2 open”可以获取该函数的原型。open系统调用接口规范说明如表3.2所示。表3.2open函数的接口规范说明 函数名称open函数功能打开或创建文件头文件#include #include #include 函数原型int open(const char pathname, int flags); int open(const char pathname, int flags, mode_t mode);参数pathname: 要打开或创建的含路径的文件名 flags: 文件状态标志,表示打开文件的方式 mode: 如果文件被新建,指定其权限为mode(八进制表示法)返回值大于或等于0的整数: 成功(即文件描述符) -1: 失败说明: 对open函数而言,仅当创建新文件时才使用mode参数。 flags参数可以用来说明open函数的多个选择,参数值可分为两类,一类是主标志,一类是副标志。用下列一个或多个标志常量进行“或”运算构成flags参数(这些常量在头文件中定义)。下面对这两类标志常量做详细的介绍。 (1) 主标志如下:  O_RDONLY: 以只读方式打开文件。  O_WRONLY: 以只写方式打开文件。  O_RDWR: 以可读可写方式打开文件。 主标志是互斥的,使用其中一种则不能再使用另外一种。除了主标志以外,还有副标志可与它们配合使用,副标志可同时使用多个,使用时在主标志和副标志之间加入按位与(|)运算符。 (2) 副标志如下:  O_CREAT: 如果文件不存在,则创建该文件,只有在此时,才需要用到第三个参数mode,以说明新文件的存取权限。  O_EXCL: 如果O_CREAT 也被设置,此指令会去检查文件是否存在。文件若不存在则创建该文件,若文件已存在将导致打开文件出错。  O_TRUNC: 若文件存在并且以写的方式打开时,此标志将文件长度清为0,即源文件中保存的数据丢失,但文件属性不变。  O_APPEND: 所写入的数据以追加的方式加入到文件后面。  O_NOCTTY: 如果文件为终端,那么终端不可以作为调用open()系统调用的那个进程的控制终端。  O_CLOEXEC: 把FD_CLOEXEC常量设置为文件描述符标志。  O_DIRECTORY: 如果pathname引用的不是目录,则出错。  O_NOFLLOW: 如果pathname引用的是一个符号链接,则出错。  O_NONBLOCK: 如果pathname引用的是一个FIFO、一个块特殊文件或一个字符特殊文件,则此选项为文件的本次打开操作和后续的I/O操作设置非阻塞方式。 新建文件操作可以在open函数中加入O_CREAT副标志实现,创建新文件时还可以通过参数mode设置文件的权限。参数mode含义与3.2.5节中的st_mode相同。 系统调用open可以用来打开普通文件、块设备文件、字符设备文件、链接文件和管道文件,但只能用来创建普通文件,创建特殊文件需要使用特定的函数。 成功调用open后会返回一个文件描述符,若有错误发生则返回-1,并把错误代码赋给errno。详细的错误代码说明可以参考man手册。 2. creat系统调用 创建文件还可以通过系统调用creat来完成。creat的接口规范说明如表3.3所示。表3.3creat函数的接口规范说明 函数名称creat函数功能打开或创建文件头文件#include #include #include 函数原型int creat(const char pathname, mode_t mode);参数pathname: 要打开或创建的含路径的文件名 mode: 设置新文件的权限返回值大于或等于0的整数: 成功(即文件描述符) -1: 失败说明: creat只能以只写的方式打开创建的文件,creat无法创建设备文件,设备文件的创建要使用mknod函数。 creat函数的第一个参数pathname是要打开或创建的文件名,如果pathname指向的文件不存在,则创建一个新文件;如果pathname指向的文件存在,则原文件将被新文件覆盖。 第二个参数mode与open函数含义相同。creat相当于这样使用open: int open(const char pathname, (O_CREAE|O_WRONLY|O_TRUNC));成功调用creat后会返回一个文件描述符,若有错误发生则会返回-1,并把错误代码赋给errno。 3. close系统调用 系统调用close用来关闭一个已经打开的文件。在对文件操作完成之后,不再使用时,需要通过close函数关闭已经打开的文件并释放相应的资源,防止内核为继续维护它而付出不必要的代价。close系统调用接口规范说明如表3.4所示。表3.4close函数的接口规范说明 函数名称close函数功能打开或创建文件头文件#include 函数原型int close(int fd);参数fd: 即将要关闭的文件描述符返回值0: 成功 -1: 失败在示例程序3.1中,示范了如何使用open、creat、close函数。\[示例程序3.1 open_creat.c\] #include #include #include #include #include int main() { int fd; if((fd=open("test3_1.c",O_CREAT|O_EXCL,S_IRUSR|S_IWUSR))==-1) { //if((fd=creat("test3_1.c",S_IRWXU))==-1{ perror("open() error."); //printf("open:%s with errno:%d\\n",strerror(error),errno); exit(1); } else { printf("create file success\\n"); } close(fd); return 0; }示例程序3.1使用open系统调用在当前目录下创建一个名为test3_1.c的文件,且新文件的存取权限为所有者可读可写,随后关闭该文件。执行结果如下: root@ubuntu:~# ./open_creat fd: 3 create file success root@ubuntu:~# ls -l open_creat.c -rw-r--r-- 1 root root 3883月 27 15:47 open_creat.c从运行结果中可以看到,第一次执行该程序成功地创建了文件test3_1.c,且该文件的访问权限也符合预期,使用open函数打开文件test3_1.c返回的文件描述符是3,那么再次执行该程序结果又会是怎样呢?再次运行程序,结果如下: root@ubuntu:~# ./open_creat open() error.: File exists这是因为在调用open时,同时设置了O_CREAT和O_EXCL标志,则当文件存在时,open调用失败,系统将错误代码设置成EEXIST,表示文件已经存在。 把“perror("open() error.");”这一行代码注释掉,取消下一行的注释,重新编译并运行程序,可以得到如下结果: root@ubuntu:~# ./open_creat open:File exists with errno:17如果要从错误代码获取相应的错误描述,可以使用这种办法,使用时注意包含头文件errno.h。 将程序中调用open函数的代码注释掉,取消调用creat函数的注释,第二次执行该程序就不会报错了,因为对于creat而言,将会覆盖已存在的文件。 注意: 重复关闭一个已经关闭了的文件或尚未打开的文件是安全的。 3.3.2文件的读写1. read系统调用read系统调用用来从打开的文件中读取数据,其接口规范说明如表3.5所示。表3.5read函数的接口规范说明 函数名称read函数功能从指定文件中读取数据头文件#include 函数原型ssize_t read(int fd, void buf, size_t count);参数fd: 从文件fd读数据 buf: 指向存放读到的数据的缓冲区 count: 从文件fd中读取的字节数返回值实际读到的字节数: 成功(实际读到的字节数小于或等于count) 失败: -1说明:    read函数从文件描述符fd所指向的文件中读取count个字节的数据到buf所指向的缓冲区中。若参数count为0时,则read函数不会读取数据,只返回0。返回值表示实际读取到的字节数,如果返回为0,表示已到达文件尾或是无可读取的数据,此外文件读写指针会随读取到的字节移动。如果read函数顺利返回实际读到的字节数,最好能将返回值与count作比较,若返回的字节数比要求读取的字节数少,则有可能读到了文件末尾或者read函数被信号中断了读取过程,或是其他原因。 当有错误发生时返回-1,错误代码存入errno变量中,详细错误代码说明请参考man手册。 2. write系统调用 write系统调用用来将数据写入已打开的文件中,其接口规范说明如表3.6所示。表3.6write函数的接口规范说明 函数名称write函数功能将数据写入指定的文件头文件#include 函数原型ssize_t write(int fd, void buf, size_t count);参数fd: 将数据写入文件fd中 buf: 指向要写入fd的数据所在的缓冲区 count: 写入的字节数返回值实际写入的字节数: 成功 -1: 失败说明:   write函数将buf所指向的缓冲区中的count个字节数据写入到由文件描述符fd所指示的文件中。当然,文件读写指针也会随之移动。如果调用成功,write函数会返回写入的字节数。当有错误发生时则返回-1,错误代码存入errno中。详细的错误代码说明请参考man手册。 注意:  read函数和write函数实际读/写字节数要通过返回值来判断,参数count只是一个“愿望值”。  读/写操作都会对内核中表示文件偏移位置的f_pos起作用,文件的位置偏移量都会加上实际读写的字节数,不断地往后偏移。 3. fcntl系统调用 fcntl系统调用可以用来对已打开的文件描述符进行各种控制操作以改变已打开文件的各种属性,例如,可以重新设置文件的读、写、追加、非阻塞等标志。fcntl系统调用接口规范说明如表3.7所示。 说明: fcntl的功能依据cmd值的不同而不同,cmd具体有以下几种功能。 (1) F_DUPFD: 表示复制由fd指向的文件描述符。调用成功返回新的文件描述符,失败返回-1,错误代码存入errno中。表3.7fcntl函数的接口规范说明 函数名称fcntl函数功能文件控制头文件#include #include 函数原型int fcntl(int fd, int cmd); int fcntl(int fd, int cmd, long arg); int fcntl(int fd, int cmd, struct flock lock);参数fd: 要控制的文件的描述符 cmd: 控制命令字 变参: 根据不同的命令字而不同返回值根据不同的cmd,返回值不同: 成功 -1: 失败(2) F_DUPFD_CLOEXEC: 作用和F_DUPFD一样,但新复制的描述符的FD_CLOEXEC状态会被设置为1。 (3) F_GETFD: fcntl用来获取文件描述符的closeonexec标志。调用成功返回标志值,若此标志值的最后一位是0,则该标志没有设置,即意味着在执行exec相关函数后文件描述符仍保持打开;否则在执行exec相关函数时将关闭该文件描述符;调用失败返回-1。 (4) F_SETFD: fcntl用来将文件描述符的closeonexec标志设置为第三个参数arg的最后一位。成功返回0,失败返回-1。 (5) F_GETFL: fcntl用来获得文件打开的方式,即获取文件状态标志flags。成功返回标志值,失败返回-1。标志值的含义同open系统调用一致。 (6) F_SETFL: fcntl用来将文件打开的方式flags设置为第三个参数arg指定的方式。在Linux系统只能选择将flags设置为O_APPEND、O_NONBLOCK 或O_ASYNC,它们的含义同open系统调用一致。 下面通过示例语句来说明fcntl的基本用法。flags=fcntl(fd,F_GETFL,0);//获取文件的flags fcntl(fd,F_SETFL, flags); //设置文件的flags //将文件设置为非阻塞状态 flags=fcntl(fd,F_GETFL,0); flags|=O_NONBLOCK; fcntl(fd,F_SETFL,flags); //将文件设置为阻塞状态 flags=fcntl(fd,F_GETFL,0); flags&= ~ O_NONBLOCK; fcntl(fd,F_SETFL,flags);示例程序3.2使用fcntl函数设置和获取文件打开方式、文件状态标志flags。\[示例程序3.2 fcntl_demo.c\] #include #include #include #include #include void my_err(const char err_string,int line) { fprintf(stderr,"line: %d",line); perror(err_string); exit(1); } int main() { int ret; int access_mode; int fd; if((fd=open("hello.c",O_CREAT|O_TRUNC|O_RDWR,S_IRWXU))==-1) { my_err("open",__LINE__); } if((ret=fcntl(fd,F_SETFL,O_APPEND))<0) { my_err("fcntl",__LINE__); } if((ret=fcntl(fd,F_GETFL,0))<0) { my_err("fcntl",__LINE__); } access_mode=ret&O_ACCMODE; if(access_mode==O_RDONLY) { printf("hello.c access mode: read only"); } if(access_mode==O_WRONLY) {