当前位置: 首页 > news >正文

Linux下基础IO

1 文件

这里首先得理解一下文件,文件存放在磁盘中(磁盘是永久性存储介质,是一种外设,也是一种输入输出设备),磁盘上的文件的所有操作,都是对外设的输入和输出简称IO,linux下一切皆⽂件,无论是键盘、显示器、网卡、磁盘都可以抽象的理解为文件。

1.对于0KB的空⽂件是占用磁盘空间的(就是文件属性占用了空间)

2.文件是文件属性(元数据)和文件内容的集合(文件=属性(元数据)+内容

3.所有的文件操作本质是文件内容操作和文件属性操作

理解:谁来操作文件?谁来管理文件 ?

文件操作的本质:进程对文件的操作。

管理者:磁盘的管理者是操作系统。

(另外在之前学习C语言时,我们通过C语言的函数接口,实现了对文件的操作,其实这些库函数只是方便用户使用,本质是封装的是系统调用接口来实现的)

2 文件路径

程序在当前路径下,系统怎么寻找到程序的当前路径?

ls  /proc/[进程id] -l 

可以查看当前正在运行进程的信息:

lrwxrwxrwx 1 iu iu 0 Aug 26 16:53 cwd -> /home/iu/io 
lrwxrwxrwx 1 iu iu 0 Aug 26 16:53 exe -> /home/iu/io/test

cwd:指向当前进程运行目录的⼀个符号链接。

exe:指向启动当前进程的可执行文件(完整路径)的符号链接。

 打开文件,本质是进程打开,所以,进程知道自己在哪里,所以文件不带路径,进程也知道。由此OS就能知道要创建的文件放在哪里。(之前讲的环境变量中,也存放着路径)

 3 stdin&stdout&stderr

启动进程,C默认会打开三个输⼊输出流,分别是stdin,stdout,stderr

#include <stdio.h>
extern FILE *stdin;
extern FILE *stdout;
extern FILE *stderr;

可以看到这是三个结构体指针。

 所以向显示器上打印,除了使用printf,还有fwrtie,fprintf等,只需要修改一下写入对象为stdout即可。

#include <stdio.h>
#include <string.h>
int main()
{const char *msg = "hello world\n";fwrite(msg, strlen(msg), 1, stdout);printf("hello world\n");fprintf(stdout, "hello world\n");return 0;
}

在之前学习C语言打开文件等操作时,还有就是权限问题:只读,只写,可读可写,追加等

r:只读

r+:可读可写

w:只写,如果文件不存在创建文件,每次写文件都会进行清空

w+ :可读可写

a:在文件末尾追加

a+:可读,在文件末尾追加

 这里会和后面使用系统调用操作文件进行比较。

4 系统文件IO

4.1接口介绍  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);

pathname: 要打开或创建目标文件(只有文件名,默认在当前路径下创建)

flags: 打开文件时,可以传入多个参数选项,用下面的⼀个或者多个常量进行“或”运算,构成 flags。

返回值:

        成功返回新打开的文件符(fd)

        失败返回-1

O_RDONLY: 只读打开

O_WRONLY: 只写打开

O_RDWR : 读,写打开

(这三个常量,有且只有一个)

O_CREAT : 若文件不存在,则创建它。(这里必须确定文件访问权限,也就是需要传入mode参数)

O_APPEND: 追加写

 4.2 open返回值

这里还需要区分一下,库函数和系统调用的概念,也就是库函数其实就是对系统调用进行了一层封装,可以通过下图理解一下:

 4.2.1 文件描述符fd

文件描述符其实就是一个整数。

在前面说进程启动会默认打开标准输入,标准输出,标准错误,其实对应的fd也就是0,1,2,这里对应的一般物理设备是:键盘,显示器,显示器。

所以就可以通过下面代码进行从键盘读取向屏幕打印:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main()
{char buf[1024];ssize_t s = read(0, buf, sizeof(buf));if(s > 0){buf[s] = 0;write(1, buf, strlen(buf));write(2, buf, strlen(buf));}return 0;
}

这里就有新的问题要思考了?0,1,2到底有什么含义,还是随机分配的数字?

 从图片中可以看到,文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体。表示已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有⼀个指针*files,指向⼀张表files_struct,该表最重要的部分就是包含⼀个指针数组,每个元素都是⼀个指向打开文件的指针

得出结论本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件。 

可以查看内核源码观察确实如此:

 4.2.2 文件描述符的分配规则

