Linux基础IO
- 1.理解文件
- 1.1 狭义理解
- 1.2 广义理解
- 1.3 文件操作的归类认知
- 1.4 系统角度
- 2.c的文件接口
- 2.1 hello.c打开文件
- 2.2 hello.c写文件
- 2.3 hello.c读文件
- 2.4 stdin & stdout & stderr
- 3.系统打开文件接口
- 3.1 一种传递标记位的方法
- 3.2 open函数
- 3.3 文件描述符
- 3.3.0 文件描述符表
- 3.3.1 文件描述符0&1&2
- 3.3.2 文件描述符的分配规则
- 3.3.3 重定向
- 3.3.4 系统调用dup2
- 4. 重谈重定向
- 5.理解一切皆文件
- 6.缓冲区
- 缓冲区是什么
- 缓冲类型
- File
- libc读写函数的封装
1.理解文件
1.1 狭义理解
- ⽂件在磁盘⾥
- 磁盘是永久性存储介质,因此⽂件在磁盘上的存储是永久性的
- 磁盘是外设(即是输出设备也是输⼊设备)
- 磁盘上的⽂件 本质是对⽂件的所有操作,都是对外设的输⼊和输出简称 IO
1.2 广义理解
- Linux 下⼀切皆⽂件(键盘、显⽰器、⽹卡、磁盘…… 这些都是抽象化的过程)
1.3 文件操作的归类认知
- 对于 0KB 的空⽂件是占⽤磁盘空间的
- ⽂件是⽂件属性(元数据)和⽂件内容的集合(⽂件 = 属性(元数据)+ 内容)
- 所有的⽂件操作本质是⽂件内容操作和⽂件属性操作
1.4 系统角度
-
对⽂件的操作本质是进程对⽂件的操作
-
磁盘的管理者是操作系统
-
⽂件的读写本质不是通过 C 语⾔ / C++ 的库函数来操作的(这些库函数只是为⽤⼾提供⽅便),⽽是通过⽂件相关的系统调⽤接⼝来实现的
2.c的文件接口
2.1 hello.c打开文件
#include<stdio.h>
int main()
{FILE* fp = fopen("test.txt", "w");if (fp == NULL){perror("open test.txt failure!!\n");}return 0;
}
2.2 hello.c写文件
#include<stdio.h>
#include<string.h>
int main()
{FILE* fp = fopen("test.txt", "w");const char* message = "Hello Day";if (fp == NULL){perror("open failure!!!\n");return 0;}for (int i = 1; i <= 5; i++){char bufer[1024];snprintf(bufer, sizeof(bufer), "%s%d\n", message, i);fwrite(bufer, 1, strlen((const char*)bufer), fp);}fclose(fp);
}
2.3 hello.c读文件
#include<stdio.h>
#include<string.h>
int main()
{//FILE * ptr=fopen("test.txt","r");if(ptr==NULL){perror("open failure");return 0;}char buffer[1024];fread(buffer,sizeof(buffer),1,ptr);printf("%s",buffer);return 0;
}
2.4 stdin & stdout & stderr
- C默认会打开三个输⼊输出流,分别是stdin, stdout, stderr
- 仔细观察发现,这三个流的类型都是FILE*, fopen返回值类型,⽂件指针
3.系统打开文件接口
3.1 一种传递标记位的方法
#include <stdio.h>
#define ONE 0001 //0000 0001
#define TWO 0002 //0000 0010
#define THREE 0004 //0000 0100
void func(int flags) {
if (flags & ONE) printf("flags has ONE! ");
if (flags & TWO) printf("flags has TWO! ");
if (flags & THREE) printf("flags has THREE! ");
printf("\n");
}
int main() {
func(ONE);
func(THREE);
func(ONE | TWO);
func(ONE | THREE | TWO);
return 0;
}
这样我们想要那些模式就可以通过传入一个整形来进行多个操作。
3.2 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表示你想要打开这个文件的方式
- O_RDONLY: 只读打开;O_WRONLY: 只写打开;O_RDWR : 读,写打开这三个常量,必须指定⼀个且只能指定⼀个
O_CREAT : 若⽂件不存在,则创建它。需要使⽤mode选项,来指明新⽂件的访问
O_APPEND: 追加写 - mode就是我们要传入的权限位通常是我们打算穿件文件时需要指定权限。
函数返回值:
- 成功:新打开的⽂件描述符
- 失败:-1
3.3 文件描述符
3.3.0 文件描述符表
当我们打开一个文件时会创建一个struct file结构体来描述这个被打开的文件,该结构体里面有一个内存级别的缓冲区我们的数据会先加载到该缓存区。因此当我们打开多个文件那我们是不是要来管理这些被打开的文件因此文件描述符表就出来了。
3.3.1 文件描述符0&1&2
- Linux进程默认情况下会有3个缺省打开的⽂件描述符,分别是标准输⼊0, 标准输出1, 标准错
误2. - 0,1,2对应的物理设备⼀般是:键盘,显⽰器,显⽰器
3.3.2 文件描述符的分配规则
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>int main()
{umask(0);int fd=open("myfile,txt",O_WRONLY|O_CREAT,0666);printf("%d\n",fd);close(fd);return 0;
}
//运行结果:3
我们关闭0
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>int main()
{umask(0);close(0);int fd=open("myfile,txt",O_WRONLY|O_CREAT,0666);printf("%d\n",fd);close(fd);return 0;
}
//运行结果:0
可⻅,⽂件描述符的分配规则:在files_struct数组当中,找到当前没有被使⽤的最⼩的⼀个下标,作为新的⽂件描述符。
3.3.3 重定向
因此我们的重定向就是修改我们文件描述符表指向的文件。
3.3.4 系统调用dup2
#include <unistd.h>
int dup2(int oldfd, int newfd);
//参数解释:表示newfd是oldfd的一份拷贝因此此时两个文件描述符都是oldfd;全都只想oldfd指向的文件了。
4. 重谈重定向
下面我们进行重定向
可以看到我们往1里面写的数据写到了新文件中;这是因为我们
./test.exe > log.txt其实就是./test.exe 1>log.txt;因此我们1里面现在指向了新打开的文件。而2我们没有改变它的指向因此还会向显示器输出。
5.理解一切皆文件
上图中的外设,每个设备都可以有⾃⼰的read、write,但⼀定是对应着不同的操作⽅法!!但通过struct file 下 file_operation 中的各种函数回调,让我们开发者只⽤file便可调取 Linux 系统中绝⼤部分的资源!!这便是“linux下⼀切皆⽂件”的核⼼理解。因此在上层我们就把一个个设备当做一个打开的文件。调用各自的读写方法。
6.缓冲区
缓冲区是什么
缓冲区其实就是一块存数据的地方,有了·缓冲区可以方便我们用户的操作加快效率,在语言层的缓冲区的建立有利于我们减少调用系统接口的次数,从而减少我们调用系统的成本。
缓冲类型
- 全缓冲区:这种缓冲⽅式要求填满整个缓冲区后才进⾏I/O系统调⽤操作。对于磁盘⽂件的操作通常使⽤全缓冲的⽅式访问
- ⾏缓冲区:在⾏缓冲情况下,当在输⼊和输出中遇到换⾏符时,标准I/O库函数将会执⾏系统调⽤操作。当所操作的流涉及⼀个终端时(例如标准输⼊和标准输出),使⽤⾏缓冲⽅式。因为标准I/O库每⾏的缓冲区⻓度是固定的,所以只要填满了缓冲区,即使还没有遇到换⾏符,也会执⾏I/O系统调⽤操作,默认⾏缓冲区的⼤⼩为1024。
- ⽆缓冲区:⽆缓冲区是指标准I/O库不对字符进⾏缓存,直接调⽤系统调⽤。标准出错流stderr通
常是不带缓冲区的,这使得出错信息能够尽快地显⽰出来。
上面缓冲类型全缓冲效率最高除此之外当缓冲区满是或者调用fflush会强制刷新缓冲区
示例代码如下:
可以看到是正常的输出表现
当我们进行重定向后发现系统调用最先打出来,后面才是对应的库函数。这是因为我们没进行重定向之前是默认往1号文件描述符打的因此是行刷新,但是重定向到一个文件就是全刷新,因此在进程退出时,我们的库函数才会打印出来。
接着我们在代码后面加上fork()接着进行重定向
这是因为我们的库函数现在是全刷新因此我们创建子进程后两者结束时会刷新两次用户级的缓冲区因此我们的库函数被刷新了两次。
File
- 因为IO相关函数与系统调⽤接⼝对应,并且库函数封装系统调⽤,所以本质上,访问⽂件都是通过fd访问的。
- 所以C库当中的FILE结构体内部,必定封装了fd。
libc读写函数的封装
//mylib.h
#pragma once
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include<stdlib.h>
#include <fcntl.h>
#include<string.h>
#include<unistd.h>
#define SIZE 1024
#define NOFRUSG 1<<0
#define LINEFRUSH 1<<1
#define QUANFUSH 1<<2
typedef struct MyFile
{int fd;char buffer[SIZE];int flag; // 标记打开方式int bufferlen; // 当前缓冲区大小int frush_method;
} MyFile;
size_t myfread(void *ptr, size_t size, size_t nmemb, MyFile *stream);
size_t myfwrite(const void *ptr, size_t size, size_t nmemb, MyFile *stream);
MyFile *myfopen(const char *pathname, const char *mode);
int myfclose(MyFile *stream);
int myfflush(MyFile *stream);//mylib.c
#include "mylib.h"void Init(MyFile *file, int fdd, int flag)
{memset(file->buffer, 0, sizeof(file->buffer));file->bufferlen = 0;file->fd = fdd;file->flag = flag;file->frush_method=QUANFUSH;
}
MyFile *myfopen(const char *pathname, const char *mode)
{int flag = -1;int st = 1;if (strcmp(mode, "w") == 0){st = 0;flag = O_WRONLY | O_CREAT | O_TRUNC;}else if (strcmp(mode, "r") == 0){flag = O_RDONLY;}else if (strcmp("a", mode) == 0){flag = O_APPEND | O_CREAT | O_WRONLY;st = 0;}else{}int fdd = -1;umask(0);if (st == 0)fdd = open(pathname, flag, 0666);elsefdd = open(pathname, flag);if (fdd < 0)return NULL;MyFile *file = (MyFile *)malloc(sizeof(MyFile));Init(file,fdd,flag);return file;
}
int myfflush(MyFile *stream)
{//语言层的刷新其实就是直接写入到内核中int n=write(stream->fd,stream->buffer,stream->bufferlen);(void)n;
}
void printfbuff(MyFile*stream)
{for(int i=0;i<=stream->bufferlen;i++) printf("%c",stream->buffer[i]);
}
size_t myfwrite(const void *ptr, size_t size, size_t nmemb, MyFile *stream)
{//写入本质先拷贝到用户的缓冲区memcpy(stream->buffer+stream->bufferlen,ptr,size*nmemb);//更新缓冲区长度stream->bufferlen+=nmemb*size;//考虑是不是要进行刷新到我们的内核缓冲区if(stream->fd==1||stream->fd==2) stream->frush_method=LINEFRUSH;if(stream->frush_method==LINEFRUSH&&stream->buffer[stream->bufferlen-1]=='\n'||stream->bufferlen==SIZE){myfflush(stream);}printfbuff(stream);}
int myfclose(MyFile *stream)
{close(stream->fd);
}
size_t myfread(void *ptr, size_t size, size_t nmemb, MyFile *stream)
{char buffer[1024];return read(stream->fd,buffer,sizeof(buffer));
}//test.c
#include"mylib.h"
int main()
{MyFile*ptr=myfopen("log.txt","w");if(ptr==NULL){perror("myfopen failure\n");return 0;}const char*message="hello world\n";myfwrite(message,1,strlen(message),ptr);return 0;
}
执行结果:
可以看到我们的打印缓冲区写入了我们的helloworld但是由于我们打开的文件不是显示器文件因此是全缓冲并不会直接打印,但是们的缓冲区是有的。