1.C语言中的文件IO
文件打开和关闭
FILE * fopen ( const char * filename, const char * mode );
//关闭文件
int fclose ( FILE * stream )
文件的使用方式
文件使用方式 | 含义 | 如果指定文件不存在 |
---|---|---|
“r”(只读) | 为了输入数据,打开一个已经存在的文本文件 | 出错 |
“w”(只写) | 为了输出数据,打开一个文本文件 | 建立一个新的文件 |
“a”(追加) | 向文本文件尾添加数据 | 建立一个新的文件 |
“rb”(只读) | 为了输入数据,打开一个二进制文件 | 出错 |
“wb”(只写) | 为了输出数据,打开一个二进制文件 | 建立一个新的文件 |
“ab”(追加) | 向一个二进制文件尾添加数据 | 出错 |
“r+”(读写) | 为了读和写,打开一个文本文件 | 出错 |
“w+”(读写) | 为了读和写,建议一个新的文件 | 建立一个新的文件 |
“a+”(读写) | 打开一个文件,在文件尾进行读写 | 建立一个新的文件 |
“rb+”(读写) | 为了读和写打开一个二进制文件 | 出错 |
“wb+”(读写) | 为了读和写,新建一个新的二进制文件 | 建立一个新的文件 |
“ab+”(读写) | 打开一个二进制文件,在文件尾进行读和写 | 建立一个新的文件 |
文件读写
功能 | 函数名 | 适用于 |
---|---|---|
字符输入函数 | fgetc | 所有输入流 |
字符输出函数 | fputc | 所有输出流 |
文本行输入函数 | fgets | 所有输入流 |
文本行输出函数 | fputs | 所有输出流 |
格式化输入函数 | fscanf | 所有输入流 |
格式化输出函数 | fprintf | 所有输出流 |
二进制输入 | fread | 文件 |
二进制输出 | fwrite | 文件 |
C语言文件操作示例
向log.txt中写入内容
#include <stdio.h>
int main(){
FILE* f=fopen("log.txt","w");
if(f==NULL){
perror("fopen");
return 0;
}
char buf[64]="hello world\n";
int cnt=10;
while(cnt--){
fwrite(buf,sizeof(char),sizeof(buf),f);
}
fclose(f);
return 0;
}
读取log.txt的内容
#include <stdio.h>
int main(){
FILE* f=fopen("log.txt","r");
if(f==NULL){
perror("fopen");
return 0;
}
char buf[256];
for(int i=0;i<10;i++){
fgets(buf,sizeof(buf),f);
printf("%s",buf);
}
fclose(f);
return 0;
}
1.1当前路径
我们知道,在C语言的fopen接口,如果打开的文件不存在,那么会在当前路径下创建一个文件,那么什么是当前路径?
int main(){
FILE* fp=fopen("centfile.txt","w");
return 0;
}
在ostest下生成了一个centfile.txt文件。那么是否表示当路径,表示当前可执行程序所处的路径呢。
这时我们可以将刚才可执行程序生成的centfile.txt文件先删除,然后再做一个测试:回退到上级目录,在上级目录下运行该可执行程序。
cd ..
./ostest/myfile
在ostest的上级目录下生成了centfile.txt文件。查看可执行程序myfile的相关信息
当该可执行程序运行起来变成进程后,我们可以获取该进程的PID,然后根据该PID在根目录下的proc目录下查看该进程的信息。
存在两个软连接
- cmd:进程在运行【可执行程序在运行时】的路径
- exe:可执行程序所处的路径
所以当前路径指的是:进程或者可执行程序在运行时所处的路径【也就是cmd】
2.系统IO
- 操作文件的第一步:需要将文件加载到内存中,由进程对文件进行管理。
2.1系统接口
2.1.1open
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
pathname: 要打开或创建的目标文件
flags参数: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags。
参数选项 | 含义 |
---|---|
O_RDONLY | 以只读的方式打开文件 |
O_WRNOLY | 以只写的方式打开文件 |
O_APPEND | 以追加的方式打开文件 |
O_RDWR | 以读写的方式打开文件 |
O_CREAT | 当目标文件不存在时,创建文件 |
打开文件时,可以传入多个参数选项,当有多个选项传入时,将这些选项用“或”运算符隔开。
例如,若想以只写的方式打开文件,但当目标文件不存在时自动创建文件,则第二个参数设置如下:
以读写的方式打开,当文件不存在时,创建文件
open("log.txt",O_RDWR|O_CREAT);
为什么可以用|操作设置选项?
flag的底层其实是一个位图,一个int类型的一个用32给比特位,所以flag最多有32个选项。下面是内核中的,flag参数每一个选项在系统中的宏定义:
#define O_RDONLY 00
#define O_WRONLY 01
#define O_RDWR 02
#define O_CREAT 0100
#define O_TRUNC 01000
模拟open的flag参数的传递方式
#define PRINT_A 0x1
#define PRINT_B 0x2
#define PRINT_C 0x4
#define PRINT_D 0x8
#define PRINT_FAUL 0X0
void print(int a){
if(a & PRINT_A){
printf("this is PRINT_A\n");
}
if(a&PRINT_B){
printf("this is PRINT_B\n");
}
if(a&PRINT_C){
printf("this is PRINT_C\n");
}
if(a&PRINT_D){
printf("this is PRINT_D\n");
}
if(a==PRINT_FAUL){
printf("prinf fault\n");
}
}
int main()
{
printf("PRINT_A\n");
print(PRINT_A);
printf("PRINT_B\n");
print(PRINT_B);
printf("PRINT_C\n");
print(PRINT_C);
printf("PRINT_D\n");
print(PRINT_D);
printf("PRINT_FAUL\n");
print(PRINT_FAUL);
return 0;
}
mode参数:表示创建文件的文件权限
由于文件权限还会收到权限掩码umask的影响,实际上创建出来的权限位mode&(~umask)。【一般情况下默认的umask为0002,所以mode设置为666创建出来的文件权限为0664】
在程序中还可以使用umask()接口暂时修改权限掩码:
int main()
{
umask(0);
int fd=open("log.txt",O_RDONLY|O_CREAT,0666);
return 0;
}
返回值:
open如果打开文件成功,返回一个文件描述符,失败返回-1。关于文件描述符后面会详细结束,目前可以理解为一个文件描述符就是管理一个文件的钥匙。
2.1.2close
系统接口中用于关闭被打开的文件, 函数原型
int close(int fd)
使用时需要传入需要关闭文件的文件描述符。成功返回0,失败返回-1
2.1.3write
系统接口中使用write函数向文件中写入信息,函数原型:
ssize_t write(int fd, const void *buf, size_t count);
- fd:写入文件的文件描述符
- buf:写入的信息
- count:写信息的大小【字节】
- 返回值:返回写入的字节大小
示例
int main(){
umask(0);
int fd=open("log.txt",O_RDWR|O_CREAT,0666);
const char* s="hello world\n";
for(int i=0;i<5;i++){
write(fd,s,strlen(s));
}
close(fd);
return 0;
}
2.1.4read
系统接口中,从文件中读完内容,函数原型:
ssize_t read(int fd, void *buf, size_t count);
2.2文件描述符
先看下面的代码示例
int main(){
umask(0);
int fda = open("loga.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
int fdb = open("logb.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
int fdc = open("logc.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
int fdd = open("logd.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
int fde = open("loge.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
printf("fda: %d\n", fda);
printf("fdb: %d\n", fdb);
printf("fdc: %d\n", fdc);
printf("fdd: %d\n", fdd);
printf("fde: %d\n", fde);
return 0;
}
可以看到文件描述符是从3开始分配,而0,1,2分别对应了stdin,stdout,stderr
在进程被创建的时候,操作系统默认打开了标准输入、标准输出、标准错误,分别将文件描述符0,1,2分配给对应的标准IO文件。
2.2.1FILE结构体和fd的关系
C语言的文件IO是通过结构体FILE与文件关联,而系统IO是通过文件描述符与文件关联。
而我们知道C语言的接口是对系统接口做了一层封装:
所以可能struct FILE内部封装有文件描述符fd,查看stdio.h中的源码:
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags
//缓冲区相关
/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno; //封装的文件描述符
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
/* char* _save_gptr; char* _save_egptr; */
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
在结构体中_fileno成员就是FILE结构体封装的文件描述符
验证
int main(){
printf("stdin:%d,stdout:%d,stderr:%d\n",stdin->_fileno,stdout->_fileno,stderr->_fileno);
return 0;
}
3.Linux下的一切皆文件
一个进程可以打开多个文件,而系统中存在多个进程;也就是说,系统在任意一个时刻,都存在大量被打开的文件,那么操作系统是如何对被打开的文件进行管理?
我们先回忆操作系统是如何管理多个进程的
每一个进程在被加载到内存中时,操作系统都会给进程分配一个task_struct,便于操作系统对进程进行管理:
然后再将不同优先级的进程用链表连接,对进程的管理就转换为了对链表的增删改查。
进程对文件的管理
进程对文件的管理采用了同样的方式,每个文件被进程分配一个FCB,用于管理文件信息。
被加载到内存中的文件,转化为了一个双向链表。在进程的task_struct中存在一个files_struct结构体,结构体的内部存放了一个指针数组struct file* fd_array[ ],双向链表中的每一个文件管理模块都存放在指针数组中。
文件描述符就是该指针数组的下标,因此可以通过数组下标得到对应的struct file*,从而实现对文件的管理。
3.1C语言实现面向对象
C语言实现面向对象,成员函数需要依靠函数指针进行模拟
struct file{
//成员属性
//成员方法:用函数指针模拟
void(*readp)(int fd);
void(*writep)(int fd);
.....
}
比如,基于面向对象实现一个计数器
typedef struct count{
int (*add)(int,int);
int (*redu)(int,int);
int (*mult)(int,int);
int (*divi)(int,int);
}count;
int _add(int a,int b){
return a+b;
}
int _redu(int a,int b){
return a-b;
}
int _mult(int a,int b){
return a*b;
}
int _divi(int a,int b){
if(b==0){
return -1;
}
return a/b;
}
count cnt;
int main(){
cnt.add=_add;
cnt.redu=_redu;
cnt.divi=_divi;
cnt.mult=_mult;
int a=cnt.add(4,4);
int b=cnt.redu(4,4);
int c=cnt.mult(4,4);
int d=cnt.divi(4,4);
printf("add:%d,redu:%d,mult:%d,divi:%d\n",a,b,c,d);
return 0;
}
3.2Linux的一切皆文件的实现
无论是磁盘、显示器这样的硬件,还是应用程序层面的软件,都需要将信息加载到内存中进行管理。
Linux下一切皆文件,那么所有的信息都需要通过struct file结构体进行统一管理。
4.应用特征
4.1文件描述符的分配规律
我们依次打开多个文件,观察对应的文件描述符
int main()
{
umask(0);
int fd1 = open("log1.txt", O_RDONLY | O_CREAT, 0666);
int fd2 = open("log2.txt", O_RDONLY | O_CREAT, 0666);
int fd3 = open("log3.txt", O_RDONLY | O_CREAT, 0666);
int fd4 = open("log4.txt", O_RDONLY | O_CREAT, 0666);
int fd5 = open("log5.txt", O_RDONLY | O_CREAT, 0666);
printf("fd1:%d\n", fd1);
printf("fd2:%d\n", fd2);
printf("fd3:%d\n", fd3);
printf("fd4:%d\n", fd4);
printf("fd5:%d\n", fd5);
return 0;
}
打开的文件描述符是从3开始依次递增;由于0,1,2分别对应了标准输入。标准输出和标准错误,所以进程只能从3开始分配。
假如我们先关闭文件描述符1?
观察下面的程序 f.c
int main()
{
umask(0);
close(0);
close(2);
int fd1 = open("log1.txt", O_RDONLY | O_CREAT, 0666);
int fd2 = open("log2.txt", O_RDONLY | O_CREAT, 0666);
int fd3 = open("log3.txt", O_RDONLY | O_CREAT, 0666);
int fd4 = open("log4.txt", O_RDONLY | O_CREAT, 0666);
int fd5 = open("log5.txt", O_RDONLY | O_CREAT, 0666);
printf("fd1:%d\n", fd1);
printf("fd2:%d\n", fd2);
printf("fd3:%d\n", fd3);
printf("fd4:%d\n", fd4);
printf("fd5:%d\n", fd5);
return 0;
}
当我们关闭了0和2号文件描述符后,创建文件时从0和2开始分配。
分配规则:从最小未使用的文件描述符开始分配
4.2重定向的原理
int dup2(int oldfd, int newfd);
makes newfd be the copy of oldfd, closing newfd first if necessary。
//将oldfd的值赋值给newfd,关闭时先关闭newfd
dup这里的拷贝的是数组中的file*指针,属于是内核级的拷贝,是将oldfd中的struct file赋值给newfd对应的struct file。
比如:我们将打开文件log.txt时获取的文件描述符传入dup2函数,执行dup(fd,1)函数,那么fd_array[ fd ]中的内容会赋值给fd_array[ 1 ]中,最后本应该打印在屏幕中的内容会被重定向到log.txt文件中。
int main(){
int fd=open("log.txt",O_RDWR|O_CREAT,0666);
if(fd<0){
perror("open");
return 1;
}
close(1);
dup2(fd,1);
printf("hello world\n");
fprintf(stdout,"hello world\n");
fputs("hello world\n",stdout);
return 0;
}
4.3缓冲区
- 缓冲区的本质就是一段内存
为什么有缓冲区?
- a.解放使用缓冲区的进程时间
- b.缓冲区的存在可以集中处理数据刷新,减少IO的次数,从而提高整体的效率
缓存策略
常规
- a.无缓冲
- b.行缓存---->显示器文件的缓存策略(stdout,stderr)
- c.全缓存---->缓冲区满了再刷新,普通文件的刷新策略
特殊:
- a.进程退出刷新
- b.用户强制刷新—fflush
/*比如下面这个例子*/
int main()
{
close(1);
int fd=open("log.txt",O_RDWR|O_CREAT,0666);
int cnt=0;
while(cnt<5){
printf("hello world\n");
cnt++;
}
close(fd);
return 0;
}
我们查看log.txt中的内容:
log.txt为空的原因:log.txt属于是磁盘文件,缓冲区的刷新策略是全缓存,此时打印的内容在缓冲区中并没有刷新出
int main()
{
close(1);
int fd=open("log.txt",O_RDWR|O_CREAT,0666);
int cnt=0;
while(cnt<5){
printf("hello world\n");
cnt++;
}
fflush(stdout); //刷新一下缓冲区
close(fd);
return 0;
}
缓冲区在哪?
从下面这段代码看起
int main(){
printf("printf:hello world");
const char* msg="write: hello world";
write(1,msg,strlen(msg));
sleep(3);
return 0;
}
先只打印【wirte:hello world,3秒钟后再打印【printf:hello world】。
原因
printf在打印的时候并没有使用\n进行缓存,所以内容在缓冲区中。而write立即刷新;而printf封装了write,说明了缓冲区一定不在write的内部,我们所说的缓冲区,不是内核级的缓冲区,而是语言级别的封装的缓冲区,这里printf的缓冲区就是C语言封装的缓冲区。
缓冲区在哪呢?
int main(){
printf("printf:hello world");
fprintf(stdout,"fprintf: hello world");
fputs("fputs:hello world",stdout);
const char* msg="write: hello world";
write(1,msg,strlen(msg));
sleep(3);
return 0;
}
我们可以看到,C语言的IO接口,都会有一个FILE*类型的参数,而C语言的缓冲区就封装在FILE结构体中。
一个奇怪的现象
int main(){
const char* str1="printf:hello\n";
const char* str2="fprintf:hello\n";
const char* str3="fputs:hello\n";
const char* str4="write:hello\n";
printf(str1);
fprintf(stdout,str2);
fputs(str3,stdout);
write(1,str4,strlen(str4));
fork();
}
//将结果重定向到log.txt中
./proc > log.txt
为什么?
刷新缓冲区的本质是什么?把缓冲区的内容write()到内核缓冲区中,然后清空缓冲区【注意:需要清空缓冲区】
而缓冲区是由FILE结构体内部维护的,属于是父进程的数据,在fork()以后,进程退出前,会刷新缓冲区,子进程发生了写时拷贝,因此C接口的内容被打印了两次。
4.4标准输出和标准错误的区别
从下面的代码可以看出,标准输出和标准错误的结果都是打印到显示器上,为什么文件描述符不同?
int main(){
umask(0);
int fd1=open("log1.txt",O_RDWR);
if(fd1<0){
perror("open error");
}
int fd2=open("log2.txt",O_RDWR|O_CREAT,0666);
if(fd2>0){
printf("open sucess\n");
}
return 0;
}
标准输出和标准错误对应的是同一个硬件设备(显示器)
./a,out > stdout.txt 2>stderr.txt可以将标准输出和标准错误重定向到两个文件中。
将标准错误和标准输出重定向到不同文件很重要,标准错误信息常常在工程中作为日志。
C语言有一个全局变量errno,用来记录最近一次函数调用失败的原因
可以配合strerror(errno)函数打印错误信息
#include<stdio.h>
#include<errno.h>
int main(){
umask(0);
int fd1=open("log1.txt",O_RDWR);
if(fd1<0){
perror("open error");
printf("errno:%s\n",strerror(errno)); //通过strerror(errno)获取对应的错误信息
}
int fd2=open("log2.txt",O_RDWR|O_CREAT,0666);
if(fd2>0){
printf("open sucess\n");
}
return 0;
}
实现my_perror
//errno会自动保存最后一次函数调用失败对应的信号码
void my_perror(const char* err){
fprintf(stderr,"%s:%s\n",err,strerror(errno));
}
int main(){
int fd=open("log.txt",O_RDONLY); //调用失败,对应的原因已经被存入到errno当中
if(fd<0){
my_perror("open err");
}
return 0;
}
5.封装自己的IO库
封装自己的C语言文件IO函数:my_open,my_fread,my_write
同时需要自定义一个MYFILE结构体,结构体内部需要封装文件描述符,缓冲区,缓冲区策略等
MYFILE结构体如下:
typedef struct _MyFILE{
int _fileno;
char _buffer[NUM];
int _end;
int _flags; //fflush method
}MyFILE;
主接口
MyFILE *my_fopen(const char *filename, const char *method)
{
assert(filename);
assert(method);
int flags = O_RDONLY;
if(strcmp(method, "r") == 0)
{}
else if(strcmp(method, "r+") == 0)
{}
else if(strcmp(method, "w") == 0)
{
flags = O_WRONLY | O_CREAT | O_TRUNC;
}
else if(strcmp(method, "w+") == 0)
{}
else if(strcmp(method, "a") == 0)
{
flags = O_WRONLY | O_CREAT | O_APPEND;
}
else if(strcmp(method, "a+") == 0)
{}
int fileno = open(filename, flags, 0666);
if(fileno < 0)
{
return NULL;
}
MyFILE *fp = (MyFILE *)malloc(sizeof(MyFILE));
if(fp == NULL) return fp;
memset(fp, 0, sizeof(MyFILE));
fp->_fileno = fileno;
fp->_flags |= LINE_FLUSH;
fp->_end = 0;
return fp;
}
void my_fflush(MyFILE *fp)
{
assert(fp);
if(fp->_end > 0)
{
write(fp->_fileno, fp->_buffer, fp->_end);
fp->_end = 0;
syncfs(fp->_fileno);
}
}
void my_fwrite(MyFILE *fp, const char *start, int len)
{
assert(fp);
assert(start);
assert(len > 0);
// abcde123
// 写入到缓冲区里面
strncpy(fp->_buffer+fp->_end, start, len); //将数据写入到缓冲区了
fp->_end += len;
if(fp->_flags & NONE_FLUSH)
{}
else if(fp->_flags & LINE_FLUSH)
{
if(fp->_end > 0 && fp->_buffer[fp->_end-1] == '\n')
{
//仅仅是写入到内核中
write(fp->_fileno, fp->_buffer, fp->_end);
fp->_end = 0;
syncfs(fp->_fileno);
}
}
else if(fp->_flags & FULL_FLUSH)
{
}
}
void my_fclose(MyFILE *fp)
{
my_fflush(fp);
close(fp->_fileno);
free(fp);
}
测试接口
int main()
{
MyFILE *fp = my_fopen("log.txt", "w");
if(fp == NULL)
{
printf("my_fopen error\n");
return 1;
}
const char *s = "hello my 111\n";
my_fwrite(fp, s, strlen(s));
printf("消息立即刷新");
sleep(3);
const char *ss = "hello my 222";
my_fwrite(fp, ss, strlen(ss));
printf("写入了一个不满足刷新条件的字符串\n");
sleep(3);
const char *sss = "hello my 333";
my_fwrite(fp, sss, strlen(sss));
printf("写入了一个不满足刷新条件的字符串\n");
sleep(3);
const char *ssss = "end\n";
my_fwrite(fp, ssss, strlen(ssss));
printf("写入了一个满足刷新条件的字符串\n");
sleep(3);
my_fclose(fp);
}
bash命令
while :; do cat log.txt; echo "#####"; sleep 1; done
6.向minshell中添加重定向功能
1.重定向一共有三种情况,分别是<, >, >>,用g_redir_flag来记录重定向的类型。
2.g_redir_filename用来记录重定向的文件名。
代码如下:
#include <stdio.h>
#include <string.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <assert.h>
#include <ctype.h>
#define NODE_REDIR -1 //没有重定向
#define INPUT_REDIR 0 //输入重定向
#define OUTPUT_REDIR 1 //输出重定向
#define APPEND_REDIR 2 //追加重定向
#define DROP_SPACE(start) do{ while(isspace(*start)) start++;}while(0)
#define SEP " "
#define NUM 1024
#define SIZE 128
char sin[NUM]; //存放获取的指令
char* sin_arg[SIZE]; //存放拆分后的指令
int g_redir_flag=NODE_REDIR; //指令是输入重定向还输出重定向还是追加重定向
char* g_redir_filename=NULL; //文件名
void checkdir(char*command_lin){
assert(command_lin);
char* start=command_lin;
char* end=command_lin+strlen(command_lin);
while(start<end){
if(*start=='>'){
if(*(start+1)=='>'){
*start='\0';
start+=2;
g_redir_flag=APPEND_REDIR;
DROP_SPACE(start);
g_redir_filename=start;
break;
}
else{
*start='\0';
start++;
DROP_SPACE(start);
g_redir_flag=OUTPUT_REDIR;
g_redir_filename=start;
break;
}
}
else if(*start=='<'){
*start='\0';
start++;
DROP_SPACE(start);
g_redir_flag=INPUT_REDIR;
g_redir_filename=start;
break;
}
else{
start++;
}
}
}
int main()
{
while(1)
{
//初始化
g_redir_flag=NODE_REDIR;
g_redir_filename=NULL;
printf("[west@shadow myshell]$ ");
fflush(stdout);
memset(sin,'\0',sizeof(sin));
//阻塞等待用户输入
//读取用户输入,由于用户输入存在空格,所以不能使用scanf函数
//fgets遇到换行读取结束
fgets(sin,1024,stdin);
sin[strlen(sin)-1]='\0';
//查看是否有重定向
checkdir(sin);
//将得到的参数,按照空格为分割,可以使用strtok函数
sin_arg[0]=strtok(sin,SEP);
int i=1;
if(strcmp(sin_arg[0],"ls")==0){
sin_arg[i++]=(char*)"--color=auto";
}
//截取失败,返回NULL
while(sin_arg[i++]==strtok(NULL,SEP)){;}
//如果是内建命令,就这父进程执行
if(strcmp(sin_arg[0],"cd")==0&&sin_arg[1]!=NULL){
changdir(sin_arg[1]);
continue;
}
pid_t pid=fork();
if(pid==0)
{
int fd=-1;
switch(g_redir_flag){
case NODE_REDIR:
break;
case INPUT_REDIR:
fd=open(g_redir_filename,O_RDONLY);
dup2(fd,0);
break;
case OUTPUT_REDIR:
fd=open(g_redir_filename,O_WRONLY|O_CREAT|O_TRUNC,0666);
dup2(fd,1);
break;
case APPEND_REDIR:
fd=open(g_redir_filename,O_WRONLY|O_CREAT|O_APPEND,0666);
dup2(fd,1);
break;
default:
printf("bug happend");
}
//这里sin_arg[0]保存的就是想要执行的指令名称
execvp(sin_arg[0],sin_arg);
exit(1);
}
int status=0;
pid_t ret=waitpid(pid,&status,0);
if(ret<0)
{
printf("进程等待失败,sig=%d,code=%d\n",status&0x7f,(status>>8)&0xff);
}
else{
printf("等待进程成功:sig=%d,code=%d\n",status&0x7f,(status>>8)&0xff);
}
}
return 0;
}
7.文件系统
上面我们探讨了内存文件的管理方式;除了加载到内存中文件,大量的文化存放在磁盘中,操作系统需要对这些磁盘文件进行管理
7.1理解文件系统
磁盘文件都存放在磁盘上。下面是磁盘的基本结构。
磁盘上存储的基本单位是扇区,一个扇区的大小是512字节。每个扇区根据自己所在的盘面,磁道和所处磁道的位置,具有一个唯一的编号CHS地址;读写磁盘的时候,磁头通过CHS地址,找到某个盘面的某一个磁道,在该磁盘上确定某一个扇区。
磁盘的逻辑抽象结构
像磁带一样,想象将磁盘所有盘面拉直,成为一个线性结构。
操作系统,将对磁盘扇区的管理,转化为了对数组空间的管理;要定为一般扇区,只需要找到相应数组下标即可。【这个下标叫做LBA,逻辑地地址】
要想访问对应的扇区,操作系统通过下面的操作进行:
逻辑地地址转化为CHS地址,类型与一种哈希算法
我们知道,操作系统IO的基本单位为页,一页的大小是4096字节。
通过上面的方式,操作系统访问磁盘可以:
- 提高IO效率
- 不让软件(os)和硬件(磁盘)具有强相关性,也就是解耦合
7.2OS如何对磁盘进行管理
分区和分组
这么大的磁盘(几百上千个G),OS如何进行管理?
为了管理这些磁盘空间,操作系统进行了人为的分区
电脑上大多数都只有一个硬盘,而电脑上的C/D/E/F盘,就是磁盘分区的结果。
为了方便管理这些分区,操作系统又对每个分区进行了分组。最后操作系统对磁盘的管理转化为了对若干个相同的文件组进行管理,只需要明确一个文件组的管理方式,就能了解整个操作系统对磁盘的管理方式
7.3分组管理
文件=文件内容+文件属性
磁盘文件由两部分构成,分别是文件内容和文件属性。文件内容就是文件当中存储的数据,文件属性是文件的基本信息,如文件名、文件大小以及创建时间等。
文件内容存放在一个一个Block中,每个Block(块)的大小为4KB,文件属性存放在inode中,每个inode的大小为128字节
上面说过,操作系统对磁盘的管理,最后转化为了对若干个相同文件组的管理:
Linux ext2文件系统,上图为磁盘文件系统图。Boot Block为启动块。
一般来说计算机启动的时候都需要运行一个初始化程序,这个初始化程序做的事情就是初始化系统的各个方面,从CPU寄存器到设备控制器和内存,然后启动操作系统。所以初始化程序需要找到磁盘上的操作系统内核,装入内存,并且需要转到起始地址,从而开始操作系统的执行。而这个初始化程序,就存放在Boot Block中。
- Block Group:ext2文件系统会根据分区的大小划分为数个Block Group。而每个Block Group都有着相
同的结构组成。政府管理各区的例子
每个Block Group的内容
- Data Blocks:以块为单位(一个块的大小是4KB),进行对文件内容的保存
- inode Table:以128字节为单位,对inode进行保存。每个inode属性中都有一个inode编号,标识一个磁盘文件。
- Block Bitmap:块位图,记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用
- inode Bitmap:inode位图,每个bit表示一个inode是否空闲可用 。
- Group Descriptor Table(GDT):块组描述符,描述块组属性信息 。比如该组有多少inode?起始的inode编号是多少?整个组的大小是多少?用于管理具体的每个组的信息
- Super Block(SB):文件系统的顶层设计。用于管理整个分区。记录的信息主要有:bolck 和 inode的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。 用于管理整个分区
根据Super Block的作用:管理整个分区。每个分区应该只需要保存一份Super Block即可,为什么分区的每个组都保留了一份Super Block?不会造成磁盘空间的浪费吗?
解释:分区的每个组都保存保留一份Super BLock,主要的作用是备份,Super Block的作用过于重要,防止数据丢失而导致无法恢复文件系统,所以每个组都保留了当前分区的Super Block。
7.4inode与Data Block关联
从上面的文件系统可以得到,一个文件的文件属性inode与文件内容是分开存储的;inode与Data Block如何关联起来?
blocks[15]数组中,有12个块直接保存的是block编号,那么当文件的内容超过了4*12=48KB怎么办?
block[12:15]分别指向一个Data Blcok,但是这些Data Blcok不像前12个Data Block保存的是文件内容,而保存的是Data Blcok编号,用来指向文件剩下的内容块。(和go语言的map底层结构一样)
7.5文件的查找和删除
需要注意的是:inode里面没有文件名属性,也标识linux底层,实际上是通过inode编号识别文件
ll -i #可以查看文件的inode编号
既然inode中没有文件名属性,linux系统如何通过文件名找到inode编号,从而识别文件?
linux下一切皆文件,那么目录也是文件,我们可以查看一个目录文件的内容是什么:
目录也是一个文件:文件=内容+属性
目录的Data Block中的内容:该目录下 [ 文件名 ]与[ 文件inode编号 ]的映射关系。-------【所以在linux同一个目录下,不可以创建同名的文件】
创建一个文件,OS做了什么?
- 通过inode Bitmap,查找一个没有被使用的inode
- 将文件属性(如:文件创建时间,用户等)写入inode中
- 通过Block Bitmap,为文件分配存放文件内容的Data Block
- 根据所处目录的inode编号,找到目录的Data Block
- 向目录的Data Block中写入目标文件和inode编号的映射关系
删除一个文件,OS做了什么?
- 文件名------>找到inode编号
- 将inode编号对应的inode Bitmap的比特位置为0,表示该inode可用
- 删除目录Data Block中[ 文件名 ]与[ inode编号 ]的映射关系
OS是否真正的消除数据?
答案是没有。删除文件,仅仅只是将对应的inode标记为设置为可用;写入新数据只是对原数据进行覆盖。
想要恢复文件,只需要找到对应文件的inode编号,再将inode编号和文件名的映射关系恢复就可。
知道自己所处的目录名,如何确定目录的inode编号?
Linux下一切皆文件,目录是上一级目录的文件,所以目录的inode编号存放在上一级目录的Data Block中。
通过目录名,查找当前目录的inode编号:
先找到根目录,再从根目录依次向下找到所处目录的inode编号。
7.6struct inode与struct file的关系
(1)struct inode结构体和struct file结构体 都是用来描述文件信息的,struct inode结构体是描述静态的文件(磁盘文件),struct file结构体描述动态的文件(也就是打开的文件,被加载到内存中);
(2)每个文件只有唯一的struct inode结构体,但是可以有多个struct file结构体,文件每被打开一次就多一个 struct file结构体 ;
7.7软硬链接
stat 文件名 #查看文件属性
创建硬链接
ln 目标文件名 硬链接名
创建软连接
ln -s 目标文件名 软连接名
软硬链接的区别
- 硬链接:硬链接与目标文件共享inode和inode编号。
- 软连接:软连接是一个独立文件,有自己独立的inode和inode编号。【文件内容存放的是原目标文件的路径,方便快速跳转到原目标文件】
- 软连接相当于快捷方式
- 创建硬链接,在目录下,给目标文件增加了目标文件名和inode编号的映射关系。
观察下面的新创建的目录和文件的属性:
为什么新目录的硬链接为2,而新普通文件硬链接为1?
在newdir目录下有一个隐藏文件 . 它指向的是当前目录,上例中指向的是newdir。所以新创建的目录的硬链接是2。
…指向的上级目录;在当前目录下创建一个新的目录,当前目录的硬链接数+1
如今计算一个目录下的目录数?
目录数=硬链接数-2(本身和.文件)