欢迎您访问365答案网,请分享给你的朋友!
生活常识 学习资料

linux就该这么学【基础IO】

时间:2023-05-22


目录

c语言IO基础操作:系统文件I/O

open函数返回值

文件描述符fd文件描述符的分配规则总结: 重定向

总结: 如何理解linux下一切皆文件?FILE使用dup2系统调用

dup2原理: 理解文件系统

inode文件创建、删除、查找过程创建一个新文件主要有一下4个操作:理解硬链接软链接硬链接 c语言IO基础操作:

在我们学习C语言的时候唯一对IO流有过的接触的就是c语言的文件操作了,回顾一下c语言对文件的操作

练习1:向一个文本文件写入5个hello word

#include #include int main(){ FILE *fp = fopen("word.txt", "w"); if(!fp) printf("文件打开失败n"); int count = 5; const char* str = "hello wordn"; while(count--){ fwrite(str, strlen(str), 1, fp); //往文件中写入5个hello word } fclose(fp); fp = NULL; return 0;}

将刚写入文件的hello word存储回buf 数组中

#include #include int main(){ FILE *fp = fopen("word.txt", "r"); if(!fp) printf("文件打开失败n"); char buf[12];while (fread(buf, 1, strlen(str), fp)) {buf[11] = 0;printf("%s", buf);}fclose(fp); fp = NULL; return 0;}

系统文件I/O

操作文件,除了上述C接口(当然,C++也有接口,其他语言也有),我们还可以采用系统接口来进行文件访问,先来直接以代码的形式,实现和上面一模一样的代码

open接口介绍

#include #include #include int open(const char *pathname, int flags);int open(const char *pathname, int flags, mode_t mode);pathname: 要打开或创建的目标文件flags: 打开文件时,可以传入多个参数选项(创建文件的方式),用下面的一个或者多个常量进行“或”运算,构成flags。mode:指明创建属性//创建文件的三种方式参数: O_RDONLY: 只读打开 O_WRONLY: 只写打开 O_RDWR : 读,写打开 这三个常量,必须指定一个且只能指定一个 O_CREAT : 若文件不存在,则创建它。需要使用mode选项, 来指明新文件的访问权限 O_APPEND: 追加写(类似于c语言中的fopen()的”a“选项, 会追加信息) 返回值: 成功:新打开的文件描述符 失败:-1

flags参数:

grep -ER 'O_CREAT | O_APPEND | O_RDONLY)' /usr/include//usr/include/bits/fcntl-linux.h //flags对应参数所在头文件

系统标准头文件中:/usr/include/bits/fcntl-linux.h

如果理解flags

int fd = open("w.txt", O_WRonLY | O_CREAT | O_APPEND, 0644);

其实当我们写出这行代码的时候,会去调用系统调用open,open函数的第一个
参数代表的是要打开的目标文件,而 O_WRonLY | O_CREAT | O_APPEND
这是一个位运算表达式,而按位或(|)的结果值表示的是:需要使用open函数
打开目标文件后,执行的功能,其实每一个选项(O_WRonLY | O_CREAT | O_APPEND)他都有对应的一个值,这个值被包含在/usr/include/bits/fcntl-linux.h这个头文件中(也就是上述图)。

程序演示:

#include #include #include #include #include #define SIZE 256//使用系统调用接口完成文件读写操作int main(){ int fd = open("word.txt", O_RDONLY); //只读打开 if(fd < 0) { perror("文件打开失败n"); return 0; } close(fd); //关闭文件 return 0;}

read文档:

read接口介绍:

#include ssize_t read(int fd, void *buf, size_t count);Read()尝试从文件描述符fd中读取字节数 从buf开始的缓冲区。 对于支持查找的文件,读操作从当前位置开始 租用文件偏移量,文件偏移量随数量递增 字节读。 如果当前文件的偏移量位于或超过文件的末尾, 不读取字节,read()返回零。 如果count为零,read()可能会检测到下面描述的错误。 在 没有任何错误,或者如果read()不检查错误,则read() 计数为0时返回0,没有其他效果。 如果count大于SSIZE_MAX,结果是不指定的。

