【Linux】基础IO


一、C语言文件IO

1、C文件操作一览

在这里插入图片描述

2、对文件写入操作

#include<stdio.h>
#include<string.h>
int main()
{
   
    FILE* fp = fopen("myfile.txt", "w");
    if(!fp)
    {
   
        perror("fopen failed\n");
        exit(-1);
    }
    const char* msg = "hello linux\n";
    int count = 5;
    while(count--)
    {
   
        fwrite(msg, strlen(msg), 1, fp);
    }
    fclose(fp);
    return 0;
}

在这里插入图片描述

3、对文件读出操作

#include<stdio.h>
#define MAXSIZE 64
int main()
{
   
    FILE* fp = fopen("myfile.txt", "r");
    if(!fp)
    {
   
        perror("fopen filed\n");
        exit(-1);
    }
    char buffer[MAXSIZE];
    for(int i = 0; i < 5; i++)
    {
   
        fgets(buffer, sizeof(buffer), fp);
        printf("%s", buffer);
    }
    return 0;
}

在这里插入图片描述

4、什么是当前路径

先说结论:当前路径不是指可执行程序所处的路径,而是指该可执行程序运行成为进程时我们所处的路径。换而言之,是先有进程(进程是可执行程序跑起来形成的!!!)才来的路径。

在这里插入图片描述

此时我们在进行删除这个.txt文件后再在上一个路径进行跑可执行程序的时候,我们看一下我们的.txt文件在那边进行创建。

在这里插入图片描述
按理来讲应该在class2中有.txt文件的,而现象中是在当前路径,也就是~这个目录下出现了.txt文件,那么我们就能简单理清楚当前路径的概念,同样,我们用进程PID看一下吧:
在这里插入图片描述

在这里插入图片描述

5、默认打开的三个流

linux下一切皆文件,也就是说linux下所有的东西都是被看做文件的,所以我们的显示器和和键盘当然也可以看做文件,我们能看到显示器上的数据,是因为我们向“显示器文件”写入了数据,电脑能获取到我们敲击键盘时对应的字符,是因为电脑从“键盘文件”读取了数据。

那么就有一个问题了,我们在打开我们系统中存储的东西的时候,是需要打开文件的,而我们向“显示器文件”写入数据以及从“键盘文件”读取数据前,不需要进行打开“显示器文件”和“键盘文件”的相应操作吗?
答案很简单,那就是打开文件一定是进程运行的时候打开的,而任何进程在运行的时候都会默认打开三个输入输出流,即标准输入流、标准输出流以及标准错误流,对应到C语言当中就是stdin、stdout以及stderr。其中,标准输入流对应的设备就是键盘,标准输出流和标准错误流对应的设备都是显示器。

extern FILE *stdin;
extern FILE *stdout;
extern FILE *stderr;

当我们的C程序运行起来的时候,操作系统就会默认使用C语言的相关接口将这三个输入输出流打开,之后我们才能调用类似于scanf和printf之类的函数向键盘和显示器进行相应的输入输出操作。

我们用fputs函数来进行说明一下:
当我们fputs参数第二个参数设置成为stdout的时候,我们运行程序建立进程的时候,fputs函数会直接将数据打印到显示器文件上的,此时我们使用fputs函数向显示器文件上写数据的时候,则直接就能在显示器上进行打印出来了。
在这里插入图片描述

当然,不止是C语言当中有标准输入流、标准输出流和标准错误流,C++当中也有对应的cin、cout和cerr,其他所有语言当中都有类似的概念。实际上这种特性并不是某种语言所特有的,而是由操作系统所支持的。

二、系统文件I/O

1、C库文件接口

操作文件除了C语言接口、C++接口或是其他语言的接口外,操作系统也有一套系统接口来进行文件的访问。相比于C库函数或其他语言的库函数而言,系统调用接口更贴近底层,实际上这些语言的库函数都是对系统接口进行了封装。

在这里插入图片描述

C库文件接口很厉害,我们文件底层的东西是通过系统接口出去的,而系统接口上层还有个C库文件接口,这个接口是进行封装的接口,其跨平台性非常好,当我们在linux下跑程序的时候,我们的C库文件接口是给Linux系统接口进行封装的,而我们在windows下进行跑程序的时候,我们的C库文件接口是给windows系统封装的,同理在mac中进行跑程序的时候,同样也是对mac进行封装的,这样跨平台性就非常好了。而我们的系统接口在最底层,更加接近kernel,所以,我们的系统接口更贴地气,我们看一下系统接口。

2、open

系统调用中使用open函数打开文件:
我们的open函数原型是:

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);

(1)第一个参数

pathname: 要打开或创建的目标文件

若pathname以路径的方式给出,则当需要创建该文件时,就在pathname路径下进行创建。
若pathname以文件名的方式给出,则当需要创建该文件时,默认在当前路径下进行创建。

(2)第二个参数

flags: 表示文件打开的方式。打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags。

在这里插入图片描述
打开文件时,可以传入多个参数选项,当有多个选项传入时,将这些选项用“|”运算符隔开。
如下所示:

