Linux-应用编程学习笔记(二、文件I/O、标准I/O)

一、文件I/O基础

文件 I/O 指的是对文件的输入/输出操作,就是对文件的读写操作。Linux 下一切皆文件。

1.1 文件描述符

在 open函数执行成功的情况下, 会返回一个非负整数, 该返回值就是一个文件描述符(file descriptor) , 这说明文件描述符是一个非负整数;

对于 Linux 内核而言,所有打开的文件都会通过文件描述符进行索引。

在 Linux 系统中,一个进程可以打开的文件数是有限制的。默认1024个(0~1023)系统占用:系统标准输入(0)、 标准输出(1)以及标准错误(2)。

通过 ulimit 命令来查看进程可打开的最大文件数:

ulimit -n

Tips:每一个硬件设备都会对应于linux系统下的某一个文件(设备文件),应用程序对设备文件进行读写操作来使用或控制硬件设备。 

1.1.1 复制文件描述符

复制得到的文件描述符和旧的文件描述符拥有相同的权限,但是号是不一样的。可以多次复制。

“复制”的含义实则是复制文件表。

  • dup 函数用于复制文件描述符,可以实现两个写入的接续进行。
#include <unistd.h>
int dup(int oldfd);函数参数和返回值含义如下:
oldfd: 需要被复制的文件描述符。
返回值: 成功时将返回一个新的文件描述符,由操作系统分配,分配置原则遵循文件描述符分配原则;
如果复制失败将返回-1,并且会设置 errno 值。
  • dup2也可以复制文件描述符,并且可以手动指定文件描述符。
#include <unistd.h>
int dup2(int oldfd, int newfd);函数参数和返回值含义如下:
oldfd: 需要被复制的文件描述符。
newfd: 指定一个文件描述符(需要指定一个当前进程没有使用到的文件描述符)。
返回值: 成功时将返回一个新的文件描述符,也就是手动指定的文件描述符 newfd;如果复制失败将返
回-1,并且会设置 errno 值。

1.2 open打开文件(文件权限)

可以通过man命令查看系统调用的信息。

