Linux编程入门一文件操作

linux 编程入门

Linux/UNIX系统编程手册 【德】Michael Kerrisk著 着眼于Linux 2.6.x和GNU C语言库(glibc)版本2

Web站点  http://man7.org/tlpi  勘误 http://man7.org/tlpi/errata

为调试程序,或是研究程序的运作机制,可使用附录A所介绍的strace命令,对程序发起的系统调用进行跟踪

一般情况下,会将Linux内核可执行文件命名为/boot/vmlinuz或与之类似的路径名。早期的UNIX实现称其内核为UNIX,后续实现虚拟内存机制的UNIX系统中,其内核名称变更为vmunix。对Linux来说,文件名称中的系统名需要调整,以z替换linux末尾的x,意在表明内核是经过压缩的可执行文件。

系统的每个用户都拥有唯一的登录名和与之对应的整数型用户ID(UID)。系统密码文件/etc/password为每个用户都定义有一行记录,除了上述两项信息外,该记录还包含如下信息:组ID(用户所属第一个组的整数型组ID),主目录(用户登录后所居于的初始目录),登录shell(执行以解释用户命令的程序名称)。

一个用户可同时属于多个组,每个用户组对应着系统组文件/etc/group中的一行记录:组名(唯一的组名称),组ID(GID,与组相关的整数型ID),用户列表(隶属于该组的用户登录列表,通过上述密码文件记录的group ID字段未能标识出的该组其他成员,也在此列)

超级用户账号的用户ID为0,通常登录名为root。在一般的UNIX系统上,超级用户都可以凌驾于系统权限检查之上。无论对文件施以何种访问权限限制,超级用户都可以访问系统中的任何文件,也能发送信号干预系统运行的所有用户进程。

 

 

目录是一种特殊型的文件,内容采用表格形式,数据项包含文件名以及对相应文件的引用(文件名+i-node编号)。文件名+引用的组合称为链接。每个文件都可以有多条链接,因而可以有多个名称,在相同或不同的目录中出现。目录可包含指向文件或其他目录的链接。每个目录至少包含两条记录:.(指向目录自身的链接), ..(指向其上级目录的链接)。对于根目录而言,..是指向根目录自身的链接(/..等于/)

每个进程都有两个目录相关属性根目录及当前工作目录,分别用于为解释绝对路径名和相对路径名提供参照点。虽然一个进程能够打开一个目录,但却不能使用read()去读取目录的内容,也不能使用write()来改变一个目录的内容。若目录条目的i-node字段值为0,则表明该条目尚未使用。

当前工作目录:每个进程都有一个当前目录,是进程解释相对路径名的参照点。进程的当前目录继承自其父进程。可针对目录进行权限设置,读权限允许列出目录内容(即该目录下的文件名),写权限允许对目录内容进行修改(比如:添加,修改或删除文件名),执行权限允许对目录中的文件进行访问(但需受文件自身访问权限的约束)。

链接(硬链接)

文件i-node的存储信息列表,并未包含文件名,而通过目录列表内的一个映射来定义文件名称。(能够在相同或者不同目录中创建多个名称,每个均相同的i-node节点)。硬链接为目录中的一条记录:文件名+i-node编号。

打印索引或者大家俗称的inode号,我们可以使用-i选项,即“ls -li”(索引号会显示在第一列)。第三列显示指向该i-node的链接计数。执行ln abc xyz后,abc所指向i-node的链接计数升至2。名称abc和xyz均指向相同的i-node条目。

rm命令从目录列表中删除一文件,将相应i-node的链接计数减一,若链接计数因此而将为0,则还将释放该文件名所指代的i-node和数据块。同一文件的所有名字(链接)地位平等,在移除与文件相关的第一个名称后,物理文件继续存在,但只能通过另一文件名来访问其内容。

同级目录下不能创建同名硬链接

