Linux 基础IO

理解文件

狭义理解

文件是在磁盘里面的

磁盘是永久性介质,因此文件存储也是永久性的

磁盘是外设
磁盘上的⽂件 本质是对⽂件的所有操作,都是对外设的输⼊和输出 简称 IO

广义理解

Linux下一切皆文件(键盘、显⽰器、⽹卡、磁盘...... 这些都是抽象化的过程)

文件构成

文件除了文件内容还有文件属性也是它的一部分。

文件 = 属性 + 内容

文件读写

C语言、C++的文件流我就不说了。大家可以看我这个博客内容:C++IO C IO

Linux文件读写

open、write、read、lseek、fcntl

基于标志位的打开方式

对于C语言使用的是w r r+ a等操作来表示是什么操作。对于C++在实例化对象时就已经声明是什么操作了。

对于Linux的IO系统调用采用的是标志位来声明我要进行哪些文件操作:

标志位

O_RDONLY :只读 O_WRONLY :只写 O_RDWR:读写

O_CREAT:文件不存在时创建,使用后需要跟上权限位

O_TRUNC :文件清空写 O_APPEND:文件追加写

SEEK_SET :开头文件指针 SEEK_CUR :当前文件指针 SEEK_END:文件末尾指针

O_CLOEXEC:调用exec后子进程会关闭当前fd

函数
cpp 复制代码
#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);

第二个函数的第三个参数就是权限位,当flags传了O_CREAT并且文件确实要创建的时候就会使用这个权限位来创建文件。

cpp 复制代码
int fd = open("a.txt",O_CREAT|O_RDWR|O_TRUNC|O_CLOEXEC,0666);

返回值返回一个文件描述符,使用读写等函数需要传文件描述符来操作文件。当文件打开失败的等异常情况就会返回-1

这里再讲一下O_CLOEXEC,它是当使用exec对应的函数时,因为创建的子进程会主动关闭对应的fd。fork不会,fork如果不希望子进程继承fd需要手动关闭。

cpp 复制代码
#include <unistd.h>

ssize_t read(int fd, void *buf, size_t count);

read是二进制读取的,不会区分是否有换行符

这个不用多说,看一眼就会,count传入buf的大小,返回值是返回成功读取的字符长度

cpp 复制代码
char arr[1024];
ssize_t ret = read(fd,static_cast<void*>(arr),sizeof(arr)-1);
arr[ret]=0;

这里有个细节,传比arr小一个字节的长度,这样可以流出空间放'\0'

cpp 复制代码
#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t count);

write函数返回值是成功写入的长度,count是buf的大小

cpp 复制代码
ret = write(fd,static_cast<const void*>(arr),sizeof(arr));

write是二进制打印的。

cpp 复制代码
#include <sys/types.h>
#include <unistd.h>

off_t lseek(int fd, off_t offset, int whence);

例如实现一个查找文件大小的代码

cpp 复制代码
off_t end = lseek(fd,0,SEEK_END);
off_t  begin =lseek(fd,0,SEEK_SET);
size_t fileSize = end-begin;
cout<<fileSize<<endl;
cpp 复制代码
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );

这个是对文件进行全局的控制,可以分为两大类:

F_GETFD/F_SETFD这个目前唯一能用的就是设置exec创建子进程关闭继承的fd的作用,但是我们是可以在open的时候设置的。大家如果想知道可以自己去找

F_GETFL/F_SETFL通过这一套可以实现非阻塞的读取:

cpp 复制代码
int fd = open("a.txt",O_CREAT|O_TRUNC|O_RDWR,0666) ;
int ret = fcntl(fd,F_GETFL);
fcntl(fd,F_SETFL,ret|O_NONBLOCK);
write(fd,"123",3);

返回值-1表示错误,大家写的时候记得加判断。

fcntl在网络编程套接字经常使用。

stdin&stdout&stderr

大家学习语法的时候应该接触过这三个标注流,分别是标准输入、标准输出、标准错误,他们对应的是键盘文件/显示屏文件/显示屏文件

我们分别用文件描述符0 1 2 来进行写入和读取操作:

cpp 复制代码
char buffer[1024];
int ret = read(0,buffer,1023);
buffer[ret]=0;
write(1,buffer,ret) ;
write(2,buffer,ret) ;
//12313(输入)
//12313(输出)
//12313(输出)

说明0是标准输入,顺便告诉大家1是标准输出,2是标准错误

文件描述符存储方式

一个进程就有一个task_struct,它有一个专门管理文件的files_struct,里面就有一个fd数组来存放fd,长度一般是1024,可以扩容,但是并不会无穷下去,当到达1024再添加新的fd就会出现报错。就不能创建新的了。

每个文件描述符指向的就是对应的文件的相关信息,就能通过相关信息找到对应的文件进行相关的操作。

文件描述符

文件描述符的作用

我们尝试关闭0文件,并打开a.txt文件,然后向0文件打印:

cpp 复制代码
close(0);
open("a.txt",O_WRONLY|O_TRUNC);
write(0,"hello",5);

