我们现在正式看一下系统调用API。文件是一个很好的起点,因为它们是UNIX的核心。本章的重点是用于执行文件输入和输出的系统调用。
我们会介绍 文件描述符(file descriptor) 的概念,然后看一下I/O模型中用到的系统调用。这些系统调用用于打开和关闭文件,读取和写入数据。
我们主要介绍磁盘文件的I/O。但是这里涉及到的大部分知识点与后面章节相关,因为执行I/O的系统调用适用于所有文件类型,如pipes和终端。
第5章对本章的文件I/O进行更深入细致的讨论。主要讲文件I/O的缓冲,缓冲足够的复杂,所以值得专门拿出一个章节进行讨论。第13章讲内核中和stdio库中的I/O缓冲。
第4章 文件IO:通用的IO模型-LMLPHP

4.1 概述 (Overview)

所有用于执行I/O的系统调用都会涉及到使用 文件描述符file descriptor 来打开文件。文件描述符是一个(很小的)非负整数。文件描述符适用于打开文件的所有类型,包括管道、FIFOs、socket、终端和普通文件。每个进程都有自己的一组文件描述符。
按照惯例,大部分程序都应该可以使用 Table 4.1中列出的三种标准文件描述符。程序会继承shell的文件描述符的拷贝。如果在一个命令行中指定了I/O重定向(rediresction),那么shell会确保程序启动之前文件描述符都会被合适的修改。

当在程序中涉及到这些文件描述符时,我们可是使用数字(0,1,或2)或者更好的是使用在 <unistd.h> 中定义的 POSIX标准名称

以下是执行文件I/O的四个核心系统调用(编程语言和软件包,一般只有通过I/O库间接采用这些调用):

  • fd = open(pathname, flags, mode) 打开pathname中指定的文件,返回一个文件描述符,用于在随后的调用中,指代打开的文件。如果文件不存在,open()会根据flags参数,决定是否创建它。flags参数还指定了将要打开的文件是用于读的、写的还是同时可读写的。如果该文件是这个调用新创建的,mode参数用于设定该文件的权限。如果该文件不是新创建的,mode参数不起作用或者可以被忽略。
  • numread = read(fd, buffer, count) 用于从fd所指向的已打开的文件中读取最多count个字节,并将这些字节存储到buffer中。read()这个调用返回实际读取字节的个数。如果没有更多字节可以读取(例如,遇到end-of-file,EOF),那么read()就返回0。
  • numwritten = write(fd, buffer, count) 用于将count个字节从buffer中写到fd所指向的已打开的文件中。write()这个调用返回实际写入的字节个数,可能小于count。
  • status = close(fd) 在所有I/O完成的情况下调用,用于释放文件描述符fd和它相关的内核资源。

在我们讲解这些系统调用的细节之前,我们使用Listing 4-1来简短地展示一下这些系统调用的用法。

#include <sys/stat.h>
#include <fcntl.h>
#include "lib/tlpi_hdr.h"

#ifndef BUF_SIZE        /* Allow "cc -D" to override definition */
#define BUF_SIZE 1024
#endif

int main(int argc, char *argv[])
{
    int inputFd, outputFd, openFlags;
    mode_t filePerms;
    ssize_t numRead;
    char buf[BUF_SIZE];

    if (argc != 3 || strcmp(argv[1], "--help") == 0)
        usageErr("%s old-file new-file\n", argv[0]);

    /* Open input and output files */
    inputFd = open(argv[1], O_RDONLY);
    if (inputFd == -1)
    	errExit("opening file %s", argv[1]);

    openFlags = O_CREAT | O_WRONLY | O_TRUNC;
    filePerms = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP |
                S_IROTH | S_IWOTH;      /* rw-rw-rw- */
    outputFd = open(argv[2], openFlags, filePerms);
    if (outputFd == -1)
        errExit("opening file %s", argv[2]);

    /* Transfer data until we encounter end of input or an error */
    while ((numRead = read(inputFd, buf, BUF_SIZE)) > 0)
        if (write(outputFd, buf, numRead) != numRead)
            fatal("couldn't write whole buffer");
    if (numRead == -1)
        errExit("read");

    if (close(inputFd) == -1)
        errExit("close input");
    if (close(outputFd) == -1)
        errExit("close output");

    exit(EXIT_SUCCESS);
}

4.2 I/O的通用性 (Universality of I/O)