因为目录条目(硬链接)对文件的指代采用i-node编号,而i-node编号的唯一性仅在一个文件系统之内才能得到保障,所以硬链接必须与指代的文件驻留在同一文件系统中。不能为目录创建硬链接,从而避免出现令诸多程序陷入混乱的链接环路。

符号链接  (软链接)

类似于普通链接,符号链接给文件起一个别号。在目录列表中,普通链接是内容的“文件名+指针”的一条记录,而符号链接则是经过特殊标记的文件,内容包含了另一文件的名称。符号链接也是目录中的一条记录(文件名+i-node编号),不过其对应的i-node节点的数据块所指向的内容为该符号链接所指代的文件的文件名(可以是绝对路径,也可以是相对路径,解释相对符号链接时以链接本身的位置作为参照点)。

符号链接是由ln -s命令创建,ls -F命令的输出结果中会在符号链接的尾部标记@。文件的链接计数中并未将符号链接计算在内。如果移除符号链接所指向的文件名,符号链接本身还将继续存在,尽管无法再对其进行解引用(下溯)操作,将此类链接称为悬空链接。更有甚者,还可以为并不存在的文件名创建一个符号链接。因为符号链接指代一个文件名,而非i-node编号,所以可以用其来链接不同文件系统中的一个文件,且可以为目录创建符号链接。(工具命令有能力识别硬软链接,默认情况下不对软链接进行解引用,避免因使用软链接而陷入引用环路)

 

3种标准文件描述符(标准输入,标准输出,标准错误),在程序开始运行之前,shell代表程序打开这3个文件描述符,程序继承了shell文件描述符的副本。(在交互式shell中,这3个文件描述符通常指向shell运行所在终端)如果命令行指定对输入/输出进行重定向操作,那么shell会对文件描述符做适当的修改,然后再启动程序。

虽然stdin,stdout,stderr变量在程序初始化时用于指代其他进程的标准输入,标准输出,标准错误(按照惯例,UNIX系统shell把文件描述符0与进程的标准输入关联,文件描述符1与标准输出关联,文件描述符2与标准错误关联),但是调用freopen()库函数可以使这些变量指代其他任何文件对象。(作为其操作的一部分,freopen()可以在将流(stream)重新打开之际一并更换隐匿其中的文件描述符。比如:针对stdout调用freopen()后,无法保证stdout变量值仍然为文件描述符1)如果关闭了STDIN_FILENO(即关闭了文件描述符0),如果使用open()打开其他文件,由于文件描述符0未用,open()调用势必使用此描述符来打开文件。

 

获取文件信息:stat()

#include <sys/stat.h>
int stat(const char *pathname, struct stat *statbuf);
int lstat(const char *pathname, struct stat *statbuf);
int fstat(int fd, struct stat *statbuf);

系统调用stat()和lstat()无需对其所操作的文件拥有任何权限,但针对指定pathname的父母录要有执行(搜索)权限。

与常量S_IFMT相与,可从该字段中析取文件类型(Linux使用st_mode字段中的4位来标识文件类型位)。

使用utime()和utimes()改变文件时间戳

使用utime()或与之相关的系统调用集之一,可显式改变存储于文件i节点中的文件上次访问时间戳和上次修改时间戳。

#include <utime.h>
int utime(const char *pathname, const struct utimbuf *buf);

参数pathname用来标识欲修改时间的文件。若该参数为符号链接,则会进一步解除引用。参数buf既可以为NULL,也可为指向utimbuf结构的指针。

 

文件描述符和打开文件的关系:多个文件描述符指向同一打开文件

内核维护的数据结构:进程级的文件描述符表,系统级的打开文件表,文件系统的i-node表。

针对每个进程,内核为其维护打开的文件的描述符表(open file descriptor),该表的每一条目都记录了单个文件描述符的相关信息:控制文件描述符操作的一组标志(目前此类标志仅定义一个,即close-on-exec标志),对打开文件句柄的引用。

 