通过下面两段代码对比:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>//例子1
int main()
{int fd = open("myfile", O_RDONLY);if(fd < 0){perror("open");return 1;}printf("fd: %d\n", fd);close(fd);return 0;
}//例子2
int main()
{close(0);//close(2);int fd = open("myfile", O_RDONLY);if(fd < 0){perror("open");return 1;}printf("fd: %d\n", fd);close(fd);return 0;
}

结果:分别是3和0;

首先第一段代码,进程启动默认打开0,1,2三个文件,所以再打开“myfile”时fd就为3了,第二段代码将一给关闭了,再打开“myfile”fd就变成了0;

得出结论:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。

4.3 重定向

这里通过一段代码来说明:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
int main()
{close(1);int fd = open("myfile", O_WRONLY|O_CREAT, 00644);if(fd < 0){perror("open");return 1;}printf("fd: %d\n", fd);fflush(stdout);close(fd);return 0;
}

将1(标准输出流)关闭,打开“myfile”文件,再调用printf,发现并不会写入到显示器文件上了,而是写入到myfile中去了,这个就叫做输出重定向 (前面linux指令中 >, >> , <等符号也就是表示的是输出重定向,追加重定向,输入重定向)

4.3.1 重定向的本质

前面有一个结论:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。所以这里将1关闭之后,再打开一个文件,就会占据1下标(文件描述符)的位置,而printf默认是向1中写入,所以但printf并不知道文件描述符为1的文件已将改变了,所以就写入到新打开的文件中去了。

 4.3.2 dup2系统调用

这就是系统提供的一个重定向的函数。

#include <unistd.h>
int dup2(int oldfd, int newfd);

将oldfd重定向到newfd中。

例如:

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main()
{int fd = open("./log", O_CREAT | O_RDWR);if (fd < 0) {perror("open");return 1;}close(1);dup2(fd, 1);while(true){char buf[1024] = {0};ssize_t read_size = read(0, buf, sizeof(buf) - 1);if (read_size < 0) {perror("read");break;}printf("%s", buf);fflush(stdout);}return 0;
}

这里通过将打开的文件重定向到1(标准输出)中,再从标准输入中读取,再利用printf向文件中写入。printf是C库当中的IO函数,⼀般往stdout中输出,但是stdout底层访问文件的时候,找的还是fd:1, 但此时,fd:1下标所表示内容,已经变成了myfifile的地址,不再是显示器文件的地址,所以,输出的任何消息都会往文件中写入,进而完成输出重定向。

5 linux中“一切皆文件”

这里我们可以从windows中理解,再windows中看到C盘,D盘中的文件,在linux下也是文件,但在linux下,进程,磁盘,显示器,键盘这样的设备也被抽象成文件,这样就可以通过访问文件的方式访问文件。

开发者仅需要使用⼀套API和开发工具,即可调取Linux系统中绝大部分的资源。举个简单的例子,Linux中几乎所有读(读文件,读系统状态,读PIPE(管道))的操作都可以用read 函数来进行;几乎所有更改(更改文件,更改系统参数,写PIPE(管道))的操作都可以用 write 函 数来进行。

 这里通过观察file结构体来看看:

struct file {...struct inode *f_inode; /* cached value */const struct file_operations *f_op;...atomic_long_t f_count; // 表⽰打开⽂件的引⽤计数,如果有多个⽂件指针指向它,就会增加f_count的值。 unsigned int f_flags; // 表⽰打开⽂件的权限 fmode_t f_mode; // 设置对⽂件的访问模式,例如:只读,只写等。所有的标志在头⽂件<fcntl.h> 中定义 loff_t f_pos; // 表⽰当前读写⽂件的位置 ...} __attribute__((aligned(4))); 

值得注意的是这里有一个f_op的结构体成员, f_op 指针指向了⼀个 file_operations 结构体,这个结构体中的成员除了struct module* owner其余都是函数指针。

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

每个设备都可以有自己的read、write,但一定是对应着不同的操作方法!!但通过 struct file 下 file_operation 中的各种函数回调,让我们开发者只用file便可调取Linux系统中绝大部分的资源。这也就是前面所说的“一切皆文件”。

6 缓冲区

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