O_WRNOLY | O_CREAT

系统接口中open的第二个参数flags是整型,那么就是有32个比特位,若将一个比特位作为一个标志位,则理论上flags可以传递32种不同的标志位。实际上flags是用下面的宏定义来进行定义的,我们如下四个常用的参数选项。

#define O_RDONLY         00
#define O_WRONLY         01
#define O_RDWR           02
#define O_CREAT        0100

这些宏定义选项的共同点就是,它们的二进制序列当中有且只有一个比特位是1(O_RDONLY选项的二进制序列为全0,表示O_RDONLY选项为默认选项),且为1的比特位是各不相同的,这样一来,在open函数内部就可以通过使用“与”运算来判断是否设置了某一选项。

在这里插入图片描述

(3)第三个参数

mode参数表示创建文件的默认权限

我们在前面说过,这个权限我们可以进行更改的,怎么更改呢,用chmod来更改,我们也都知道,默认的mod是0666,但是,有一个umask,默认是0002,那么我们设置0666的时候,我们实际得到的是0664,所以我们需要进行设置umask(0)。

(4)返回值

open函数的返回值是新打开文件的文件描述符。

文件描述符是个啥?我们可以打开多个文件来进行看一下:

#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
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);
    printf("fd1:%d\n", fd1);
    printf("fd2:%d\n", fd2);
    printf("fd3:%d\n", fd3);
    printf("fd4:%d\n", fd4);
    return 0;
}

在这里插入图片描述
我们看上面的四个文件描述符,怎么是从3开始的?前面的012呢,其实012是留给stdin,stdout,stderr的!我们后期讲。

我们在尝试打开一个并不存在的文件:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
   
    int fd = open("test.txt", O_RDONLY);
    printf("%d\n", fd);
    return 0;
}

在这里插入图片描述

我们则可以进行总结了:返回值不成功的时候返回-1,成功则返回从3开始往后递增的文件描述符,而0,1,2这三个文件描述符是留给stdin,stdout,stderr的。

3、close

int close(int fd);

4、write

使用write函数向文件写入信息,原型如下:

ssize_t write(int fd, const void *buf, size_t count);

我们可以使用write函数,将buf位置开始向后count字节的数据写入文件描述符为fd的文件当中。

如果数据写入成功,实际写入数据的字节个数被返回。
如果数据写入失败,-1被返回。

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
   
    int fd = open("test.txt", O_WRONLY | O_CREAT, 0666);
    if(fd < 0)
    {
   
        perror("open failed\n");
        exit(-1);
    }
    // 打开成功
    const char* msg = "hello linux\n";
    int count = 5;
    while(count--)
    {
   
        write(fd, msg, strlen(msg));
    }
    close(fd);
    return 0;
}

在这里插入图片描述

5、read

我们用这个read与前面的write做配合,write先将数据写进test.txt文件中,再利用read的读取功能来进行读取和打印到显示器文件中直到在显示器中显示出来。

函数原型为:

ssize_t read(int fd, void *buf, size_t count);

我们可以使用read函数,从文件描述符为fd的文件读取count字节的数据到buf位置当中。

如果数据读取成功,实际读取数据的字节个数被返回。
如果数据读取失败,-1被返回。

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
   
    int fd = open("test.txt", O_RDONLY);
    if(fd < 0)
    {
   
        perror("open failed\n");
        return 1;
    }
    // 打开成功了
    char ch;
    while(1)
    {
   
        // 读操作
        ssize_t s = read(fd, &ch, 1);
        if(s <= 0)
            break;
        // 写到显示器文件中进行打印
        write(1, &ch, 1);
    }
    close(fd);
    return 0;
}

在这里插入图片描述

6、文件描述符fd

我们都知道,文件是进程运行的时候打开的,一个进程可以打开多个文件,而系统中随时存在着多个进程,那么必然会存在很多个文件。这么多文件如果不加以管理必然会造成系统的混乱,所以操作系统就有自己的一套管理模式,那就是“先描述,后组织”的操作方法,它会给每个文件创造task_struct结构体,并且以双链表的形式进行组织起来之后操作系统对这个文件的管理也就变成了对双链表的增删改查了,而为了区分已经打开的文件哪些属于特定的某一个进程,我们就还需要建立进程和文件之间的对应关系。

进程和文件之间的对应关系是如何建立的?

我们知道,当一个程序运行起来时,操作系统会将该程序的代码和数据加载到内存,然后为其创建对应的task_struct、mm_struct、页表等相关的数据结构,并通过页表建立虚拟内存和物理内存之间的映射关系。如下图:

在这里插入图片描述

我们有了上面的结构印象,那么我们同样就也有了另一种结构,那么就是task_struct这个玩意还有一个指针指向files_struct这个结构体,这个files_struct结构体中同样有一个名为fd_array的指针数组,该数组的下标就是我们所谓的文件描述符。
而当我们打开test.txt的时候,是从磁盘加载到物理内存的文件内存中的,形成对应的struct file,将该struct file连入文件双链表,并将该结构体的首地址填入到fd_array数组当中下标为3的位置,使得fd_array数组中下标为3的指针指向该struct file,最后返回该文件的文件描述符给调用进程即可。

