【Linux学习】基础IO

前言

该文章基于Linux环境用于介绍基础IO。本文涉及C语言文件IO相关操作、认识文件相关的系统调用接口、认识文件操作符,理解重定向、通过对比fd和FILE,理解系统调用与库函数之间的关系、理解文件系统中的inode相关概念、认识软硬连接以及认识动静态库,结合gcc制作动静态库等。

一、C语言文件IO

1. C语言文件接口以及打开方式

  1. 文件接口
文件操作函数功能说明
fopen打开文件
fclose关闭文件
fwrite以二进制形式写入文件
fread以二进制形式读取文件
fputc写入一个字符
fgetc读取一个字符
fputs写入一个字符串
fgets读取一个字符串
fprintf格式化写入数据
fscanf格式化读取数据
fseek设置文件指针的位置
ftell计算当前文件指针相对于起始位置的偏移量
rewind设置文件指针到文件的起始位置
ferror判断文件操作过程中是否发生错误
feof判断文件指针是否读取到文件末尾
  1. 打开方式
打开方式说明
r打开文件用于只读
r+打开文件用于读和写
w打开文件用于写文件,若文件存在,则会清空文件内容,若文件不存在,则会创建之
w+与 “w” 的区别在于,增加了读
rb打开文件,用二进制形式读
rb+与"rb" 的区别在于,增加了写
wb打开文件,以二进制形式写
wb+与"wb" 的区别在于,增加了读
a以尾部追加的方式打开一个文本文件用于只写
a+与"a" 的区别在于,增加了读
ab以尾部追加的方式打开一个二进制文件用于只写
ab+与"a" 的区别在于,增加了读

下面是用C语言进行读写操作的例子:

  • 写文件
#include <stdio.h>
#include <string.h>
int main()
{
	FILE* fp = fopen("myfile.txt", "w");
	if (!fp) {
		printf("fopen error!\n");
	}
	const char* msg = "hello world!\n";
	int count = 5;
	while (count--) {
		fwrite(msg, strlen(msg), 1, fp);
	}
	fclose(fp);
	return 0;
}

结果:

  • 读文件
#include <stdio.h>
#include <string.h>
int main()
{
	FILE* fp = fopen("myfile.txt", "r");
	if (!fp)
	{
		printf("fopen error!\n");
	}
	char buf[1024];
	const char* msg = "hello world!\n";
	while (1)
	{
		//注意返回值和参数,此处有坑,仔细查看man手册关于该函数的说明
		size_t s = fread(buf, 1, strlen(msg), fp);
		if (s > 0) {
			buf[s] = 0;
			printf("%s", buf);
		}
		if (feof(fp)) {
			break;
		}
	}
	fclose(fp);
	return 0;
}

结果:
=

2. 对当前路径的理解

学习了C语言文件操作过后,我们知道当用fopen以写入的方式打开一个文件时,如果该文件不存在,则会在当前路径下创建该文件,那么什么是当前路径呢?

例如,我们在test_file目录下运行mytest,发现该目录出现了一个myfile.txt文件。

那么,是否可以下结论说,当前路径就是我的可执行程序所在的路径呢?现在我们将myfile.txt删除,回退到上一级目录下再次执行mytest。

我们可以发现,myfile.txt最终出现在了当前所执行mytest的路径中。
当该可执行程序执行起来变成进程之后,我们可以通过获取该进程的PID,然后根据PID在根目录下的proc目录查看该进程的信息。

在这里我们可以看到两个软链接文件cwd和exe,cwd就是进程运行时我们所处的路径,而exe就是该可执行程序的所处路径。

由此我们可以得出结论:我们这里所说的当前路径不是指可执行程序所处的路径,而是指该可执行程序运行成为进程时我们所处的路径

3. 默认打开的三个流

Linux下一切皆文件,意思是在Linux中我们可以把任何东西都看成文件,那么显示器和键盘等也就相当于是文件。因为我们能在显示器中看到数据,是因为我们向显示器里面写入了数据,电脑能够得到我们从键盘敲入的字符,是因为电脑从键盘中读取了数据。

那么既然显示器和键盘等都是文件,为什么我们不需要提前打开"显示器文件" 和 “键盘文件”,就能直接进行键盘和显示器的相关操作呢?

需要注意的是,打开文件一定是进程运行的时候打开的,而任何进程在运行的时候都会默认打开三个输入输出流,即标准输入流、标准输出流以及标准错误流,对应到C语言当中就是stdin、stdout以及stderr。