UNIX I/O模型的一个显著性的特征是I/O的通用性(universality)。这意味着以下四种系统调用–open()、read()、write()和close()可用于文件的I/O,包括设备(如终端)。如果我们使用这些系统调用写一个程序,这个程序可用于所有类型的文件。
之所以I/O具有通用性,是因为每个文件系统和设备驱动器的实现使用了相同的一组I/O系统调用。因为文件系统或设备的细节是由内核内部处理的,所以我们在写应用程序时一般可以忽略特定设备的因素。当需要访问系统或设备的特定特征时,程序可以使用ioctl()系统调用,它提供了超出I/O模型通用性范围的特征。

4.3 使用 open() 打开文件 (opening a File: open())

open() 系统调用可以打开一个的文件或者一个新文件。

#include <sys/stat.h>
#include <fcntl.h>
// Returns file descriptor on success, or -1 on error
int open(const char *pathname, int flags, ... /*mode_t mode*/);

被打开的文件的路径是由pathname参数确定的。如果pathname是一个符号链接,会被间接引用(dereferenced)(译者注:即引用到目标文件)。如果执行成功,open()会返回一个文件描述符,用于指向这个文件,随后的系统调用就可以使用这个文件描述符来访问这个文件。如果执行时出现错误,open()就会返回 -1,并且会赋予 errno 相应的值。
flags参数是一个位屏蔽(bit mask) 用于指定文件的 访问模式(access mode), 使用Table4-2的的其中一个常量。

当open()创建一个新文件时,mode位屏蔽参数用于给文件赋予权限(mode是一个mode_t数据类型,它是SUSv3中定义的整型类型)。如果open()调用的flags中不指定O_CREATE,mode就会被忽略。
第4章 文件IO:通用的IO模型-LMLPHP

我们会在章节15.4详细描述文件权限。随后,我们可以看到新文件上设置的权限不仅仅跟mode参数有关,而且还跟process umask(章节15.4.6)、父级目录的默认访问控制列表(access control list)有关。同时,我们注意到mode参数可以被设置成一个数字(一般是八进制)或者更好的是使用Table15-4中列出的由或操作(|)连接的位屏蔽常量。
Listing 4-2 的例子中使用了open()以及刚才简要描述的flags位:

//Listing 4-2: Examples of the use  of open()
fd = open("startup", O_RDONLY);
if (fd == -1)
	errExit("open");
/* open new or existing file for reading and writing, truncating to zero
bytes; file permssions read+write for woner, nothing for all others
*/
fd = opne("myfile", O_RDWR | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR);
if (fd == -1)
	errExit("open");

/* Open new or existing file for writing; writes should always append to end of file */
fd = open("w.log", O_WRONLY | O_CREATE | O_TRUNC | O_APPEND, S_IRUSR, S_IWUSR);

if (fd == -1)
	errExit("open");

File descriptor number returned by open()

SUSv3中规定:如果 open() 执行成功,它会确保使用该进程中未使用的最小文件描述符。我们可以使用这个特点来确保使用特定的文件描述符来打开一个文件。例如,以下代码确保使用标准输入(文件描述符0)来打开一个文件:

int open_file_with_df_0()
{
    /* Close file descriptor 0. if not close, the result will be 3 */
    if (close(STDIN_FILENO) == -1)
        errExit("close", O_RDONLY);
    int fd = open("/data/work/luciuschina/c_study/main.c", O_RDONLY);
    if (fd == -1)
        errExit("open");
    printf("%d",fd);
    return 0;
}

因为文件描述符0未被使用,open()就会使用描述符0来打开这个文件。在章节5.5,我们会使用dup2()和fcntl()来实现相似的结果,但是会使用更加灵活的方式来对文件描述符进行控制。在那节中,还会展示一个例子,告诉我们为什么对文件描述符的控制是很有用的。

4.3.1 The *open() flags * Argument

在Listing 4-2的例子中,我们介绍了flags中的一些位(O_CREAT, O_TRUNC和O_APPEND)以及文件访问模式。现在我们更加详细的看下flags参数。Table 4-3总结了flags中可用于位或操作( | )的所有常量。最后一列表示这些常量是在SUSv3还是SUSv4中标准化的。
第4章 文件IO:通用的IO模型-LMLPHP
Table 4-3中的常量可以分为以下组:

  • 文件访问模式标志 (file access mode flags):它们是O_RDONLY、O_WRONLY和O_RDWR标志(flags)。它们可以使用 fcntl() F_GETFL操作恢复(章节5.3)。
  • 文件创建标志 (file creation flags):它们是Table 4-3的第二部分描述的flags。它们用于控制open()调用的各个方面以及随后I/O操作的选项。这些操作不能被恢复或改变。
  • 打开文件状态标志(Open file status flags) : 这是Table 4-3剩下的flags。它们可以使用fcntl()的F_GETFL和F_SETFL操作(Section 5.3)来恢复和修改。这种flags有时简称为文件状态标志(file status flags)

