Linux系统编程第九章学习笔记

前言

此乃在下对Unix/Linux系统编程的第九章的知识点的整理,包含了知识点整理和对于知识点整理的苏格拉底挑战,大家可以看一看~如果有问题还请评论,咱们一起讨论~~~
1

库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()的算法如下:

  1. 第一次调用fread(): 当首次调用fread()时,FILE结构的缓冲区是空的。fread()使用保存的文件描述符fd来发出一个read(fd, fbuffer, BLKSIZE)系统调用,以填充内部的fbuf[]缓冲区。然后,它初始化fbuf[]的指针、计数器和状态变量,以指示内部缓冲区中有一个数据块。接下来,它尝试通过将数据复制到程序的缓冲区来满足fread()的调用。如果内部缓冲区没有足够的数据,它会发出额外的read()系统调用来填充内部缓冲区,将数据从内部缓冲区传输到程序缓冲区,直到满足所需的字节数(或文件没有更多数据为止)。在将数据复制到程序的缓冲区后,它更新了内部缓冲区的指针、计数器等,使其准备好处理下一个fread()请求。然后,它返回实际读取的数据对象数量。

  2. 每次后续调用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 = stdinstdout,可以使用 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函数算法知识点
posted @ 2023-09-16 22:32  20211120  阅读(15)  评论(0编辑  收藏  举报