man 2 open/*
man 命令后面跟着两个参数:
数字 2 表示系统调用, Linux 命令(对应数字 1)以及标准 C 库函数(对应数字 3)所对应的帮助信息。
最后一个参数 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);int creat(const char *pathname, mode_t mode);int openat(int dirfd, const char *pathname, int flags);
int openat(int dirfd, const char *pathname, int flags, mode_t mode);

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

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

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

  • O_RDONLY:只读。    
  • O_WRONLY:只写。    
  • O_RDWR:可读可写。    
  • O_CREAT:不存在则创建。    
  • O_DIRECTORY:pathname指向的不是目录,调用open失败。    
  • O_EXCL:与O_CREAT一起使用,判断是否已经存在。    
  • O_NOFOLLOW:pathname是一个符号链接,将不对其进行解引用,直接返回错误。
  • O_TRUNC :将文件原本的内容全部丢弃,文件大小变为 0。
  • O_APPEND :调用 open 函数打开文件,当每次使用 write()函数对文件进行写操作时,都会自动把文件当前位置偏移量移动到文件末尾, 从文件末尾开始写入数据,也就是意味着每次写入数据都是从文件末尾开始。使用了 O_APPEND 标志,即使是通过 lseek 函数也是无法修改写文件时对应的位置偏移量。
  • O_DSYNC:每个write()之后,自动调用fdatasync()进行数据同步
  • O_SYNC:每个write()之后,自动调用fsync()进行数据同步

mode: 
此参数用于指定新建文件的访问权限,只有当 flags 参数中包含 O_CREAT 或 O_TMPFILE 标志
时才有效(O_TMPFILE 标志用于创建一个临时文件)。 权限对于文件来说是一个很重要的属性,那么在 Linux系统中,我们可以通过 touch 命令新建一个文件,此时文件会有一个默认的权限,如果需要修改文件权限,可通过 chmod 命令对文件权限进行修改,譬如在 Linux 系统下我们可以使用"ls -l"命令来查看到文件所对应的权限。mode 参数的类型是 mode_t(u32)

S---这 3 个 bit 位用于表示文件的特殊权限,文件特殊权限一般用的比较少;

U---这 3 个 bit 位用于表示文件所属用户的权限,即文件或目录的所属者;

G---这 3 个 bit 位用于表示同组用户(group)的权限,即与文件所有者有相同组 ID 的所有用户;

O---这 3 个 bit 位用于表示其他用户的权限;

3 个 bit 位中,按照 rwx 顺序来分配权限位。例如:7(八进制),可读可写可执行,5(八进制),可读可执行。

open 函数文件权限宏:(通过位或可以组合)

宏定义说明
S_IRUSR允许文件所属者读文件
S_IWUSR允许文件所属者写文件
S_IXUSR允许文件所属者执行文件
S_IRWXU允许文件所属者读、写、执行文件
S_IRGRP允许同组用户读文件
S_IWGRP允许同组用户写文件
S_IXGRP允许同组用户执行文件
S_IRWXG允许同组用户读、写、执行文件
S_IROTH允许其他用户读文件
S_IWOTH允许其他用户写文件
S_IXOTH允许其他用户执行文件
S_IRWXO允许其他用户读、写、执行文件
S_ISUID
S_ISGID
S_ISVTX
set-user-ID(特殊权限)
set-group-ID(特殊权限)
sticky(特殊权限)

返回值

成功将返回文件描述符,文件描述符是一个非负整数;失败将返回-1。

1.3 write写文件

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

函数参数和返回值含义如下:
fd: open之后返回的文件描述符。 关于文件描述符,需要将进行写操作的文件所对应的文件描述符传递给 write 函数。
buf: 指定写入数据对应的缓冲区。
count: 指定写入的字节数
返回值: 如果成功将返回写入的字节数(0 表示未写入任何字节),如果此数字小于 count 参数,这不是错误,譬如磁盘空间已满,可能会发生这种情况;如果写入出错,则返回-1。

一般是默认写文件写到文件起始位置。如果当前位置偏移量为 1000 个字节处,调用 write()写入或 read()读取 500 个字节之后,当前位置偏移量将会移动到 1500 个字节处。

1.4 read读文件

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

函数参数和返回值含义如下:
fd: 文件描述符。与 write 函数的 fd 参数意义相同。

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

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

返回值: 如果读取成功将返回读取到的字节数,实际读取到的字节数(没那么多可以读的字节)可能会小于 count 参数指定的字节数,也有可能会为 0。

1.5 close关闭文件

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

fd: 文件描述符,需要关闭的文件所对应的文件描述符。
返回值: 如果成功返回 0,如果失败则返回-1。

进程终止的时候,内核会自动关闭进程打开的所有文件。显式关闭不再需要的文件描述符是良好的编程习惯,文件描述符是有限资源。

1.6 lseek

系统会自动记录读写位置偏移量。文件第一个字节数据的位置偏移量为 0。
当打开文件时,会将读写偏移量设置为指向文件开始位置处,以后每次调用 read()、 write()将自动对其进行调整,以指向已读或已写数据后的下一字节,因此,连续的调用 read()和 write()函数将使得读写按顺序递增,对文件进行操作。

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

fd: 文件描述符。
offset: 偏移量,以字节为单位。
whence: 用于定义参数 offset 偏移量对应的参考值, 该参数为下列其中一种(宏定义) :

  • SEEK_SET:读写偏移量将指向 offset 字节位置处(从文件头部开始算) ;
  • SEEK_CUR:读写偏移量将指向当前位置偏移量 + offset 字节位置处, offset 可以为正、也可以为负,如果是正数表示往后偏移,如果是负数则表示往前偏移;
  • SEEK_END:读写偏移量将指向文件末尾 + offset 字节位置处,同样 offset 可以为正、也可以为负,如果是正数表示往后偏移、如果是负数则表示往前偏移。

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

二、文件I/O深入

2.1 Linux系统如何管理文件

2.1.1 静态文件和inode

静态文件:文件存放在磁盘文件系统中,并且以一种固定的形式进行存放。

文件储存在硬盘上, 硬盘的最小存储单位叫做“扇区” (Sector), 每个扇区储存 512 字节(相当于 0.5KB),操作系统读取硬盘的时候,不会一个个扇区地读取,这样效率太低,而是一次性连续读取多个扇区,即一次性读取一个“” (block)。这种由多个扇区组成的“块” ,是文件存取的最小单位。 “块” 的大小,最常见的是 4KB,即连续八个 sector 组成一个 block。

磁盘两个区域:数据区(用于存储文件中的数据),inode区(用于存放 inode table(inode 表))。

通过"ls -i"命令或stat 命令查看文件的 inode 编号。

打开一个文件,系统内部会将这个过程分为三步:
1) 系统找到这个文件名所对应的 inode 编号;
2) 通过 inode 编号从 inode table 中找到对应的 inode 结构体;
3) 根据 inode 结构体中记录的信息,确定文件数据所在的 block,并读出数据。


2.1.2 文件打开时的状态

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

读写动态文件后,动态文件与静态文件不同步,内核会将动态文件更新至磁盘设备中。

在 Linux 系统中, 内核会为每个进程设置一个专门的数据结构用于管理该进程,譬如用于记录进程的状态信息、运行特征等,我们把这个称为进程控制块(Process control block,缩写PCB) 。

PCB 数据结构体中有一个指针指向了文件描述符表(File descriptors), 文件描述符表中的每一个元素索引到对应的文件表(File table),文件表也是一个数据结构体,其中记录了很多文件相关的信息,譬如文件状态标志、 引用计数、 当前文件的读写偏移量以及 i-node 指针(指向该文件对应的 inode)等, 进程打开的所有文件对应的文件描述符都记录在文件描述符表中,每一个文件描述符都会指向一个对应的文件表。

2.2 返回错误处理与errno

errno 本质上是一个 int 类型的变量,用于存储错误编号, 但是需要注意的是,并不是执行所有的系统调用或 C 库函数出错时,操作系统都会设置 errno。

通过man命令可以查询返回值。

#include <errno.h>

2.2.1 strerror函数(C库函数)

将对应的 errno 转换成适合我们查看的字符串信息

#include <string.h>
char *strerror(int errnum);函数参数和返回值如下:
errnum: 错误编号 errno。
返回值: 对应错误编号的字符串描述信息。

2.2.2 perror函数(C库函数)

调用此函数不需要传入 errno,函数内部会自己去获取 errno 变量的值, 调用此函数会直接将错误提示字符串打印出来,而不是返回字符串,除此之外还可以在输出的错误提示字符串之前加入自己的打印信息。perror 函数会在附加信息后面自动加入冒号和空格以区分

#include <stdio.h>
void perror(const char *s);函数参数和返回值含义如下:
s: 在错误提示字符串信息之前,可加入自己的打印信息,也可不加,不加则传入空字符串即可。
返回值: void 无返回值。

2.3终止进程

终止进程的方法:

  • main 函数中运行 return;
  • 调用 Linux 系统调用_exit()或_Exit();
  • 调用 C 标准库函数 exit()。

2.3.1 _exit、_Exit

调用_exit()函数会清除其使用的内存空间,并销毁其在内核中的各种数据结构, 关闭进程的所有文件描述符, 并结束进程、将控制权交给操作系统。_Exit与_exit一样。

_exit()和_Exit()是系统调用

#include <unistd.h>
void _exit(int status);status:0 表示正常结束、若为其它值则表示程序执行过程中检测到有错误发生。

2.3.2 exit

exit()是一个标准 C 库函数

#include <stdlib.h>
void exit(int status);

2.4 空洞文件

使用lseek函数将文件偏移量(6000字节)超过文件长度(4096字节),write开始在6000写,在4096~6000之间没有写入任何数据,就形成了文件空洞,那么该文件就是空洞文件。文件空洞不占用物理空间。直到在某个时刻对空洞部分进行写入数据时才会为它分配对应的空间,但是空洞文件形成时,逻辑上该文件的大小是包含了空洞部分的大小的,这点需要注意。


空洞文件的用处:对于很大的文件,可以使用多线程进行分段读写。

使用 ls 命令查看到的大小是文件的逻辑大小,包括了空洞部分大小和真实数据部分大小; du 命令查看到的大小是文件实际占用存储块的大小


2.5 多次打开同一文件

  • 一个进程内多次 open 打开同一个文件,那么会得到多个不同的文件描述符 fd,同理在关闭文件的时候也需要调用 close 依次关闭各个文件描述符。
  • 一个进程内多次 open 打开同一个文件,在内存中并不会存在多份动态文件。
  • 一个进程内多次 open 打开同一个文件,在不同文件描述符所对应的读写位置偏移量是相互独立的。
  • 分别写入同一文件,现象是:第二次写入的覆盖第一次写入的。想要接着写就用O_APPEND。

2.6 文件共享

同一个文件(譬如磁盘上的同一个文件,对应同一个 inode) 被多个独立的读写体同时进行 IO 操作(一个读写体操作文件尚未调用 close 关闭的情况下,另一个读写体去操作文件)。

常见的三种文件共享的实现方式
(1)同一个进程中多次调用 open 函数打开同一个文件。

多次调用open,得到不同的文件描述符,对应多个不同地文件表,文件表索引到同一inode。

(2)不同进程中分别使用 open 函数打开同一个文件。

(3)同一个进程中通过 dup(dup2)函数对文件描述符进行复制。

2.7 竞争冒险

操作共享资源的两个进程(或线程),其操作之后的所得到的结果往往是不可预期的, 因为每个进程(或线程)去操作文件的顺序是不可预期的,即这些进程获得 CPU 使用权的先后顺序是不可预期的,完全由操作系统调配, 这就是所谓的竞争状态。

2.8 原子操作

所谓原子操作, 是由多步操作组成的一个操作,原子操作要么一步也不执行,一旦执行, 必须要执行完所有步骤,不可能只执行所有步骤中的一个子集。

  • O_APPEND实现原子操作

移动当前写位置偏移量到文件末尾、写入数据”这两个操作步骤就组成了一个原子操作。

  • pread()和pwrite()
#include <unistd.h>
ssize_t pread(int fd, void *buf, size_t count, off_t offset);
ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);函数参数和返回值含义如下:
fd、 buf、 count 参数与 read 或 write 函数意义相同。
offset: 表示当前需要进行读或写的位置偏移量。
返回值: 返回值与 read、 write 函数返回值意义一样。

调用 pread 函数时,无法中断其定位和读操作(也就是原子操作);不更新文件表中的当前位置偏移量

  • 创建一个文件(O_EXCL 标志)

文件创建的竞争状态:

进程 A 和进程 B 都会创建出同一个文件,同一个文件被创建两次这是不允许的。

将“判断文件是否存在、创建文件”这两个步骤合成为一个原子操作

2.9 fcntl和ioctl

#include <unistd.h>
#include <fcntl.h>int fcntl(int fd, int cmd, ... /* arg */ )函数参数和返回值含义如下:
fd: 文件描述符。
cmd: 对fd进行的操作命令。cmd 操作命令大致可以分为以下 5 种功能:
1.复制文件描述符(cmd=F_DUPFD 或 cmd=F_DUPFD_CLOEXEC);
2.获取/设置文件描述符标志(cmd=F_GETFD 或 cmd=F_SETFD);
3.获取/设置文件状态标志(cmd=F_GETFL 或 cmd=F_SETFL);
4.获取/设置异步 IO 所有权(cmd=F_GETOWN 或 cmd=F_SETOWN);
5.获取/设置记录锁(cmd=F_GETLK 或 cmd=F_SETLK);
fcntl 函数是一个可变参函数,第三个参数需要根据不同的 cmd 来传入对应的实参,配合 cmd 来使
用。
返回值: 执行失败情况下,返回-1,并且会设置 errno;执行成功的情况下,其返回值与 cmd(操作命
令)有关,譬如 cmd=F_DUPFD(复制文件描述符)将返回一个新的文件描述符、 cmd=F_GETFD(获取文
件描述符标志)将返回文件描述符标志、 cmd=F_GETFL(获取文件状态标志)将返回文件状态标志等。

