Linux文件IO基础

本章给大家介绍 Linux 应用编程中最基础的知识,即文件 I/O(Input、Outout),文件 I/O 指的是对文件的输入/输出操作,说白了就是对文件的读写操作;Linux 下一切皆文件,文件作为 Linux 系统设计思想的核心理念,在 Linux 系统下显得尤为重要,所以对文件的 I/O 操作既是基础也是最重要的部分。

一个通用的 IO 模型通常包括打开文件、读写文件、关闭文件这些基本操作,主要涉及到 4 个函数:open()、read()、write()以及 close(),在学习这些系统调用之前,我们先了解一些相关概念。

文件描述符

调用 open 函数会有一个返回值,该返回值经常被命名为"xxfdxx",这是一个 int 类型的数据,在open函数执行成功的情况下,会返回一个非负整数,该返回值就是一个文件描述符(file descriptor),这说明文件描述符是一个非负整数;对于 Linux 内核而言,所有打开的文件都会通过文件描述符进行索引。

当调用 open 函数打开一个现有文件或创建一个新文件时,内核会向进程返回一个文件描述符,用于指代被打开的文件,所有执行 IO 操作的系统调用都是通过文件描述符来索引到对应的文件,当调用 read/write 函数进行文件读写时,会将文件描述符传送给 read/write 函数。

一个进程可以打开多个文件,但是在 Linux 系统中,一个进程可以打开的文件数是有限制,并不是可以无限制打开很多的文件,大家想一想便可以知道,打开的文件是需要占用内存资源的,文件越大、打开的文件越多那占用的内存就越多,必然会对整个系统造成很大的影响,如果超过进程可打开的最大文件数限制,内核将会发送警告信号给对应的进程,然后结束进程;在 Linux 系统下,我们可以通过 ulimit 命令来查看进程可打开的最大文件数,用法如下所示:

ulimit -n

该最大值默认情况下是 1024,也就意味着一个进程最多可以打开 1024 个文件,当然这个限制数其实是可以设置的,这个就先不给大家介绍了,当然除了进程有最大文件数限制外,其实对于整个 Linux 系统来说,也有最大限制,那么关于这些问题,如果后面的内容中涉及到了再给大家进行介绍。

所以对于一个进程来说,文件描述符是一种有限资源,文件描述符是从 0 开始分配的,譬如说进程中第一个被打开的文件对应的文件描述符是 0、第二个文件是 1、第三个文件是 2、第 4 个文件是 3……以此类推,所以由此可知,文件描述符数字最大值为 1023(0~1023)。每一个被打开的文件在同一个进程中都有一个唯一的文件描述符,不会重复,如果文件被关闭后,它对应的文件描述符将会被释放,那么这个文件描述符将可以再次分配给其它打开的文件、与对应的文件绑定起来。

每次给打开的文件分配文件描述符都是从最小的没有被使用的文件描述符(0~1023)开始,当之前打开的文件被关闭之后,那么它对应的文件描述符会被释放,释放之后也就成为了一个没有被使用的文件描述符了。

当我们在程序中,调用 open 函数打开文件的时候,分配的文件描述符一般都是从 3 开始,这里大家可能要问了,上面不是说从 0 开始的吗,确实是如此,但是 0、1、2 这三个文件描述符已经默认被系统占用了,分别分配给了系统标准输入(0)、标准输出(1)以及标准错误(2),关于这个问题,这里不便给大家说太多,毕竟这是后面的内容,这里只是给大家提一下,后面遇到了再具体讲解。

Tips:Linux 系统下,一切皆文件,也包括各种硬件设备,使用 open 函数打开任何文件成功情况下便会返回对应的文件描述符 fd。每一个硬件设备都会对应于 Linux 系统下的某一个文件,把这类文件称为设备文件。所以设备文件对应的其实是某一硬件设备,应用程序通过对设备文件进行读写等操作、来使用、操控硬件设备,譬如 LCD 显示器、串口、音频、键盘等。

标准输入一般对应的是键盘,可以理解为 0 便是打开键盘对应的设备文件时所得到的文件描述符;标准输出一般指的是 LCD 显示器,可以理解为 1 便是打开 LCD 设备对应的设备文件时所得到的文件描述符;而标准错误一般指的也是 LCD 显示器。

open打开文件

在 Linux 系统中要操作一个文件,需要先打开该文件,得到文件描述符,然后再对文件进行相应的读写操作(或其他操作),最后在关闭该文件;open 函数用于打开文件,当然除了打开已经存在的文件之外,还可以创建一个新的文件,函数原型如下所示:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);

在应用程序中调用 open 函数即可传入 2 个参数(pathname、flags)、也可传入 3 个参数(pathname、flags、mode),但是第三个参数 mode 需要在第二个参数 flags 满足条件时才会有效,稍后将对此进行说明。

在应用程序中使用 open 函数时,需要包含 3 个头文件“#include <sys/types.h>”、“#include <sys/stat.h>”、“#include <fcntl.h>”。

函数参数和返回值含义如下:

pathname:字符串类型,用于标识需要打开或创建的文件,可以包含路径(绝对路径或相对路径)信息,譬如:"./src_file"(当前目录下的 src_file 文件)、"/home/dengtao/hello.c"等;如果 pathname 是一个符号链接,会对其进行解引用。

