Linux系统编程第九章学习笔记
前言
此乃在下对Unix/Linux系统编程的第九章的知识点的整理,包含了知识点整理和对于知识点整理的苏格拉底挑战,大家可以看一看~如果有问题还请评论,咱们一起讨论~~~
库I/O函数
9.1 库I/O函数
系统调用是文件操作的基础,但它们只支持对数据块进行读写。在实际应用中,用户程序可能希望按照最适合应用程序的逻辑单位(如行、字符、结构化记录等)来读写文件,而这些逻辑单位不受系统调用的支持。库I/O函数是一组文件操作函数,它们既提供了用户方便性,又提高了整体效率
9.2 库I/O函数与系统调用的比较
几乎每个支持C编程的操作系统都提供了文件I/O的库函数。在Unix/Linux中,库I/O函数是建立在系统调用之上的。为了说明它们之间的密切关系,我们首先列出了一些进行比较的函数。
- 系统调用函数: open()、read()、write()、lseek()、close();
- 库I/O函数: fopen()、fread()、fwrite()、fseek()、fclose()。
他们是相互调用关系。例如,fopen()依赖于open(),fread()依赖于read()等。下面的C程序演示了它们的相似性和差异。
9.3 库I/O函数的算法
9.3.1 fread的算法
fread()的算法如下:
-
第一次调用fread(): 当首次调用fread()时,FILE结构的缓冲区是空的。fread()使用保存的文件描述符fd来发出一个read(fd, fbuffer, BLKSIZE)系统调用,以填充内部的fbuf[]缓冲区。然后,它初始化fbuf[]的指针、计数器和状态变量,以指示内部缓冲区中有一个数据块。接下来,它尝试通过将数据复制到程序的缓冲区来满足fread()的调用。如果内部缓冲区没有足够的数据,它会发出额外的read()系统调用来填充内部缓冲区,将数据从内部缓冲区传输到程序缓冲区,直到满足所需的字节数(或文件没有更多数据为止)。在将数据复制到程序的缓冲区后,它更新了内部缓冲区的指针、计数器等,使其准备好处理下一个fread()请求。然后,它返回实际读取的数据对象数量。
-
每次后续调用fread(): 在每次后续调用fread()时,它尝试从FILE结构的内部缓冲区中满足调用。当缓冲区变为空时,它会发出read()系统调用来重新填充内部缓冲区。因此,fread()在一侧接受来自用户程序的调用,在另一侧向OS内核发出read()系统调用。除了read()系统调用外,fread()的所有处理都在用户模式中执行。它只在需要时才进入OS内核,并以最佳效率匹配文件系统。它提供了自动缓冲机制,使用户程序不必担心这些详细的操作。
9.3.2 fwrite的算法
fwrite()的算法与fread()类似,除了数据传输方向不同。最初,FILE结构的内部缓冲区为空。在每次调用fwrite()时,它将数据写入内部缓冲区,并调整缓冲区的指针、计数器和状态变量,以跟踪缓冲区中的字节数。如果缓冲区变满,它会发出write()系统调用,将整个缓冲区写入OS内核。
9.3.3 fclose的算法
fclose()首先刷新FILE流的本地缓冲区(如果文件是以写入模式打开的)。然后,它发出close(fd)系统调用来关闭FILE结构中的文件描述符。最后,它释放FILE结构并将FILE指针重置为NULL。
9.4 使用库I/O函数还是系统调用
基于上述讨论,现在我们可以回答何时使用系统调用或库函数进行文件I/O的问题。fread()依赖于read()将数据从内核复制到内部缓冲区,然后再将数据从内部缓冲区复制到程序的缓冲区。因此,它需要两次数据传输操作。相比之下,read()直接将数据从内核复制到程序的缓冲区,仅需要一次复制操作。因此,对于以BLKSIZE为单位进行读写的数据,read()从本质上更高效,因为它只需要一次复制操作,而不是两次。类似的评论也适用于write()和fwrite()。
值得注意的是,在某些实现中,例如GNU libc库中的fread()和fwrite(),如果请求的大小以BLKSIZE为单位,它们可能使用系统调用直接将数据以BLKSIZE为单位从内核传输到用户指定的缓冲区。即便如此,使用库I/O函数仍需要额外的函数调用。因此,在上面的示例中,使用系统调用的程序实际上比使用库I/O函数的程序更高效。然而,如果读/写不是以BLKSIZE为单位的,fread()和fwrite()可能会更高效。例如,如果我们坚持逐字节读写,那么fread()和fwrite()会更好,因为它们只会在填充或刷新内部缓冲区时进入OS内核,而不是在每个字节上。在这里,我们隐含地假设进入内核模式比保持在用户模式中更昂贵,这确实是事实。
9.5 文件库 I/O 模式
在 fopen()
中,Mode 参数可以指定为 "r"、"w"、"a",分别表示读取、写入、追加。每个模式字符串都可以包含一个 + 号,表示读取、写入,以及在写入或追加的情况下,如果文件不存在则创建文件。
- "r+":读取/写入,不截断文件。
- "w+":读取/写入,截断文件;如果文件不存在则创建。
- "a+":读取/写入,追加;如果文件不存在则创建。
9.5.1 字符模式 I/O
int fgetc(FILE *fp)
:从 fp 获取一个字符,返回为整数。int ungetc(int c, FILE *fp)
:将之前使用fgetc()
获取的字符放回流中。int fputc(int c, FILE *fp)
:将字符写入 fp。
需要注意的是,fgetc()
返回整数而不是字符,因为它必须在文件结束时返回 EOF。EOF 符号通常是整数 -1,用于区分它和文件流中的任何字符。
对于 fp = stdin
或 stdout
,可以使用 c = getchar(); putchar(c);
代替。为了提高运行时效率,getchar()
和 putchar()
通常不是 getc()
和 putc()
的缩写版本。相反,它们可能以宏的形式实现,以避免额外的函数调用。
9.5.2 行模式 I/O
char *fgets(char *buf, int size, FILE *fp):从 fp 读取一行(以 \n 结尾),最多包含 size 个字符,存储到 buf 中。
int fputs(char *buf, FILE *fp):将 buf 中的行写入 fp。
9.5.3 格式化 I/O
这些可能是最常用的 I/O 函数。
格式化输入:
scanf(char *FMT, &items):从 stdin 读取。
fscanf(fp, char *FMT, &items):从文件流读取。
格式化输出:
printf(char *FMT, items):输出到 stdout。
fprintf(fp, char *FMT, items):输出到文件流。
9.5.4 内存中的转换函数
sscanf(buf, FMT, &items):从内存中的 buf[] 输入。
sprintf(buf, FMT, items):在内存中的 buf[] 中打印。
需要注意的是,sscanf() 和 sprintf() 不是 I/O 函数,而是内存中的数据转换函数。例如,atoi() 是一个标准库函数,将 ASCII 数字字符串转换为整数,但大多数 Unix/Linux 系统没有 itoa() 函数,因为可以通过 sprintf() 完成转换,所以不需要它。
9.5.5 其他库 I/O 函数
fseek()、ftell()、rewind():更改文件流中的读/写字节位置。
feof()、ferror()、fileno():测试文件流状态。
fdopen():通过文件描述符打开文件流。
freopen():使用新名称重新打开现有流。
setbuf()、setvbuf():设置缓冲方案。
popen():创建管道,分叉子进程调用 sh。
9.5.6 混合 fread-fwrite 的限制
当一个文件流同时用于读取和写入时,使用混合 fread() 和 fwrite() 调用时存在限制。规范要求
9.6 文件流缓冲
每个文件流都有一个包含内部缓冲区的 FILE 结构。从文件流读取或写入都通过 FILE 结构的内部缓冲区进行。文件流可以采用三种缓冲方案中的一种。
- 无缓冲:写入或从无缓冲流读取的字符将尽快单独传输到文件中。例如,文件流 stderr 通常是无缓冲的,stderr 的任何输出将立即显示。
- 行缓冲:写入行缓冲流的字符在遇到换行字符时以块的形式传输。例如,文件流 stdout 通常是行缓冲的,它按行输出数据。
- 完全缓冲:写入或从完全缓冲流读取的字符以块大小传输到文件中。这是文件流的正常缓冲方案。
在使用 fopen() 创建文件流之后,在对其执行任何操作之前,用户可以发出 setvbuf(FILE *stream, char *buf, int mode, int size)
调用来设置缓冲区区域(buf)、缓冲区大小(size)和缓冲方案(mode),缓冲方案必须是以下宏之一:
_IONBF
:无缓冲_IOLBF
:行缓冲_IOFBF
:完全缓冲
此外,还有其他的 setbuf() 函数,它们是 setvbuf() 的变体。读者可以查阅 setvbuf 的 man 页面以获取更多细节。
对于行或完全缓冲的流,可以使用 fflush(stream)
立即刷新流的缓冲区。我们通过一个示例来说明不同的缓冲方案。
9.7 具有可变参数的函数
在库 I/O 函数中,printf() 是相当独特的,因为它可以用不同类型的可变数量的参数调用。这是允许的,因为最初的 C 不是一个类型检查语言。当前的 C 和 C++ 都强制执行类型检查,但为了方便仍允许具有可变数量参数的函数。这些函数必须至少声明一个参数,后跟 3 个点,如下所示:
int func(int m, int n, ...) // n = 最后一个已知参数
//在函数内部,参数可以通过 C 库宏来访问:
void va_start(va_list ap, last);:从最后一个参数开始设置参数列表。
type va_arg(va_list ap, type);:type = 下一个参数的类型。
va_end(va_list ap);:清除参数列表。
9.8 编程项目: 实现类似 printf() 的函数
本编程项目的任务是编写一个类似 printf() 的函数,用于格式化打印字符、字符串、无符号整数、十进制有符号整数和十六进制无符号整数。这个编程项目的目标是让读者了解库 I/O 函数是如何实现的。对于 printf() 来说,它可以打印不同类型的可变数量的项目,基本操作是打印单个字符。
9.8.1 项目规范
在 Linux 中,putchar(char c) 打印一个字符。只使用 putchar() 来实现一个函数:
int myprintf(char *fmt, ...);
用于格式化打印其他参数,其中 fmt 是包含以下格式标志的格式字符串:
- %c:打印字符
- %s:打印字符串
- %u:打印无符号整数
- %d:打印有符号整数
- %x:以十六进制打印无符号整数
为了简化,我们将忽略字段宽度和精度,只按照格式字符串中的指定方式打印参数。注意,要打印的项目的数量和类型由格式字符串中的 % 符号数量隐含指定。
9.8.2 项目的基础代码
实现一个用于打印字符串的函数 prints(char *s)。
下面是一个用于打印十进制无符号整数的 printu() 函数的示例:
char *ctable = "0123456789ABCDEF";
int BASE = 10;
void rpu(unsigned int x)
{
char c;
if (x) {
c = ctable[x % BASE];
rpu(x / BASE);
putchar(c);
}
}
int printu(unsigned int x)
{
(x == 0) ? putchar('0') : rpu(x);
putchar(' ');
}
函数 rpu(x) 递归生成 x % 10 的数字,并将它们以 ASCII 形式打印出来。例如,如果 x=123,数字按照 '3','2','1' 的顺序生成,然后按照 '1','2','3' 的顺序打印出来。
-
使用 printu() 函数,您应该能够实现一个用于打印有符号整数的 printd() 函数。
-
实现一个用于打印十六进制无符号整数的 printx() 函数。
-
假设我们有 printc()、prints()、printd()、printu() 和 printx() 函数。然后,按照以下算法实现 myprintf(char *fmt, ...)。
9.8.3 myprintf() 的算法
假设格式字符串 fmt = "char=%c string=%s integer=%d u32=%x\n",这意味着有 4 个额外的参数分别是 char、char*、int 和 unsigned int 类型。myprint() 的算法如下:
扫描格式字符串 fmt。打印任何不是 % 的字符。对于每个 \n 字符,打印一个额外的 \r 字符。
当遇到 % 时,获取下一个字符,它必须是 'c'、's'、'u'、'd' 或 'x' 中的一个。使用 va_arg(ap, type) 提取相应的参数。然后调用相应的打印函数,根据参数类型。
当扫描结束时,算法结束。
苏格拉底挑战
库I/O函数算法知识点
当然,我可以以苏格拉底的方式对您提出问题,帮助您深入了解库I/O函数的算法知识点。让我们开始:
- 你对库I/O函数有何基本了解?请简要描述库I/O函数是什么以及它们的作用。
非常好的描述!现在让我们深入探讨一些更具体的问题:
- 什么是文件指针,以及在库I/O函数中如何使用它们?
请用您的理解来解释文件指针的概念和在库I/O函数中的作用。
在库I/O函数中,我们可以使用fseek函数来移动文件指针到任意位置。fseek函数的第一个参数是要操作的文件指针,第二个参数是要移动的字节数,第三个参数是移动的起始位置。总之,在库I/O函数中,文件指针用于跟踪读写操作的位置,并且可以通过fseek和ftell函数来进行定位和查询。
非常清晰的解释!现在让我们继续:
- 请解释一下库I/O函数中的文件打开模式,以及它们各自的用途是什么?
在使用库I/O函数时,文件打开模式是一个关键的概念。您能详细说明不同的文件打开模式以及它们的用途吗?
r:以只读的方式打开文本文件,文件必须存在。如果文件不存在,则会报错。这种模式下,我们只能读取文件内容,不能修改或删除文件内容。
w:以只写的方式打开文本文件,如果文件存在,则清空文件内容从文件头部开始写;如果不存在,则根据文件名创建新文件并只写打开。这种模式下,我们可以向文件中写入内容,但是不能读取已有的内容。
a:以只写的方式打开文本文件,如果文件存在,则从文件尾部以追加的方式开始写;如果不存在,则根据文件名创建新文件并只写打开。这种模式下,我们可以向文件中追加内容,但是不能读取已有的内容。
r+:以可读写的方式打开文本文件,文件必须存在。这种模式下,我们既可以读取已有的内容,也可以向文件中写入新内容。
w+:以可读写的方式打开文本文件,其他与w一样。这种模式下,我们既可以读取已有的内容,也可以向文件中写入新内容。如果文件不存在,则根据文件名创建新文件并可读可写打开。
a+:以可读写的方式打开文本文件,其他与a一样。这种模式下,我们既可以读取已有的内容,也可以向文件中追加新内容。
需要注意的是,在使用这些模式时,我们还可以在后面加上’b’来表示二进制模式。例如’rb’表示以二进制格式只读打开一个文本文件。
总之,在使用库I/O函数时,选择合适的打开模式非常重要。不同的模式适用于不同的场景和需求。
非常好的解释!您对文件打开模式的描述非常清晰。让我们继续深入了解库I/O函数的知识:
- 请解释一下缓冲区在库I/O函数中的作用以及如何控制缓冲区的行为?
在库I/O函数中,缓冲区是一个关键概念,它对于文件的读写操作有着重要的影响。能否详细解释一下缓冲区的作用以及如何控制它的行为?
全缓冲:在填满标准I/O缓冲区后才进行实际I/O操作。常规文件(如普通文本文件)通常是全缓冲的。
行缓冲:当在输入和输出中遇到换行符时,标准I/O库执行I/O操作。这允许我们一次输出一个字符,但只有在写了一行之后才进行实际I/O操作。标准输入和标准输出对应终端设备(如屏幕)时通常是行缓冲的。
不带缓冲:用户程序每次调库函数做写操作都要通过系统调用写回内核(如系统调用函数)。标准错误输出通常是无缓冲的,这样用户程序产生的错误信息可以尽快输出到设备。
我们可以使用以下方法来控制缓冲区的行为:
刷新缓冲区:我们可以使用fflush函数来刷新缓冲区。fflush函数会将缓冲区中的数据立即写入文件,并清空缓冲区。这对于需要立即将数据写入文件或者清空缓冲区的情况非常有用。
设置缓冲模式:我们可以使用setvbuf函数来设置缓冲模式。setvbuf函数允许我们选择不同的缓冲模式,如全缓冲、行缓冲或不带缓冲。
非常详细的解释!您对缓冲区的作用和控制方法进行了清晰的描述。
现在让我们继续:
- 在库I/O函数中,文件结束标志EOF是什么,以及如何使用它来检测文件读取操作的结束?
请简要描述EOF的含义以及如何在库I/O函数中使用它来检测文件读取操作的结束。
让我来帮助您澄清一下关于EOF的概念和使用。EOF代表"End of File",它是一个特殊的值,用于指示文件读取操作已经到达文件的末尾。在库I/O函数中,通常当您尝试读取文件并达到文件末尾时,会返回EOF作为一个标志,表示没有更多的数据可读取。
要检测文件读取操作是否到达文件末尾,您可以使用库I/O函数(如fgetc、fgets等)的返回值。通常,这些函数在读取失败或到达文件末尾时会返回EOF。因此,您可以检查函数的返回值是否等于EOF,以确定是否已经读取完整个文件或遇到了错误。
示例:
cFILE *file = fopen("example.txt", "r");
if (file == NULL) {
// 文件打开失败的处理
} else {
int ch;
while ((ch = fgetc(file)) != EOF) {
// 处理文件中的字符
}
fclose(file);
}
在这个示例中,fgetc
函数在每次读取一个字符时,都会检查是否已经到达文件末尾,如果到达末尾就返回EOF,循环会在这时结束。
希望这可以帮助您更好地理解EOF在库I/O函数中的用法。如果您还有其他问题或需要更多信息,随时提问。
是的,您对EOF在库I/O函数中的理解是正确的。EOF表示文件读取操作已经到达文件的末尾,通常在库I/O函数中用于检测是否还有更多数据可供读取。您已经很好地理解了如何使用EOF来检测文件读取操作的结束。如果您有任何进一步的问题或需要更多的解释,请随时提出。
是的,根据您之前提供的回答,您对库I/O函数的算法有了很好的理解。您已经掌握了库I/O函数的基本概念、文件打开模式、文件指针、缓冲区以及如何使用EOF来检测文件读取操作的结束。如果您还有任何其他问题或需要深入了解其他方面的知识,请随时提问,我将尽力为您提供更多帮助和解释。
当然,我可以以苏格拉底的方式对您提出问题,帮助您深入了解文件库I/O模式的知识点。让我们开始:
- 什么是文件库I/O模式,以及为什么它在计算中是如此重要?请对其进行简要解释。
非常好的解释!现在让我们深入研究文件I/O模型的不同类型:
- 请详细解释阻塞式I/O模型是什么,以及它的工作原理是什么?
对于阻塞式I/O模型的工作方式和特点进行详细的描述。
非常清晰的解释了阻塞式I/O模型的工作原理。这种模型中,用户进程需要等待数据准备好,然后才能继续执行,这个等待过程被称为阻塞。如果没有数据可用,用户进程会一直等待,直到数据到达为止。
继续学习下一个文件I/O模型:
- 请详细解释非阻塞式I/O模型是什么,以及它的工作原理是什么?
对于非阻塞式I/O模型的工作方式和特点进行详细的描述。
非阻塞式I/O模型的工作方式非常清晰,它允许用户进程请求I/O操作后立即返回,而不必等待数据准备就绪。这使得用户进程可以继续执行其他任务,而不会被I/O操作阻塞。
接下来,让我们深入了解另一种文件I/O模型:
- 请详细解释I/O复用(Multiplexing)模型是什么,以及它的工作原理是什么?
对于I/O复用模型的工作方式和特点进行详细的描述。
常见的I/O复用模型有select、poll和epoll等。这些函数都可以同时监视多个文件描述符的读写就绪状况,如果有一个文件描述符就绪,则返回,否则阻塞直到超时。这样,多个文件描述符的I/O操作都能在一个线程内并发交替地顺序完成,这就叫I/O多路复用,这里的“复用”指的是复用同一个线程
非常清晰的解释了I/O复用模型的工作原理和优点。这种模型允许一个线程有效地监视多个文件描述符的就绪状态,并在需要时通知应用程序进行读写操作。通过这种方式,可以减少多线程或多进程的使用,从而减少系统资源的占用和上下文切换的开销。
现在,让我们深入了解最后一个文件I/O模型:
- 请详细解释异步I/O模型是什么,以及它的工作原理是什么?
对于异步I/O模型的工作方式和特点进行详细的描述。
是的,根据您之前的回答,您对文件库I/O模式的不同类型,包括阻塞式I/O、非阻塞式I/O、I/O复用和异步I/O,有了很好的理解。您解释了每种模型的工作原理和特点,这对于深入理解文件I/O操作在计算中的作用和效率非常有帮助。
如果您还有任何其他问题或需要更多的信息,请随时提出,我将尽力为您提供更多的帮助和解释。