程序演示:

#include #include #include #include #include #define SIZE 256//使用系统调用接口完成文件读写操作int main(){ int fd = open("word.txt", O_RDONLY); //只读打开 if(fd < 0) { perror("文件打开失败n"); return 0; } //文件打开成功 char buf[SIZE]; read(fd, buf ,SIZE); //从文件描述符fd中读取size个字节的字符到buf数组中。 printf("%s", buf); //输出buf数组中的字符串 close(fd); //关闭文件 return 0;}

open函数返回值

在认识返回值之前,先来认识一下两个概念: 系统调用 和 库函数上面的 fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数(libc)。而, open close read write lseek 都属于系统提供的接口,称之为系统调用接口回忆一下我们讲操作系统概念时,画的一张图:

系统调用接口和库函数的关系,一目了然。所以,可以认为,在c语言中 f#系列的函数,都是对系统调用的封装,方便二次开发

文件描述符fd

代码:

#include #include #include #include #include #define SIZE 256//使用系统调用接口完成文件读写操作int main(){ int fd = open("word.txt", O_RDONLY); //只读打开 if(fd < 0) { perror("文件打开失败n"); return 0; } //文件打开成功 char buf[SIZE]; read(fd, buf ,SIZE); //从文件fd中读取size个字节的字符到buf数组中。 printf("fileindex:%d , src: %s", fd, buf); //打印fopen的返回值,以及buf数组中存放的字符串 close(fd); //关闭文件 return 0;}

程序运行结果:

从程序的运行结果来看,确实当我们的程序运行起来之后,会将会打开"word.txt"文件,并在执行read函数的时候将"word.txt"文件中的内容写入到buf这个字符数组中去,另外需要关心的是open在执行成功之后带回来一个val值为3的返回值。这是为什么呢?

为了一探究竟接下来我们可以写一个程序用来测试fd的返回值

#include #include #include #include #include #define SIZE 256//使用系统调用接口完成文件读写操作int main(){ int fd = open("word.txt", O_RDONLY); //只读打开 if(fd < 0) { perror("文件打开失败n"); return 0; } //读取文件的信息存放到buf数组中 char buf[SIZE]; read(fd, buf ,SIZE); //从文件fd中读取size个字节的字符到buf数组中。 printf("fileindex:%d , src: %s", fd, buf); int fd1 = open("word1.txt", O_WRonLY | O_CREAT); //打开文件只写,如果没有就创建 int fd2 = open("word2.txt", O_WRonLY | O_CREAT); //只写 。。。。。。。。。。 int fd3 = open("word3.txt", O_WRonLY | O_CREAT); //只写 。。。。。。。 。 。 int fd4 = open("word4.txt", O_WRonLY | O_CREAT); //只写 。。 。 。。 。 。。 int fd5 = open("word5.txt", O_WRonLY | O_CREAT); //只写 。 。。 。 。。 。。 printf("fd1:%dn", fd1); printf("fd2:%dn", fd2); printf("fd3:%dn", fd3); printf("fd4:%dn", fd4); printf("fd5:%dn", fd5); close(fd); //关闭文件 return 0;}

程序运行结果:

现象1:从fd的返回值来看,他是连续打印的,fd每次的返回值也是顺序递增的,并且fd的返回值默认会从3开始打印

现象2:open("word1.txt", O_WRonLY | O_CREAT);执行打开文件的时候,带了两个选项,而我们使用c语言的fopen打开文件的时候只需要带”w“

文件描述符的分配规则

疑问?为什么fd的返回值默认从3开始?

其实c语言的程序一旦执行起来就会默认打开三个IO流,分别是stdio、stdin、stderr标准输入输出流。而后面的3、4、5、6、7、8其实是文件描述符,文件描述符在系统层面就是一个整数,这个整数的取值范围是【0,n】,
那么之前的0~2数字其实依次代表的是:
0:标准输入(stdin) 1:标准输出(stdout) 2:标准错误(stderr)
其实【0,n】的数字本质上是数组的下标。