一般用于操作特殊文件或硬件外设。#include <sys/ioctl.h>
int ioctl(int fd, unsigned long request, ...);函数参数和返回值含义如下:
fd: 文件描述符。
request: 此参数与具体要操作的对象有关,没有统一值, 表示向文件描述符请求相应的操作;
...: 此函数是一个可变参函数, 第三个参数需要根据 request 参数来决定,配合 request 来使用。
返回值: 成功返回 0,失败返回-1。

2.10 截断文件

截断:如果文件目前的大小大于参数 length 所指定的大小,则多余的数据将被丢失,类似于多余的部分被“砍”掉了;如果文件目前的大小小于参数 length 所指定的大小,则将其进行扩展, 对扩展部分进行读取将得到空字节"\0"。

#include <unistd.h>
#include <sys/types.h>
int truncate(const char *path, off_t length);
int ftruncate(int fd, off_t length);ftruncate()使用文件描述符 fd 来指定目标文件。
truncate()则直接使用文件路径 path 来指定目标文件。

使用 ftruncate()函数进行文件截断操作之前,必须调用 open()函数打开该文件得到文件描述符,并且必须要具有可写权限,也就是调用 open()打开文件时需要指定 O_WRONLY 或 O_RDWR。
调用这两个函数并不会导致文件读写位置偏移量发生改变,所以截断之后一般需要重新设置文件当前的读写位置偏移量,以免由于之前所指向的位置已经不存在而发生错误(譬如文件长度变短了,文件当前所指向的读写位置已不存在)。
 

