Linux复习——基础IO,认识文件描述符、软硬件链接
1.复习C文件接口
1.1 fopen
FILE *fopen(const char *path, const char *mode);
path:带路径的文件名称(待打开的文件)
mode:
r:以可读方式打开,不可写,文件不存在,则报错
r+:以读写方式打开,文件不存在,则报错
w:以可写方式打开,但是不能读。文件不存在,则创建。文件存在,则截断(清空文件内容)文件。
w+:以读写方式打开。文件不存在,则创建。文件存在,则截断(清空文件内容)文件
a:追加写,不能读。在文件末尾进行追加写,文件不存在则创建文件。文件存在,则在文件末尾开始写
a+:可读追加写,在文件末尾进行追加写,文件不存在则创建文件。文件存在,则在文件的末尾进行追加写
返回值:文件流指针 FILE
1.2 fclose
int fclose(FILE *fp);
关闭一个文件流指针
1.3 fread
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
ptr:要将读到的保存到哪里去,ptr保存用户准备的一个空间(缓冲区)的地址
size:块的大小,单位是字节
nmemb:块的个数
eg:size = 2 nmemb = 5 ==> 10
常见的用法:size = 1,nmemb可以指定任意块,读到的字节数量也就可以用nmemb表示
stream:文件流指针,表示要从哪里进行读
返回值:返回读到的块的个数
1.4 fwrite
size_t fwrite(const void *ptr, size_t size, size_t nmemb,FILE *stream);
ptr:要写入到文件当中的内容
size:块的大小,单位字节
nmemb:块的个数
总写入的字节数量 = 块的大小 * 块的个数
stream:文件流指针,表示要写到哪里
返回值:写入块的个数
1.5 fseek
int fseek(FILE *stream, long offset, int whence);
stream:文件流指针
offset:偏移量,单位字节。相对于whence而言的偏移量
whence:
SEEK_SET:文件的头部
SEEK_CUR:当前文件流指针的位置
SEEK_END:文件的尾部注意:我们向文件写入内容以后文件流指针会发生偏移,此时如果我们要从文件里读取内容,就需要通过fseek来设置流指针的位置不然从当前流指针的位置将读不到任何内容
2.系统文件I/O接口
2.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 flag,mode_t mode);
pathname :要打开的文件
flags:打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags
参数:
O_RDONLY:只读打开
O_WRONLY:只写打开
O_RDWR:读,写打开
这三个常量,必须指定一个且只能指定一个
O_CREAT:若文件不存在,则创建它,需要使用mode选项,来指明新文件的访问权限
O_APPEND:追加写
返回值:成功:新打开的文件描述符 失败:-1
2.2 close
int close(int fd); 这里的fd就是open的返回值
2.3 read
ssize_t read(int fd, void *buf, size_t count);
fd:文件描述符
buf:将读到的内容放到buf这个缓冲区当中
count:表示最大能够读多少字节,和buf的大小有关,需要在buf当中预留一个‘\0’的位置
返回值:
>0:读到的字节数量
-1:读失败
2.4 write
ssize_t write(int fd, const void *buf, size_t count);
- fd:文件描述符
- buf:写到文件当中的内容
- count:写入的字节数量
- 返回值:
>0:表示写入文件真实的字节数量
-1:写入失败
2.5 lseek
off_t lseek(int fd, off_t offset, int whence);
- fd:文件描述符
- offset:偏移量
- whence:
SEEK_SET:文件的头部
SEEK_CUR:当前文件流指针的位置
SEEK_END:文件的尾部
3.文件描述符
3.1 open函数返回值
open的返回值其实就是这里要介绍的文件描述符,但是在正式认识他之前我们先来认识两个概念:系统调用和库函数
上面的 fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数(libc)。
而, open close read write lseek 都属于系统提供的接口,称之为系统调用接口
回忆一下我们讲操作系统概念时,画的一张图
通过上图我们可以清晰的看到其实f#系列函数,都是对系统调用的封装,方便二次开发
3.2 文件描述符fd
文件描述符其实就是一个小整数
linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0,标准输出1,标准错误2
0,1,2对应的物理设备一般是:键盘、显示器、显示器,所以输入输出还可以采用如下方式
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.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开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是有了file结构体,其表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来,每一个进程都有一个指针*file,指向一张file_struct,该表最重要的部分就是包含一个指针数组,每个元素都是一个指向打开文件的指针!所以本质上,文件描述符就是数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件。
3.3 文件描述符分配规则
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
int fd = open("myfile", O_RDONLY);
if (fd < 0)
{
perror("open");
return 1;
}
printf("fd: %d\n", fd);
close(fd);
return 0;
}
输出发现是:fd:3 现在我们关闭0或者2再看
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
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;
}
发现的结果是:fd:0或者fd:2可见,文件描述符的分配规则;在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符
3.4 重定向
在3.3中我们了解了文件描述符的分配规则,如果我们将标准输出1关闭,那么printf还会打印在屏幕吗?下面进行测试
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.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);
exit(0);
}
此时我们发现,本来应该输出在显示器的内容,输出到了文件myfile当中,其中,fd=1,这种现象叫做输出重定向。常见的重定向有 >,>>,<
3.5 使用dup2系统调用
#include <unistd.h>int dup2(int oldfd, int newfd); //将原来写到newfd的内容写到oldfd
#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);
for (;;)
{
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;
}
3.6 FILE
因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过fd访问的。
所以C库中的FILE结构体内部,必然封装了fd。
来段代码研究一下
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main()
{
const char *msg0 = "hello printf\n";
const char *msg1 = "hello fwrite\n";
const char *msg2 = "hello write\n";
printf("%s", msg0);
fwrite(msg1, strlen(msg0), 1, stdout);
write(1, msg2, strlen(msg2));
fork();
return 0;
}
我们正常运行的结果和我们的预期一样,但是当我们对进程实现输出重定向后发现printf和fwrite(库函数)都输出了两次,而write(系统调用)只输出了一次,这显然和fork有关系
一般C库函数写入文件时是全缓冲的,而写入显示器是行缓冲的
printf和fwrite库函数会自带缓冲区,当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲。而我们放在缓冲区中的数据,就不会被立即刷新,甚至fork之后
但是进程退出之后,会统一刷新,写入文件当中
但是fork的时候,父子进程会发生写实拷贝,所以当你的父进程准备刷新的时候,子进程也有同样的一份数据也准备刷新,所以产生了两份数据
write没有变化,说明write没有所谓的缓冲。
综上:printf和fwrite库函数会自带缓冲区,而write系统调用没有缓冲区。另外,我们这里所说的缓冲区都是用户级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区,不过不在我们讨论范围之内。
那这个用户级缓冲区谁提供呢? printf fwrite 是库函数, write 是系统调用,库函数在系统调用的“上层”, 是对系统调用的“封装”,但是 write 没有缓冲区,而 printf fwrite 有,足以说明,该缓冲区是二次加上的,又因为是C,所以由C标准库提供。
4.理解文件系统
我们使用ls -l的时候看到的除了文件名,还有文件元数据
每行7列:模式、硬链接数、文件拥有者、组、大小、最后修改时间、文件名