最后发现在a.txt中出现了hello,而我们并没有用open的返回值作为write传参。因此说明新fd创建是就近找一个空的地方返回的。

那么利用这个原理就可以完成文件的输入输出重定向

输入输出重定向

我们可以自己通过close要重定向的fd,然后open对应重定向去的文件,让fd指向对应的文件。但是close+open这两个操作并不是原子操作,可能因为信号或者系统调用的失败导致重定向并不可靠。例如,我在close关闭对应的fd后,此时来一个信号进行抢占,open了某个文件,那么fd就关闭失败了,如果我们再关闭,那么可能会有更多的问题出现。因此有更加稳妥的方式来重定向:

dup2

cpp 复制代码
#include <unistd.h>
int dup2(int oldfd, int newfd);

newfd是要关闭的fd,oldfd是要打开并重定向newfd的fd。它是原子操作的,可以一次性的将重定向操作完成。

cpp 复制代码
int fd = open("append.txt", O_WRONLY | O_CREAT | O_APPEND, 0644);
if (fd == -1) { perror("open append failed"); exit(1); }

if (dup2(fd, STDOUT_FILENO) == -1) { perror("dup2 failed"); exit(1); }
close(fd);

printf("追加内容\n");

操作就是先打开一个对应重定向的文件,然后dup2,然后关闭这个fd,因为这个fd也是指向对应的文件的。导致两个fd都指向同一个文件,没有必要。

用重定向可以实现shell命令的> < >>

shell< > >>的实现

这里我说一下大致原理就不实现代码了:首先我们真正执行指令代码有两种情况,一种是内置命令直接shell自己运行,另一种是shell调用exec创建子进程让子进程替换指令库里面程序的代码和数据来进行执行

首先我们重温一下< > >>是改什么的,<是需要重定向到固定文件里面读取数据,>是需要重定向输出到对应文件写数据且是清除再写,>>是输出到对应的文件追加写数据

内置命令的重定向实现

如果内置命令例如echo等需要对文件进行读取接收的:如果有< ,那么就要对标准输入进行重定向,>、>>就是对标准输出进行重定向,具体操作是先打开对应的文件,如果是<就O_RDONLY,如果是>就是O_WRONLY|O_TRUNC,如果是>>就是O_WRONLY|O_APPEND,然后就是dup2重定向,然后进行代码的输入输出就行了。但是这里是有一个问题的,如果我们做完后,没有重新将012置为对应标准IO,那么后续的指令就出错了。因此我们dup2之前,应该先保存这个fd,需要用到dup函数

cpp 复制代码
#include <unistd.h>
int dup(int oldfd);

他会返回一个备用的fd,这个fd和oldfd都可以操作文件。我们先保存标准IO的fd即用dup,然后在dup2,再执行指令,在dup2回去即可。

非内置命令的重定向实现

如果是ls这种的非内置命令,需要调用fork和exec相关函数来创建子进程并执行对应代码。那么我们还是一样的卡开< > >>指定的重定向文件,fork,然后直接dup2,再执行exec替换程序数据和代码执行即可。也不需要重定向回来,因为子进程运行完会直接结束。

文件缓冲区

C、C++都有自己的文件缓冲区,有了文件缓冲区,就不需要频繁调用底层的read/write系统调用,这样可以提高读写效率,同时减少系统的压力。C和C++的都是一样的缓冲区操作,我直接说C++的。

读缓冲区

读缓冲区没有什么限制,尽量吧读缓冲区一次性装满,这样可能减少底层系统调用read。

写缓冲区

写缓冲区如果是写入文件,也不会有什么大的考虑,只要写缓冲区写满,那么就直接往文件里面刷新,但是在屏幕文件刷新就不一样了,他需要考虑的用户的可读性,因此有了行缓冲,当换行符产生时,就会触发写缓冲区的刷新。还有当使用到cin这种读取的情况,底层会刷新到屏幕。还有主动调用flush函数,是可以直接刷新的。

相关推荐
小魏每天都学习2 小时前
【网络拓扑部署-网络设备-网络安全】
运维·网络
南棱笑笑生2 小时前
20260123让天启AIO-3576Q38开发板在天启Buildroot下读写TF卡
linux·运维·服务器·rockchip
zzh_my2 小时前
tcp 服务端(用于测试)
服务器·网络·tcp/ip
噎住佩奇2 小时前
PVC和PV等概念解释
linux·运维·服务器
lvbinemail2 小时前
配置jenkins.service
java·运维·jenkins·systemctl
橙露2 小时前
工业控制嵌入式开发:Modbus 协议在 STM32 中的实现与调试
服务器·网络·stm32
笨手笨脚の2 小时前
Linux JDK NIO 源码分析
java·linux·nio
鸠摩智首席音效师2 小时前
如何创建带参数的 Bash 别名 ?
linux·bash
云雾J视界2 小时前
AI服务器供电革命:为何交错并联Buck成为算力时代的必然选择
服务器·人工智能·nvidia·算力·buck·dgx·交错并联