三、标准I/O库

3.1 标准I/O库简介

<stdio.h>

标准 I/O 和文件 I/O 的区别如下:

  • 虽然标准 I/O 和文件 I/O 都是 C 语言函数,但是标准 I/O 是标准 C 库函数,而文件 I/O 则是 Linux系统调用
  • 标准 I/O 是由文件 I/O 封装而来,标准 I/O 内部实际上是调用文件 I/O 来完成实际操作的;
  • 可移植性:标准 I/O 相比于文件 I/O 具有更好的可移植性,通常对于不同的操作系统,其内核向应用层提供的系统调用往往都是不同,譬如系统调用的定义、功能、参数列表、返回值等往往都是不一样的;而对于标准 I/O 来说,由于很多操作系统都实现了标准 I/O 库,标准 I/O 库在不同的操作系统之间其接口定义几乎是一样的,所以标准 I/O 在不同操作系统之间相比于文件 I/O 具有更好的可移植性。
  • 性能、效率: 标准 I/O 库在用户空间维护了自己的 stdio 缓冲区, 所以标准 I/O 是带有缓存的,而文件 I/O 在用户空间是不带有缓存的,所以在性能、效率上,标准 I/O 要优于文件 I/O。

3.2 FILE指针

对于标准 I/O 库函数来说,它们的操作是围绕 FILE 指针进行的,当使用标准 I/O 库函数打开或创建一个文件时,会返回一个指向 FILE 类型对象的指针(FILE *) ,使用该 FILE 指针与被打开或创建的文件相关联,然后该 FILE 指针就用于后续的标准 I/O 操作(使用标准 I/O 库函数进行 I/O 操作),所以由此可知,FILE 指针的作用相当于文件描述符。