flags:调用 open 函数时需要提供的标志,包括文件访问模式标志以及其它文件相关标志,这些标志使用宏定义进行描述,都是常量,open 函数提供了非常多的标志,我们传入 flags 参数时既可以单独使用某一个标志,也可以通过位或运算(|)将多个标志进行组合。这些标志介绍如下:

  • O_RDONLY :以只读方式打开文件
  • O_WRONLY :以只写方式打开文件
  • O_RDWR :以可读可写方式打开文件
  • 以上三个是文件访问权限标志,传入的flags 参数中必须要包含其中一种标志,而且只能包含一种,打开的文件只能按照这种权限来操作,譬如使用了 O_RDONLY 标志,就只能对文件进行读取操作,不能写操作。
  • O_CREAT:如果 pathname 参数指向的文件不存在则创建此文件
  • 使用此标志时,调用 open 函数需要传入第 3 个参数 mode,参数 mode 用于指定新建文件的访问权限。open 函数的第 3 个参数只有在使用了 O_CREAT 或 O_TMPFILE 标志时才有效。
  • O_DIRECTORY :如果 pathname 参数指向的不是一个目录,则调用 open 失败
  • O_EXCL :此标志一般结合 O_CREAT 标志一起使用,用于专门创建文件。
  • 在 flags 参数同时使用到了 O_CREAT 和O_EXCL 标志的情况下,如果 pathname 参数指向的文件已经存在,则 open 函数返回错误。可以用于测试一个文件是否存在,如果不存在则创建此文件,如果存在则返回错误,这使得测试和创建两者成为一个原子操作。
  • O_NOFOLLOW :如果 pathname 参数指向的是一个符号链接,将不对其进行解引用,直接返回错误。
  • O_TRUNC :调用 open 函数打开文件的时候会将文件原本的内容全部丢弃,文件大小变为 0;
  • O_APPEND :调用 open 函数打开文件,当每次使用 write()函数对文件进行写操作时,都会自动把文件当前位置偏移量移动到文件末尾, 从文件末尾开始写入数据,也就是意味着每次写入数据都是从文件末尾开始。

小细节:O_APPEND标志并不会影响读文件,当读取文件时, O_APPEND 标志并不会影响读位置偏移量, 即使使用了 O_APPEND标志,读文件位置偏移量默认情况下依然是文件头,使用 lseek 函数来改变 write()时的写位置偏移量也不会成功,当执行 write()函数时,检测到 open 函数携带了 O_APPEND 标志,所以在 write 函数内部会自动将写位置偏移量移动到文件末尾。

Tips:不同内核版本所支持的 flags 标志是存在差别的,譬如说新版本内核所支持的标志可能在老版本是不支持的,亦或者老版本支持的标志在新版本已经被取消、替代,man 手册中对一些标志是从哪个版本开始支持的有简单地说明,读者可以自行阅读!

前面我们说过,flags 参数时既可以单独使用某一个标志,也可以通过位或运算(|)将多个标志进行组合,譬如:

open("./src_file", O_RDONLY)//单独使用某一个标志

open("./src_file", O_RDONLY | O_NOFOLLOW)//多个标志组合

mode:此参数用于指定新建文件的访问权限,只有当 flags 参数中包含 O_CREAT 或 O_TMPFILE 标志时才有效(O_TMPFILE 标志用于创建一个临时文件)。

open 函数使用示例

(1)使用 open 函数打开一个已经存在的文件(例如当前目录下的 app.c 文件),使用只读方式打开:

int fd = open("./app.c", O_RDONLY)
if (-1 == fd)
return fd;

(2)使用 open 函数打开一个已经存在的文件(例如当前目录下的 app.c 文件),使用可读可写方式打开:

int fd = open("./app.c", O_RDWR)
if (-1 == fd)
return fd;

关于open函数的第三个参数mode_t mode

Linux 系统中,文件的基本权限由 9 个字符组成,以 rwxrw-r-x 为例,我们可以使用数字来代表各个权限,各个权限与数字的对应关系如下:

r --> 4 
w --> 2 
x --> 1

由于这 9 个字符分属 3 类用户,因此每种用户身份包含 3 个权限(r、w、x),通过将 3 个权限对应的数字累加,最终得到的值即可作为每种用户所具有的权限。 拿 rwxrw-r-x 来说,所有者、所属组和其他人分别对应的权限值为:

所有者 = rwx = 4+2+1 = 7 所属组 = rw- = 4+2 = 6 其他人 = r-x = 4+1 = 5

所以,此权限对应的权限值就是 765。

这其实是一种8进制的思维。

注意,在linux中执行chmod的时候,系统可以按照8进制解析;但是如果在c语言里直接显示765,那么就会被当做十进制的765,所以在c语言里,需要显式地表明为8进制数据。

当我们使用chmod命令时,可以直接输入644。这是因为chmod命令将输入的644(可能以字符串形式接收)视为8进制数,然后转换为相应的10进制数,最后通过系统调用传递给chmod函数。

这也就是为什么open函数在指定权限时,需要写成0765的形式,而不是直接写成765,其实就是显式地指定为8进制.不要和linux里的命令搞混了。

虽然mode_t是一个unsigned int类型,但当我们使用chmod函数时,mode参数并不是简单地作为一个整数来处理。它实际上是以8进制(octal)的形式来解析的。

这里有一些常用的mode参数选项:

S_ISUID                                    04000  // Set user-id on execution
S_ISGID                                    02000  // Set group-id on execution
S_ISVTX                                    01000  // Sticky bit
S_IRUSR(S_IREAD)      00400           //文件所有者具可读取权限
S_IWUSR(S_IWRITE)     00200           //文件所有者具可写入权限 
S_IXUSR(S_IEXEC)      00100           //文件所有者具可执行权限
S_IRGRP                 00040           //用户组具可读取权限
S_IWGRP                                    00020           //用户组具可写入权限
S_IXGRP                                    00010           //用户组具可执行权限
S_IROTH                                    00004           //其他用户具可读取权限
S_IWOTH                                    00002            //其他用户具可写入权限
S_IXOTH                                    00001            //他用户具可执行权限

假设我们要将文件test的权限设置为644,有以下几种方式:

fd=open("test", O_RDWR | O_CREAT | O_EXCL, 0644); // Method 1

fd=open("test", O_RDWR | O_CREAT | O_EXCL, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH); // Method 2

fd=open("test", O_RDWR | O_CREAT | O_EXCL, 420); // Method 3

这三种方法实际上是等效的。第一种方法是通过位运算将各个权限合并在一起,最终得到的是8进制的0644,这等于10进制的420

write写文件

调用 write 函数可向打开的文件写入数据,其函数原型如下所示(可通过"man 2 write"查看):

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

首先使用 write 函数需要先包含 unistd.h 头文件。

函数参数和返回值含义如下:

fd:文件描述符。关于文件描述符,前面已经给大家进行了简单地讲解,这里不再重述!我们需要将进行写操作的文件所对应的文件描述符传递给 write 函数。

buf:指定写入数据对应的缓冲区。

count:指定写入的字节数。

返回值:如果成功将返回写入的字节数(0 表示未写入任何字节),如果此数字小于 count 参数,这不是错误,譬如磁盘空间已满,可能会发生这种情况;如果写入出错,则返回-1。