内核对所有打开的文件维护有一个系统级的描述符表(open file description table),也称打开文件表(open file table),并将表中各条目称为打开文件句柄(open file handle)。打开文件句柄存储了与一个打开文件相关的全部信息:当前文件偏移量(调用read()和write()时更新,或使用lseek()直接修改),打开文件时所使用的状态标志(即open()的flags参数),文件访问模式(如调用open()时所设置的只读模式、只写模式或读写模式),与信号驱动I/O相关的设置,对该文件i-node 对象的引用。

 

每个文件系统都会为驻留其上的所有文件建立一个i-node表。i-node信息:文件类型(例如,常见文件、套接字或FIFO)和访问权限,一个指针(指向该文件所持有的锁的列表),文件的各种属性(包括文件大小以及与不同类型操作相关的时间戳)。

 

 

进程A中,文件描述符1和20都指向同一个打开的文件句柄(标号为23),可能是通过dup(),dup2()或fcntl()形成。进程A的文件描述符2和进程B的文件描述符2指向同一打开的文件句柄(标号为73),可能是调用fork(),或通过UNIX域套接字将一个打开的文件描述符传递给另一个进程。进程A描述符0和进程B的描述符3分别指向不同的打开文件句柄,这些句柄均指向i-node表中相同条目(1976),即指向同一文件,可能是每个进程各自对同一文件发起open()调用,也可能同一个进程两次打开同一文件。

 

要点:1.两个不同的文件描述符,若指向同一打开文件句柄,将共享同一文件偏移量。因此,如果通过其中一个文件描述符来修改文件偏移量(由调用read(),write()或lseek()所致),那么从另一文件描述符中也会观察到这一变化。无论这两个文件描述符分属于不同进程,还是同属于一个进程,情况都是如此。2.要获取和修改打开的文件标志(例如,O_APPEND、O_NONBLOCK和O_ASYNC),可执行fcntl()的F_GETFL和F_SETFL操作,其对作用域的约束与上一条颇为类似。3.向形之下,文件描述符标志(亦即,close-on-exec标志)为进程和文件描述符所私有。对这一标志的修改将不会影响同一进程或不同进程中的其他文件描述符。

 

 

 

复制文件描述符

 

dup()调用复制一个打开的文件描述符,并返回一个新描述符,二者都指向同一打开的文件句柄。系统会保证新描述符一定是编号值最低的未用文件描述符。

#include <unistd.h>
int dup(int oldfd);

假设发起如下调用:newfd = dup(1); //标准输出(文件描述符1)

 

再假定在正常情况下,shell已经代表程序打开了文件描述符0,1,2,且没有其他描述符在用,dup()调用会创建文件描述符1的副本,返回的文件描述符编号为3。

 

如果希望返回文件描述符2,可以使用如下代码:

 

close(2);    //Frees file descriptor 2

 

newfd = dup(1);   //Should reuse file descriptor 2

 

只有当描述符0已经打开时,这段代码方可工作。如果想进一步简化上述代码,同时总是能获得所期望的文件描述符,可调用dup2()。

 

 

 

dup2()系统调用会为oldfd参数所指定的文件描述符创建副本,其编号由newfd参数指定。如果由newfd()参数所指定编号的文件描述符之前已经打开,那么dup2()会首先将其关闭。(dup2()调用会默认忽略newfd关闭期间出现的任何错误。故此,编码时更为安全的做法是:在调用dup2()之前,若newfd已经打开,则应显示调用close()将其关闭。)

#include <unistd.h>
int dup2(int oldfd, int newfd);

前述调用close()和dup()的代码可以简化为:dup2(1, 2); 若调用dup2()成功,则将返回副本的文件描述符编号(即newfd参数指定的值)。如果oldfd并非有效的文件描述符,那么dup2()调用将失败并返回错误EBADF,且不关闭newfd。如果oldfd有效,且与newfd值相等,那么dup2()将什么也不做,不关闭newfd,并将其作为调用结果返回。

 

 

文件描述符的正副本之间共享同一打开文件句柄所含的文件偏移量和状态标志。然而,新文件描述符有其自己的一套文件描述符标志,且其close-on-exec标志(FD_CLOEXEC)总是处于关闭状态。

 

 