FILE 是一个结构体数据类型,它包含了标准 I/O 库函数为管理文件所需要的所有信息,包括用于实际I/O 的文件描述符、指向文件缓冲区的指针、缓冲区的长度、当前缓冲区中的字节数以及出错标志等。


3.3 标准输入、标准输出和标准错误

#include <unistd.h>/* Standard file descriptors. */
#define STDIN_FILENO 0 /* Standard input. */
#define STDOUT_FILENO1 /* Standard output. */
#define STDERR_FILENO2 /* Standard error output. */
#include <stdio.h>/* Standard streams. */
extern struct _IO_FILE *stdin; /* Standard input stream. */
extern struct _IO_FILE *stdout; /* Standard output stream. */
extern struct _IO_FILE *stderr; /* Standard error output stream. */
/* C89/C99 say they're macros. Make them happy. */
#define stdin stdin
#define stdout stdout
#define stderr stderr

3.4 fopen、fclose

#include <stdio.h>
FILE *fopen(const char *path, const char *mode);函数参数和返回值含义如下:
path: 参数 path 指向文件路径,可以是绝对路径、也可以是相对路径。
mode: 参数 mode 指定了对该文件的读写权限,是一个字符串,稍后介绍。
返回值: 调用成功返回一个指向 FILE 类型对象的指针(FILE *),该指针与打开或创建的文件相关联,
后续的标准 I/O 操作将围绕 FILE 指针进行。 如果失败则返回 NULL,并设置 errno 以指示错误原因。
  • r = O_RDONLY
  • r+ = O_RDWR
  • w = O_WRONLY| O_CREAT|O_TRUNC
  • w+ =  O_RDWR| O_CREAT|O_TRUNC
  • a = O_WRONLY| O_CREAT|O_APPEND
  • a+ = O_RDWR| O_CREAT|O_APEND

新建文件的权限:默认值S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH (0666)

#include <stdio.h>
int fclose(FILE *stream);

3.5 fread、fwrite

#include <stdio.h>size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);ptr: fread()将读取到的数据存放在参数 ptr 指向的缓冲区中;
size: fread()从文件读取 nmemb 个数据项,每一个数据项的大小为 size 个字节,所以总共读取的数据大
小为 nmemb * size 个字节。
nmemb: 参数 nmemb 指定了读取数据项的个数。
stream: FILE 指针。
返回值: 调用成功时返回读取到的数据项的数目(数据项数目并不等于实际读取的字节数,除非参数
size 等于 1);如果发生错误或到达文件末尾,则 fread()返回的值将小于参数 nmemb, fread()不能区分文件结尾和错误。size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);ptr: 将参数 ptr 指向的缓冲区中的数据写入到文件中。
size: 参数 size 指定了每个数据项的字节大小,与 fread()函数的 size 参数意义相同。
nmemb: 参数 nmemb 指定了写入的数据项个数,与 fread()函数的 nmemb 参数意义相同。
stream: FILE 指针。
返回值: 调用成功时返回写入的数据项的数目(数据项数目并不等于实际写入的字节数,除非参数 size
等于 1);如果发生错误,则 fwrite()返回的值将小于参数 nmemb(或者等于 0)。

3.6 fseek定位

#include <stdio.h>
int fseek(FILE *stream, long offset, int whence);
//用于设置文件读写位置偏移量。函数参数和返回值含义如下:
stream: FILE 指针。
offset: 与 lseek()函数的 offset 参数意义相同。
whence: 与 lseek()函数的 whence 参数意义相同。
返回值: 成功返回 0;发生错误将返回-1,并且会设置 errno 以指示错误原因; 与 lseek()函数的返回值
意义不同,这里要注意!#include <stdio.h>
long ftell(FILE *stream);
//用于获取文件当前的读写位置偏移量。

3.7 检测或复位状态

1.feof函数

用于测试参数 stream 所指文件的 end-of-file 标志。

#include <stdio.h>
int feof(FILE *stream);

2.ferror函数

用于测试参数 stream 所指文件的错误标志。

#include <stdio.h>
int ferror(FILE *stream);

3.clearerr函数

用于清除 end-of-file 标志和错误标志。

#include <stdio.h>
void clearerr(FILE *stream);

3.8 格式化I/O