对于普通文件(我们一般操作的大部分文件都是普通文件,譬如常见的文本文件、二进制文件等),不管是读操作还是写操作,一个很重要的问题是:从文件的哪个位置开始进行读写操作?也就是 IO 操作所对应的位置偏移量,读写操作都是从文件的当前位置偏移量处开始,当然当前位置偏移量可以通过 lseek 系统调用进行设置,关于此函数后面再讲;默认情况下当前位置偏移量一般是 0,也就是指向了文件起始位置,当调用 read、write 函数读写操作完成之后,当前位置偏移量也会向后移动对应字节数,譬如当前位置偏移量为 1000 个字节处,调用 write()写入或 read()读取 500 个字节之后,当前位置偏移量将会移动到 1500 个字节处。

read读文件

调用 read 函数可从打开的文件中读取数据,其函数原型如下所示(可通过"man 2 read"查看):

#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);

首先使用 read 函数需要先包含 unistd.h 头文件。

函数参数和返回值含义如下:

fd:文件描述符。与 write 函数的 fd 参数意义相同。

buf:指定用于存储读取数据的缓冲区。

count:指定需要读取的字节数。

返回值:如果读取成功将返回读取到的字节数,实际读取到的字节数可能会小于 count 参数指定的字节数,也有可能会为 0,譬如进行读操作时,当前文件位置偏移量已经到了文件末尾。实际读取到的字节数少于要求读取的字节数,譬如在到达文件末尾之前有 30 个字节数据,而要求读取 100 个字节,则 read 读取成功只能返回 30;而下一次再调用 read 读,它将返回 0(文件末尾)。

close关闭文件

可调用 close 函数关闭一个已经打开的文件,其函数原型如下所示(可通过"man 2 close"查看):

#include <unistd.h>
int close(int fd);

首先使用 close 函数需要先包含 unistd.h 头文件,当我们对文件进行 IO 操作完成之后,后续不再对文件进行操作时,需要将文件关闭。

函数参数和返回值含义如下:

fd:文件描述符,需要关闭的文件所对应的文件描述符。

返回值:如果成功返回 0,如果失败则返回-1。

除了使用 close 函数显式关闭文件之外,在 Linux 系统中,当一个进程终止时,内核会自动关闭它打开的所有文件,也就是说在我们的程序中打开了文件,如果程序终止退出时没有关闭打开的文件,那么内核会自动将程序中打开的文件关闭。很多程序都利用了这一功能而不显式地用 close 关闭打开的文件。

显式关闭不再需要的文件描述符往往是良好的编程习惯,会使代码在后续修改时更具有可读性,也更可靠,进而言之,文件描述符是有限资源,当不再需要时必须将其释放、归还于系统。

lseek

对于每个打开的文件,系统都会记录它的读写位置偏移量,我们也把这个读写位置偏移量称为读写偏移量,记录了文件当前的读写位置,当调用 read()或 write()函数对文件进行读写操作时,就会从当前读写位置偏移量开始进行数据读写。

读写偏移量用于指示 read()或 write()函数操作时文件的起始位置,会以相对于文件头部的位置偏移量来表示,文件第一个字节数据的位置偏移量为 0。

当打开文件时,会将读写偏移量设置为指向文件开始位置处,以后每次调用 read()、write()将自动对其进行调整,以指向已读或已写数据后的下一字节,因此,连续的调用 read()和 write()函数将使得读写按顺序递增,对文件进行操作。我们先来看看 lseek 函数的原型,如下所示(可通过"man 2 lseek"查看):

#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);

首先调用 lseek 函数需要包含<sys/types.h>和<unistd.h>两个头文件。

函数参数和返回值含义如下:

fd:文件描述符。

offset:偏移量,以字节为单位。

whence:用于定义参数 offset 偏移量对应的参考值,该参数为下列其中一种(宏定义):

⚫ SEEK_SET:读写偏移量将指向 offset 字节位置处(从文件头部开始算);

⚫ SEEK_CUR:读写偏移量将指向当前位置偏移量 + offset 字节位置处,offset 可以为正、也可以为负,如果是正数表示往后偏移,如果是负数则表示往前偏移;

⚫ SEEK_END:读写偏移量将指向文件末尾 + offset 字节位置处,同样 offset 可以为正、也可以为负,如果是正数表示往后偏移、如果是负数则表示往前偏移。

返回值:成功将返回从文件头部开始算起的位置偏移量(字节为单位),也就是当前的读写位置;发生错误将返回-1。

使用示例:

(1)将读写位置移动到文件开头处:
off_t off = lseek(fd, 0, SEEK_SET);
if (-1 == off)
return -1;

(2)将读写位置移动到文件末尾:
off_t off = lseek(fd, 0, SEEK_END);
if (-1 == off)
return -1;

(3)将读写位置移动到偏移文件开头 100 个字节处:
off_t off = lseek(fd, 100, SEEK_SET);
if (-1 == off)
return -1;

(4)获取当前读写位置偏移量:
off_t off = lseek(fd, 0, SEEK_CUR);
if (-1 == off)
return -1;
函数执行成功将返回文件当前读写位置。

Tips:再给大家说一点,就是关于函数返回值的问题,我们可以发现,本章给大家所介绍的这些函数都是有返回值的,其实不管是 Linux 应用编程 API 函数,还是驱动开发中所使用到的函数,基本上都是有返回值的,返回值的作用就是告诉开发人员此函数执行的一个状态是怎样的,执行成功了还是失败了,在Linux 系统下,绝大部分的函数都是返回 0 作为函数调用成功的标识、而返回负数(譬如-1)表示函数调用失败,如果大家学习过驱动开发,想必对此并不陌生,所以很多时候可以使用如下的方式来判断函数执行成功还是失败:

if (func()) {
    //执行失败
} else {
    //执行成功
}

当然以上说的是大部分情况,并不是所有函数都是这样设计,所以呢,这里笔者也给大家一个建议,自己在进行编程开发的时候,自定义函数也可以使用这样的一种方法来设计你的函数返回值,不管是裸机程序亦或是 Linux 应用程序、驱动程序。

Linux 系统如何管理文件

静态文件与 inode