其中,stdin对应的是键盘,stdout和stderr对应的是显示器。

通过查看man手册,我们会发现stdin、stdout以及stderr都是FILE* 类型的,也就是文件类型。

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

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

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

二、 系统文件IO

1. 系统接口

open

系统接口中使用open函数打开或者创建目标文件,man手册中的open:

参数解读:

pathname:要打开或创建的目标文件
flags: 打开文件时,传入的参数选项,用一个或者多个常量进行 “或” 运算,构成flags。
mode:给目标文件设置的权限

flags参数:

常量名说明
O_RDONLY只读打开
O_WRONLY只写打开
O_RDWR读,写打开
O_CREAT若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
O_APPEND追加写

注意:前面三个常量,必须指定一个且只能指定一个。

返回值:

打开成功返回目标文件的文件描述符fd,失败则返回 -1。

write

系统接口中使用write向目标文件写入数据,man手册中的write:

参数解读:

fildes:文件描述符
buf:数据缓冲区
nbyte:向文件中写入数据的字节数

返回值:

ssize_t:有符号整型,在32位机器上等同于int,在64位机器上等同于long
写入成功,则返回实际写入数据的字节数;
写入失败,则返回 -1。

利用系统接口写文件的例子:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main()
{
    umask(0);
    int fd = open("myfile.txt", O_WRONLY | O_CREAT, 0664);
    if(fd < 0)
    {
        perror("open");
        return 1;
    }

    int count = 5;
    const char* msg = "hello world !\n";
    int len = strlen(msg);

    while(count--)
    {
        write(fd, msg, len);

    }
    close(fd);
    return 0;

}

read

系统接口中使用read函数读取文件中的数据,man手册中的read:

参数解读:

fildes:文件描述符
buf:数据缓冲区
nbyte:向文件中写入数据的字节数

返回值:

ssize_t:有符号整型,在32位机器上等同于int,在64位机器上等同于long
读取成功,则返回实际读取数据的字节数;
读取失败,则返回 -1。

利用系统接口读文件的例子:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
	int fd = open("myfile.txt", O_RDONLY);
	if(fd < 0)
	{
		perror("open");
		return 1;
	}
	
	const char* msg = "hello world !\n";
	char buf[1024];
	while(1)
	{
		ssize_t s = read(fd, buf, strlen(msg));//类比write
		if(s > 0)
		{
			printf("%s", buf);
		}
		else
		{
			break;
		}
	}
	close(fd);
	return 0;
}

close

系统接口中使用close关闭文件,man手册中的close:

参数解读:

fildes:文件描述符

返回值:

关闭成功,则返回 0;
关闭失败,则返回 -1。

系统接口和库函数

上面C语言的 fopen、fclose、fread、fwrite 都是C标准库当中的函数,我们称之为库函数(libc)。而 open、close、read、write 都属于系统提供的接口,称之为系统调用接口。回忆一下学习操作系统概念的一张图:

系统调用接口和库函数的关系,一目了然。所以我们可以认为编程语言库中的文件操作相关函数,都是对系统调用的封装。

2. 文件描述符及其分配规则

文件描述符

有了上面对open系统函数的了解,我们知道了文件描述符其实就是一个整数。进程和文件的关系就是,文件是在进程运行的时候打开并加入到内存中,一个进程可以打开多个文件,而系统中有存在了许多的进程,所以在操作系统之中存在着大量被打开的文件。那么操作系统如果管理这些被打开的文件呢?其实操作系统会为每个打开的文件创建各自的 struct file 结构体,这个结构体封装了文件的各种属性和内容,并且包含了 struct file* prev 和 struct file* next 指针。因此操作系统将这些文件以双向链表的形式链接起来,之后对文件的管理也就变成了对链表的增删查改等操作。

我们知道了组织文件的方式之后,还需要知道进程是如何与打开的文件建立映射关系的。

通过前面对进程的学习,我们知道了当一个程序运行起来时,操作系统会将该程序的代码和数据资源加载到内存,为其创建对应的task_struct、mm_struct、页表等数据结构,并通过页表建立虚拟内存与物理内存之间的映射关系。

在task_struct中有一个的指针,该指针指向一个名为 struct_file的结构体,在结构体中有一个fd_array的指针数组,数组的下标其实就是文件描述符。