在这里插入图片描述

所以我们只要有某一文件的文件描述符,就可以找到与该文件相关的文件信息,进而对文件进行一系列输入输出操作。
注意: 向文件写入数据时,是先将数据写入到对应文件的缓冲区当中,然后定期将缓冲区数据刷新到磁盘当中。

什么叫做进程创建的时候会默认打开0、1、2?

0就是标准输入流,对应键盘;1就是标准输出流,对应显示器;2就是标准错误流,也是对应显示器。键盘和显示器都是硬件,所以操作系统能够识别到当某一进程创建时,操作系统就会根据键盘、显示器、显示器形成各自的struct file,将这3个struct file连入文件双链表当中,并将这3个struct file的地址分别填入fd_array数组下标为0、1、2的位置,至此就默认打开了标准输入流、标准输出流和标准错误流。

磁盘文件vs内存文件

当文件存储在磁盘中的时候,就是磁盘文件,而当磁盘文件加载到物理内存中,就变成了内存文件。我们的磁盘文件和内存文件与程序和进程的关系是一样的,当程序运行起来了后便形成了进程;磁盘文件加载到物理内存中了后便形成了内存文件。

磁盘文件又由两个性质组成,一个是文件内容,另一个是文件属性。文件内容就是文件当中存储的数据,文件属性就是文件的一些基本信息,例如文件名、文件大小以及文件创建时间等信息都是文件属性。文件加载到内存时,一般先加载文件的属性信息,当需要对文件内容进行读取、输入或输出等操作时,再加载文件数据。
在这里插入图片描述

7、重定向

(1)输出重定向

看着很高级,其实就是将输出的文件变成我们自定义的文件,操作系统中本来让输出放到文件描述符为1这个文件中,我们所想要做的动作是关闭这个文件描述符为1的文件,我们想将这些输出到显示器文件的信息放到我们自定义的文件当中。

原理:
在这里插入图片描述
也就是,关闭显示器文件!将该文件位置用自定义文件替代!

我们写一下代码:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
   
    umask(0);
    close(1);
    int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);
    if(fd < 0)
    {
   
        perror("open failed\n");
        return 1;
    }
    // 成功替换
    printf("hello linux1\n");
    printf("hello linux2\n");
    printf("hello linux3\n");
    printf("hello linux4\n");
    printf("hello linux5\n");
    fflush(stdout);
    close(fd);
    return 0;
}

在这里插入图片描述

printf函数是默认向stdout输出数据的,而stdout指向的是一个struct FILE类型的结构体,该结构体当中有一个存储文件描述符的变量,而stdout指向的FILE结构体中存储的文件描述符就是1,因此printf实际上就是向文件描述符为1的文件输出数据。C语言的数据并不是立马写到了内存操作系统里面,而是写到了C语言的缓冲区当中,所以使用printf打印完后需要使用fflush将C语言缓冲区当中的数据刷新到文件中。

(2)追加重定向

这个理解起来就很简单了,前面的输出重定向是全部覆盖,而追加则是往后追加

在这里插入图片描述

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
   
	close(1);
	int fd = open("log.txt", O_WRONLY|O_APPEND|O_CREAT, 0666);
	if(fd < 0){
   
		perror("open");
		return 1;
	}
	printf("hello Linux\n");
	printf("hello Linux\n");
	printf("hello Linux\n");
	printf("hello Linux\n");
	printf("hello Linux\n");
	fflush(stdout);
	close(fd);
	return 0;
}

在这里插入图片描述

(3)输入重定向

输入重定向就是,将我们本应该从一个文件读取数据,现在重定向为从另一个文件读取数据。

在这里插入图片描述

如果我们想让本应该从“键盘文件”读取数据的scanf函数,改为从log.txt文件当中读取数据,那么我们可以在打开log.txt文件之前将文件描述符为0的文件关闭,也就是将“键盘文件”关闭,这样一来,当我们后续打开log.txt文件时所分配到的文件描述符就是0。

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define MAX_SIZE 64
int main()
{
   
    umask(0);
    close(0);
    int fd = open("log.txt", O_RDONLY | O_CREAT, 0666);
    if(fd < 0)
    {
   
        perror("open failed\n");
        return 1;
    }
    // 成功
    char buff[MAX_SIZE];
    while(scanf("%s", buff) != EOF)
    {
   
        printf("%s\n", buff);
    }
    close(fd);
    return 0;
}

在这里插入图片描述

scanf函数是默认从stdin读取数据的,而stdin指向的FILE结构体中存储的文件描述符是0,因此scanf实际上就是向文件描述符为0的文件读取数据。

(4)标准输出流和标准错误流对应的都是显示器,它们有什么区别?

区别在于重定向的不同,我们的重定向的是文件描述符是1的标准输出流,而并不会对文件描述符是2的标准错误流进行重定向。

我们利用代码来进行看一下:

#include <stdio.h>
int main()
{
   
	printf("hello printf\n"); //stdout
	perror("perror"); //stderr

	fprintf(stdout, "stdout:hello fprintf\n"); //stdout
	fprintf(stderr, "stderr:hello fprintf\n"); //stderr
	return 0;
}

在这里插入图片描述

8、dup2

完成重定向我们在上面搞那么复杂干嘛,还要进行替换,那么先辈们肯定有封装的一些新的思路来进行重定向,没错,那就是dup2,其思路也很简单,也就是将其复制,那么其文件描述符的指向也发生了改变,那么不就很简单了,我们看图认识一下吧!

在这里插入图片描述

要完成重定向我们只需进行fd_array数组当中元素的拷贝即可。例如,我们若是将fd_array[3]当中的内容拷贝到fd_array[1]当中,因为C语言当中的stdout就是向文件描述符为1文件输出数据,那么此时我们就将输出重定向到了文件log.txt。

我们看一下dup2的原型:

int dup2(int oldfd, int newfd);

功能描述:dup2会将fd array[oldfd]拷贝到fd array[newfd]这个文件描述符中,必要时候将newfd给关闭掉

返回值:dup2如果调用成功,返回newfd,否则返回-1。

1、如果oldfd不是有效文件描述符,那么则dup2调用失败,并且此时文件描述符为newfd的文件没有被关闭。
2、如果oldfd是有效文件描述符,但是newfd和oldfd具有相同的值,则dup2不做任何操作,并返回newfd。

我们将打开文件log.txt时获取到的文件描述符和1传入dup2函数,那么dup2将会把fd_arrya[fd]的内容拷贝到fd_array[1]中,在代码中我们向stdout输出数据,而stdout是向文件描述符为1的文件输出数据,因此,本应该输出到显示器的数据就会重定向输出到log.txt文件当中。

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
   
    umask(0);
    int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);
    if(fd < 0)
    {
   
        perror("open failed\n");
        return 1;
    }
    close(1); // 关闭输入标准流的文件
    dup2(fd, 1); // 将fd的文件描述符中的文件内容拷贝到1中
    // 用printf和fprintf来进行打印到显示器文件,此时是到log.txt文件中
    printf("hello linux\n");
    printf("hello linux\n");
    printf("hello linux\n");
    fprintf(stdout, "hello world\n");
    fprintf(stdout, "hello world\n");
    fprintf(stdout, "hello world\n");
    return 0;
}

在这里插入图片描述
原本应该写在显示器文件中的写在了log.txt文件中了。

9、添加重定向到myshell中

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <ctype.h>
#include <fcntl.h>
#define LEFT "["        // 左括号
#define RIGHT "]"       // 右括号
#define LABLE "#"       // 提示符
#define DELIM " \t"     // 空格符
#define LINE_SIZE 1024  // 命令最长长度
#define ARGC_SIZE 32    // 分割
#define EXIT_CODE 44    // 退出码
#define NONE       -1
#define IN_RDIR     0
#define OUT_RDIR    1
#define APPEND_RDIR 2

// 最后代码
int lastcode = 0;
// 退出
int quit = 0;
// 环境变量
extern char **environ;
// 分割
char commandline[LINE_SIZE];
// 变量
char *argv[ARGC_SIZE];
// 路径
char pwd[LINE_SIZE];
// 自定义的环境变量
char myenv[LINE_SIZE];
// 重定向
char *rdirfilename = NULL;
int rdir = NONE;

// 调用环境变量函数调用username和hostname
const char* getusername()
{
   
    return getenv("USER");      // 环境变量中本来就有
}
const char* gethostname()
{
   
    return getenv("HOSTNAME");  // 环境变量中本来就有
}
void getpwd()
{
   
    getcwd(pwd, sizeof(pwd));   // 传上面的全局变量
}

// 重定向
void check_redir(char* cmd)
{
   
    char* pos = cmd;
    while(*pos)
    {
   
        if(*pos == '>')
        {
   
            // 追加
            if(*(pos + 1) == '>')
            {
   
                *pos++ = '\0';
                *pos++ = '\0';
                while(isspace(*pos))
                {
   
                    pos++;
                }
                rdirfilename = pos;
                rdir = APPEND_RDIR;
                break;
            }
            // 写出
            else
            {
   
                *pos = '\0';
                pos++;
                while(isspace(*pos)) pos++;
                rdirfilename = pos;
                rdir=OUT_RDIR;
                break;
            }
        }
        else if(*pos == '<') // 输入重定向
        {
   
            *pos = '\0';
            pos++;
            while(isspace(*pos)) pos++;
            rdirfilename = pos;
            rdir=IN_RDIR;
            break;
        }
        else // do nothing}
        {
   
        pos++;
        }
    }
}