文件在没有被打开的情况下一般都是存放在磁盘中的,譬如电脑硬盘、移动硬盘、U 盘等外部存储设备,文件存放在磁盘文件系统中,并且以一种固定的形式进行存放,我们把他们称为静态文件。文件储存在硬盘上,硬盘的最小存储单位叫做“扇区”(Sector),每个扇区储存 512 字节(相当于 0.5KB),操作系统读取硬盘的时候,不会一个个扇区地读取,这样效率太低,而是一次性连续读取多个扇区,即一次性读取一个“块”(block)。这种由多个扇区组成的“块”,是文件存取的最小单位。“块”的大小,最常见的是 4KB,即连续八个 sector 组成一个 block。

所以由此可以知道,静态文件对应的数据都是存储在磁盘设备不同的“块”中,那么问题来了,我们在程序中调用 open 函数是如何找到对应文件的数据存储“块”的呢,难道仅仅通过指定的文件路径就可以实现?这里我们就来简单地聊一聊这内部实现的过程。

我们的磁盘在进行分区、格式化的时候会将其分为两个区域,一个是数据区,用于存储文件中的数据;另一个是 inode 区,用于存放 inode table(inode 表),inode table 中存放的是一个一个的 inode(也成为 inode节点),不同的 inode 就可以表示不同的文件,每一个文件都必须对应一个 inode,inode 实质上是一个结构体,这个结构体中有很多的元素,不同的元素记录了文件了不同信息,譬如文件字节大小、文件所有者、文件对应的读/写/执行权限、文件时间戳(创建时间、更新时间等)、文件类型、文件数据存储的 block(块)位置等等信息,如图3.1.1 中所示(这里需要注意的是,文件名并不是记录在 inode 中,这个问题后面章节内容再给大家讲)。

所以由此可知,inode table 表本身也需要占用磁盘的存储空间。每一个文件都有唯一的一个 inode,每一个 inode 都有一个与之相对应的数字编号,通过这个数字编号就可以找到 inode table 中所对应的 inode。

在 Linux 系统下,我们可以通过"ls -i"命令查看文件的 inode 编号,如下所示:

上图中 ls 打印出来的信息中,每一行前面的一个数字就表示了对应文件的 inode 编号。除此之外,还可以使用 stat 命令查看,用法如下:

由以上的介绍大家可以联系到实际操作中,譬如我们在 Windows 下进行 U 盘格式化的时候会有一个“快速格式化”选项,如下所示:

如果勾选了“快速格式化”选项,在进行格式化操作的时候非常的快,而如果不勾选此选项,直接使用普通格式化方式,将会比较慢,那说明这两种格式化方式是存在差异的,其实快速格式化只是删除了 U 盘中的 inode table 表,真正存储文件数据的区域并没有动,所以使用快速格式化的 U 盘,其中的数据是可以被找回来的。

通过以上介绍可知,打开一个文件,系统内部会将这个过程分为三步:

1) 系统找到这个文件名所对应的 inode 编号;

2) 通过 inode 编号从 inode table 中找到对应的 inode 结构体;

3) 根据 inode 结构体中记录的信息,确定文件数据所在的 block,并读出数据。

文件打开时的状态

当我们调用 open 函数去打开文件的时候,内核会申请一段内存(一段缓冲区),并且将静态文件的数据内容从磁盘这些存储设备中读取到内存中进行管理、缓存(也把内存中的这份文件数据叫做动态文件、内核缓冲区)。打开文件后,以后对这个文件的读写操作,都是针对内存中这一份动态文件进行相关的操作,而并不是针对磁盘中存放的静态文件。

当我们对动态文件进行读写操作后,此时内存中的动态文件和磁盘设备中的静态文件就不同步了,数据的同步工作由内核完成,内核会在之后将内存这份动态文件更新(同步)到磁盘设备中。由此我们也可以联系到实际操作中,譬如说:

⚫ 打开一个大文件的时候会比较慢;

⚫ 文档写了一半,没记得保存,此时电脑因为突然停电直接掉电关机了,当重启电脑后,打开编写的文档,发现之前写的内容已经丢失。

想必各位读者在工作当中都遇到过这种问题吧,通过上面的介绍,就解释了为什么会出现这种问题。

好,我们再来说一下,为什么要这样设计?

因为磁盘、硬盘、U 盘等存储设备基本都是 Flash 块设备,因为块设备硬件本身有读写限制等特征,块设备是以一块一块为单位进行读写的(一个块包含多个扇区,而一个扇区包含多个字节),一个字节的改动也需要将该字节所在的 block 全部读取出来进行修改,修改完成之后再写入块设备中,所以导致对块设备的读写操作非常不灵活;而内存可以按字节为单位来操作,而且可以随机操作任意地址数据,非常地很灵活,所以对于操作系统来说,会先将磁盘中的静态文件读取到内存中进行缓存,读写操作都是针对这份动态文件,而不是直接去操作磁盘中的静态文件,不但操作不灵活,效率也会下降很多,因为内存的读写速率远比磁盘读写快得多。

在 Linux 系统中,内核会为每个进程(关于进程的概念,这是后面的内容,我们可以简单地理解为一个运行的程序就是一个进程,运行了多个程序那就是存在多个进程)设置一个专门的数据结构用于管理该进程,譬如用于记录进程的状态信息、运行特征等,我们把这个称为进程控制块(Process control block,缩写PCB)。PCB 数据结构体中有一个指针指向了文件描述符表(File descriptors),文件描述符表中的每一个元素索引到对应的文件表(File table),文件表也是一个数据结构体,其中记录了很多文件相关的信息,譬如文件状态标志、引用计数、当前文件的读写偏移量以及 i-node 指针(指向该文件对应的 inode)等,进程打开的所有文件对应的文件描述符都记录在文件描述符表中,每一个文件描述符都会指向一个对应的文件表,其示意图如下所示:

前面给大家介绍了 inode,inode 数据结构体中的元素会记录该文件的数据存储的 block(块),也就是说可以通过 inode 找到文件数据存在在磁盘设备中的那个位置,从而把文件数据读取出来。

空洞文件

什么是空洞文件(hole file)?在上一章内容中,笔者给大家介绍了 lseek()系统调用,使用 lseek 可以修改文件的当前读写位置偏移量,此函数不但可以改变位置偏移量,并且还允许文件偏移量超出文件长度,这是什么意思呢?譬如有一个 test_file,该文件的大小是 4K(也就是 4096 个字节),如果通过 lseek 系统调用将该文件的读写偏移量移动到偏移文件头部 6000 个字节处,大家想一想会怎样?如果笔者没有提前告诉大家,大家觉得不能这样操作,但事实上 lseek 函数确实可以这样操作。