flags常量的详细介绍如下:

  • O_APPEND: 写入的数据追加到文件的末尾。我们会在章节(5.1)讨论这个flag的重要性。
  • O_ASYNC: 当open()返回的文件描述符可以进行I/O时,产生一个signal。这个特点被称为 信号驱动I/O(signal-drivern I/O) ,只有特定文件类型(例如终端、FIFOs和sockets)才有。在Linux中调用open()时指定O_ASYNC不会产生影响。想要开启信号驱动I/O功能,必须使用fcntl() F_SETFL操作设置这个flag(章节5.3)。
  • O_CLOEXEC (从Linux2.6.23开始):为新的文件标识符开启close-on-exec flag(FD_CLOEXEC)。我们会在章节27.4描述FD_CLOEXEC flag。使用O_CLOEXEC flag可以避免程序进行额外的fcntl() F_SETFD和F_SETFD操作来设置close-on-exec flag。使用后者技术可以避免多线程程序竞态(race condition)的发生。这种竞态会发生在:当一个程序打开一个文件描述符,然后尝试将它标识为close-on-exec,同时另一线程进行fork(),然后一个任意程序的exec() (假设第二个线程在第一个线程打开文件描述符和使用fcntl()设置close-on-exec flag期间同时管理fork()和exec())。这种竞态会导致打开的文件描述符会无意地传入不安全的程序 (我们会在章节5.1对竞态有更多的描述)。
  • O_CREAT:如果文件不存在,就会创建一个新的、空的文件。该flag是高效的,即使被打开的文件只用于读取。如果我们指定O_CREAT,那么必须在open()调用中提供一个mode参数。否则,新文件的权限会被设置成栈中的一个随机值。
  • O_DIRECT: 允许文件I/O绕过buffer缓存。这个特点会在章节13.6中描述。
  • O_DIRECTORY:如果pathname不是一个目录,返回一个错误(errno等于ENOTDIR)。该flag是专门为实现 opendir() 而设计的。
  • O_DSYNC (从Linux 2.6.33开始):根据同步I/O数据完整性完成的需求执行文件写入。请看章节13.3中内核I/O缓存。
  • O_EXCL:该flag用于结合O_CREAT来表示如果该文件已经存在,就不应该被打开。如果文件已经存在了,Open就会失败,errno被设置成EEXIST。换句话说,该调用确保是该进程创建了这个文件。文件存在的检查和创建是原子的(atomically)。我们会在章节5.1讨论原子(atomicity)的概念。如果O_CREAT和O_EXCL都在flags中被设定,如果pathname是一个符号链接,那么Open()就会失败(errno是EEXIST)。SUSv3需要这种行为,这样特权应用可以在一个已知的地方创建一个文件,而不会存在符号链接引起的在不同位置(例如一个系统目录)创建文件,引起安全隐患的可能性。
  • O_LARGEFILE: 支持打开一个很大的文件。该flag在32位系统中使用,为的是可以打开大文件。尽管不在SUSv3中指定,但是在某些UNIX实现中可以找到O_LARGEFILE flag。在64位Linux系统中,这个flag不会产生影响。更多信息请看章节5.10。
  • O_NOATIME (从Linux 2.6.8开始):当从文件读取数据时,不更新文件的最近访问时间 (在章节15.1中会描述st_atime字段)。想要使用该flag,调用进程的 必须与文件的拥有者相匹配,或者该进程必须是特权的(CAP_FOWNER);否则open()会失败,错误是EPERM。
  • O_NOCTTY: 如果被打开的文件是一个终端设备,防止它称为控制终端(controlling termianl)。控制终端会在章节34.4中讨论。如果被打开的文件不是一个终端,该flag不产生效果。
  • O_NOFOLLOW:通常情况下,如果open()的是一个符号链接,会间接引用到目标文件。但是如果指定O_NOFOLLOW flag,那么open()的是符号链接时就会失败(errno被设成ELOOP)。这个flag很有用,特别是在具有特权的程序中,会确保open()不会打开一个符号链接。
  • O_NONBLOCK:以非阻塞模式打开文件。请看章节5.9。
  • O_SYNC:为同步的 (synchronous) I/O打开文件。请看章节13.3中内核I/O缓存的讨论。
  • O_TRUNC:如果已经存在的文件是一个普通文件,那么删除存在的数据(清空文件),文件长度变为零。在Linux中,无论是为了读或写而打开文件,truncation可能发生。

