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]type

flags: 标志,可包含 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]type

width: 最大字符宽度;
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);

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Python3 笔记:sort() 和 sorted() 的区别

1、sort() 可以对列表中的元素进行排序&#xff0c;会改变原列表&#xff0c;之前的顺序不复存在。 list.sort&#xff08;key&#xff0c; reverse None&#xff09; key&#xff1a;默认值是None&#xff0c;可指定项目进行排序&#xff0c;此参数可省略。 reverse&#…

零基础PHP入门(一)选择IDE和配置环境

配置环境 官网下载安装包&#xff0c;windows https://windows.php.net/download#php-8.3 我是下载的最新版&#xff0c;也可以切换其他版本 https://windows.php.net/downloads/releases/archives/ 下载好压缩文件后&#xff0c;双击解压到一个目录 D:\soft\php 复制ph…

Vue 3 的 setup语法糖工作原理

前言 我们每天写vue3项目的时候都会使用setup语法糖&#xff0c;但是你有没有思考过下面几个问题。setup语法糖经过编译后是什么样子的&#xff1f;为什么在setup顶层定义的变量可以在template中可以直接使用&#xff1f;为什么import一个组件后就可以直接使用&#xff0c;无需…

匝间冲击耐压试验仪产品介绍及工作原理

产品简介 武汉凯迪正大KD2684S匝间冲击耐压试验仪适用于电机、变压器、电器线圈等这些由漆包线绕制的产品。因漆包线的绝缘涂敷层本身存在着质量问题&#xff0c;以及在绕线、嵌线、刮线、接头端部整形、绝缘浸漆、装配等工序工艺中不慎而引起绝缘层的损伤等&#xff0c;都会造…

零基础代码随想录【Day42】|| 1049. 最后一块石头的重量 II,494. 目标和,474.一和零

目录 DAY42 1049.最后一块石头的重量II 解题思路&代码 494.目标和 解题思路&代码 474.一和零 解题思路&代码 DAY42 1049.最后一块石头的重量II 力扣题目链接(opens new window) 题目难度&#xff1a;中等 有一堆石头&#xff0c;每块石头的重量都是正整…

Axure软件安装教程

链接&#xff1a;https://pan.baidu.com/s/1fHrSrZ7PIeDZZpn6QyJ6jQ?pwdb4mv 提取码&#xff1a;b4mv 安装完后点击Finish 名字随便起 关闭Axure 复制到安装目录下 最后成果

ASP+ACCESS基于WEB社区论坛设计与实现

摘要&#xff1a;系统主要实现BBS网站全部功能。采用目前应用最为广泛的ASP作为开发工具来开发此系统、以保证系统的稳定性。采用目前最为流行的网页制作工具Dreamweaver和目前最为流行的动画制作工具Flash MX。整个系统从符合操作简便、界面友好、灵活、实用、安全的要求出发&…

第七步 实现打印函数

文章目录 前言一、如何设计我们的打印函数&#xff1f;二、实践检验&#xff01; 查看系列文章点这里&#xff1a; 操作系统真象还原 前言 现在接力棒意见交到内核手中啦&#xff0c;只不过我们的内核现在可谓是一穷二白啥都没有&#xff0c;为了让我们设计的内核能被看见被使用…

uniapp微信小程序在ios端返回不显示弹窗的bug解决

这个问题其实是因为返回页面的时候弹的太快了导致的解决办法&#xff1a; 其实就是返回页面的弹窗加个延迟就好啦

电脑同时配置两个版本mysql数据库常见问题

1.配置时&#xff0c;要把bin中的mysql.exe和mysqld.exe 改个名字&#xff0c;不然两个版本会重复&#xff0c;当然&#xff0c;在初始化数据库的时候&#xff0c;如果时57版本的&#xff0c;就用mysql57(已经改名的)和mysqld57 代替 mysql 和 mysqld 例如 mysql -u root -p …

golang通过go-aci适配神通数据库

1. go-aci简介 go-aci是神通数据库基于ACI(兼容Oracle的OCI)开发的go语言开发接口&#xff0c;因此运行时需要依赖ACI驱动和ACI库的头文件。支持各种数据类型的读写、支持参数绑定、支持游标范围等操作。 2. Linux部署步骤 2.1. Go安装&#xff1a; 版本&#xff1a;1.9以上…

[数据集][目标检测]吸烟检测数据集VOC+YOLO格式1449张1类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;1449 标注数量(xml文件个数)&#xff1a;1449 标注数量(txt文件个数)&#xff1a;1449 标注…

深度学习之基于Pytorch框架手写数字识别

欢迎大家点赞、收藏、关注、评论啦 &#xff0c;由于篇幅有限&#xff0c;只展示了部分核心代码。 文章目录 一项目简介 二、功能三、系统四. 总结 一项目简介 一、项目背景与意义 手写数字识别是数字图像处理领域的一个经典问题&#xff0c;也是深度学习技术的一个常用应用场…

初识C语言——第二十八天

代码练习1&#xff1a; 用函数的方式实现9*9乘法表 void print_table(int n) {int i 0;int j 0;for (i 1; i< n; i){for (j 1; j< i; j){printf("%d*%d%-3d ", i, j, i * j);}printf("\n");}}int main() {int n 0;scanf("%d", &a…

【加密与解密(第四版)】第十六章笔记

第十六章 脱壳技术 16.1 基础知识 壳的加载过程&#xff1a;保存入口参数、获取壳本身需要使用的API地址、解密原程序各个区块的数据、IAT的初始化、重定位项的处理、HOOK API、跳转到程序原入口点 手动脱壳步骤&#xff1a;查找真正的入口点、抓取内存映像文件、重建PE文件&…

pod介绍之 容器分类与重启策略

目录 一 pod 基础概念介绍 1&#xff0c;pod 是什么 2&#xff0c;Pod使用方式 3&#xff0c;如何解决一个pod 多容器通信 4&#xff0c;pod 组成 5&#xff0c; k8s 中的 pod 二 pause容器 1&#xff0c;pause容器 是什么 2&#xff0c;pause容器作用 3&#xff…

Android studio关闭自动更新

Windows下&#xff1a; 左上角file - setting - Appearance & Behavier - system setting - update - 取消勾选

汉明码(海明码)的计算的规则

一.汉明码的由来 1.汉明码&#xff08;Hamming Code&#xff09;&#xff0c;是在电信领域的一种线性调试码&#xff0c;以发明者理查德卫斯里汉明的名字命名。汉明码在传输的消息流中插入验证码&#xff0c;当计算机存储或移动数据时&#xff0c;可能会产生数据位错误&#x…

vivado2020.2创建hls仿真工程实现led闪烁

下载vivado2020.2后会有这个出现在桌面 点击进入创建工程&#xff0c;这里注意不要有前面的\我再复制的时候复制错了导致创建失败 按f光标就会跳转到下一个f开头的函数处&#xff0c;要查找其他函数也同理 生成了一个synthesis summary文件 找到目录下生成的.v文件 an 点…

U-Mail邮件系统为用户提供更加安全的数据保护机制

据外媒报道&#xff0c;近日美国国家安全委员会泄露了其成员的近1万封电子邮件和密码&#xff0c;暴露了政府组织和大公司在内的2000家公司。其中包括美国国家航空航天局和特斯拉等。报道称该漏洞于3月7日被研究人员发现&#xff0c;通过该漏洞攻击者能够访问对web服务器操作至…