1.格式化输出

#include <stdio.h>
int printf(const char *format, ...);
//将程序中的字符串信息输出显示到终端int fprintf(FILE *stream, const char *format, ...);
//将格式化数据写入到由 FILE 指针指定的文件中int dprintf(int fd, const char *format, ...);
//将格式化数据写入到由文件描述符 fd 指定的文件中int sprintf(char *buf, const char *format, ...);
//将格式化数据存储在由参数 buf 所指定的缓冲区中int snprintf(char *buf, size_t size, const char *format, ...);
//与sprintf一样,为了防止缓冲区溢出,加入了参数size,多出的将丢弃。

printf举例:%[flags][width][.precision][length]typeflags: 标志,可包含 0 个或多个标志;
width: 输出最小宽度,表示转换后输出字符串的最小宽度;
precision: 精度,前面有一个点号" . ";
length: 长度修饰符;
type: 转换类型,指定待转换数据的类型。

type:

  • d/i:  int
  • o:    unsigned int,无符号八进制
  • u:    unsigned int,无符号十进制
  • x/X: unsigned int,无符号十六进制,大小写
  • f/F: double,浮点数,默认保留小数后六位
  • e/E: double,科学计数法表示浮点数
  • g/G: double,根据数值的长度,选择以最短的方式输出
  • s:   char *,字符串
  • p:   void *,十六进制表示的指针
  • c:   char,字符型,可以把输入的数字转换到 ASCII 码字符输出

flags:

  • #:带前缀
  • 0:在输出的数字前补0
  • -:左对齐
  • ‘ ’(空格):正数前面加空格,负数前面加负号
  • +:输出正数的正号

width:

最小的输出宽度,用十进制数来表示输出的最小位数,若实际的输出位数大于指定的输出的最小位数,则以实际的位数进行输出,若实际的位数小于指定输出的最小位数,则可按照指定的 flags 标志补 0 或补空格。

precision精度:显示小数点后的位数。

length:指明待转换数据的长度。

2.格式化输入

#include <stdio.h>
int scanf(const char *format, ...);
//将用户输入(标准输入)的数据进行格式化转换并进行存储int fscanf(FILE *stream, const char *format, ...);
//从指定文件中读取数据,作为格式转换的输入数据int sscanf(const char *str, const char *format, ...);
//从参数 str 所指向的字符串缓冲区中读取数据,作为格式转换的输入数据

format:

%[*][width][length]type
%[m][width][length]typewidth: 最大字符宽度;
length: 长度修饰符,与格式化输出函数的 format 参数中的 length 字段意义相同。
type: 指定输入数据的类型。与printf一致。

3.9 I/O缓冲

1.文件I/O的内核缓冲

read()和 write()系统调用在进行文件读写操作的时候并不会直接访问磁盘设备,而是仅仅在用户空间缓冲区和内核缓冲区(kernel buffer cache)之间复制数据。

2.刷新文件I/O的内核缓冲区

控制文件I/O内核缓冲的系统调用

#include <unistd.h>int fsync(int fd);
//将参数 fd 所指文件的内容数据和元数据写入磁盘,写完之后返回int fdatasync(int fd);
//与fsync类似,只写入内容数据,不写元数据(记录文件属性相关的数据信息,比如文件大小、时间戳、权限等)void sync(void);
//刷新所有文件 I/O 内核缓冲区

控制文件I/O内核缓冲的标志

fd = open(filepath, O_WRONLY | O_DSYNC);
//在每个 write()调用之后调用 fdatasync()函数进行数据同步fd = open(filepath, O_WRONLY | O_SYNC);
//在每个 write()调用之后调用 fsync()函数进行数据同步

对性能的影响
在程序中频繁调用 fsync()、 fdatasync()、 sync()(或者调用 open 时指定 O_DSYNC 或 O_SYNC 标志)对性能的影响极大,大部分不会使用。
 

3.直接I/O:绕过内核缓冲

Linux 允许应用程序在执行文件 I/O 操作时绕过内核缓冲区,从用户空间直接将数据传递到文件或磁盘设备。

fd = open(filepath, O_WRONLY | O_DIRECT);

直接 I/O 的对齐限制

⚫ 应用程序中用于存放数据的缓冲区,其内存起始地址必须以块大小的整数倍进行对齐;
⚫ 写文件时,文件的位置偏移量必须是块大小的整数倍;
⚫ 写入到文件的数据大小必须是块大小的整数倍。

如何确定磁盘分区的块大小呢?可以使用 tune2fs 命令进行查看
tune2fs -l /dev/sda1 | grep "Block size"

4.stdio缓冲

对 stdio 缓冲进行设置

