《Unix/Linux系统编程》教材学习笔记第九章

发布时间 2023-09-16 17:00:08作者: シバ鳥

chapter9


I/O库函数与系统调用

系统调用是文件操作的基础,但它们只支持数据块的读/写。

系统调用函数:open()、read()、write()、lseek()、close();
I/O库函数:fopen()、fread()、fwrite()、fseek()、fclose();

I/O库函数的根都在对应的系统调用函数中,比如fopen()依赖于open()。

I/O库函数的算法

fread()算法:
1.第一次调用fread()时,FILE结构体缓冲区是空的,fread()使用保存的文件描述符fd发出n = read(fd,fbuffer,BLKSIZE);系统调用,用数据块填充内部的fbuf[]。然后它会初始化fbuf[]的指针、计数器和状态变量,表明内部缓冲区有一个数据块。接着,通过将数据赋值到程序的缓冲区,尝试满足来自内部缓冲区的fread()调用。如果内部缓冲区没有足够的数据,则会再发出一个read()系统调用来填充内部缓冲区,将数据从内部缓冲区传输到程序缓冲区,直到满足所需的字节数(或者文件无更多数据)。将数据复制到程序的缓冲区之后,它会更新内部缓冲区的指针、计数器等,为下一个fread()请求做好准备。然后它会返回实际读取的数据对象数量。
2.在随后的每次fread()调用中,它都尝试满足来自FILE结构体内部缓冲区的调用。当缓冲区变为空时,它就会发出read()系统调用来重新填充内部缓冲区。因此fread()一方面接受来自用户程序调用,另一方面向操作系统内核发出read()系统调用。除了read()系统调用外,所有fread()处理都在用户模式映像中执行。它只在需要时才会以一种最搞笑匹配文件的方式进入操作系统内核,并且会提供自动缓冲机制。

fwrite()算法:
与fread()算法类似,只是数据传输方向不同。最开始FILE结构体内部缓冲区为空,每次调用fwrite()时,将数据写入内部缓冲区,并调整缓冲区的指针、计数器和状态变量,以跟踪缓冲区中的字节数。如果缓冲区已满,则发出write()系统调用,将整个缓冲区写入操作系统内核。

fclose()算法:
若文件以写方式打开,fclose()会先关闭文件流的局部缓冲区。然后发出close(fd)系统调用来关闭FILE结构体中的文件描述符,最后释放FILE结构体并将FILE指针重置为NULL。

I/O库模式

fopen()模式参数可指定为:"r"、"w"、"a",分别代表读、写、追加,后面添加"+"表示,若文件不存在先创建文件。

字符模式I/O:
int fgetc(FILE *fp);
int ungetc(int c, FILE *fp);
int fputc(int c, FILE*fp);
当fp=stdin或stdout时可能会使用c=getchar();putchar(c);来代替。

行模式I/O:
char *fgets(char *buf, int size, FILE *fp);
int fputs(char *buf, FILE *fp);
当fp=stdin或stdout时也可以使用
gets(char *buf);
puts(char *buf);

格式化I/O:
格式化输入
scanf(char *FMT, &items);
fscanf(fp, char *FMT, &items);
格式化输出
printf(char *FMT, items);
fprintf(fp, char *FMT, items);

内存中数据转换函数(非I/O):
sscanf(buf, FMT, &items);
sprintf(buf, FMT, items);

其他I/O库函数
fseek()、ftell()、rewind():更改文件流中的读/写字节位置。
feof()、ferr()、fileno():测试文件流状态。
fdopen():用文件描述符打开文件流。
freopen():以新名称重新打开现有的流。
setbuf()、setvbuf():设置缓冲方案。
popen():创建管道,复刻子进程来调用sh。

限制混合fread-fwrite:规范要求每对fread()和fwrite()之间至少有一个fseek()或ftell()。

函数用法总结:
1. 文件读写:
   - 打开文件:使用 `fopen` 函数打开文件,它接受文件名和打开模式作为参数,返回文件指针。
   - 读取文件:使用 `fread` 函数从文件中读取数据,它接受文件指针、数据缓冲区和读取字节数作为参数。
   - 写入文件:使用 `fwrite` 函数将数据写入文件,它接受文件指针、数据缓冲区和写入字节数作为参数。
   - 关闭文件:使用 `fclose` 函数关闭文件,释放资源。

