您的位置:首页 > 财经 > 金融 > 广州网站设计推荐刻_如何建立一个app_精准拓客软件哪个好_上海seo网络优化

广州网站设计推荐刻_如何建立一个app_精准拓客软件哪个好_上海seo网络优化

2025/1/24 11:29:45 来源:https://blog.csdn.net/2301_78022459/article/details/144189334  浏览:    关键词:广州网站设计推荐刻_如何建立一个app_精准拓客软件哪个好_上海seo网络优化
广州网站设计推荐刻_如何建立一个app_精准拓客软件哪个好_上海seo网络优化

【Linux】基础IO

🥕个人主页:开敲🍉

🔥所属专栏:Linux🍊

🌼文章目录🌼

0. 前言

    0.1 回顾C语言文件接口

1. 理解 "文件"

    1.1 狭义理解

    1.2 广义理解

    1.3 文件操作和归类认知

    1.4 系统角度理解

3. 系统文件 I/O

    3.1 open

    3.2 write

    3.3 close

    3.3 文件描述符 fd

        3.3.1 文件描述符的分配规则

        3.3.2 重定向

        3.3.3 dup2系统调用

4. 理解一切皆“文件”

5. 缓冲区

    5.1 什么是缓冲区

    5.2 为什么要引入缓冲区机制

    5.3 缓冲类型

    5.4 FILE

0. 前言
    0.1 回顾C语言文件接口

写入文件

读文件

将信息输出到显示器上的方法

文件打开方式

r:Open text file for reading.
  The stream is positioned at the beginning of the file.
r+:Open for reading and writing.
  The stream is positioned at the beginning of the file.
w:Truncate(缩短) file to zero length or create text file for writing.
  The stream is positioned at the beginning of the file.
w+:Open for reading and writing.
  The file is created if it does not exist, otherwise it is truncated.
  The stream is positioned at the beginning of the file.
a:Open for appending (writing at end of file).
  The file is created if it does not exist.
  The stream is positioned at the end of the file.
a+:Open for reading and appending (writing at end of file).
  The file is created if it does not exist. The initial file positionfor reading is at the beginning of the file,but output is always appended to the end of the file.

stdin / stdout / stderr

  C程序默认会打开三个流:stdin、stdout、stderr。这三个流的返回值都是 FILE* 类型的指针——文件指针。

如上,就是C语言的部分文件相关操作。下面开始本章的正片

1. 理解 "文件"
    1.1 狭义理解

  ① 文件存储在磁盘中。

  ② 磁盘是永久性存储介质,因此文件在磁盘上的存储是永久性的。

  ③ 磁盘是外设。(即是输出设备也是输入设备)

  ④ 对磁盘上的文件的操作,本是上都是对外设的输入和输出 简称I/O

    1.2 广义理解

  Linux下一切皆文件(键盘、鼠标、网卡、磁盘 ..... 这些都是抽象化的过程)

    1.3 文件操作和归类认知

  ① 0Kb大小的文件也是占用了内存的。

  ② 文件 = 内容 + 属性:0Kb指的是文件的内容为0,但是文件是有属性的,因此会占用内存存储属性。

  ③ 所有对文件的操作本质都是对 文件内容 + 文件属性 的操作

    1.4 系统角度理解

  ① 对文件的操作 本质是进程对文件的操作。

  ② 磁盘的管理者是操作系统

  ③ 文件的读写操作本质不是 C/C++  的库函数来实现的(这些库函数是作了封装,方便上层使用),而是通过文件相关的系统调用来实现的。

3. 系统文件 I/O

  fopen、ifstream等打开文件的方案都是语言层的方案,而系统相关的调用接口函数才是真正底层的调用函数。

    3.1 open

  open函数就是系统层面打开一个文件的接口函数。这里可能会有些困惑:C语言不是不支持重载吗?为什么这里好像就是函数重载呢?这个问题我们当前还没法回答,在现阶段将其认为是宏即可。

open函数的参数:

const char* pathname:要打开文件的名称

int flags:打开的方式(这里类型是int后面解释)

mode_t mode:设置打开文件的权限  rwx  rw-  r-x 等

第一个参数没什么好解释的,这里要着重解释一下第二个参数:

  flags,顾名思义,标志的意思。在这里flags就是作为标志进行传递的,什么标志呢?下面列举几个系统提供的宏:

O_CREAT:如果不存在,则创建一个新文件

O_WRONLY:打开一个文件,只写操作