在前面使用open函数的例子中我们会发现,不管怎么打开文件,返回给我们的文件描述符都是从3开始的,那么文件描述符0、1、2哪去了呢?其实0、1、2就是分别对应系统默认打开的stdin、stdout、stderror。我们可以通过以下代码进行验证:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
 

int main()
{
    // 先验证0,1,2就是标准IO
    char buffer[1024];
    ssize_t s = read(0, buffer, sizeof(buffer)-1);
    if(s > 0)
    {
        buffer[s] = '\0';

        printf("echo: %s", buffer);
    }
    const char *str = "hello write\n";
    int len = strlen(str);

    write(1, str, len);
    write(2, str, len);
   
    
    // 验证012和stdin,stdout,stderr的对应关系
    printf("stdin: %d\n", stdin->_fileno);
    printf("stdout: %d\n", stdout->_fileno);
    printf("stderr: %d\n", stderr->_fileno);

    return 0;
}


通过验证我们就能够发现0对应的是标准输入,1对应的是标准输出,2对应的是标准错误。

文件描述符分配原则

当我们连续打开几个文件时,看看这几个文件的文件描述符。

#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);
	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的位置以及被标准IO所占用,所有在有文件被打开,那么文件描述符就从3开始。

如果在打开这些文件之前,把文件描述符为0所对应的文件关闭,会怎么样呢?

close(0);


我们发现空闲出来的0号位置也能被新的文件所使用了。再试试同时关闭0和2(注意不能关闭1,因为1对应的是标准输出,关闭了屏幕上就看不到任何输出内容了)。

close(0);
close(2);


可以看到前两个打开的文件获取到的文件描述符是0和2,之后打开文件获取到的文件描述符才是从3开始依次递增的。因此我们可以得出结论,文件描述符的分配原则是从最小的没有被使用的fd_array数组的下标开始进行分配的。

3. 重定向及dup2系统调用

重定向

  1. 输出重定向

输出重定向就是,将我们本应该输出到一个文件的数据重定向输出到另一个文件中。

当我们关闭标准输出,打开一个新的文件,还向屏幕输出内容就会发生下面的现象:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
int main()
{
	umask(0);
	close(1);
	int fd = open("myfile.txt", O_WRONLY|O_CREAT, 00644);
	if(fd < 0)
	{
		perror("open");
		return 1;
	}
	printf("hello world!");
	printf("hello world!");
	printf("hello world!");
	printf("hello world!");
	printf("hello world!");
	printf("fd: %d\n", fd);
	
	fflush(stdout);
	close(fd);
	return 0;
}


此时,我们发现,本来应该输出到显示器上的内容,输出到了文件 myfile.txt 当中,其中,fd=1。这种现象叫做输出
重定向。

注意:

C语言的数据并不是立马写到了内存操作系统里面,而是写到了C语言的缓冲区当中,所以使用printf打印完后需要使用fflush将C语言缓冲区当中的数据刷新到文件中。

  1. 追加重定向

**追加重定向与输出重定向的区别在于,输出重定向是覆盖式输出数据,而追加重定向是追加式输出数据。**例如下面的例子:

#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 World\n");
	printf("hello World\n");
	printf("hello World\n");
	printf("hello World\n");
	printf("hello World\n");
	
	fflush(stdout);
	close(fd);
	return 0;
}

结果如下:

  1. 输入重定向

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

例如,我们本打算从键盘中读取数据,现在重定向为从log.txt中读取数据。

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
    close(0);
    int fd = open("log.txt", O_RDONLY | O_CREAT, 0666);
    if(fd < 0)
    {
        perror("open");
        return 1;
    }

    char str[1024];
    while(scanf("%s", str) != EOF)
    {
        printf("%s\n", str);
    }

    close(fd);
    return 0;
}


这里的scanf函数默认是从stdin中读取数据的,当我们把0号文件描述符,也就是stdin关闭后,在打开log.txt,此时log.txtd的文件描述符就是0,因此scanf就变成了从log.txt中读取数据了,这就是输入重定向的原理。

标准输出和标准错误的区别

让我们先看以下代码:

int main() 
{
    printf("hello printf\n"); //stdout
    perror("perror"); // stderr

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

    return 0;
}

此时的输出结果为:

这样看来标准输出和标准错误没有任何区别,都是打印到屏幕上,但我们若是将程序运行结果重定向输出到文件log.txt当中,我们会发现log.txt文件当中只有向标准输出流输出的两行字符串,而向标准错误流输出的两行数据并没有重定向到文件当中,而是仍然输出到了显示器上。