#include <stdio.h>int setvbuf(FILE *stream, char *buf, int mode, size_t size);
stream: FILE 指针,用于指定对应的文件。
buf:指向 size 大小的内存区域将作为该文件的 stdio 缓冲区
mode: 参数 mode 用于指定缓冲区的缓冲类型:_IONBF(不缓冲),_IOLBF(行缓冲),_IOFBF(全缓冲)。
size: 指定缓冲区的大小。
返回值: 成功返回 0,失败将返回一个非 0 值,并且会设置 errno 来指示错误原因。void setbuf(FILE *stream, char *buf);
buf:NULL无缓冲,或者BUFSIZ字节的缓冲区void setbuffer(FILE *stream, char *buf, size_t size);
//允许调用者指定 buf 缓冲区的大小

刷新stdio缓冲区

#include <stdio.h>
int fflush(FILE *stream);
//stream 指定需要进行强制刷新的文件,如果该参数设置为 NULL,则表示刷新所有的 stdio 缓冲区调用 fflush()库函数可强制刷新指定文件的 stdio 缓冲区;
调用 fclose()关闭文件时会自动刷新文件的 stdio 缓冲区;
程序退出时会自动刷新 stdio 缓冲区(注意区分不同的情况:return会刷新,_exit和_Exit不会刷新) 。

5.I/O缓冲小节

3.10 文件描述符和FILE指针互转

#include <stdio.h>
int fileno(FILE *stream);
FILE *fdopen(int fd, const char *mode);

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/15059.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

解禁谷歌等浏览器禁止网站使用麦克等媒体设备

1、浏览器地址栏输入chrome://flags/ 微软的chromium内核的edge浏览器&#xff0c;既可以输入&#xff1a;chrome://flags/ &#xff0c;也可以输入edge://flags/ 2、打开后&#xff0c;界面如下 3、输入搜索&#xff0c;unsafe&#xff0c;并启用、输入需要启用的网址

对AI 感兴趣的小伙伴

如图&#xff0c;欢迎来玩儿&#xff01; 欢迎来玩儿

Python异常处理:打造你的代码防弹衣!

Hi&#xff0c;我是阿佑&#xff0c;上文咱们讲到——揭秘Python的魔法&#xff1a;装饰器的超能力大揭秘 ‍♂️✨&#xff0c;阿佑将带领大家通过精准捕获异常、使用with语句和上下文管理器、以及异常链等高级技巧来增强代码的健壮性。就像为代码穿上防弹衣&#xff0c;保护它…

生活小区火灾预警新篇章:泵吸式可燃气体报警器的检定与运用

在现代化的生活小区中&#xff0c;燃气设备广泛应用于居民的日常生活之中&#xff0c;但同时也带来了潜在的火灾风险。 可燃气体报警器作为一种安全监测设备&#xff0c;能够及时检测到燃气泄漏等安全隐患&#xff0c;并在达到预设的阈值时发出警报&#xff0c;提醒居民采取相…

SpringBoot Redis 扩展高级功能

环境&#xff1a;SpringBoot2.7.16 Redis6.2.1 1. Redis消息发布订阅 Spring Data 为 Redis 提供了专用的消息传递集成&#xff0c;其功能和命名与 Spring Framework 中的 JMS 集成类似。Redis 消息传递大致可分为两个功能区域&#xff1a; 信息发布 信息订阅 这是一个通常…

北斗短报文终端 | 什么是北斗短报文功能?如何实现北斗短报文通信?

北斗短报文功能是指通过北斗卫星进行短报文通信的功能。这种功能允许用户在没有移动通信信号覆盖的偏远山区、海洋、沙漠等地带&#xff0c;通过北斗短报文终端发送和接收文本信息&#xff0c;进行基本的数据通信。 北斗短报文功能是指北斗卫星导航系统特有的双向报文通信功能。…

urllib_post请求_百度翻译

打开百度翻译&#xff0c;并打开控制台&#xff0c;输入spider&#xff0c;然后在网络中找到对应的接口&#xff0c;可以看出&#xff0c;该url是post请求 在此案例中找到的接口为sug&#xff0c;依据为&#xff1a; 可以看到&#xff0c;传递的数据为kw : XXX&#xff0c; 所…

[Linux]服务管理

一.服务的概念&#xff0c;状态&#xff0c;查看系统服务 服务(service)本质就是进程 如(mysqld&#xff0c;sshd 防火墙等) 是运行在后台的&#xff0c;通常都会监听某个端口&#xff0c;等待其它程序的请求 -------比如mysqld&#xff0c;防火墙等&#xff0c;因此我们又称为…

3D瓦片地图组件上线|提供DEM数据接入,全方位呈现三维地图地形!

在用户调研中&#xff0c;我们了解到很多用户自身的可视化项目&#xff0c;需要在垂直空间上表现一些业务&#xff0c;例如&#xff1a;3D地形效果&#xff0c;数据底板建设等&#xff0c;而传统的地图效果不满足此用户需求。瓦片地图能够无限加载大地图&#xff0c;以更三维的…