O_RDONLY:打开一个文件,只读操作

  flags位置传递上面这类宏:

传递方式:

打开一个文件,要求不存在时直接创建,并且进行只写操作:O_CREAT | O_WRONLY

打开一个文件,要求不存在时直接创建,并且进行只读操作:O_CREAT | O_RDONLY

打开一个文件,进行只读操作:O_RDONLY

  可以看到,在传递参数时,我们有多个要求时,就将这些值直接或起来。

  第三个参数就是创建文件的权限:--- --- ---,参数的类型我们不需要关心,这里我们传递八进制数字,比如:0666 表示创建文件的权限为  rw-rw-rw-

  返回值 int:具体这里的返回值是什么后面使用 write 函数的时候再说。这里如果返回值 <0,说明打开文件失败;否则打开成功。

  下面来实操一下 open:

现在我们目录下有个 log.txt 的文件,里面没有内容,我们对它进行只写操作:

这里我们用 open 打开了 log.txt 文件进行只写操作,这里还用上了 write 函数还没介绍,先看现象,后面讲解:

  可以看到,我们确实将所要写入的内容写进了log.txt中。

下面再来进行只读操作:

这里我们打开 log.txt 文件进行只读操作,将 log.txt 的内容读到 buffer 中并打印出来,同样的,这里用到了 read 后面介绍,看现象:

    3.2 write

参数解释:

fd:fd就是 open 的返回值,上面没有说,这里可以解释了。open打开一个文件时,如果打开失败会返回-1;打开成功则会返回一个 ≥ 0的值。拿到这个值传给write,write就知道要往哪个文件中进行写入。

buf:要写入的数据的地址

count:要写入数据的长度

open:

O_CREAT:打开文件时,如果文件不存在则新建

0666:打开文件的权限,这里给的值为 0666 是八进制数字,表示打开文件的权限为 rw-rw-rw-

write:

fd:open的返回值,返回值 < 0说明打开文件失败;否则打开成功。这里将返回值传给write,write就知道向哪个文件中写入数据。

    3.3 close

  close用于关闭一个文件,和C语言库的 fclose 作用一样。这里也是传 fd 给 close,close就知道要关闭哪个文件。

    3.3 文件描述符 fd

  在上面介绍使用 write 时我们粗略讲了一下 fd 怎么用,接下来我们就要详细讲解 fd 是什么。

  首先直接明确:文件描述符就是数组的下标。那么是什么数组的下标呢?

  在进程中会存在一张表:文件指针表。顾名思义,这个表存储的就是指向一个一个文件的指针。这个表本质上就是一个数组,因此是通过下标来访问的。

  到这相信你也明白了,fd就是这张表的下标,当 open 打开一个文件时,就会在表中找一个位置存储一个指向这个文件的指针,类型为 FILE* 。随后返回 fd,fd便是存储位置的下标。因此我们在使用 write 时传递 fd 给 write ,write就知道要去哪个文件中进行写入操作:通过下标去表中找到对应的文件指针,进而访问对应的文件。

        3.3.1 文件描述符的分配规则

  知道了文件描述符就是数组的下标后,我们接下来就要知道文件描述符(也就是数组下标)是如何分配的呢?我们可不可以自己控制文件存储到我们想要存储的位置呢?答案是可以的,下面讲解文件描述符的分配规则。

  这里我们打开一个文件,输出fd:

是3。为什么是从3开始呢?难道说 0、1、2 下标处都有文件吗?我们把 0 位置的文件关了试试看。

这里我们先将 0 位置的文件关闭(我们当前也不知道 0 位置是否有文件,不管有没有先关了看看现象)。

可以看到,此时的 fd 就变为了 0,那大概就可以说明,0位置是有文件的。并且通过这个现象我们也可以推导出文件描述符分配规则为:用最小的、没有存放文件的下标来存放新打开的文件。

既然 0 位置有文件,就可以说明 1、2位置也是有文件的,接下来我们分别关闭 1、2 位置的文件来看看现象:

关闭 1:

关闭 2:

  可以看到,当我们关闭 2 号文件时是符合我们的预期的,fd = 2。但是我们关闭 1 号文件时为什么啥也不输出了?

  这就要聊到 "一切皆文件" 的话题上了,不卖关子:

0号文件:标准输入,从键盘上获取数据

1号文件:标准输出,将数据打印到显示屏上