对于进程来讲,默认的文件描述符从3开始分配(因为0~2是被标准输入输出和stderr占用了)。

总结:

1、open是属于封装出来的系统调用接口
2、open函数的返回值是一个文件描述符fd
3、文件描述符有他的分配规则,站在进程的角度,文件描述符默认只会分配最小值。

使用程序证明, 如果stdin流被关闭了,那么返回的fd会是多少?

#include #include #include #include #include #define SIZE 256//使用系统调用接口完成文件读写操作int main(){ close(0); //将标准输入流关闭 int fd = open("word.txt", O_RDONLY); //只读打开 if(fd < 0) { perror("文件打开失败n"); return 0; } printf("fd:%dn", fd); close(fd); //关闭文件 return 0;}

程序运行结果:

结论:给文件描述符分配时,默认从最小开始分配。

重定向

有趣的实验:

#include #include #include #include #include #define SIZE 256//使用系统调用接口完成文件读写操作int main(){ close(1); //将标准输入流关闭 int fd = open("word.txt", O_WRonLY | O_CREAT); //只读打开 if(fd < 0) { perror("文件打开失败n"); return 0; } printf("hhhhhhh....%dn", fd); fflush(stdout); close(fd); //关闭文件 return 0;}

程序运行结果:

我们会发现原本应该应该打印到显示屏的hhhhhhh....1却被输出到了文件中,那么为什么呢?其实一开始的标准输出流是被打开了的,可是我们却close关闭了stdout流,那么1这个文件标识符的值就会空着,而当使用open函数打开"word.txt"文件时,系统就会将原本属于stdout的文件标识符1分配给"word.txt"文件,所以导致原本应该输出到屏幕的值却输出到了文件"word.txt"上,而这也是重定向的原理

总结:

1、当fd等于1,这种现象也叫做重定向(常见的重定向有:>, >>, <)
2、重定向的本质其实是,当原本属于stdout的文件描述符1,被分配给了被open打开文件的fd,并在输出的过程将原本应该输出到stdout流的内容被重定向到了被open打开的文件

问题? 文件被打开了那么需不需要被管理?交给谁管理?

文件其实还是交给OS管理的,而OS管理的原则是:先描述,再组织,当文件被加载到内存中之后,操作系统会以某种数据结构的方式管理起文件,而PCB中有一个task_struct类型的结构体,里面会存放一个指针,该指针会指向一个指针数组,获得他的首地址。


那如果我们需要写入一些数据进入文件的话,操作系统会怎么做呢?

操作系统会使用task_struct中的数组指针通过获取struct file *arr[0]的地址,通过数组是线性存储的特性,通过下标索引找到对应的文件,再向该文件中写入数据

问题使用open()函数打开文件,返回的fd值又是谁的?

答案是指针数组的索引位置

下面的代码均摘自linux 2.6.24。
struct files_struct在中定义如下:

struct files_struct { atomic_t count; struct fdtable *fdt; struct fdtable fdtab; int next_fd; struct embedded_fd_set close_on_exec_init; struct embedded_fd_set open_fds_init; struct file * fd_array[NR_OPEN_DEFAULT]; };

如何理解linux下一切皆文件?

在我们实际使用硬盘、显示器、键盘的时候,不管是读取还是写入的过程其实调用的接口分别是:
(h_read()、h_write() )硬盘读写
(x_read()、x_write() )显示器读写
(j_read()、j_write() )键盘读写

其实struct file结构体中会包含open、write、read的函数指针,而驱动层也会有对应的几个函数接口,由于他们的参数是相同的,所以可以通过struct file结构体中的函数指针指向驱动层的函数接口,达到open、write、read可以分别使用硬盘、显示器、键盘。(读者可以理解为是类似于多态,因为同一个函数可以实现不同的功能) ,由于操作系统站在上层的角度,所以并不需要关心底层的实现,操作系统真正关心的是使用了软件封装出来的虚拟层,所以在操作系统看来一切皆文件!

以下是该结构体 在内核2.6.5中看起来的样子:

struct file_operations {undefined struct module *owner; loff_t(*llseek) (struct file *, loff_t, int); ssize_t(*read) (struct file *, char __user *, size_t, loff_t *); ssize_t(*aio_read) (struct kiocb *, char __user *, size_t, loff_t); ssize_t(*write) (struct file *, const char __user *, size_t, loff_t *); ssize_t(*aio_write) (struct kiocb *, const char __user *, size_t, loff_t); int (*readdir) (struct file *, void *, filldir_t); unsigned int (*poll) (struct file *, struct poll_table_struct *); int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long); int (*mmap) (struct file *, struct vm_area_struct *); int (*open) (struct inode *, struct file *); int (*flush) (struct file *); int (*release) (struct inode *, struct file *); 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 *); ssize_t(*readv) (struct file *, const struct iovec *, unsigned long, loff_t *); ssize_t(*writev) (struct file *, const struct iovec *, unsigned long, loff_t *); ssize_t(*sendfile) (struct file *, loff_t *, size_t, read_actor_t, void __user *); 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);};

FILE

有趣实验:

#include #include #include #include #include int main(){ close(1); int fd = open("w.txt", O_WRonLY | O_CREAT | O_APPEND, 0644); if(fd < 0){ printf("opend errorn"); return 1; } const char* str = "hello word!:writen"; const char *str1 = "hello word!:printfn"; const char *str2 = "hello word!:fprintfn"; write(1, str, strlen(str)); printf(str1); fprintf(stdout, str2); fflush(stdout); close(fd); return 0;}//程序运行结果:[mzt@VM-16-4-centos test]$ cat w.txt hello word!:writehello word!:printfhello word!:fprintf

当我们将标准输出流关闭之后,我们原本需要输出到屏幕的信息全都被输出到了文件中,其实这个现象博主上面也解释过了。那么还有一些知识需要大家掌握。

因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过fd访问的。

所以C库当中的FILE结构体内部,必定封装了fd

其实struct FILE 结构体是被存放在系统头文件libio.h中的,而这个系统头文件的路径是:/usr/include/libio.h 。

FILE其实只是struct _IO_FILE 结构体类型的别名

其实stdout也是属于FILE类型结构体的指针,既然是FILE类型,那么stdout中一定会包含_fileno

对比write(1, str, strlen(str)) 这句代码能够向文件中写入可以理解,但是为什么 fprintf(stdout, str2);这句代码也能向文件中写入呢?因为stdout中存放的_fileno中的值并没有被改变他还是1!所以这两个代码的功能是一样的!

当我们看完他的相同点之后,再来看他的不同点,这里还是用一段小程序测试:

#include #include #include #include #include #include int main(){ close(1); int fd = open("w.txt", O_WRonLY | O_CREAT | O_APPEND, 0644); if(fd < 0){ printf("opend errorn"); return 1; } const char* str = "hello word!:writen"; const char *str1 = "hello word!:printfn"; const char *str2 = "hello word!:fprintfn"; write(1, str, strlen(str)); printf(str1); fprintf(stdout, str2); fork(); fflush(stdout); close(fd); return 0;}

从程序的运行结果我们不难发现,write只被执行了一次,而printf和fprintf却被执行了两次

write和printf/fprintf他们之间的区别就是write是系统调用接口,而printf / fprintf确实库函数,其实向显示器上输出结果这种方式也是有缓冲区的,这种缓冲方式是行缓冲(遇到n就刷新,因为需要被立马看到),而如果是往文件中输出内容并不需要被用户立马就看见,所以文件是以全缓冲(缓冲区存不下了才会往硬盘中的文件输出)
而这个重定向其实引起来的最大的变化是:在重定向的时候,我们写入数据的目的地发生了变化,没有重定向本身是要往显示器上写,而重定向之后就必须要往文件中写,所以一旦重定向之后是影响缓冲方式的!!

我们发现 printf 和 fprintf (库函数)都输出了2次,而 write 只输出了一次(系统调用)。为什么呢?肯定和fork执行后的fflush(stdout);有关!