实际上我们使用重定向时,重定向的是文件描述符是1的标准输出流,而并不会对文件描述符是2的标准错误流进行重定向。

dup2系统调用

在Linux系统中提供了系统dup2,我们可以使用该函数完成重定向。dup2的函数原型如下:

#include <unistd.h>
int dup2(int oldfd, int newfd);

功能:

dup2 会把fd_arrar[oldfd] 当中的内容拷贝到fd_array[newfd] 中,因此如果有必要我们需要先关闭文件描述符为newfd的文件。

返回值:

dup2调用成功返回newfd,失败则返回 -1 。

注意:

  1. 如果oldfd不是有效文件的文件描述符,则会调用失败,newfd不受影响。
  2. 如果oldfd和newfd的值相同,dup2则不会做任何事,直接返回newfd。

例子如下:

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


我们发现,stdout输出到显示器的内容重定向到了log.txt中。

学习了dup2,现在可以为我们前面自己实现的myShell添加重定向功能。实现的大致步骤如下:

  1. 对获取到的当前命令进行判断,是否包含 > 、>> 或者 < 命令,若包含则要对其进行处理,这里实现的一个checkDir对输入的命令进行处理。
  2. 设置了四个宏常量,NONE_REDIR 表示没有重定向,INPUT_REDIR 表示输入重定向,OUTPUT_REDIR 表示输出重定向,APPEND_REDIR 表示追加重定向,他们的值分别为 -1、0、1、2。
  3. 根据checkDir得到的结果g_redir_flag 和g_redir_filename ,在子进程中分别实现不同的打开文件和重定向的方式。
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<assert.h>
#include<sys/types.h>
#include<fcntl.h>
#include<sys/stat.h>
#include<ctype.h>

#define SEP " "
#define NUM 1024
#define SIZE 128

#define DROP_SPACE(s) do { while(isspace(*s)) ++s;} while(0)

#define NONE_REDIR -1
#define INPUT_REDIR 0 //输入重定向 
#define OUTPUT_REDIR 1 //输出重定向 
#define APPEND_REDIR 2 //追加重定向

int g_redir_flag = NONE_REDIR;
char* g_redir_filename = NULL;


char command_line[NUM];
char *command_args[SIZE];

extern char **environ;
char env_buffer[NUM];


//对应上层的内建命令
int ChangeDir(const char* new_path)
{
    chdir(new_path);
    return 0; //调用成功
}

void PutEnvInMyShell(char* new_env)
{
    putenv(new_env);
}

void CheckDir(char* commands)
{
    assert(commands);
    char* start = commands ;
    char* end = commands  + strlen(commands) ;

    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++;
                g_redir_flag = OUTPUT_REDIR;
                DROP_SPACE(start);
                g_redir_filename = start;
                break;
            }
        }
        else if(*start == '<') 
        {
            *start = '\0';
            start++;
            DROP_SPACE(start);
            g_redir_flag = INPUT_REDIR;
            g_redir_filename = start;
        }
        else
        {
            ++start;
        }
    }
}