// 获取命令行函数
void GetCommandLine(char* cline, int sz)
{
   
    // 调用路径
    getpwd();
    // 打印输出
    printf(LEFT"%s@%s %s"RIGHT""LABLE" ", getusername(), gethostname(), pwd); // 打印出输出的格式
    // 用键盘进行输入指令了
    char* s = fgets(cline, sz, stdin); // 键盘输入
    assert(s);
    (void)s; // 将s这个字符串强转成void以及断言一下,不能让s啥也没输入
    // 将最后一个字符的下一个位置填入\0
    cline[strlen(cline) - 1] = '\0'; // 防止越界
    // 检查一下是否是输入/输出/追加重定向
    check_redir(cline);
}

// 分割字符串
int splitstring(char* cline, char** _argv)
{
   
    int i = 0;
    argv[i++] = strtok(cline, DELIM);
    while(_argv[i++] = strtok(NULL, DELIM)); // 故意写的=
    return i - 1;
}

// 指令的判断,即创建进程判断是内建命令还是普通命令
int AdjustCommand(char* _argc, char** _argv)
{
   
    // 我们写一下下面三个命令的指令
    if(_argc == 2 && strcmp(_argv[0], "cd") == 0)          // 进入目录
    {
   
        chdir(_argv[1]);      // 改变目录地址
        getpwd();             // 获取目录
        sprintf(getenv("PWD"), "%s", pwd); // 从环境变量中获取地址并打印出来
        return 1;
    }
    else if(_argc == 2 && strcmp(_argv[0], "export") == 0) // 设置环境变量
    {
   
        strcpy(myenv, _argv[1]); // 将输入的环境变量复制到系统的环境变量中
        putenv(myenv);
        return 1;
    }
    else if(_argc == 2 && strcmp(_argv[0], "echo") == 0)   // 打印
    {
   
        if(strcmp(_argv[1], "$?") == 0) // 有可能使用?来看一下有些啥
        {
   
            printf("%d\n", lastcode);
            lastcode = 0;
        }
        else if(*_argv[1] == '$') // 打印不同的系统指令
        {
   
            char *val = getenv(_argv[1] + 1);
            if(val) 
                printf("%s\n", val);
        }
        else
        {
   
            printf("%s\n", _argv[1]);
        }
        return 1;
    }
    return 0;
}

// 普通命令
void NormalExcute(char** _argv)
{
   
    pid_t id = fork();
    if(id < 0)
    {
   
        perror("fork error");
        return;
    }
    else if(id == 0)
    {
   
        int fd = 0;
        if(rdir == OUT_RDIR)
        {
   
            fd = open(rdirfilename, O_CREAT|O_WRONLY|O_TRUNC, 0666);
            dup2(fd, 1);
        }
        else if(rdir == IN_RDIR)
        {
   
            fd = open(rdirfilename, O_RDONLY);
            dup2(fd, 0);
        }
        else if(rdir == APPEND_RDIR)
        {
   
            fd = open(rdirfilename, O_CREAT|O_WRONLY|O_APPEND, 0666);
            dup2(fd, 1);
        }
        //让子进程执行命令
        execvpe(_argv[0], _argv, environ);
        exit(EXIT_CODE);
    }
    else
    {
   
        // 父进程等待
        int status = 0;
        pid_t rid = waitpid(id, &status, 0);
        if(rid == id) 
        {
   
            lastcode = WEXITSTATUS(status);
        }
    }
}

int main()
{
   
    while(!quit)
    {
   
        // 0.准备工作,给重定向做准备
        rdirfilename = NULL;
        rdir = NONE;
        // 1. 交互问题,获取命令行 传参数为命令行+命令行的大小
        GetCommandLine(commandline, sizeof(commandline));

        // 2. 子串分割的问题,解析命令行
        int argc = splitstring(commandline, argv); // 进行分割,分割成功了则返回非零数,解析不成功则返回0
        if(argc == 0) 
            continue;

        // 3. 指令的判断 
        int n = AdjustCommand(argc, argv); //内键命令,本质就是一个shell内部的一个函数

        // ls -a -l | wc -l
        // 3.0 分析输入的命令行字符串,获取有多少个|, 命令打散多个子命令字符串
        // 3.1 malloc申请空间,pipe先申请多个管道
        // 3.2 循环创建多个子进程,每一个子进程的重定向情况。最开始. 输出重定向, 1->指定的一个管道的写端 
        // 中间:输入输出重定向, 0标准输入重定向到上一个管道的读端 1标准输出重定向到下一个管道的写端
        // 最后一个:输入重定向,将标准输入重定向到最后一个管道的读端
        // 3.3 分别让不同的子进程执行不同的命令--- exec* --- exec*不会影响该进程曾经打开的文件,不会影响预先设置好的管道重定向
        // 4. 普通命令的执行
        if(!n)
            NormalExcute(argv);
    }

    return 0;
}

在这里插入图片描述

三、FILE

因为库函数是对系统调用接口的封装,本质上访问文件都是通过文件描述符fd进行访问的,所以C库当中的FILE结构体内部必定封装了文件描述符fd。

我们在/usr/include/stdio.h头文件中可以看到下面这句代码,也就是说FILE实际上就是struct _IO_FILE结构体的一个别名。
在这里插入图片描述
也就是说:

typedef struct_IO_FILE FILE