dup3()系统调用完成的工作与dup2()相同,只是新增了一个附加参数flag,这是一个可以修改系统调用行为的位掩码。

#define _GNU_SOURCE
#include <unistd.h>
int dup3(int oldfd, int newfd, int flags);

目前,dup3()只支持一个标志O_CLOEXEC,这将促使内核为新文件描述符设置close-on-exec标志(FD_CLOEXEC)。dup3()系统调用始见于Linux 2.6.27,为Linux所独有。

 

 

 

 

/dev/fd 目录(SUSv3对/dev/fd特性未做规定,有些UNIX实现提供这一特性)

 

对于每个进程,内核都提供一个特殊的虚拟目录/dev/fd。该目录中包含/dev/fd/n形式的文件名(其中n是与进程中的打开文件描述符相对应的编号,例如/dev/fd/0就是对应进程标准输入)。打开/dev/fd目录中的一个文件等同于复制相应的文件描述符。

 

例如 fd =open("/dev/fd/1", O_WRONLY); 等同于 fd = dup(1);

/dev/fd实际上是一个符号链接欸,链接到Linux所专有的/proc/self/fd目录(Linux特有的/proc/PID/fd目录族的特例之一,此目录族中每一个目录都包含符号链接,与进程所打开的所有文件相对应)。

 

 

 

文件空洞

如果程序的文件偏移量已然跨越了文件结尾,然后再执行I/O操作,read()调用将返回0,表示文件结尾,write()调用可以在文件结尾后的任意位置写入数据。从文件结尾后到新写入数据间的这段空间被称为文件空洞。从编程角度看,文件空洞是存在字节的,读取空洞将返回以0(空字节)填充的缓冲区。然而,文件空洞不占用任何磁盘空间。直到后续,在文件空洞中写入数据,文件系统才会为之分配磁盘块。(文件空洞的优势在于稀疏填充的文件会占用较少的磁盘空间)在大多数文件系统中,文件空间的分配是以块为单位,如果空洞的边界落在块内,而非落在块边界上,则会分配一个完整的块来存储数据,块中的空洞相关部分则以空字节填充。

 

原子操作

以独占方式创建一个文件:当同时指定O_EXCL与O_CREAT作为open()的标志位时,如果要打开的文件已然存在,则open(0将返回一个错误。保证进程是打开文件的创建者,对文件是否存在的检查和创建文件属于同一原子操作。

向文件尾部追加数据:在打开文件时加入O_APPEND标志,可以保证文件偏移量的移动lseek()和数据写操作write()为原子操作。

 

 

 

 

在文件特定偏移量处的I/O:pread()和pwrite()

系统调用pread()和pwrite()完成与read()和write()相似的工作,只是前两者会在offset参数所指定的位置进行文件I/O操作,而非始于文件的当前偏移量处,且它们不会改变文件的当前偏移量。对pread()和pwrite()而言,fd所指代的文件必须是可定位的(即允许对文件描述符执行lseek()调用)。

#include <unistd.h>
ssize_t pread(int fd, void *buf, size_t count, off_t offset);
ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);

pread()调用等同于将如下调用纳入同一原子操作:

off_t orig;

orig = lseek(fd, 0, SEEK_CUR);  // Save current offset

lseek(fd, offset, SEEK_SET);

s = read(fd, buf, len);

lseek(fd, orig, SEEK_SET); // Restore original file offset

进程下辖的所有线程将共享同一文件描述符表。意味着每个已打开文件的文件偏移量为所有线程所共享。当调用pread()或pwrite()时,多个线程可以同时对同一文件描述符执行I/O操作,且不会因其他线程修改文件偏移而受影响。如果使用read()或write(),那么将引发竞争状态。当多个进程的文件描述符指向相同的打开文件句柄时,使用pread()和pwrite()系统调用同样能够避免进程间出现竞争状态。

 

 

 