int main()
{
    //shell 本质上就是一个死循环
    while(1)
    {
        g_redir_flag = NONE_REDIR;
        g_redir_filename = NULL;

        //不关心获取这些属性的接口, 搜索一下
        //1. 显示提示符
        printf("[张三@我的主机名 当前目录]# ");
        fflush(stdout);
        //2. 获取用户输入
        memset(command_line, '\0', sizeof(command_line)*sizeof(char));
        fgets(command_line, NUM, stdin); //键盘,标准输入,stdin, 获取到的是c风格的字符串, '\0'
        command_line[strlen(command_line) - 1] = '\0';// 清空\n
       
        // ls -a -l>log.txt     ls -a -l >> log.txt  ...
        CheckDir(command_line);
        

        //3. "ls -a -l -i" -> "ls" "-a" "-l" "-i" 字符串切分
        command_args[0] = strtok(command_line, SEP);
        int index = 1;

        //给ls命令添加颜色
        if(strcmp(command_args[0], "ls") == 0)
            command_args[index++] =(char*) "--color=auto";

        // = 是故意这么写的
        // strtok 截取成功,返回字符串其实地址
        // 截取失败,返回NULL
        while(command_args[index++] = strtok(NULL, SEP));

        //for debug
        //for(int i = 0 ; i < index; i++)
        //{
        //    printf("%d : %s\n", i, command_args[i]);
        //}
        // 4. TODO, 编写后面的逻辑, 内建命令
        

        if(strcmp(command_args[0], "cd") == 0)
        {
            ChangeDir(command_args[1]); //让调用方进行路径切换,即父进程
            continue;
        }
        


        if(strcmp(command_args[0], "cd") == 0)
        {
            //目前环境变量信息在command_line, 会被清空
            //需要自己保存
            strcpy(env_buffer, command_args[1]);
            PutEnvInMyShell(env_buffer);
            continue;
        }


        //
        // 5. 创建进程,执行
        pid_t id = fork();
        if(id == 0)
        {
            int fd = -1;
            switch(g_redir_flag)
            {
                case NONE_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);
                    dup2(fd, 1);
                    break;
                case APPEND_REDIR:
                    fd = open(g_redir_filename, O_WRONLY | O_CREAT | O_APPEND);
                    dup2(fd, 1);
                    break;

                default:
                    printf("BUG?\n");
                    break;
            }

            //child
            // 6. 程序替换
            //exec*?
            execvp(command_args[0]/*不就是保存的是我们要执行的程序名字吗?*/, command_args);
            exit(1); //执行到这里,子进程一定替换失败
        }
        int status = 0;
        pid_t ret = waitpid(id, &status, 0);
        if(ret > 0)
        {
            printf("等待子进程成功: sig: %d, code: %d\n", status&0x7F, (status>>8)&0xFF);
        }
    }// end while
    return 0;
}

结果:

三、FILE

1. FILE当中的文件描述符

库函数其实就是对系统接口的封装,访问文件也和系统接口一样是通过文件描述符进行访问的,所以C语言库中的FILE也封装了文件描述符。首先,我们在/usr/include/stdio.h头文件中可以看到下面这句代码:

typedef struct _IO_FILE FILE;

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

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
};

2. 对缓冲区的理解

代码如下:

#include <stdio.h>
#include <string.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 没有变化,说明没有所谓的缓冲。

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

四、文件系统

1. 理解文件系统

文件系统的结构

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

  • 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 Bitmap(inode位图):每个bit表示一个inode是否空闲可用。
  • inode Table:存放文件属性。如文件大小,所有者,最近修改时间等。
  • Date blocks:存放文件内容。

注意:位图的概念可以见文章:【哈希原理、模拟封装unordered系列关联式容器及其应用】

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

[lhf@hecs-197241 myfile]$ touch test
[lhf@hecs-197241 myfile]$ ls -i test 
263466 test

为了说明问题,我们将上图简化:

创建一个新文件主要有一下4个操作:

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

inode的理解

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

在命令行当中输入ls -l,即可显示当前目录下各文件的属性信息

[lhf@hecs-197241 myfile]$ ls -l
total 20
-rw-rw-r-- 1 lhf lhf   64 Oct 29 16:57 log.txt
-rwxrwxr-x 1 lhf lhf 8592 Oct 29 16:56 myfile
-rw-rw-r-- 1 lhf lhf 1658 Oct 29 16:56 myfile.c

其中,各列信息所对应的文件属性如下:

在Linux操作系统中,文件的元信息和内容是分离存储的,其中保存元信息的结构称之为inode,因为系统当中可能存在大量的文件,所以我们需要给每个文件的属性集起一个唯一的编号,即inode号。也就是说,inode是一个文件的属性集合,Linux中几乎每个文件都有一个inode,为了区分系统当中大量的inode,我们为每个inode设置了inode编号。

在命令行当中输入ls -i,即可显示当前目录下各文件的inode编号

[lhf@hecs-197241 myfile]$ ls -i
1839068 log.txt  1839067 myfile  1839066 myfile.c  

2. 软硬链接

软链接

可以通过以下命令创建一个文件的软连接。

ln -s myfile myfile-s


通过ls -i -l命令我们可以看到,软链接文件的inode号与源文件的inode号是不同的,并且软链接文件的大小比源文件的大小要小得多。
软链接又叫做符号链接,软链接文件相对于源文件来说是一个独立的文件,该文件有自己的inode号,但是该文件只包含了源文件的路径名,所以软链接文件的大小要比源文件小得多。软链接就类似于Windows操作系统当中的快捷方式。软链接文件只是其源文件的一个标记,当删除了源文件后,链接文件不能独立存在,虽然仍保留文件名,但却不能执行或是查看软链接的内容了

硬链接

可以通过以下命令创建一个文件的硬连接。