4.3.2 Errors from open()

如果尝试打开文件的过程中发生错误,open()会返回-1,并且errno会标识错误的原因。下面是一些可能发生的错误:

  • EACCES:在flags指定的模式下,文件权限不允许调用进程打开文件。另外,因为目录权限,文件不能被访问,或者文件不存在,且不能被创建。
  • EISDIR:想要打开的文件是一个目录,调用者尝试打开文件并写入。这是不被允许的。
  • EMFILE:打开文件描述符的数量达到了进程的资源限制 (章节36.3描述的RLIMIT_NOFILE)。
  • ENFILE:打开文件描述符的数量达到了系统范围(system-wide)的限制。
  • ENOENT:想要打开的文件不存在,并且没有指定O_CREAT,或者指定了O_CREAT,但是pathname的目录不存在,或者是一个指向不存在的pathname的符号链接(a dangling link)
  • EROFS:想要打开的文件是文件系统中的只读文件,但是调用者尝试打开文件并用于写。
  • ETXTBSY:想要打开的文件是一个可执行文件(一个程序),并且当前正在执行。与正在执行程序相关的文件不允许被修改(想要修改必须先终止这个程序)。
    当我们随后描述其他的系统调用或者库函数时,我们一般不会列出上述的这些可能存在的错误。上面的列表是不完整的,open()方法失败的更多原因可以通过open(2)手册页查看。

4.3.3 The creat() System Call

在早期的UNIX系统实现中,open()只有两个参数,并且不能用于创建新文件。而creat()系统调用用于创建和打开一个新文件。

#include <fcntl.h>
//返回一个文件描述符,发生错误时返回-1
int creat(const char *pathname, mode_t mode);

creat()系统调用以给定的pathname,创建和打开一个新的文件,如果这个文件已经存在,打开文件,并且清空文件中的数据。creat()返回一个文件描述符,可以在随后的系统调用中使用。调用creat()与下面的open()等价:

fd = open(pathname, O_WRONLY | O_CREAT | O_TRUNC, mode)

因为open() flag参数提供了对打开文件的更强大的控制(例如,使用O_RDWR而不是O_WRONLY)。creat()现在已经是过期的了,尽管在旧的程序中仍然可以看到 。

4.4 Reading from a File:read()

read()系统调用从描述符fd指向的打开的文件中读取数据。

# include <unistd.h>
//返回读取的字节数,EOF时返回0,发生错误时返回-1
ssize_t read(int fd, void *buffer, size_t count);

count 参数指定了可以读取的最大字节数。(size_t数据类型是一种无符号整数类型)buffer参数提供了输入数据存储到的buffer内存的地址。这块buffer的长度至少是count个字节。

4.5 Writing to a File:write()

write()系统调用将数据写入到一个打开的文件。

#include <unistd.h>
//返回写入的字节数,当发生错误时返回-1
ssize_t write(int fd, void *buffer, size_t count);

write()的参数与read()的相似:buffer是将要被写入数据的地址;count是从buffer中写入的字节数,fd指向数据将要写入文件的文件描述符。
执行成功时,write()返回实际写入的字节数,这可能小于count。对于磁盘文件来说,可能的原因是partial write时磁盘满了或者是达到了file sizes这个进程资源限制(章节36.3中描述的RLIMIT_FSIZE)。
当在一个磁盘文件执行I/O时,从write()成功返回不能确保数据已经被写入到磁盘中了,因为内核执行了 磁盘I/O缓冲 ,为的是减少磁盘活跃性和加快write()调用 (我们会在第13章来考虑这些细节)。

4.6 Closing a File: close()

close() 系统调用关闭一个打开的文件描述符,释放文件描述符,供给进程随后重用。当进程终止时,所有打开的文件描述符会自动关闭。

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

明确地关闭不再需要的文件描述符是一个很好的习惯,因为这使得我们的代码可读性更高,程序可靠性更高。此外,文件描述符是一种消耗性资源,所以关闭文件描述符失败可能会导致进程耗尽描述符资源。当程序是一个长时间存活的,且会处理很多文件的程序时,这个问题尤其严重。
像其他系统调用那样,对close()的调用应该加上错误检查代码,例如:

if (close(fd) == -1)
	errExit("close");

4.7 Changing the File Offset: lseek()