接下来使用 write()函数对文件进行写入操作,也就是说此时将是从偏移文件头部 6000 个字节处开始写入数据,也就意味着 4096~6000 字节之间出现了一个空洞,因为这部分空间并没有写入任何数据,所以形成了空洞,这部分区域就被称为文件空洞,那么相应的该文件也被称为空洞文件。

文件空洞部分实际上并不会占用任何物理空间,直到在某个时刻对空洞部分进行写入数据时才会为它分配对应的空间,但是空洞文件形成时,逻辑上该文件的大小是包含了空洞部分的大小的,这点需要注意。

那说了这么多,空洞文件有什么用呢?空洞文件对多线程共同操作文件是及其有用的,有时候我们创建一个很大的文件,如果单个线程从头开始依次构建该文件需要很长的时间,有一种思路就是将文件分为多段,然后使用多线程来操作,每个线程负责其中一段数据的写入;这个有点像我们现实生活当中施工队修路的感觉,比如说修建一条高速公路,单个施工队修筑会很慢,这个时候可以安排多个施工队,每一个施工队负责修建其中一段,最后将他们连接起来。

来看一下实际中空洞文件的两个应用场景:

⚫ 在使用迅雷下载文件时,还未下载完成,就发现该文件已经占据了全部文件大小的空间,这也是空洞文件;下载时如果没有空洞文件,多线程下载时文件就只能从一个地方写入,这就不能发挥多线程的作用了;如果有了空洞文件,可以从不同的地址同时写入,就达到了多线程的优势;

⚫ 在创建虚拟机时,你给虚拟机分配了 100G 的磁盘空间,但其实系统安装完成之后,开始也不过只用了 3、4G 的磁盘空间,如果一开始就把 100G 分配出去,资源是很大的浪费。

关于空洞文件,这里就介绍这么多,以后真正用到时再补充。

非阻塞 I/O

到底什么是可读和可写?

  • 可读事件,当文件描述符关联的内核读缓冲区可读,则触发可读事件。 (可读:内核缓冲区非空,有数据可以读取)

  • 可写事件,当文件描述符关联的内核写缓冲区可写,则触发可写事件。 (可写:内核缓冲区不满,有空闲空间可以写入)

关于“阻塞”一词前面已经给大家多次提到,阻塞其实就是进入了休眠状态,交出了 CPU 控制权。前面所学习过的函数,譬如 wait()、pause()、sleep()等函数都会进入阻塞,本小节来聊一聊关于阻塞式 I/O 与非阻塞式 I/O。

阻塞式 I/O 顾名思义就是对文件的 I/O 操作(读写操作)是阻塞式的,非阻塞式 I/O 同理就是对文件的I/O 操作是非阻塞的。这样说大家可能不太明白,这里举个例子,譬如对于某些文件类型(读管道文件、网络设备文件和字符设备文件),当对文件进行读操作时,如果数据未准备好、文件当前无数据可读,那么读操作可能会使调用者阻塞,直到有数据可读时才会被唤醒,这就是阻塞式 I/O 常见的一种表现;如果是非阻塞式 I/O,即使没有数据可读,也不会被阻塞、而是会立马返回错误!

普通文件的读写操作是不会阻塞的,不管读写多少个字节数据,read()或 write()一定会在有限的时间内返回,所以普通文件一定是以非阻塞的方式进行 I/O 操作,这是普通文件本质上决定的;但是对于某些文件类型,譬如上面所介绍的管道文件、设备文件等,它们既可以使用阻塞式 I/O 操作,也可以使用非阻塞式 I/O进行操作。

阻塞 I/O 与非阻塞 I/O 读文件

本小节我们将分别演示使用阻塞式 I/O 和非阻塞式 I/O 对文件进行读操作,在调用 open()函数打开文件时,为参数 flags 指定 O_NONBLOCK 标志,open()调用成功后,后续的 I/O 操作将以非阻塞式方式进行;这就是非阻塞 I/O 的打开方式,如果未指定 O_NONBLOCK 标志,则默认使用阻塞式 I/O 进行操作。

对于普通文件来说,指定与未指定 O_NONBLOCK 标志对其是没有影响,普通文件的读写操作是不会阻塞的,它总是以非阻塞的方式进行 I/O 操作,这是普通文件本质上决定的,前面已经给大家进行了说明。

本小节我们将以读取鼠标为例,使用两种 I/O 方式进行读取,来进行对比,鼠标是一种输入设备,其对应的设备文件在/dev/input/目录下,如下所示:

示例代码 13.1.1 演示了以阻塞方式读取鼠标,调用 open()函数打开鼠标设备文件"/dev/input/event3",以只读方式打开,没有指定 O_NONBLOCK 标志,说明使用的是阻塞式 I/O;程序中只调用了一次 read()读取鼠标。

示例代码 13.1.1 阻塞式 I/O 读取鼠标数据 
#include <stdio.h> 
#include <stdlib.h> 
#include <sys/types.h> 
#include <sys/stat.h> 
#include <fcntl.h> 
#include <unistd.h> 
#include <string.h> 
int main(void) 
{ 
     char buf[100]; 
     int fd, ret; 
     /* 打开文件 */ 
     fd = open("/dev/input/event3", O_RDONLY); 
     if (-1 == fd) { 
         perror("open error"); 
         exit(-1);
     } 
     /* 读文件 */ 
     memset(buf, 0, sizeof(buf)); 
     ret = read(fd, buf, sizeof(buf)); 
     if (0 > ret) { 
         perror("read error"); 
         close(fd); 
         exit(-1); 
     } 
     printf("成功读取<%d>个字节数据\n", ret); 
     /* 关闭文件 */ 
     close(fd); 
     exit(0); 
}

编译上述示例代码进行测试:

执行程序之后,发现程序没有立即结束,而是一直占用了终端,没有输出信息,原因在于调用 read()之后进入了阻塞状态,因为当前鼠标没有数据可读;如果此时我们移动鼠标、或者按下鼠标上的任何一个按键,阻塞会结束,read()会成功读取到数据并返回,如下所示:

接下来,我们将示例代码 13.1.1 修改成非阻塞式 I/O,如下所示:

示例代码 13.1.2 非阻塞式 I/O 读取鼠标数据 
#include <stdio.h> 
#include <stdlib.h> 
#include <sys/types.h> 
#include <sys/stat.h> 
#include <fcntl.h> 
#include <unistd.h>
#include <string.h> 
int main(void) 
{ 
     char buf[100]; 
     int fd, ret; 
     /* 打开文件 */ 
     fd = open("/dev/input/event3", O_RDONLY | O_NONBLOCK); 
     if (-1 == fd) { 
         perror("open error"); 
         exit(-1); 
     } 
     /* 读文件 */ 
     memset(buf, 0, sizeof(buf)); 
     ret = read(fd, buf, sizeof(buf)); 
     if (0 > ret) { 
         perror("read error"); 
         close(fd); 
         exit(-1); 
     } 
     printf("成功读取<%d>个字节数据\n", ret); 
     /* 关闭文件 */ 
     close(fd); 
     exit(0); 
} 

修改方法很简单,只需在调用 open()函数时指定 O_NONBLOCK 标志即可,对上述示例代码进行编译测试:

执行程序之后,程序立马就结束了,并且调用 read()返回错误,提示信息为"Resource temporarily unavailable",意思就是说资源暂时不可用;原因在于调用 read()时,如果鼠标并没有移动或者被按下(没有发生输入事件),是没有数据可读,故而导致失败返回,这就是非阻塞 I/O。

可以对示例代码 13.1.2 进行修改,使用轮训方式不断地去读取,直到鼠标有数据可读,read()将会成功返回:

示例代码 13.1.3 轮训+非阻塞方式读取鼠标 
#include <stdio.h> 
#include <stdlib.h> 
#include <sys/types.h> 
#include <sys/stat.h> 
#include <fcntl.h> 
#include <unistd.h> 
#include <string.h> 
int main(void) 
{ 
     char buf[100]; 
     int fd, ret; 
     /* 打开文件 */ 
     fd = open("/dev/input/event3", O_RDONLY | O_NONBLOCK); 
     if (-1 == fd) { 
         perror("open error"); 
         exit(-1); 
     } 
     /* 读文件 */ 
     memset(buf, 0, sizeof(buf)); 
     for ( ; ; ) { 
         ret = read(fd, buf, sizeof(buf)); 
         if (0 < ret) { 
             printf("成功读取<%d>个字节数据\n", ret); 
             close(fd); 
             exit(0); 
         } 
     } 
} 

具体的执行的效果便不再演示了,各位读者自己动手试试。

阻塞 I/O 的优点与缺点

当对文件进行读取操作时,如果文件当前无数据可读,那么阻塞式 I/O 会将调用者应用程序挂起、进入休眠阻塞状态,直到有数据可读时才会解除阻塞;而对于非阻塞 I/O,应用程序不会被挂起,而是会立即返回,它要么一直轮训等待,直到数据可读,要么直接放弃!

所以阻塞式 I/O 的优点在于能够提升 CPU 的处理效率,当自身条件不满足时,进入阻塞状态,交出 CPU资源,将 CPU 资源让给别人使用;而非阻塞式则是抓紧利用 CPU 资源,譬如不断地去轮训,这样就会导致该程序占用了非常高的 CPU 使用率!

执行示例代码 13.1.3 对应的程序时,通过 top 命令可以发现该程序的占用了非常高的 CPU 使用率,如下所示:

其 CPU 占用率几乎达到了 100%,在一个系统当中,一个进程的 CPU 占用率这么高是一件非常危险的事情。而示例代码 13.1.1 这种阻塞式方式,其 CPU 占用率几乎为 0,所以就本章的所举例子来说,阻塞式I/O 绝地要优于非阻塞式 I/O,那既然如此,我们为何还要介绍非阻塞式 I/O 呢?

下一小节我们将通过一个例子给大家介绍,阻塞式 I/O 的困境!

使用非阻塞 I/O 实现并发读取

上一小节给大家所举的例子当中,只读取了鼠标的数据,如果要在程序当中同时读取鼠标和键盘,那又该如何呢?本小节我们将分别演示使用阻塞式 I/O 和非阻塞式 I/O 同时读取鼠标和键盘;同理键盘也是一种输入类设备,但是键盘是标准输入设备 stdin,进程会自动从父进程中继承标准输入、标准输出以及标准错误,标准输入设备对应的文件描述符为 0,所以在程序当中直接使用即可,不需要再调用 open 打开。

首先我们使用阻塞式方式同时读取鼠标和键盘,示例代码如下所示:

示例代码 13.1.4 阻塞式同时读取鼠标和键盘 
#include <stdio.h> 
#include <stdlib.h> 
#include <sys/types.h> 
#include <sys/stat.h> 
#include <fcntl.h> 
#include <unistd.h> 
#include <string.h> 
#define MOUSE "/dev/input/event3" 
int main(void) 
{ 
     char buf[100]; 
     int fd, ret; 
     /* 打开鼠标设备文件 */ 
     fd = open(MOUSE, O_RDONLY); 
     if (-1 == fd) { 
         perror("open error"); 
         exit(-1); 
     }
     /* 读鼠标 */ 
     memset(buf, 0, sizeof(buf)); 
     ret = read(fd, buf, sizeof(buf)); 
     printf("鼠标: 成功读取<%d>个字节数据\n", ret); 
     /* 读键盘 */ 
     memset(buf, 0, sizeof(buf)); 
     ret = read(0, buf, sizeof(buf)); 
     printf("键盘: 成功读取<%d>个字节数据\n", ret); 
     /* 关闭文件 */ 
     close(fd); 
     exit(0); 
}

上述程序中先读了鼠标,在接着读键盘,所以由此可知,在实际测试当中,需要先动鼠标在按键盘(按下键盘上的按键、按完之后按下回车),这样才能既成功读取鼠标、又成功读取键盘,程序才能够顺利运行结束。因为 read 此时是阻塞式读取,先读取了鼠标,没有数据可读将会一直被阻塞,后面的读取键盘将得不到执行。

这就是阻塞式 I/O 的一个困境,无法实现并发读取(同时读取),主要原因在于阻塞,那如何解决这个问题呢?当然大家可能会想到使用多线程,一个线程读取鼠标、另一个线程读取键盘,亦或者创建一个子进程,父进程读取鼠标、子进程读取键盘等方法,当然这些方法自然可以解决,但不是我们要学习的重点。