一般C库函数写入文件时是全缓冲的,而写入显示器是行缓冲。printf fprintf 库函数会自带缓冲区(进度条例子就可以说明),当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲。而我们放在缓冲区中的数据,就不会被立即刷新,甚至fork之后但是进程退出之后,会统一刷新,写入文件当中。但是fork的时候,父子数据会发生写时拷贝,所以当你父进程准备刷新的时候,子进程也就有了同样的一份数据,随即产生两份数据。write 没有变化,说明没有所谓的缓冲。

综上:
printf fwrite 库函数会自带缓冲区,而 write系统调用没有带缓冲区。另外,我们这里所说的缓冲区,都是用户级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区,不过不再我们讨论范围之内。那这个缓冲区谁提供呢?printf fwrite是库函数, write 是系统调用,库函数在系统调用的“上层”, 是对系统调用的“封装”,但是 write没有缓冲区,而 printf fwrite 有,足以说明,该缓冲区是二次加上的,又因为是C,所以由C标准库提供。

使用dup2系统调用


Dup2()使newfd成为oldfd的副本(使用oldfd拷贝出newfd),必要时先关闭newfd‐sary。

dup2原理:

如果想要stdout(标准输出流)打印到屏幕的信息写入文件当中,完成标准输出重定向到文件中的操作,我们需要将指向stdou这个文件的指针去指向myfile这个被创建出来的file对象就行,中间的过程是将index位置为3的数组中的指针拷贝给index下标位置为1中,那么就会让index位置1处的*file去指向新打开的myfile文件所对应的对象(也就是所谓的oldfd拷贝给newfd,那么index位置为1处的指针就会被更新为指向myfile文件所关联的file对象)

#include #include #include #include #include #include int main(){ //close(1); int fd = open("w.txt", O_WRonLY | O_CREAT | O_APPEND, 0644); if(fd < 0){ printf("opend errorn"); return 1; } dup2(fd, 1); //将位于指针数组中index位置为fd的指针,拷贝给index位置为1处的指针 const char* str = "hello word!:writen"; const char *str1 = "hello word!:printfn"; const char *str2 = "hello word!:fprintfn"; write(1, str, strlen(str)); printf(str1); fprintf(stdout, str2); fork(); fflush(stdout); close(fd); //文件描述符是有一定的上限的,需要关闭节省资源 return 0;}

理解文件系统

我们使用ls -l的时候看到的除了看到文件名,还看到了文件元数据

每行包含7列:

模式硬链接数文件所有者组大小最后修改时间文件名

ls -l读取存储在磁盘上的文件信息,其实本质上是将文件的信息放到内存,然后显示出来。

其实这个信息除了通过这种方式来读取,还有一个stat命令能够看到更多信息

上面的执行结果有几个信息需要解释清楚

inode

为了能解释清楚inode我们先简单了解一下文件系统

Linux ext2文件系统,上图为磁盘文件系统图(内核内存映像肯定有所不同),磁盘是典型的块设备,硬盘分区被划分为一个个的block。一个block的大小是由格式化的时候确定的,并且不可以更改。例如mke2fs的-b选项可以设定block大小为1024、2048或4096字节。而上图中启动块(Boot Block)的大小是确定的。

Boot Block其实是一个启动块,启动块一般是第一个物理盘的扇区,这个启动块会包含硬盘的分区表这样的信息, 并且这个分区也会指明你的操作系统盘在哪个分区,并且他能找到操作系统

Block Group:ext2文件系统会根据分区的大小划分为数个Block Group。而每个Block Group都有着相同的结构组成。政府管理各区的例子超级块(Super Block):存放文件系统本身的结构信息。记录的信息主要有:bolck 和 inode的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了,注意:Super Block 结构在每一个块组里面都有一个Super Block,并且是一样的,这样做的目的是为了防止Super Block 被破快导致剩下的空间块无法使用,GDT,Group Descriptor Table:块组描述符,描述块组属性信息,并且里面记录了inode的使用情况和inode的总个数。块位图(Block Bitmap):Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用(记录inode哪些被使用,哪些没有被使用)inode位图(inode Bitmap):inode Bitmap它其实是一个位图,他是利用了一种哈希的原理将每个bit表示一个inode是否空闲可用。i节点表(inode table):存放文件属性 如 文件大小,所有者,最近修改时间等, 其中inode:存放文件的属性, inode必须包含data blocks对应的映射关系,inode id会 标识一个inode
数据区(data blocks):存放文件内容文件和inode的关系是1对1,文件和data blocks的关系是1对n