对于每个打开的文件,内核会记录一个 文件偏移量(file offset) ,有时也称为 读写偏移量read-write offset 或者 指针(pointer)。这是文件中的位置,指向下一次read()或者write()开始的位置。文件的偏移量表示成有序的字节的位置,文件中第一个字节的偏移量是0
当打开文件时,文件的偏移量会指向文件的起始位置。后面的每一次read()或者write()都会自动调整偏移量。所以在读取或者写入字节后,文件偏移量会指向下一个字节。因此,连续的read()和write()调用会在文件中按序执行。
lseek()系统调用 可以根据 offsetwhence 指定的值调整 fd 文件描述符所指向文件的偏移量。

#include <unistd.h>
//如果执行成功,返回文件偏移量,发生错误则返回-1
off_t lseek(int fd, off_t offset, int whence);

offset 参数用于指定一个值(off_t数据类型是有符号的整型)。whence 参数有以下值:

  • SEEK_SET:文件偏移量被条成为文件起始位置(文件偏移量为0)加上offset
  • SEEK_CUR:文件偏移量被调整为当前文件偏移量加上offset
  • SEEK_END: 文件偏移量被调整为文件的size加上offst。

Figure 4-1 展示了whence参数如何被解析。

第4章 文件IO:通用的IO模型-LMLPHP
如果whence是SEEK_CUR或者SEEK_END,offset可能是负数的或者正数的;对于SEEK_SET,offset必须是非负数。
一次成功的lseek()调用会返回一个新的偏移量。下面语句会得到文件偏移量的当前位置,而不会修改它:

curr = lseed(fd, 0, SEEK_CUR)

以下是一些lseek()调用的例子,以及相应的注释,表示文件偏移量会移动到哪里:

lseek(fd, 0, SEEK_SET)/*文件起始位置*/
lseek(fd, 0, SEEK_END)/*文件末尾的下一个字节*/
lseek(fd, -1, SEEK_END)/*文件的最后一个字节*/
lseek(fd, -10, SEEK_CUR);                       /*当前位置的往前移10个字节的位置*/
lseek(fd, 10000, SEEK_END);                     /*文件最后一个字节的位置再往后移10001个字节*/

调用lseek() 仅仅调整了内核中的与文件描述符相关的文件偏移量记录。不会引起物理设备的访问。
在5.4节中我们会更详细地描述文件偏移量、文件描述符和打开文件之间的关系。
lseek()不能运用于所有的文件类型。不能在pipe、FIFO、socket或者terminal上使用lseek()。lseek()失败时,errno被设置成ESPIPE。另外,在设备上使用lseek()是可能的。例如,可以在磁盘上寻找(seek)到特定的位置。

File holes (文件洞)

如果一个程序seek超过了文件的末尾会发生什么,仍旧会执行I/O吗?调用read()会返回0,表示文件末尾。令人奇怪的是,在超过文件末尾的任意点,都可能写入数据。
从文件末尾到新写入字节之间的 空间(space) 被称为 file hole(文件洞) 。从编程的角度看,hole中的字节是存在的,从hole中读取会返回包含0字节(null字节)的缓冲。
然而,File holes不占用任何磁盘空间。文件系统不会为hole分配任何磁盘块(disk blocks)直到在后面的某个时刻有数据被写入到文件中。File holes的主要优点是一个稀疏的填充文件(a sparsely populated flie)会消耗较少的磁盘空间,因为null bytes不占用任何磁盘空间。核心的dump文件(章节22.1)是一种包含大量holes的文件。
holes的存在意味着一个文件名义上的大小(nominal size)可能比它使用的磁盘存储的量要大。将字节写入file hole的中间会降低可用磁盘空间的数量,因为内核分配块(block)来填补hole,尽管文件的大小可能不会改变。这种场景是不常见的,但是需要值得注意。
章节 14.4 会描述文件中的holes是如何表示的。在章节15.1会描述stat()系统调用,它会告诉我们一个文件当前的大小,以及实际为这个文件分配的块的数量。

Example program

Listing 4-3 展示了lseek() 以及read()和write()的用法。该程序的第一个命令行参数是将要打开的文件的名称。剩下的参数指定了将要在文件上执行的I/O操作。