既然阻塞 I/O 存在这样一个困境,那我们可以使用非阻塞式 I/O 解决它,将示例代码 13.1.4 修改为非阻塞式方式同时读取鼠标和键盘。使用 open()打开得到的文件描述符,调用 open()时指定 O_NONBLOCK 标志将其设置为非阻塞式 I/O;因为标准输入文件描述符(键盘)是从其父进程进程而来,并不是在我们的程序中调用 open()打开得到的,那如何将标准输入设置为非阻塞 I/O,可以使用 3.10.1 小节中给大家介绍的fcntl()函数,具体使用方法在该小节中已有详细介绍,这里不再重述!可通过如下代码将标准输入(键盘)设置为非阻塞方式:

int flag; 
flag = fcntl(0, F_GETFL); //先获取原来的 flag 
flag |= O_NONBLOCK; //将 O_NONBLOCK 标志添加到 flag 
fcntl(0, F_SETFL, flag); //重新设置 flag

示例代码 13.1.5 演示了以非阻塞方式同时读取鼠标和键盘。

示例代码 13.1.5 非阻塞式方式同时读取鼠标和键盘 
#include <stdio.h> 
#include <stdlib.h> 
#include <sys/types.h> 
#include <sys/stat.h> 
#include <fcntl.h> 
#include <unistd.h> 
#define MOUSE "/dev/input/event3"
int main(void) 
{ 
     char buf[100]; 
     int fd, ret, flag; 
     /* 打开鼠标设备文件 */ 
     fd = open(MOUSE, O_RDONLY | O_NONBLOCK); 
     if (-1 == fd) { 
         perror("open error"); 
         exit(-1); 
     } 
     /* 将键盘设置为非阻塞方式 */ 
     flag = fcntl(0, F_GETFL); //先获取原来的 flag 
     flag |= O_NONBLOCK; //将 O_NONBLOCK 标准添加到 flag 
     fcntl(0, F_SETFL, flag); //重新设置 flag 
     for ( ; ; ) { 
         /* 读鼠标 */ 
         ret = read(fd, buf, sizeof(buf)); 
         if (0 < ret) 
             printf("鼠标: 成功读取<%d>个字节数据\n", ret); 
         /* 读键盘 */ 
         ret = read(0, buf, sizeof(buf)); 
         if (0 < ret) 
             printf("键盘: 成功读取<%d>个字节数据\n", ret); 
     } 
     /* 关闭文件 */ 
     close(fd); 
     exit(0); 
} 

将读取鼠标和读取键盘操作放入到一个循环中,通过轮训方式来实现并发读取鼠标和键盘,对上述代码进行编译,测试结果:

这样就解决了示例代码 13.1.4 所出现的问题,不管是先动鼠标还是先按键盘都可以成功读取到相应数据。

虽然使用非阻塞 I/O 方式解决了示例代码 13.1.4 出现的问题,但由于程序当中使用轮训方式,故而会使得该程序的 CPU 占用率特别高,终归还是不太安全,会对整个系统产生很大的副作用,如何解决这样的问题呢?

可以使用IO多路复用,这个参考我的另一篇文章:

Linux-IO多路复用-CSDN博客

非连续多缓冲区IO 

非连续多缓冲区IO:readv和writev函数

参考:

Unix/Linux编程:分散输入和集中输出------readv() 、 writev()_preadv-CSDN博客

/*
		* 参数: fd    文件描述符
		*       iov    指向iovec结构数组的一个指针
		*       iovcnt  指定了iovec的个数
		* 返回值:函数调用成功时返回读、写的总字节数,失败时返回-1并设置相应的errno。
		* 			writev返回输出的字节总数,通常应等于所有缓冲区长度之和。
		* 			readv返回读到的字节总数。如果遇到文件尾端,已无数据可读,则返回0。
		*/
       #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);

        
        readv() 系统调用从关联的文件读取iovcnt缓冲区文件描述符fd
		        放入iov描述的缓冲区(“分散输入”)

        writev() 系统调用将iov描述的数据的iovcnt缓冲区写入与文件描述符fd
       			相关联的文件(“聚集输出”)

		结构体指针iov定义了一组用来传输数据的缓存区.
		整数iovcnt则指定了iov的成员个数。
		iov中的每个成员都是如下形式的数据结构

		#include <sys/uio.h>
		struct iovec {
               void  *iov_base;    /* 缓冲区首地址 */
               size_t iov_len;     /*缓冲区长度 */
         };

		readv()系统调用的工作方式与read(2)相同,只是要填充多个缓冲区。
		
		writev()系统调用的工作方式与write(2)相同,只是要写出多个缓冲区 

		缓冲区以数组顺序处理。 这意味着readv()在完全填充iov[0]之前不会
		进入iov[1],依此类推。(如果数据不足,则可能不会填充iov指向的
		所有缓冲区)。类似地,writev()在进行iov[1]之前写出iov[0]的全部内容,
		依此类推。 
		
		readv() 和writev()执行的数据传输是原子的

readv和write

  • readv/writevrecvmsg/sendmsg的简化版,主要针对与文件IO(对read/write的优化)
  • readv和writev函数用于在一次函数调用中读、写多个非连续缓冲区。有时也将这两个函数称为散布读和聚集写

在一次函数调用中:
① writev以顺序iov[0]、iov[1]至iov[iovcnt-1]从各缓冲区中聚集输出数据到fd
② readv则将从fd读入的数据按同样的顺序散布到各缓冲区中,readv总是先填满一个缓冲区,然后再填下一个

为什么引入readv()和writev()

(1) 使用read函数将数据读到不连续的内存,或者wirte将不连续的内存发送出去,要多次调用read、write

如果要从文件中读一片连续的数据至进程的不同区域,有两种方案
① 使用read()一次将它们读至一个较大的缓冲区中,然后将它们分成若干部分复制到不同的区域;
② 调用read()若干次分批将它们读至不同区域。

同样,如果想将程序中不同区域的数据块连续地写至文件,也必须进行类似的处理。

(2) UNIX提供了另外两个函数—readv()和writev(),它们只需一次系统调用就可以实现在文件和进程的多个缓冲区之间传送数据,免除了多次系统调用或复制数据的开销。