文件创建、删除、查找过程

如何创建一个文件?

其实创建一个文件的过程包含以下:先给文件分配inode,但是需要在inode Bitmap该表中找一个没有被分配的比特位,它代表的是一个inode,分配成功后会将它置为1,而文件的属性会包含大小、最后修改时间、文件名、所以需要将文件的属性填入inode table,通过inode table和data blocks所对应的映射关系将数据写入到这个数据块中。

如何查找一个文价?

先找到文件所在的块组,在inode table中找到对应的inode信息,找到inode对应的信息就能够找到属性,根据他的块组映射找到对应的数据块,将数据提取出来

如何删除一个文件?

将需要删除的文件所对应的inode编号在位图inode Bitmap当中,将1置为0。

如何恢复一个文件?

根据inode将对应的inode Bitmap位置置为1,再根据inode table和data blocks所对应的映射关系置为1,这个文件就被恢复了。

将属性和数据分开存放的想法看起来很简单,但实际上是如何工作的呢?我们通过touch一个新文件来看看如何工作。

我们将上图简化:

创建一个新文件主要有一下4个操作: 存储属性
内核先找到一个空闲的i节点(这里是263466)。内核把文件信息记录到其中。存储数据
该文件需要存储在三个磁盘块,内核找到了三个空闲块:300,500,800。将内核缓冲区的第一块数据
复制到300,下一块复制到500,以此类推。记录分配情况
文件内容按顺序300,500,800存放。内核在inode上的磁盘分布区记录了上述块列表。添加文件名到目录
新的文件名abc。linux如何在当前的目录中记录这个文件?内核将入口(263466,abc)添加到目录文
件。文件名和inode之间的对应关系将文件名和文件的内容及属性连接起来

问题? 目录算是文件吗?

是,目录也是由inode和数据块构成的,而一个数据块中保存的其实是文件名和inode id,所以目录中的数据存放的是文件名到inode的映射关系。

理解硬链接

了解硬链接:

[root@localhost linux]# touch abc
[root@localhost linux]# ln abc def
[root@localhost linux]# ls -1i
abc def 263466 abc 263466 def

我们看到,真正找到磁盘上文件的并不是文件名,而是inode。 其实在linux中可以让多个文件名对应于同一个

abc和def的链接状态完全相同,他们被称为指向文件的硬链接。内核记录了这个连接数,inode 263466 的硬连接数为2。我们在删除文件时干了两件事情:1.在目录中将对应的记录删除,2.将硬连接数-1,如果为0,则将对应的磁盘释放。 软链接


具有独立的inode,因为log.txt和log.txt的软链接都是只有一个inode,既然各自都有一个inode那么它就是一个独立的文件,

硬链接

其实硬链接h_link和log、txt文件共享的是同一个inode,所以硬链接并不是一个独立的文件,那么硬链接是什么? 其实h_link在本质上还是一个文件名,只是它使用的是log、txt的inode编码,其实test这个当前目录中也是会存放文件名和inode的映射关系,而h_link只是一个特定文件名和inode的映射关系,

为什么目录的链接数是2?其实文件名dir和1835039( inode id )属于一组映射关系,还有的就是dir目录中的隐藏文件 、会和1835039属于一组映射关系,所以dir的连接数是2

但是dir中也会存放当前目录和上级目录,如果dir的连接数为3,那么dir和1835039属于一组映射关系,而dir中的隐藏文件 .和1835039属于一组映射关系,tmp中的隐藏文件、.和1835039属于一组映射关系。所以dir中就会有3个连接数

Copyright © 2016-2020 www.365daan.com All Rights Reserved. 365答案网 版权所有 备案号:

部分内容来自互联网,版权归原作者所有,如有冒犯请联系我们,我们将在三个工作时内妥善处理。