我们在/usr/include/libio.h头文件中可以找到struct _IO_FILE结构体的定义,在该结构体的众多成员当中,我们可以看到一个名为_fileno的成员,这个成员实际上就是封装的文件描述符。
在这里插入图片描述

1、C语言中的fopen函数在做什么

我们既然知道了有文件描述符和FILE这个结构体变量,那么我们就很好理解了,我们的C语言fopen在上层为用户申请FILE结构体变量,并返回该结构体的地址(FILE*),在底层通过系统接口open打开对应的文件,得到文件描述符fd,并把fd填充到FILE结构体当中的_fileno变量中,至此便完成了文件的打开操作。
当然了,C语言其他的fwrite、fread、fputs、fgets等都是根据我们传入传入的文件指针找到对应的FILE结构体,然后在FILE结构体当中找到文件描述符,最后通过文件描述符对文件进行的一系列操作。

2、FILE中的缓冲区

看下面这段代码,代码当中分别用了两个C库函数和一个系统接口向显示器输出内容,在代码最后还调用了fork函数。

#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main()
{
   
    const char* msg1 = "hello printf\n";
    const char* msg2 = "hello fwrite\n";
    const char* msg3 = "hello write\n";
    // C语言
    printf("%s", msg1);
    fwrite(msg2, strlen(msg2), 1, stdout);
    // 系统调用
    write(1, msg3, strlen(msg3));
    fork();
    return 0;
}

在这里插入图片描述

而当我们重定向到文件呢?
在这里插入图片描述

所以,延伸出问题了,为什么C函数库中调用了两次而系统只调用了一次?
其实肯定与fork有关,不然我们为什么最后写了个fork呢?
首先我们应该知道的是,缓冲的方式有以下三种:

1、无缓冲。
2、行缓冲。(常见的对显示器进行刷新数据)
3、全缓冲。(常见的对磁盘文件写入数据)

1、我们执行这个可执行程序的时候,将数据打印到显示器上就是行缓冲,因为有\n的作用,所以当我们执行完对应代码后就立即将数据刷新到了显示器上。
2、当我们重定向到log.txt文件中的时候,刷新策略就变成了全缓冲了,由于C函数的printf和fwrite这两个C语言接口函数,我们使用printf和fputs函数打印的数据都打印到了C语言自带的缓冲区当中,之后我们通过fork创建子进程,由于进程间具有相互独立性,而之后当父进程或是子进程对要刷新缓冲区内容时,本质就是对父子进程共享的数据进行了修改,就要进行写时拷贝,父子进程代码和数据各独一份,缓冲区的代码和数据就变成了两份,一份子进程的,一份父进程的,所以重定向到log.txt文件中则打印了两份,而由于write这个函数是系统函数,仅仅重定向后调用了一份,因为系统调用的没有缓冲区!

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

(1)缓冲区谁提供的?

实际上这个缓冲区是C语言自带的,如果说这个缓冲区是操作系统提供的,那么printf、fputs和write函数打印的数据重定向到文件后都应该打印两次。

(2)缓冲区在哪里?

还记得我们上面的struct _IO_FILE结构体吗?我们再拿出来看一看。

struct _IO_FILE {
   
  // 缓冲区相关的
  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. */
};

也就是说,这里的缓冲区是由C语言提供,在FILE结构体当中进行维护的,FILE结构体当中不仅保存了对应文件的文件描述符还保存了用户缓冲区的相关信息。

(3)操作系统是否有缓冲区

答案肯定是有的,但操作系统有自己的一套缓冲区概念,当我们刷新用户级缓冲区的时候,不是讲其用户区的数据刷新到磁盘当中,而是先将其刷新到kernel内核操作系统缓冲区,然后将其刷新到磁盘当中。
在这里插入图片描述

我们通过用户层要往下逐步进行刷新,所以不能一下子就到操作系统。
在这里插入图片描述

四、理解文件系统

1、inode

磁盘文件由两部分构成,分别是文件内容和文件属性。文件内容就是文件当中存储的数据,文件属性就是文件的一些基本信息,例如文件名、文件大小以及文件创建时间等信息都是文件属性,文件属性又被称为元信息。

在这里插入图片描述

我们看上面那么多的不同的文件的不同信息,那么就需要一个不同的特殊的编号来进行识别和辨认,那么就是inode编号,inode是一个文件的属性集合,Linux中几乎每个文件都有一个inode,为了区分系统当中大量的inode,我们为每个inode设置了inode编号。

输入ls -i就能知道inode编号:
在这里插入图片描述

2、磁盘

磁盘是一种永久性存储介质,在计算机中,磁盘几乎是唯一的机械设备。与磁盘相对应的就是内存,内存是掉电易失存储介质,目前所有的普通文件都是在磁盘中存储的。

(1)磁盘基本概念

磁盘的基本概念:
在这里插入图片描述

(2)磁盘寻找信息方式

确定读写信息在磁盘的哪个盘面。
确定读写信息在磁盘的哪个柱面。
确定读写信息在磁盘的哪个扇区。

(3)磁盘分区与格式化介绍