分散输入和集中输出(Scatter-Gather I/O)

readv()和writev()系统调用分别实现了分散输入和集中输出的功能。

#include <sys/uio.h>
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);

这些系统调用并非只对单个缓冲区进行读写操作,而是一次即可传输多个缓冲区的数据。数组iov定义了一组用来传输数据的缓冲区。整型数iovcnt则指定了iov的成员个数。iov中的每个成员都是如下形式的数据结构。

struct iovec {
  void *iov_base; /* Start address of buffer */
  size_t iov_len; /* Number of bytes to transfer to/from buffer */
};

分散输入:readv()系统调用实现了分散输入功能,从文件描述符fd所指代的文件中读取一片连续的字节,然后将其散置于iov指定的缓冲区中。这一散置动作从iov[0]开始,依次填满每个缓冲区。原子性是readv()的重要属性。从调用进程的角度来看,当调用readv()时,内核在fd所指代的文件与用户内存之间一次性地完成数据转移。即使有另一进程(或线程)与其共享同一文件偏移量,且在调用readv()的同时企图修改文件偏移量,readv()所读取的数据仍将是连续的。调用readv()成功将返回读取的字节数,若文件结束EOF将返回0.调用者必须对返回值进行检查,以验证读取的字节数是否满足要求。若数据不足以填充所有缓冲区,则只会占用部分缓冲区,其中最后一个缓冲区可能只存有部分数据。

 

集中输出:writev()系统调用实现了集中输出,将iov所指定的所有缓冲区中的数据拼接起来,然后以连续的字节序列写入文件描述符fd指代的文件中。对缓冲区中数据的集中始于iov[0]所指定的缓冲区,并按数组顺序展开。writev()调用也属于原子操作,即所有数据将一次性地从用户内存传输到fd指代的文件中。因此,在向普通文件写入数据时,writev()调用会把所有的请求数据连续写入文件,而不会在其他进程(或线程)写操作的影响下(即不受其他进(线)程改变文件偏移量的影响)分散地写入文件(readv()和writev()会改变打开文件句柄的当前文件偏移量)。如同write()调用,writev()调用也存在部分写问题。

 

在指定的文件偏移量处执行分散输入/集中输出

 

Linux 2.6.30版本新增了两个系统调用:preadv()、pwritev(),将分散输出/集中输出和于指定文件偏移量处的I/O二者集于一身。它们并非标准的系统调用,但获得现代BSD的支持。

#define _BSD_SOURCE
#include <sys/uio.h>
ssize_t preadv(int fd, const struct iovec *iov, int iovcnt, off_t offset);
ssize_t pwritev(int fd, const struct iovec *iov, int iovcnt, off_t offset);

preadv()和pwritev()系统调用所执行的任务与readv()和writev()相同,但执行I/O的位置将由offset参数指定(类似于preadv()和pwritev()系统调用)。

 

 

 

 

截断文件:truncate()和ftruncate()系统调用

#include <unistd.h>
int truncate(const char *pathname, off_t length);
int ftruncate(int fd, off_t length);

将文件大小设置为length参数指定的值。若文件当前长度大于参数length,调用将丢弃超出部分,若小于参数length,调用将在文件尾部添加一系例空字节或是一个文件空洞。

 

 

 

 

创建临时文件

 

基于调用者提供的模板,mkstemp()函数生成一个唯一文件名并打开该文件,返回一个可用于I/O调用的文件描述符。

#include <stdlib.h>
int mkstemp(char *template)

 

模板参数采用路径名形式,其中最后6个字符必须为XXXXXX。这6个字符将被替换,以保证文件名的唯一性,且修改后的字符串将通过template参数返回(因为会对传入的template参数进行修改,所以必须将其指定为字符数组)。文件拥有者对mkstemp()函数建立的文件拥有读写权限(其他用户没有任何操作权限)且打开文件时使用了O_EXCL标志,以保证调用者以独占方式访问文件。使用unlink系统调用将该文件名删除(18.3节)。

 