ln myfile myfile-h


通过ls -i -l命令我们可以看到,硬链接文件的inode号与源文件的inode号是相同的,并且硬链接文件的大小与源文件的大小也是相同的,特别注意的是,当创建了一个硬链接文件后,该硬链接文件和源文件的硬链接数都变成了2。硬链接文件就是源文件的一个别名,一个文件有几个文件名,该文件的硬链接数就是几,这里inode号为1839067的文件有myfile和myfile-h两个文件名,因此该文件的硬链接数为2。

与软连接不同的是,当硬链接的源文件被删除后,硬链接文件仍能正常执行,只是文件的链接数减少了一个,因为此时该文件的文件名少了一个。

为什么刚刚创建的目录的硬链接数是2?

因为每个目录创建后,该目录下默认会有两个隐含文件. 和 …,它们分别代表当前目录和上级目录,因此这里创建的目录有两个名字,一个是dir另一个就是该目录下的.,所以刚创建的目录硬链接数是2。通过命令我们也可以看到dir和该目录下的.的inode号是一样的,也就可以说明它们代表的实际上是同一个文件。

[lhf@hecs-197241 test2022_10_29]$ ls -i -d myfile
1839065 myfile
[lhf@hecs-197241 test2022_10_29]$ ls -i -a myfile/
1839065 .  1839064 ..  1839068 log.txt  1839067 myfile  1839066 myfile.c
[lhf@hecs-197241 test2022_10_29]$ 

软硬链接的区别

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

3. 对磁盘的认识

磁盘是一种永久性存储介质,在计算机中,磁盘几乎是唯一的机械设备。与磁盘相对应的就是内存,内存是掉电易失存储介质,目前所有的普通文件都是在磁盘中存储的。
磁盘在冯诺依曼体系结构当中既可以充当输入设备,又可以充当输出设备。

磁盘的基本概念:

  • 盘片(platter):硬盘中承载数据存储的介质。硬盘一般由多个盘片组成,每个盘片包含两个面,每个盘面都对应地有一个读/写磁头。受到硬盘整体体积和生产成本的限制,盘片数量都受到限制,一般都在5片以内。盘片的编号自下向上从0开始,如最下边的盘片有0面和1面,再上一个盘片就编号为2面和3面。
  • 磁头(head):通过磁性原理读取磁性介质上数据的部件
  • 磁道(track):当磁盘旋转时,磁头若保持在一个位置上,则每个磁头都会在磁盘表面划出一个圆形轨迹,这些圆形轨迹就叫做磁道
  • 扇区(sector):磁盘上的每个磁道被等分为若干个弧段,这些弧段便是硬盘的扇区,同一块硬盘上的扇区大小是一致的。"每个磁道的扇区数一样的"说的是老的硬盘,外圈的密度小,内圈的密度大(简单理解就是,磁盘存储媒介为磁性记忆材料,在内圈涂的密度高),故每圈可存储的数据量是一样的。新的硬盘数据的密度都一致,这样磁道的周长越长,扇区就越多,存储的数据量就越大。
  • 柱面(cylinder):在有多个盘片构成的盘组中,由不同盘片的面,但处于同一半径圆的多个磁道组成的一个圆柱面

物理扇区与逻辑扇区:

近年来,为了追求更高的硬盘容量,便出现了扇区存储容量为2048、4096等字节的硬盘,我们称这样的扇区为"物理扇区"。这样的大扇区会导致许多兼容性问题,有的系统或软件无法适应。为了解决这个问题,硬盘内部将物理扇区在逻辑上划分为多个扇区片段并将其作为普通的扇区(一般为512字节大小)报告给操作系统及应用软件。这样的扇区片段我们称之为“逻辑扇区”。实际读写时由硬盘内的程序(固件)负责在逻辑扇区与物理扇区之间进行转换,上层程序“感觉”不到物理扇区的存在。

逻辑扇区是硬盘可以接受读写指令的最小操作单元,是操作系统及应用程序可以访问的扇区,多数情况下其大小为512字节。我们通常所说的扇区一般就是指的逻辑扇区。**物理扇区是硬盘底层硬件意义上的扇区,是实际执行读写操作的最小单元。是只能由硬盘直接访问的扇区,操作系统及应用程序一般无法直接访问物理扇区。**当要读写某个逻辑扇区时,硬盘底层在实际操作时都会读写逻辑扇区所在的整个物理扇区。

  • 10
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 9
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

求知.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值