 6.1 缓冲区机制的作用

首先要知道,系统调用是需要消耗时间,直接通过系统调用对磁盘进行操作(读、写等),那么每次对文件进行⼀次读写操作时,都需要使用读写系统调用来处理此操作,即需要执行⼀次系统调 用,执行⼀次系统调用将涉及到CPU状态的切换,即从用户空间切换到内核空间,实现进程上下文的切换,这将损耗⼀定的CPU时间,频繁的磁盘访问对程序的执行效率造成很大的影响。

(这里举个例子,比如说:寄快递,如果是本人亲自送到目的地,那是不是浪费了自己很多时间,而如果我把它放到菜鸟驿站,让快递员去负责运输,那是不是本人就从这里面“解放出来了”,就可以去干更多的事)

为了减少使用系统调用的次数,提高效率,我们就可以采用缓冲机制。可以减少磁盘的读写次数,再加上计算机对缓冲区的操作大大快于对磁盘的操作,故应用缓冲区可大提高计算机的运行速度。

 从磁盘里取信息,可以在磁盘文件进行操作时,可以⼀次从文件中读出大量的数据到缓冲区中,以后对这部分的访问就不需要再使用系统调用了,等缓冲区的数据取完后再去磁盘中读取。

6.2 缓冲类型

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

全缓冲区:这种缓冲方式要求填满整个缓冲区后才进行I/O系统调用操作。

(对于磁盘问件的操作通常使用全缓冲的方式访问)

行缓冲区:在行缓冲情况下,当在输入和输出中遇到换行符时,标准I/O库函数将会执行系统调用操作。

(当所操作的流涉及⼀个终端时(例如标准输入和标准输出),使行缓冲方式。因为标准 I/O库每行的缓冲区长度是固定的,所以只要填满了缓冲区,即使还没有遇到换行符,也会执行 I/O系统调用操作,默认行缓冲区的大小为1024)

无缓冲区:无缓冲区是指标准I/O库不对字符进行缓存,直接调用系统调用。

(标准出错流stderr通常是不带缓冲区的,这使得出错信息能够尽快地显示出来)

 以下为特殊刷新方式:

1. 缓冲区满时;

2. 执行flush语句;

 通过下面一个例子:

#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main() 
{close(1);int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);if (fd < 0) {perror("open");return 0;}printf("hello world: %d\n", fd);close(fd);return 0;
}

使用重定向,让本应该打印在显示器上的内容写到“log.txt”文件中,但我们发现, 程序运行结束后,文件中并没有被写入内容。

这里是因为1号重定向到磁盘文件后,隐式转换成全缓冲,所以这里\n,也不会刷新了,这里close了fd,缓冲区就没刷新到文件里面,可以使用fflush来强制刷新。

关闭文件之前,加一句下面代码就可以了。

fflush(stdout);

这里补充一个:

如果是重定向到2(stderr)是不带缓冲区的,就可以直接刷新到文件里面。

7 FILE

之前在调用C语言封装的文件操作的库函数,可以看到一些函数返回值是FILE*

如:

 前面讲的系统调用,都是通过文件描述符fd来访问文件的,所以这个FILE结构体内,肯定封装这个fd成员。还有一点要注意,就是FILE中也存在语言级缓冲区。

相关文章:

  • 【评测】flux-dev文生图模型初体验
  • Linux 查看两个主机之间时间是否同步 - clockdiff命令详解
  • CATIA高效工作指南——常规配置篇(四)
  • 【完整源码+数据集+部署教程】甘蔗叶片病害检测系统源码和数据集:改进yolo11-GhostDynamicConv
  • 提取 Word 中图片原始质量
  • 重构技术奇点的路径:三智双融认知大飞跃
  • 45. Jump Game II
  • CentOs7.x系列IP地址由动态改静态(解决远程连接掉线问题)
  • 代码随想录算法训练营day1
  • 3 mnist gan小试牛刀
  • 6.11 打卡
  • 亚马逊商品数据实时获取方案:API 接口开发与安全接入实践
  • Jenkins + Docker + Kubernetes(JKD)在 DevOps CI/CD 中的核心价值与实践要点
  • 鹰盾Win播放器作为专业的视频安全解决方案,除了硬件翻录外还有什么呢?
  • 网络安全中对抗性漂移的多智能体强化学习
  • R语言缓释制剂QBD解决方案之二
  • 微信小程序分享带参数地址
  • 网传西门子12亿美元收购云原生工业软件,云化PLM系统转机在协同
  • UniApp APP打包方法(Android/iOS双平台)
  • iOS 26 beta1 重新禁止 JIT 执行,Flutter 下的 iOS 真机 hot load 暂时无法使用
  • 网站底部特效/百度小说风云榜2022
  • 新加坡室内设计公司排名/优化公司结构
  • 如何让搜索引擎不收录网站/网站超级外链
  • 想接外包做网站/微信推广软件
  • 网站发布和收录怎么做/注册平台
  • 太原cms建站模板/精准客源app