分散输入

readv()实现了分散输入的功能:从文件描述符fd所指代的文件中读取一片连续的字节,然后将其分散放置到iov指定的缓存区中。这一散置动作从 iov[0]开始,依次填满整个缓存区

readv()是原子性的。

调用 readv()成功将返回读取的字节数,若文件结束1将返回 0。调用者必须对返回值进行检查,以验证读取的字节数是否满足要求。若数据不足以填充所有缓冲区,则只会占用2部分缓冲区,其中最后一个缓冲区可能只存有部分数据

例子1:读文件

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/uio.h>
#include <fcntl.h>

int main(int argc, char *argv[]){
    int fd;
    struct iovec iov[3];
    struct stat myStruct;       /* First buffer */
    int x;                      /* Second buffer */
#define STR_SIZE 100
    char str[STR_SIZE];         /* Third buffer */
    ssize_t numRead, totRequired;

    if (argc != 2 || strcmp(argv[1], "--help") == 0){
        fprintf(stderr, "%s file\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    fd = open(argv[1], O_RDONLY);
    if(fd == -1){
        fprintf(stderr, "open\n");
        exit(EXIT_FAILURE);
    }

    totRequired = 0;

    iov[0].iov_base = &myStruct;
    iov[0].iov_len = sizeof(struct stat);
    totRequired += iov[0].iov_len;

    iov[1].iov_base = &x;
    iov[1].iov_len = sizeof(x);
    totRequired += iov[1].iov_len;

    iov[2].iov_base = str;
    iov[2].iov_len = STR_SIZE;
    totRequired += iov[2].iov_len;

    numRead = readv(fd, iov, 3);
    if (numRead == -1){
        fprintf(stderr, "readv\n");
        exit(EXIT_FAILURE);
    }

    if (numRead < totRequired)
        printf("Read fewer bytes than requested\n");

    printf("total bytes requested: %ld; bytes read: %ld\n",
           (long) totRequired, (long) numRead);

    printf("%s", str);
    exit(EXIT_SUCCESS);
}
#include <stdio.h>
#include <sys/uio.h>
#include <fcntl.h>
 
int main(void){
        char buf1[5],buf2[10];
        struct iovec iov[2];
        iov[0].iov_base = buf1;
        iov[0].iov_len = 5;
        iov[1].iov_base = buf2;
        iov[1].iov_len = 10;
 
        int fd = open("a.txt",O_RDWR);
        if(fd < 0){
                perror("open");
                return -1;
        }
        int rsize = readv(fd, iov, 2);  // 从文件a.txt中读取数据,存到iov[2]中的buf1、buf2
        printf("rsize = %d\n",rsize);
 
        close(fd);
 
        fd = open("b.txt", O_RDWR|O_CREAT, S_IRUSR|S_IWUSR);
        if(fd < 0){
                perror("open");
                return -1;
        }
 
        int wsize = writev(fd,iov,2);  // 将iov[2]中的buf1、buf2,写入到文件b.txt
        printf("wsize = %d\n",wsize);
 
        close(fd);
        return 0;
}

集中输出

writev()系统调用实现了集中输出:将iov所指定的所有缓存区中的数据拼接(“集中”)起来,然后以连续的字节序列写入文件描述符fd指代的文件中。对缓冲区中数据的“集中”始于iov[0]所指定的缓冲区,并按数组顺序展开

像readv()调用一样,writev()调用也属于原子操作,即所有数据将一次性的从用户内存传入到fd指代的文件中。因此,在向普通文件写入数据时,writev()调用会把所有的请求数据连续写入文件,而即不受其他进(线)程改变文件偏移量的影响

如同 write()调用,writev()调用也可能存在部分写的问题。因此,必须检查 writev()调用的返回值,以确定写入的字节数是否与要求相符。

readv()调用和 writev()调用的主要优势在于便捷。如下两种方案,任选其一都可替代对writev()的调用。

  • 编码时,首先分配一个大缓冲区,随即再从进程地址空间的其他位置将数据复制过来,最后调用 write()输出其中的所有数据。
  • 发起一系列 write()调用,逐一输出每个缓冲区中的数据

尽管方案一在语义上等同于 writev()调用,但需要在用户空间内分配缓冲区,进行数据复制,很不方便(效率也低)。

方案二在语义上就不同于单次的 writev()调用,因为发起多次 write()调用将无法保证原子性。更何况,执行一次 writev()调用比执行多次 write()调用开销要小(参见 3.1 节关于系统调用的讨论)

应当指出,readv()和 writev()会改变打开文件句柄的当前文件偏移量

例子1:

writev:指定了两个缓冲区,str0和str1,内容输出到标准输出,并打印实际输出的字节数

// writevex.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/uio.h>

int main()
{
    char *str0 = "hello ";
    char *str1 = "world\n";
    struct iovec iov[2];
    ssize_t nwritten;

    iov[0].iov_base = str0;
    iov[0].iov_len = strlen(str0) + 1;
    iov[1].iov_base = str1;
    iov[1].iov_len = strlen(str1) + 1;

    nwritten = writev(STDOUT_FILENO, iov, 2);
    printf("%ld bytes written.\n", nwritten);

    exit(EXIT_SUCCESS);
}

例子2:读写文件

#include <stdio.h>
#include <sys/uio.h>
#include <fcntl.h>
 
int main(void){
        char buf1[5],buf2[10];
        struct iovec iov[2];
        iov[0].iov_base = buf1;
        iov[0].iov_len = 5;
        iov[1].iov_base = buf2;
        iov[1].iov_len = 10;
 
        int fd = open("a.txt",O_RDWR);
        if(fd < 0){
                perror("open");
                return -1;
        }
        int rsize = readv(fd, iov, 2);  // 从文件a.txt中读取数据,存到iov[2]中的buf1、buf2
        printf("rsize = %d\n",rsize);
 
        close(fd);
 
        fd = open("b.txt", O_RDWR|O_CREAT, S_IRUSR|S_IWUSR);
        if(fd < 0){
                perror("open");
                return -1;
        }
 
        int wsize = writev(fd,iov,2);  // 将iov[2]中的buf1、buf2,写入到文件b.txt
        printf("wsize = %d\n",wsize);
 
        close(fd);
        return 0;
}

更多待补充。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值