tmpfile()函数会创建一个名称唯一的临时文件,并以读写方式将其打开。(打开该文件时使用了O_EXCL标志,以防一个可能性极小的冲突,即另一个进程已经创建了一个同名文件。)

#include <stdio.h>
FILE *tmpfile(void);

tmpfile()函数执行成功,将返回一个文件流供stdio库函数使用。文件流关闭后将自动删除临时文件。为达到这一目的,tmpfile()函数会在打开文件后,从内部立即调用unlink()删除该文件名。

 

文件控制操作:fcntl()

#include <fcntl.h>
int fcntl(int fd, int cmd, ...);

打开文件的状态标志

fcntl()的用途之一是针对一个打开的文件,获取或修改其访问模式和状态标志(这些值是通过指定open()调用的flag参数来设置)。要获取这些设置,应将fcntl()的cmd参数设置为F_GETFL。

int flags, accessMode;
flags = fcntl(fd, F_GETFL);  /*Third argument is not required*/
if(flags == -1)
  errExit("fcntl");
if(flags & O_SYNC)
  printf("writes are synchronized\n");

flags & O_SYNC测试文件是否以同步写方式打开

SUSv3规定:针对一个打开的文件,只有通过open()或后续fcntl()的F_SETFL操作,才能对该文件的状态进行设置。但,Linux实现与标准有所偏离:如果一个程序编译时采用了打开大文件技术,那么使用F_SETFL命令获取文件状态标志时,标志中将总是包含O_LARGEFILE标志。

判定文件的访问模式有一点复杂,这是因为O_RDONLY(0),O_WRONLY(1),O_RDWR(2)这3个常量并不与打开文件状态标志中的单个比特位对应。要判定访问模式,需要使用掩码O_ACCMODE与flags相与,将结果与3个常量进行比对。

accessMode = flags & O_ACCMODE;
if (accessMode == O_WRONLY| accessMode == O_RDWR)
  printf("file is writable\n");

可以使用F_SETFL命令修改打开文件的某些状态标志(O_APPEND,O_NONBLOCK,O_NOATIME,O_ASYNC,O_DIRECT)。系统将忽略对其他标志的修改操作。(有些其他的UNIX实现允许fcntl()修改其他标志,如O_SYNC)

 

fcntl()的F_DUPFD操作是复制文件描述符的接口:

 

newfd = fcntl(oldfd, F_DUPFD, startfd);

该调用为oldfd创建一个副本,且将使用大于等于startfd的最小未用值作为描述符编号。该调用还能保证新描述符(newfd)编号落在特定的区间范围内。Linux从2.6.24开始支持fcntl()用于复制文件描述符的附加命令:F_DUPFD_CLOEXEC。该标志不仅实现了与F_DUPFD相同的功能,还为新文件描述符设置close-on-exec标志。同样,此命令之所以得以实现,其原因也类似于open()调用中的O_CLOEXEC标志。

 

 

 

通用I/O模型外的操作:ioctl()

#include <sys/ioctl.h>
int ioctl(int fd, int request, ... /* argp */)

fd参数为某个设备或文件已打开的文件描述符,request参数指定了将在fd执行的控制操作。ioctl()根据request的参数值来确定argp所期望的类型。通常情况下,argp是指向整数或结构的指针。

 

 

混合使用库函数和系统调用进行文件I/O:

#include <stdio.h>
int fileno(FILE *stream);
FILE *fdopen(int fd, const char *mode);

 

控制文件I/O的内核缓冲

 

 

UNIX环境高级编程 【美】W.Richard Stevens著 第3版 基于Linux 3.2.0

Web站点 www.apuebook.com

在Linux 2.4 和 Linux 2.6之间,线程的实现变为Native POSIX Thread Library(NPTL)。

文件I/O

不带缓冲指的是每个read和write都调用内核中的一个系统调用。

 

 

相关推荐
©️2020 CSDN 皮肤主题: 代码科技 设计师:Amelia_0503 返回首页