i、磁盘存储介质

我们看下面的磁带,是一圈一圈卷起来的,但我们将其拉直伸开,那么不就是一个线性的结构吗,所以,磁盘是一个线性的结构。
在这里插入图片描述

ii、磁盘分区

磁盘通常被称为块设备,一般以扇区为单位,一个扇区的大小通常为512字节。我们若以大小为512G的磁盘为例,该磁盘就可被分为十亿多个扇区。
在这里插入图片描述

计算机想要更好的管理磁盘,那肯定是需要进行分区管理的,那么我们的计算机内部就进行分区,磁盘分区就是使用分区编辑器在磁盘上划分几个逻辑部分,盘片一旦划分成数个分区,不同的目录与文件就可以存储进不同的分区,分区越多,就可以将文件的性质区分得越细,按照更为细分的性质,存储在不同的地方以管理文件,例如在Windows下磁盘一般被分为C盘和D盘两个区域。

那么我们利用命令看一下我们的linux中的磁盘分区情况:

ls /dev/vda* -l
在这里插入图片描述

iii、磁盘格式化

进行完磁盘分区以后,那么肯定需要的是进行磁盘格式化,磁盘格式化也就是将磁盘中的分区进行初始化的一种操作,这种操作通常会导致现有的磁盘或分区中所有的文件被清除。简而言之就是对各个分区写入对应的管理信息。

在这里插入图片描述

其中,写入的管理信息是什么是由文件系统决定的,不同的文件系统格式化时写入的管理信息是不同的,常见的文件系统有EXT2、EXT3、XFS、NTFS等。

(4)EXT2文件系统存储方案

在这里插入图片描述
启动块的大小是确定的,而块组的大小是由格式化的时候确定的,并且不可以更改。

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

块组是由下面结构组成的:

1、Block Group:ext2文件系统会根据分区的大小划分为数个Block Group。而每个Block Group都有着相同的结构组成。政府管理各区的例子
2、超级块(Super Block):存放文件系统本身的结构信息。记录的信息主要有:bolck 和 inode的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了
3、GDT,Group Descriptor Table:块组描述符,描述块组属性信息
4、块位图(Block Bitmap):Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用
5、inode位图(inode Bitmap):每个bit表示一个inode是否空闲可用。
6、i节点表(inode Block):存放文件属性 如 文件大小,所有者,最近修改时间等
7、数据区(Data Block):存放文件内容
需要注意的是:
其他块组当中可能会存在冗余的Super Block,当某一Super Block被破坏后可以通过其他Super Block进行恢复。
磁盘分区并格式化后,每个分区的inode个数就确定了。

特别说明:如何创建一个文件

在这里插入图片描述
我们用上面的这张显示图来进行讲解:

四个操作:
1、存储属性
先在i节点表中找到空闲的i结点,我们上图找的是263466这个i结点,内核把文件信息记录到其中。

2、存储数据
该文件需要存储在三个磁盘块,内核找到了三个空闲块:300,500,800。将内核缓冲区的第一块数据复制到300,下一块复制到500,以此类推。

3、记录分配情况
在磁盘的数据区记录好分配情况,就像我们上面数据区标红的地方。

4、添加文件名到目录

新的文件名abc。
内核将入口(263466,abc)添加到目录文件。文件名和inode之间的对应关系将文件名和文件的内容及属性连接起来。

i、如何理解创建一个空文件

1、通过遍历i位图的方法,先在i位图中找到一个空闲的inode。
2、通过i位图中找到的inode的数字信息在i节点表中找到相对应的inode,并将文件信息存储在这个i节点表中。
3、通过inode指针和对应的文件名添加到目录文件的数据块中。

在这里插入图片描述

ii、如何理解对文件写入信息

1、通过文件的inode编号找到对应的inode结构。
2、通过inode结构找到存储该文件内容的数据块,并将数据写入数据块。
3、若不存在数据块或申请的数据块已被写满,则通过遍历块位图的方式找到一个空闲的块号,并在数据区当中找到对应的空闲块,再将数据写入数据块,最后还需要建立数据块和inode结构的对应关系。

在这里插入图片描述

一个文件使用的数据块和inode结构的对应关系,是通过一个数组进行维护的,该数组一般可以存储15个元素,其中前12个元素分别对应该文件使用的12个数据块,剩余的三个元素分别是一级索引、二级索引和三级索引,当该文件使用数据块的个数超过12个时,可以用这三个索引进行数据块扩充。

iii、如何理解删除一个文件

1、将该文件对应的inode在inode位图当中置为无效。
2、将该文件申请过的数据块在块位图当中置为无效。

在这里插入图片描述

因为此操作并不会真正将文件对应的信息删除,而只是将其inode号和数据块号置为了无效,所以当我们删除文件后短时间内是可以恢复的。为什么说是短时间内呢,因为该文件对应的inode号和数据块号已经被置为了无效,因此后续创建其他文件或是对其他文件进行写入操作申请inode号和数据块号时,可能会将该置为无效了的inode号和数据块号分配出去,此时删除文件的数据就会被覆盖,也就无法恢复文件了。