2. 标准输入输出:
   - 读取标准输入:使用 `scanf` 函数从标准输入读取数据,可以使用格式化字符串指定数据类型。
   - 写入标准输出:使用 `printf` 函数将数据格式化并输出到标准输出,同样可以使用格式化字符串进行格式控制。

3. 格式化输出:
   - 格式化字符串:使用格式化字符串来指定输出的格式,例如 `%d` 表示整数,`%f` 表示浮点数,`%s` 表示字符串等。
   - `printf` 函数:根据格式化字符串将数据格式化为字符串并输出到标准输出或文件。
   - `sprintf` 函数:将格式化的数据写入字符数组中,而不是输出到标准输出。
   - `fprintf` 函数:将格式化的数据写入文件中,而不是输出到标准输出。

4. 缓冲区管理:
   - `setbuf` 和 `setvbuf` 函数:用于设置文件流的缓冲区,可以控制缓冲区的大小和行为。

5. 文件定位:
   - `fseek` 和 `ftell` 函数:用于在文件中定位读写位置,`fseek` 移动读写位置,`ftell` 返回当前位置。

6. 错误处理:
   - `perror` 函数:用于输出与最近一次文件操作相关的错误消息。
   - `feof` 和 `ferror` 函数:用于检查文件结束和错误状态。

文件流缓冲

文件流可使用三种缓冲方案中的一种:
_IONBF:无缓冲(尽快单独传输)。
_IOLBF:行缓冲(换行符,块形式)。
_IOFBF:全缓冲(写入全缓冲流或读取字符以块大小,正常缓冲方案)。
通过fopen()创建文件流之后,在执行任何操作前均可发出
setvbuf(FILE *stream, char *buf, int mode, int size)

变参函数

C和C++允许参数数量可变的函数,这些函数必须至少使用一个参数进行声明,后跟3个点,如:int func(int m, int n . . . )
在函数内部可以通过C语言库宏访问参数:
void va_start(va_list ap, last);
type va_arg(va_list ap, type);
va_end(va_list ap);

总结


1.文件操作通常包括字符读写、行读写以及任意位置读写等功能。以下是这些文件操作的概要:

字符读写:
字符读取:从文件中读取一个字符。
字符写入:将一个字符写入文件。

行读写:
行读取:从文件中逐行读取文本。
行写入:将文本逐行写入文件。

任意位置读写:
文件指针操作:使用文件指针移动到特定位置,然后进行读写操作。

2.文件操作

文件操作包括文件打开、文件关闭、文件读写、文件定位、缓冲区管理、错误处理等。

3.二进制文件和文本文件转换:

使用不同的模式打开文件可以实现文本到二进制或二进制到文本的转换。

4.数据结构的读写:

在C语言中可以使用数据、结构体等数据结构进行文件中的读写操作。


GPT提问环节

C语言文件操作




文件缓冲流


可以看到,GPT的提问是较为系统化的,学会使用GPT学习能够极大地提高学习效率。

在学习中遇到的一些问题

问题1:

当我在终端使用gcc ./src/*.c命令后出现报错,接着我向GPT需求帮助。

按照它给的方法再次尝试,结果还是会出现报错,如下图。

然后我又接着向GPT求助。


在它给出的方法中,我发现自己的问题出在哪里了,因为我实际上是需要给两个有main函数的C源文件编译,只是因为想要偷懒,所以才想着批量编译的方式来减少过程量,但是我忽略了一个问题,使用gcc编译的必须是只含有一个main函数的单模块或者多模块的C源文件,所以才会出现上图中的报错。

问题2:

在编译时出现没有某个库的报错。然后我向GPT求助如图。

根据GPT的回答,我尝试了它给的方法,但是依然不奏效,然后我上贴吧的Linux吧寻找解决方案,最后找到一篇帖子,根据帖子中的内容我明白自己的问题在哪里了。在编写C源文件中我引用的库名错误,在教材中虽然引用的库名没有.h后缀,但是我使用的linux版本是20.4,版本比较新,所以相较于老版的库名可能也会有所差别。于是我将文件中库名添加后缀后再次进行编译,前后多多少少出现了一些语法上的毛病,说明我在编写C语言源程序时总是粗心,语法弄错,在后面的学习中我需要在编写代码方面细心度要加强。下图是我解决问题的过程图。

教材代码实践


代码托管点击这里