Block Group : ext2 文件系统会根据分区的大小划分为数个 Block Group 。而每个 Block Group 都有着相同的结构组成。政府管理各区的例子超级块( Super Block ):存放文件系统本身的结构信息。记录的信息主要有: bolck 和 inode 的总量,未使用的block 和 inode 的数量,一个 block 和 inode 的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block 的信息被破坏,可以说整个文件系统结构就被破坏了GDT , Group Descriptor Table :块组描述符,描述块组属性信息,有兴趣的同学可以在了解一下块位图( Block Bitmap ): Block Bitmap 中记录着 Data Block 中哪个数据块已经被占用,哪个数据块没有被占用inode 位图( inode Bitmap ):每个 bit 表示一个 inode 是否空闲可用。i 节点表 : 存放文件属性 如 文件大小,所有者,最近修改时间等数据区:存放文件内容
将属性和数据分开的想法看起来很简单,但实际上是如何工作的,这里我们通过touch一个新文件来看看如何工作
为了说明问题,我们将上图简化
创建一个新文件主要有以下4个操作
1.存储属性:内核先找到一个空闲节点(这里是263466)。内核把文件信息记录到其中
2.存储数据:该文件需要存储在三个磁盘块,内核找到了三个空闲块:300,500,800.将内核缓冲区的第一块数据复制到300,下一块复制到500,以此类推
3.记录分配情况:文件内容按顺序300,500,800存放。内核在inode上的磁盘分区记录了上述块列表
4.添加文件名到目录:新的文件名abc,内核将入口(263466,abc)添加到目录文件。文件名和inode之间的对应关系将文件名和文件的内容及属性连接起来。
注意:通过4我们应该明白内核中其实存储的不是文件名而是inode,怎么说可能不准确应该是内核里的文件名是inode可能对些
理解硬链接:我们真正找到磁盘上的文件并不是文件名,而是inode。其实在linux中可以让多个文件名对应于同一个inode看下图。
abc和def的链接方式完全相同,他们被称为指向文件的硬链接。内核记录了这个链接数,inode 54850的硬链接数为2
我们在磁盘中删除文件时干了两件事:1.在目录中将对应的记录删除 2.将硬链接数-1,如果为0,则将对应的磁盘释放
连接软连接:硬链接是通过inode引用另一个文件,软连接是通过名字引用另一个文件,在shell中的做法