2号文件:标准错误

  没错,这就是之前C语言阶段所说的:程序运行时,系统默认会打开三个文件 标准输入、标准输出和标准错误。

  这也就是为什么当我们关闭 1 号文件时,啥也没输出的原因。

        3.3.2 重定向

  既然我们知道了1号文件就是标准输出,我们再来看看下面的代码:

  这里我们关闭了 1号文件,随后打开 log.txt,根据前面的知识我们可以知道,log.txt就会保存到 1号下标。随后我们往1号文件进行写入数据,按道理来说,1号文件既然是显示屏,那应该最后会将 hello world 输出出来给我们看到,来看现象:

  可以看到,当我们运行程序时,并没有将 hello world 给我们输出到显示屏上。而当我们读取 log.txt 的内容时就会惊讶的发现,hello world 居然写到了 log.txt 文件中。

  这个现象就是我们之前所学的 重定向

  解释:我们首先关闭了 1号文件,随后打开了 log.txt 文件。log.txt 占领了原本1号文件的位置,于是当我们向1号文件写入数据时,理所当然地就会写入到 log.txt 中。

  结论:重定向的本质就是一个文件占领了另外一个文件的位置。比如:4号文件占领了3号文件的位置,当我们向3号文件写入数据时,实际上是在向4号文件写入数据。

下面还有张图来帮助理解:

        3.3.3 dup2系统调用

  在前面知道了重定向的本质就是一个文件占领另外一个文件的位置后,我们就可以自己通过 close 以及 open 的方式来手动进行重定向操作。不过在系统中就有一个函数能够帮助我们完成重定向操作——dup2函数。

示例代码:

  使用 dup2 让 log.txt 占领 1位置。结果:

4. 理解一切皆“文件”

  首先,在Windows上被称为文件的东西,在Linux中同样也是文件;其次,一些在Linux中我们不认为是文件的东西,比如进程、磁盘、显示器、键盘、鼠标等,在Linux中也统统抽象为了文件。

  这样的好处是,开发者仅需使用一套API开发工具,即可调用Linux系统中绝大部分的资源。举个例子,Linux中几乎所有读的操作都可以用read函数来进行;几乎所有改的操作都可以用 write 函数来进行。

  之前我们讲过,当打开一个文件时,操作系统为了管理打开的文件,都会为这个文件创建一个file结构体,下面列举出该结构体我们比较关系的部分内容:

struct file {
...
struct inode *f_inode;

const struct file_operations  *f_op;

...

atomic_long_t  f_count//记录 打开文件的引用计数,如果有多个文件指针指向这个文件,f_count的值会++

unsigned_int  f_flags; //表示打开文件的权限

fmode_t  f_mode;  //设置对文件的访问模式,例如:只读,只写等。所有标志在 <fcntl.h> 头文件中定义

loft_t  f_pos;  //表示当前读写文件的位置

...

}__attribute__((aligned(4)));

  值得注意的是,struct file 中的 f_op 指针指向了一个 file_operations 结构体,这个结构体中的成员除了 struct module* owner 其余都是函数指针。该结构和 struct_file 都在 fs.h 下:

struct file_operations {
    struct module* owner;
    //指向拥有该模块的指针
    loff_t(*llseek) (struct file*, loff_t, int);
    //llseek 方法用作改变文件中的当前读/写位置, 并且新位置作为(正的)返回值
    ssize_t(*read) (struct file*, char __user*, size_t, loff_t*);
    //用来从设备中获取数据。在这个位置的⼀个空指针导致 read 系统调用以 -EINVAL("Invalid argument") 失败。⼀个非负返回值代表了成功读取的字节数(返回值是⼀个"signed size" 类型, 常常是目标平台本地的整数类型)
        ssize_t(*write) (struct file*, const char __user*, size_t, loff_t*);
    //发送数据给设备. 如果 NULL, -EINVAL 返回给调用 write 系统调用的程序. 如果非负, 返回值代表成功写的字节数
        ssize_t(*aio_read) (struct kiocb*, const struct iovec*, unsigned long,
            loff_t);
    //初始化⼀个异步读 -- 可能在函数返回前不结束的读操作
    ssize_t(*aio_write) (struct kiocb*, const struct iovec*, unsigned long,
        loff_t);
    //初始化设备上的⼀个异步写
    int (*readdir) (struct file*, void*, filldir_t);
    //对于设备文件这个成员应当为 NULL; 它用来读取目录, 并且仅对**文件系统**有用
    unsigned int (*poll) (struct file*, struct poll_table_struct*);
    int (*ioctl) (struct inode*, struct file*, unsigned int, unsigned long);
    long (*unlocked_ioctl) (struct file*, unsigned int, unsigned long);
    long (*compat_ioctl) (struct file*, unsigned int, unsigned long);
    int (*mmap) (struct file*, struct vm_area_struct*);
    //mmap 用来请求将设备内存映射到进程的地址空间. 如果这个方法是 NULL, mmap 系统调用返回 - ENODEV
        int (*open) (struct inode*, struct file*);
    //打开⼀个文件
    int (*flush) (struct file*, fl_owner_t id);
    //flush 操作在进程关闭它的设备文件描述符的拷贝时调用;
    int (*release) (struct inode*, struct file*);
    //在文件结构被释放时引用这个操作   如同 open, release 可以为 NULL
    int (*fsync) (struct file*, struct dentry*, int datasync);
    //用户调用来刷新任何挂着的数据
    int (*aio_fsync) (struct kiocb*, int datasync);
    int (*fasync) (int, struct file*, int);
    int (*lock) (struct file*, int, struct file_lock*);
    //lock 方法用来实现文件加锁; 加锁对常规文件是必不可少的特性, 但是设备驱动几乎从不实现它
    ssize_t(*sendpage) (struct file*, struct page*, int, size_t, loff_t*,int);
    unsigned long (*get_unmapped_area)(struct file*, unsigned long, unsigned long, unsigned long, unsigned long);
    int (*check_flags)(int);
    int (*flock) (struct file*, int, struct file_lock*);
    ssize_t(*splice_write)(struct pipe_inode_info*, struct file*, loff_t*,size_t, unsigned int);
    ssize_t(*splice_read)(struct file*, loff_t*, struct pipe_inode_info*,size_t, unsigned int);
    int (*setlease)(struct file*, long, struct file_lock**);
};

  file_operation 就是把系统调用和驱动程序关联起来的关键数据结构,这个结构的每一个成员都对应着一个系统调用。读取 file_operation 中相应的函数指针,接着把控制权转交给函数,从而完成了 Liunx设备驱动程序的工作。

  介绍完相关代码,下面一张图总结一下:

  上图中的每个外设都有自己的 read、write,但每个外设对应的 read、write 的操作方法一定是不一样的,这是很显然的事情。但是通过 struct file下的 file operation 中的各种函数回调,让我们开发者可以只使用 file 便可以调取 Linux 系统中的绝大部分资源——这便是 Linux 下“一切皆文件”的核心理解。

5. 缓冲区
    5.1 什么是缓冲区

  缓冲区是内存空间的一部分。也就是说,在内存空间中预留了一部分存储空间, 这些存储空间用来缓冲输入或者输出的数据,这部分预留的空间就叫做缓冲区。缓冲区根据其对应的是输入设备还是输出设备,分为输入缓冲区和输出缓冲区。

    5.2 为什么要引入缓冲区机制

  读写文件时,如果不开辟对文件操作的缓冲区,而是通过系统调用对磁盘进行操作,那么每次对文件进行一次读写操作时,都需要使用读写系统调用来处理此操作,即需要执行一次系统调用,执行一次系统调用将涉及到CPU状态的切换,即从用户空间切换到内核空间,实现进程上下文的切换,这将损耗一定的CPU时间,频繁的磁盘访问对程序的执行效率造成很大的影响。

  为了减少使用系统调用的次数,提高效率,我们就可以采用缓冲机制。比如我们从磁盘里取信息,可以在磁盘文件进行操作时,可以一次从文件中读出大量的数据到缓冲区中,以后对这部分的访问就不需要再使用系统调用了,等缓冲区的数据取完后再去磁盘中读取,这样就可以:减少磁盘的读写次数,再加上计算机对缓冲区的操作大大快于对磁盘的操作,故应用缓冲区可大大提升计算机的运行效率

  又比如,我们使用打印机打印文档,由于打印机的打印速度相对较慢,我们先把文档输出到打印机相应的缓冲区,打印机再自行逐步打印,在这期间CPU可以处理别的事情。可以看出,缓冲区就是一块内存区,它用在输入输出设备和CPU之间,用来缓存数据。它使得低速的输入输出设备和高速CPU能够协调工作,避免低速的输入输出设备占用CPU,解放出CPU,使其能够高效率工作。

    5.3 缓冲类型