#include <sys/stat.h>
#include <fcntl.h>
#include <ctype.h>
#include "../lib/tlpi_hdr.h"
// fileio/seek_io.c
int main(int argc, char *argv[])
 {
    size_t len;
    off_t offset;
    int fd, ap, j;
    char *buf;
    ssize_t numRead, numWritten;
    if (argc < 3 || strcmp(argv[1], "--help") == 0)
    usageErr("%s file {r<length> | R<length> | w<string> | s<offset>}...\n", argv[0]);
    fd = open(argv[1], O_RDWR | O_CREAT ,
              S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH); /*rw-rw-rw-*/
    if (fd == -1)
        errExit("open");
    for (ap = 2; ap < argc; ap++)
    {
        switch (argv[ap][0]) {
            //Display bytes at current offset, as text
            case 'r':

            //Display bytes at current offset, in hex
            case 'R':
                len = getLong(&argv[ap][1], GN_ANY_BASE, argv[ap]);
                buf = malloc(len);
                if (buf == NULL)
                    errExit("malloc");
                numRead = read(fd, buf, len);
                if (numRead == -1)
                    errExit("read");
                if (numRead == 0)
                {
                    printf("%s: end-of-file\n", argv[ap]);
                }
                else
                {
                    printf("%s:", argv[ap]);
                    for (j = 0; j < numRead; j++)
                    {
                        if (argv[ap][0] == 'r')
                            printf("%c", isprint((unsigned char) buf[j]) ? buf[j] : '?');
                        else
                            printf("%02x ", (unsigned int) buf[j]);
                    }
                    printf("\n");
                }
                free(buf);
                break;

            // Write string at current offset
            case 'w':
                numWritten = write(fd, &argv[ap][1], strlen(&argv[ap][1]));
                if (numWritten == -1)
                    errExit("write");
                printf("%s: wrote %ld bytes\n", argv[ap], (long) numWritten);
                break;
            case 's':
                offset = getLong(&argv[ap][1], GN_ANY_BASE, argv[ap]);
                if (lseek(fd, offset, SEEK_SET) == -1)
                    errExit("lseek");
                printf("%s: seek succeeded\n", argv[ap]);
                break;
            default:
                cmdLineErr("Argument must start with [rRws]: %s\n", argv[ap]);\
        }
    }
    return 0;
}

上面代码编译后的文件名为seek_io,我们看一下从file hole中读取字节会发生什么:


$ touch tfile                                    Create new,empty file
$ ./seek_io tfile s100000 wabc     Seek to offset 100000,write “abc”
s100000: seek succeeded
wabc: wrote 3 bytes
$ ls -l tfile                                       Check size of file
-rw-r–r-- 1 mtk users 100003 Feb 10 10:35 tfile
$ ./seek_io tfile s10000 R5            Seek to offset 10,000, read 5 bytes from hole
s10000: seek succeeded
R5: 00 00 00 00 00                          Bytes in the hole contain 0


4.8 Operations Outside the Universal I/O Model:ioctl()

ioctl() 系统调用是一种用于执行 和 操作的通用机制,但是正如本章之前提到的,它不具有I/O模型的通用性(即有些文件类型没有这个系统调用)。

#include <sys/ioctl.h>
//根据request,返回正确的结果值,发生错误时返回-1
int ioctl(int fd, int request, ... /*argp*/);

fd参数是文件或设备的文件描述符,需要执行的控制操作在request中指定。特定设备(device-specific)的头文件中定义的常量可以传入到request参数中。
正如标准的C省略符号(…)所示,ioctl()中的第三个参数argp可以是任意类型。request参数中的值使得ioctl()能够知道argp这个值的类型。一般来说,argp是一个指向整型或者结构体(structure)的指针。在有些情况下,不需要使用到这个参数。
在后面的章节,我们可以看到ioctl()的一些用处(例如章节15.5)。

4.9 summary

为了对普通文件执行I/O操作,我们必须首先通过使用open()获取文件描述符。然后使用read()和write()执行I/O。在执行完所有I/O后,应该使用close()释放文件描述符和相关资源。这些系统调用可用于所有文件类型的I/O。
所有的文件类型和设备驱动器实现了相同的I/O接口,这使得I/O具有通用性,也就是说程序可用于所有的文件类型,而不需要为特定文件类型编写特定的代码。
对于每个打开的文件,内核维护了一个文件偏移量,它决定了下一次读或写发生的位置。文件的偏移量会通过读或写隐式地更新。使用lseek()可以修改文件偏移量,让它指向文件的任何位置,这个位置可以在文件内,也可以超出文件的最末尾。当偏移量超出文件的最末尾,执行写入时产生file hole,执行读取时返回0。
ioctl()系统调用是为了满足不符合标准I/O模型的设备和文件操作而加入的。

10-03 17:36