iv、为什么拷贝文件的时候很慢,而删除文件的时候很快

拷贝文件,得先创建一个能被拷贝放进去的文件,所以就需要创建inode,进行映射等的操作,时间肯定是很长的,而我们删除文件仅仅是将位图中给置为无效,也就是将i位图和块位图置为无效,很快的一步操作即可。

我们举一个形象的例子:要拆这套房子的话,我们仅仅需要在外面喷一个大大的“拆”即可,住户那不就是全走了吗?而进行复制房子进行建造的时候,盖楼需要很久的时间和很多的成本。

v、如何理解目录

1、都说在Linux下一切皆文件,目录当然也可以被看作为文件。
2、目录有自己的属性信息,目录的inode结构当中存储的就是目录的属性信息,比如目录的大小、目录的拥有者等。
3、目录也有自己的内容,目录的数据块当中存储的就是该目录下的文件名以及对应文件的inode指针。

3、软硬链接

(1)软连接

下面的命令能够建立软连接:

ln -s myproc myproc_softlink // myproc被链接文件名 myproc_softlink链接名

我们用我们的myproc试一试吧:

在这里插入图片描述
软链接文件的inode号与源文件的inode号是不同的,并且软链接文件的大小比源文件的大小要小得多。软链接又叫做符号链接,软链接文件相对于源文件来说是一个独立的文件,该文件有自己的inode号,但是该文件只包含了源文件的路径名,所以软链接文件的大小要比源文件小得多。软链接就类似于Windows操作系统当中的快捷方式。

通过./myproc和./myproc_softlink均可以跑出来代码。
在这里插入图片描述

奇思妙想:删了myproc这个可执行程序呢?
在这里插入图片描述

(2)硬链接

我们可以通过以下指令来进行硬链接:

ln myproc myproc_hardlink

在这里插入图片描述
硬链接文件的inode号与源文件的inode号是相同的,并且硬链接文件的大小与源文件的大小也是相同的,特别注意的是,当创建了一个硬链接文件后,该硬链接文件和源文件的硬链接数都变成了2。也就是我们熟知的取别名。

继续奇思妙想,删掉这个myproc可执行程序后呢?
在这里插入图片描述
当硬链接的源文件被删除后,硬链接文件仍能正常执行,只是文件的链接数减少了一个,因为此时该文件的文件名少了一个。总之,硬链接就是让多个不在或者同在一个目录下的文件名,同时能够修改同一个文件,其中一个修改后,所有与其有硬链接的文件都一起修改了。

i、为什么刚创建的目录其硬连接数是2?

在这里插入图片描述
不知道大家有没有回想起来,我们在每创建一个目录的时候,会有两个隐藏文件.和… 那么就好说了,我们的dir这个文件不就是.吗?这两个是一样的,因为都是当前文件呀!还有一个…是上一个路径的文件的,所以刚创建的dir目录的硬链接数为2。我们再来看一下他们的inode来直接印证一下吧:
在这里插入图片描述
inode一模一样,那不就是取别名了吗?可以说明它们代表的实际上是同一个文件。

(3)软硬链接的区别

1、软链接是一个独立的文件,有独立的inode,而硬链接没有独立的inode。
2、软链接相当于快捷方式,硬链接本质没有创建文件,只是建立了一个文件名和已有的inode的映射关系,并写入当前目录。

4、文件的三个时间

在Linux当中,我们可以使用命令stat 文件名来查看对应文件的信息。

Access: 文件最后被访问的时间。
Modify: 文件内容最后的修改时间。
Change: 文件属性最后的修改时间。

在这里插入图片描述

当我们修改文件内容时,文件的大小一般也会随之改变,所以一般情况下Modify的改变会带动Change一起改变,但修改文件属性一般不会影响到文件内容,所以一般情况下Change的改变不会带动Modify的改变。我们若是想将文件的这三个时间都更新到最新状态,可以使用命令touch 文件名来进行时间更新。

相关推荐

最近更新

  1. TCP协议是安全的吗?

    2024-01-21 20:58:01       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-01-21 20:58:01       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-01-21 20:58:01       18 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-01-21 20:58:01       20 阅读

热门阅读

  1. Go语言学习笔记:GORM 介绍及快速入门,简单查询

    2024-01-21 20:58:01       31 阅读
  2. Nginx笔记

    2024-01-21 20:58:01       29 阅读
  3. linux 抓包

    2024-01-21 20:58:01       37 阅读
  4. UI -- Vue2

    2024-01-21 20:58:01       32 阅读
  5. 数据结构概述

    2024-01-21 20:58:01       36 阅读
  6. C++学习笔记(四十三):c++ optional

    2024-01-21 20:58:01       29 阅读
  7. react和vue的区别

    2024-01-21 20:58:01       28 阅读
  8. 【Delphi 基础知识 22】TStringList 的详细用法

    2024-01-21 20:58:01       27 阅读
  9. CSS Day10

    CSS Day10

    2024-01-21 20:58:01      28 阅读