标准I/O提供了3种类型的缓冲区。

  ① 全缓冲区:这种缓冲方式要求填满整个缓冲区后才进行I/O操作。因为每次将缓冲区填满才进行I/O操作,因此磁盘的读写次数一定是最少的,因此这种缓冲区的效率也是最高的。对于磁盘文件的操作通常使用全缓冲的方式访问。

  ② 行缓冲区:在行缓冲情况下,当在输入和输出遇到换行符时,标准I/O库函数将会执行系统调用操作。当所操作的流涉及到终端时(例如标准输入和标准输出),使用行缓冲方式。因为标准I/O库每行的缓冲区长度是固定的,因此如果缓冲区被填满了,那么即便还没有遇到换行符,也会执行I/O系统调用操作,默认行缓冲区的大小为1024。

  ③ 无缓冲区:无缓冲区是指标准I/O库不对字符进行缓存,直接调用系统调用。标准错误流 stderr 就是无缓冲,这使得错误信息能够快速地被显示出来。

除了上面列举的默认刷新方式,下列特殊情况也会引发缓冲区的刷新:

  1. 缓冲区满时

  2. 执行 fflush 语句,强制刷新

来看如下代码:

这里我们进行手动重定向,将标准输出重定向到 log.txt 文件,这时我们再进行 printf 就不会往显示器上输出信息,而是将信息写入 log.txt 中(这在我们前面 重定向部分 已经讲解过)。那么当我们运行程序后,应当会生成一个 log.txt 并且里面的内容为 hello world! ,下面来看现象:

  可以看到,当我们运行程序后确实有了 log.txt 这个文件,但是当我们读取这个文件的内容时发现什么也没读到,并且 hello world! 也确实没有显示到显示器上,说明重定向肯定是成功了的。那么这是什么情况呢?

  这是因为我们将1号文件重定向为了 log.txt 磁盘文件,前面讲过,磁盘文件的缓冲区为全缓冲,只有当数据填满缓冲区时才会进行刷新,因此这里我们在缓冲区还未刷新的情况下关闭了 fd 文件,导致缓冲区中的内容无法刷新到 log.txt 文件中,因此 log.txt 中没有内容。那么该怎么办呢?

  前面讲过,除了靠系统默认的刷新方式外,我们还可以使用 fflush 进行强制刷新,看如下代码:

在关闭 fd 文件之前,我们对缓冲区进行强制刷新,结果:

这里我们就顺便再来验证一下 标准错误(stderr) 是不带缓冲区的:

结果:

  因为 stderr 是不带缓冲区的,因此无需 fflush 就可以将内容写入文件。

    5.4 FILE

  因为I/O相关函数与系统调用接口对应,并且库函数封装系统调用,因此本质上,访问文件都是通过 fd 来访问的。所以C语言库中的 FILE 结构体内部一定封装了 fd,才能够调用系统调用。

  下面来段代码一起研究一下:

  如上代码分为调用 printf、fwrite、write 往显示器上输出 msg1、msg2、msg3,最后创建了一个子进程,看结果:

  没有问题,显示器上输出了三条信息。那如果我们对进程进行重定向输出呢?执行如下指令:

  此时我们将 test 程序运行的结果重定向到了 log.txt 文件中,此时我们再来看看 log.txt 文件的内容:

  可以发现,除了 hello write 以外,其余两条信息都输出了两遍,这就说明,除了 write 以外,printf 和 fwrite 都执行了两遍,这是为什么呢?肯定跟这最后的 fork 有关系!

  一般C库函数写入文件时是全缓冲的,而写入显示器时是行缓冲的。printf 和 fwrite 函数自带缓冲区,当发生重定向到普通文件时,数据的缓冲方式由行缓冲变为了全缓冲。而在全缓冲中的数据并不会立即刷新,甚至 fork 之后都还不会,但是当进程退出以后,就会统一刷新,写入到文件中。但是 fork 时,父子进程数据会发生写时拷贝,因此子进程缓冲区中也有了一份数据,当父进程刷新时,子进程也会同时刷新数据,随机产生两份数据。

  而 write 没有两份数据,可以说明 write 系统调用并没有缓冲区。

综上:printf 和 fwrite 库函数自带缓冲区,而 write 系统调用并没有缓冲区。另外,printf 以及 fwrite 这种库提供的函数的缓冲区都是用户级缓冲区,用户级缓冲区的数据刷新时是将数据刷新到内核级缓冲区,而内核级缓冲区的数据刷新就是写入文件当中。

                                               创作不易,点个赞呗,蟹蟹啦~

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com