文章目录
1 C接口回顾
#include<stdio.h>
#include<string.h>
int main()
{
FILE* fp=fopen("bit.txt","w");//以只写的方式创建一个文件,若文件已经存在,会先把文件清空
if(fp==NULL)
{
perror("fopen\n");
return 1;
}
const char* s="linix so easy\n";
fwrite(s,strlen(s),1,fp);//将字符串s写到文件bit.txt中
fclose(fp);
return 0;
#include<stdio.h>
#include<string.h>
int main()
{
FILE*fp=fopen("bit.txt","r");//以只读的方式打开已经存在的文件,若文件不存在,则打开失败
if(fp==NULL)
{
perror("fopen");
}
char buf[64];
while(fgets(buf,sizeof(buf),fp))//将文件中的内容读到buf中去
{
printf("%s",buf);
}
fclose(fp);
return 0;
}
#include<stdio.h>
#include<string.h>
int main()
{
FILE*fp=fopen("log.txt","a");//向文件末尾添加数据,文件不存在,则创建数据
if(fp==NULL)
{
printf("file error\n");
return 1;
}
int cnt=5;
while(cnt--)
{
fputs("hello log\n",fp);//向文件中写数据
}
fclose(fp);
return 0;
}
2 系统文件调用接口
2.1 open
- pathname:要打开或创建的目标文件
- flags: 打开文件时,可以传入多个参数选项,用下面一个或多个常量进行“或”运算
参数 | 含义 |
---|---|
O_RDONLY | 只读打开 |
O_WRONLY | 只写打开 |
O_RDWR | 读,写打开 |
O_CREAT | 文件若不存在,则创建它,需要使用mode选项,来指明新文件的访问权限 |
O_APPEND | 追加写 |
O_TRUNC | 每次打开的时候先清空文件 |
- mode:文件的访问权限,用八进制数字来表示
- 返回值:
成功:新打开的文件描述符(fd)
失败:-1
flags参数的理解
eg
#include<stdio.h>
#define ONE 0x1//0000 0001
#define TWO 0x2//0000 0010
#define THREE 0x4//0000 0100
void show(int flags)
{
if(flags & ONE)
{
printf("hello one\n");
}
if(flags & TWO)
{
printf("hello two\n");
}
if(flags & THREE)
{
printf("hello three\n");
}
}
int main()
{
show(ONE);
printf("------------------------\n");
show(TWO);
printf("------------------------\n");
show(ONE|TWO);
printf("------------------------\n");
show(ONE|TWO|THREE);
printf("------------------------\n");
}
可以看到,示例代码中的宏都是只有一个bit位为1的整数,各个宏之间并不相同,因此可以标识不同的状态,或运算可以传递不同的宏,与运算可以得到传递的宏的含义,flags的参数选项的实现也是如此。
2.2 close
使用:传入要关闭文件的描述符,即可关闭该文件
返回值:
关闭成功,返回文件描述符
关闭失败,返回-1
2.3 write
- fd:要进行写操作的文件的文件描述符
- buf:将字符串buf写入文件中
- count:写入字符的个数
- 返回值:返回真正写入字符的个数
2.4 read
- fd:要进行读操作的文件的文件描述符
- buf:将文件中读到的内容存放到数组buf中
- count:读取的字符个数
- 返回值:返回真正读取的字符个数
2.5 系统文件调用接口演示
往bite.txt里面写“hello,write\n”
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
int main()
{
int fd=open("bite.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);//文件权限为rw-rw-rw-
if(fd<0)
{
perror("open");
return 1;
}
printf("open success,fd:%d\n",fd);
const char*s="hello write\n";
write(fd,s,strlen(s));
close(fd);
return 0;
}
往bite.txt里面追加写,即每次打开文件的时候不清空,接着在文件末尾写
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
int main()
{
int fd=open("bite.txt",O_WRONLY|O_CREAT|O_APPEND,0666);
if(fd<0)
{
perror("open");
return 1;
}
printf("open success,fd:%d\n",fd);
const char*s="hello write\n";
const char*ss="hello append\n";
write(fd,s,strlen(s));
write(fd,ss,strlen(ss));
close(fd);
return 0;
}
读取bite.txt中的内容
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
int main()
{
int fd=open("bite.txt",O_RDONLY);//只读
if(fd<0)
{
perror("open");
return 1;
}
printf("open success,fd:%d\n",fd);
char buff[128];
memset(buff,'\0',sizeof(buff));
read(fd,buff,sizeof(buff));
printf("%s",buff);
close(fd);
return 0;
}
3 文件描述符
#include<stdio.h>
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
int main()
{
int fd1=open("eg1.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
int fd2=open("eg2.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
int fd3=open("eg3.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
int fd4=open("eg4.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
int fd5=open("eg5.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
printf("open success,fd1:%d\n",fd1);
printf("open success,fd2:%d\n",fd2);
printf("open success,fd3:%d\n",fd3);
printf("open success,fd4:%d\n",fd4);
printf("open success,fd5:%d\n",fd5);
close(fd1);
close(fd2);
close(fd3);
close(fd4);
close(fd5);
}
可以看到,我们打开一个文件,文件描述符的值是从3开始依次递增,那么0,1,2去哪了呢?
3.1 0&1&2
Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2
0,1,2对应的硬件设备一般是:键盘,显示器,显示器
所以输入,输出还可以采用以下方式
#include<stdio.h>
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
int main()
{
char buff[128];
ssize_t s=read(0,buff,sizeof(buff));//从标准输入里面读取内容
if(s>0)
{
write(1,buff,strlen(buff));//向标准输出写内容,即打印到显示屏上
}
return 0;
}
3.2 fd的本质
进程与文件的联系
进程要访问文件,必须先打开文件,一个进程可以打开多个文件,那么系统中可能就会存在大量被打开的文件,所以操作系统是如何管理这么多的文件呢?先描述,再组织
在内核中,OS为了管理每一个被打开的文件,都会构建一个file结构体,里面包含了一个被打开文件的几乎所有内容,再用双链表组织起来
从图中可以知道,文件描述符就是从0开始的整数,当打开文件的时候,操作系统便会为文件创建file结构体来描述目标文件。当进程进行open调用的时候,为了能找到文件,必须让进程和文件关联起来,所以每一个进程都有一个指针*files,指向一个数组,这个数组是一个指针数组,每个指针指向一个打开的文件。
所以 fd的本质就是该数组的下标,知道了文件描述符,就能找到对应的文件
3.3 fd的分配规则
我们打开一个文件,文件描述符是从3开始分配
那么先关闭0呢?
close(0);
int fd=open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
if(fd<0)
{
perror("open");
return 1;
}
printf("fd:%d\n",fd);
可以看到文件描述符变为了0
如果关闭了1呢?
close(1);
int fd=open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
if(fd<0)
{
perror("open");
return 1;
}
printf("fd:%d\n",fd);
可以看到,fd的值变为了1,但还有一个奇怪的现象,本来应该要打印到显示器上的语句,打印到了log.txt中,这其实就是所谓的输出重定向
由以上可以得出结论:fd的分配原则是:从最小的,没有被占用的文件描述符开始分配。
3.4重定向
3.4.1 输出重定向(上份代码)
3.4.2输入重定向
close(0);//从标准输入读重定向到从log.txt读
int fd=open("log.txt",O_RDWR);
if(fd<0)
{
perror("open\n");
return 1;
}
char buf[64];
printf("fd:%d\n",fd);
while(fgets(buf,sizeof(buf),stdin))
{
printf("%s",buf);
}
3.4.3追加重定向
close(1);//从标准输出追加重定向到从log.txt追加
int fd=open("log.txt",O_WRONLY|O_CREAT|O_APPEND,0666);
if(fd<0)
{
perror("open");
return 1;
}
fprintf(stdout,"you can see me\n");
printf("fd:%d\n",fd);
char buf[64];
fgets(buf,sizeof(buf),stdin);//从标准输入中读取字符串到buf中
printf("%s\n",buf);
return 0;
3.4.4 重定向的本质
重定向的本质,就是在OS内部,更改fd对应的内容指向
3.4.5 dup2系统调用
dup2是系统级可以实现重定向的函数
注意:传参的时候,是要改变newfd的指向,使newfd的指向和oldfd的指向一样
int main(int argc,char*argv[])
{
//dup2的使用
if(argc!=2)//在命令行要输入两个参数
{
return 2;
}
int fd=open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
if(fd<0)
{
perror("open");
return 1;
}
dup2(fd,1);//标准输出重定向到log.txt中
fprintf(stdout,"%s\n",argv[1]);//在log.txt中打印第二个参数
close(fd);
}
4系统硬件管理方法
对于不同的硬件设备,它们的驱动程序提供给操作系统的read,write接口一定是不一样的,操作系统为了统一管理硬件设备,给每个硬件设备都配备了struct file结构体,把硬件设备描述成为文件,结构体里面的read,write函数指针分别对应指向驱动提供的接口。
这样在上层看来,就没有任何硬件的差别了,看待所有硬件,都统一成为了struct file,这也是Linux的设计哲学,一切皆文件。
5 缓冲区
5.1 什么是缓冲区
简单理解,就是一段内存空间
5.2 为什么要有缓冲区
在和外部设备进行IO操作的时候,数据量的大小并不是主要问题,和外设预备IO的过程是最耗费时间的。所以先将数据写到缓冲区,然后通过一定的刷新策略,再将缓冲区的数据刷新到外设上,可以减少与外设的访问次数,相对而言会提高效率。
5.3 缓冲区的刷新策略
①立即刷新
②行刷新,即遇到\n才会刷新
③ 满刷新 缓冲区满了才会刷新
int main()
10 {
11 //c语言提供的函数
12 printf("hello,printf\n");
13 fprintf(stdout,"hello fprintf\n");
14 const char*s="hello fputs\n";
15 fputs(s,stdout);
16 //OS提供的
17 const char*ss="hello write\n";
18 write(1,ss,strlen(ss));
19 fork();
20
21 }
同一份代码,直接输出到显示器上和输出重定向到文件中,打印出的结果并不一样,并且观察发现输出重定向的时候,C语言提供的接口统一打印了两遍,而操作系统提供的只打印了一遍,这是为何呢?
a 一般而言,如果向显示器打印,刷新策略是行刷新,那么最后执行fork的时候,一定是函数执行完了并且数据已经被刷新了,此时缓冲区已经没有数据了,fork的话也不必再拷贝数据给子进程,所以向显示器打印,只打印了一次。
b 如果对程序进行了重定向,要向磁盘文件打印,那么此时的刷新策略就成为了满刷新,当fork的时候,一定是函数执行完了,但数据还没有刷新,所以这部分在缓冲区的数据也相当于是父进程的数据,在fork的时候会拷贝给子进程一份,因此便会打印两遍。
c 但操作系统提供的接口为何还是只打印了一遍呢?可见,我们上文中提到的缓冲区,并不是由操作系统维护的,如果是操作系统维护的,那么重定向的时候,操作系统提供的接口也会打印两次。实际上,上文所提到的缓冲区是C标准库维护的缓冲区,属于用户级的缓冲区。而系统调用write会直接到内核缓冲区,不会受到影响。
6 文件系统
6.1 磁盘文件
之前讨论的都是在内存中已经打开的文件,而还有没有被打开的文件,这些文件都属于磁盘级文件
6.2 磁盘物理结构
主要由磁盘盘片,磁头,伺服系统,音圈马达等组成
6.3 磁盘的存储结构
磁盘存储数据的基本单位是扇区,那么如何找到一个扇区呢?
CHS寻址
1 先确定在哪一个面(对应的就是哪一个磁头)
2 再确定在哪个磁道上
3 最后确定在哪个扇区上
6.4 磁盘的抽象结构
Linux管理磁盘文件,是将磁盘抽象为线性的结构进行分区管理的
此时,将数据存储到磁盘,就是将数据存储到该数组
找到磁盘特定扇区的位置,就是找到数组特定的位置
对磁盘的管理,就是对该数组的管理
将一整块磁盘,划分成为了多个磁盘分区
对于每个分区,还要做细化管理
Boot Block: 存储磁盘启动时加载操作系统的相关信息
Boot group:每一个分区都会划分为若干个Block group,而每个Block group内部,又被划分为了不同的区域
名称 | 内容 |
---|---|
Data blocks | 多个4KB大小的集合,存储文件的内容 |
inode Table | inode是一个大小为128字节的空间,保存的是对应文件的属性,而该组块内,是所有文件的inode的集合;为了标识唯一性,每一个inode都会有一个特定的inode编号 |
Block Bitmap | Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有用,(假如有10000+个DataBlock,那么就会有10000+个比特位与之相对应,其中比特位为1,代表被占用,否则未被占用) |
inode Bitmap | 每个bit表示一个inode是否被使用,被占用为1,未使用为0 |
Group Descriptor Table | 块组描述符,这个块多大,已经使用了多少,有多少个inode,使用了多少,还剩多少未被使用… |
Super Block | 文件系统的属性信息 |