【Linux】在Ubuntu 16.04上安装Gerrit + PostgreSQL + Apache服务

Gerrit是一个基于Git版本控制系统的运行于Web浏览器上的Code Review工具&#xff0c;本文叙述如何在Ubuntu 16.04上安装Gerrit服务。&#xff08;当然安装Gerrit的方法有很多&#xff0c;本文只是其中之一&#xff09; 文章目录 前提安装PostgreSQL数据库并创建用户下载、配置和…

【飞舞的花瓣】飞舞的花瓣代码||樱花代码||表白代码(完整代码)

关注微信公众号「ClassmateJie」有完整代码以及更多惊喜等待你的发现。 简介/效果展示 这段代码是一个HTML页面&#xff0c;其中包含一个canvas元素和相关的JavaScript代码。这个页面创建了一个飘落花瓣的动画效果。 代码【获取完整代码关注微信公众号「ClassmateJie」回复“…

一步将 CentOS 7.X 原地升级并迁移至 RHEL 7.9

《OpenShift / RHEL / DevSecOps 汇总目录》 在《在离线环境中将 CentOS 7.X 原地升级并迁移至 RHEL 7.9》一文中为了实现从 CentOS 7.X 原地升级并迁移至 RHEL 7.9&#xff0c;我们第一步先将一个测试环境 CentOS 7.5 升级到 CentOS 7.9&#xff0c;然后在第二步使用 convert2…

抖音运营_如何开抖店

截止20年8月&#xff0c;抖音的日活跃数高达6亿。 20年6月&#xff0c;上线抖店 &#xff08;抖音官方电商&#xff09; 一 抖店的定位和特色 1 一站式经营 帮助商家进行 商品交易、店铺管理、客户服务 等全链路的生意经营 2 多渠道拓展 抖音、今日头条、西瓜、抖音火山版…

【Python】用于发送电子邮件的标准库smtplib和构建邮件主体、添加附件、设置收件人的email

欢迎来到《小5讲堂》 这是《Python》系列文章&#xff0c;每篇文章将以博主理解的角度展开讲解。 温馨提示&#xff1a;博主能力有限&#xff0c;理解水平有限&#xff0c;若有不对之处望指正&#xff01; 目录 插件介绍邮件代码扩展知识点文章推荐 插件介绍 smtplib 是 Pytho…

海外媒体发稿的关键步骤和投稿策略:如何撰写高质量的新闻稿?国外软文发布平台有哪些?

发布国外新闻稿件是一个涉及多步骤的过程&#xff0c;旨在确保您的新闻稿能够有效覆盖目标受众。以下是一些关键步骤和实用的技巧&#xff0c;帮助你实现海外媒体发稿。 1. 明确目标和受众 首先&#xff0c;明确您发布新闻稿的目标&#xff0c;是为了增加品牌曝光、推出新产品…

惊呆了!企业数字化转型竟如工厂生产?

在众多使用蚓链数字化生态系统解决方案实现数字化转型的企业&#xff0c;你能想象吗&#xff1f;如今的企业数字化转型&#xff0c;就如同一家工厂的生产过程&#xff01;数据成为了原材料&#xff0c;而数据资源则是场景化的零件&#xff0c;最终生产出满足市场需求的数据产品…

基于Kafka的日志采集

目录 前言 架构图 资源列表 基础环境 关闭防护墙 关闭内核安全机制 修改主机名 添加hosts映射 一、部署elasticsearch 修改limit限制 部署elasticsearch 修改配置文件 启动 二、部署filebeat 部署filebeat 添加配置文件 启动 三、部署kibana 部署kibana 修…

21-信号集处理函数

屏蔽信号集 屏蔽某些信号 手动自动 未处理信号集 信号如果被屏蔽&#xff0c;则记录在未处理信号集中 非实时信号&#xff08;1~31&#xff09;&#xff0c;不排队&#xff0c;只留一个实时信号&#xff08;34~64&#xff09;&#xff0c;排队&#xff0c;保留全部 信号集…

springmvc中HandlerMapping是干什么用的

HandlerMapping处理器映射器 作用是根据request找到相应的处理器Handler和Interceptors&#xff0c;然后封装成HandlerExecutionChain对象返回 HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception; 实现类 HandlerMapping帮助DispatcherServlet进…

【AD21】BOM表文件的输出

BOM表文件通常发给采购&#xff0c;采购可以购买BOM表中所需的元器件。 在菜单栏中点击报告->Bill of Materials。 在以下界面&#xff0c;点击Columns&#xff0c;对输出的BOM表中一些说明进行不显示&#xff0c;可以不显示Description和LibRef。值&#xff0c;位号&#…