【Linux】基础IO

1. 重新谈论文件

对文件的几点认识:

  1. 空文件也要在磁盘占据空间
  2. 文件 = 内容 + 属性
  3. 文件操作 = 对内容的操作 + 对属性的操作 or 对内容和属性的操作
  4. 标定一个文件必须使用:文件路径 + 文件名。这样具有唯一性
  5. 如果没有指明对应的文件路径,默认是在当前路径(进程的当前路径)进行文件访问。
  6. 当我们把fopen,fclose,fread,fwite等接口写完,进行代码编译,形成二进制可执行程序之后,但是没有运行程序,文件的对应操作也没有被执行。对文件的操作,本质是:进程对文件的操作
  7. 一个文件如果没有被打开,不能直接对文件进行访问。一个文件要被访问,就必须先被打开。被谁打开呢?用户的进程先调用访问文件的接口,由于文件存放在磁盘上,需要操作系用帮忙打开
  8. 磁盘上的所有文件没有都被打开,需要的时候才打开。所以文件可以分为:被打开的文件和没有被打开的文件。
  9. 文件操作的本质:进程和被打开文件的关系。

2. 重新谈论文件操作(C语言)

1. 文件操作认知

C语言由文件操作接口,C++有文件操作接口,Java也有文件操作接口,几乎所有编程语言都有文件操作接口,但它们的操作接口都不一样

文件存放在磁盘上,磁盘属于硬件,操作系统是硬件的管理者,只有操作系统才能访问硬件,因此所有人想要访问磁盘,就不能绕过操作系统,必须使用操作系统提供的接口,操作系统提供文件级别的系统调用接口

所有语言都可以访问文件,但是操作系统只有一个。
所以:

  • 上层语言无论如何变化,在库函数的底层必须调用系统调用接口。
  • 库函数可以千变万化,但是底层的系统调用接口不变。

2. 文件操作

先来段代码回顾C文件接口
hello.c写文件

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

C语言以"w"方式单纯地打开文件,会自动清空文件内部地数据

hello.c读文件

#include <stdio.h>
#include <string.h>
int main()
{
	FILE *fp = fopen("myfile", "r");
	if(!fp)
	{
		printf("fopen error!\n");
		return 1;
	}
	char buf[1024];
	const char *msg = "hello world!\n";
	while(1)
	{
	//注意返回值和参数,此处有坑,仔细查看man手册关于该函数的说明
		ssize_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;
}

输出信息到显示器,你有哪些方法

#include <stdio.h>
#include <string.h>
int main()
{
	const char *msg = "hello fwrite\n";
	fwrite(msg, strlen(msg), 1, stdout);
	printf("hello printf\n");
	fprintf(stdout, "hello fprintf\n");
	
	return 0;
}

stdin & stdout & stderr

  • C默认会打开三个输入输出流,分别是stdin(标准输入流), stdout(标准输出流), stderr(标准错误)
  • 仔细观察发现,这三个流的类型都是FILE*, fopen返回值类型,文件指针

总结

  • 打开文件的方式
r 	 Open text file for reading.
	 The stream is positioned at the beginning of the file.
	 
r+ 	 Open for reading and writing.
	 The stream is positioned at the beginning of the file.
	 
w 	 Truncate(缩短) file to zero length or create text file for writing.
	 The stream is positioned at the beginning of the file.
	 
w+ 	 Open for reading and writing.
	 The file is created if it does not exist, otherwise it is truncated.
	 The stream is positioned at the beginning of the file.
	 
a 	 Open for appending (writing at end of file).
	 The file is created if it does not exist.
	 The stream is positioned at the end of the file.
	 
a+ 	 Open for reading and appending (writing at end of file).
	 The file is created if it does not exist. The initial file position
	 for reading is at the beginning of the file,
	 but output is always appended to the end of the file.

如上,是我们之前学的文件相关操作。还有fseekftellrewind 函数,在C部分已经有所涉猎,感兴趣者可以翻看前面C语言-文件操作部分。

3. 系统文件I/O

操作文件,除了上述C接口(当然,C++也有接口,其他语言也有),我们还可以采用系统接口来进行文件访问。

1. 使用标志位传递选项

C语言传标记位,一般是用一个整数传一个标记位。但如果需要10个标记位的话,要传10个整数,这太麻烦了!可以用其他方法。
一个整数int具有32个比特位,因此我们可以通过比特位来传递选项,不同的比特位代表不同的选项,我们可以定义不同的涵义。一个比特位代表一个选项,每个选项的比特位不能重复。

一个例子:

#include <stdio.h>
// 标记位传参
void show(int flag)
{
    if((flag&ONE) != 0)
    {
        printf("ONE\n");
    }

    if((flag&TWO) != 0)
    {
        printf("TWO\n");
    }

    if((flag&THREE) != 0)
    {
        printf("THREE\n");
    }
 
    if((flag&FOUR) != 0)
    {
    	printf("FOUR\n");   
 	}
}

int main()
{
     printf("------------------\n");
     //打印ONE
     show(ONE);
     printf("------------------\n");
     //打印TWO
     show(TWO);
     printf("------------------\n");
     //打印ONE TWO
     show(ONE | TWO);
     printf("------------------\n");
     //打印ONE TWO THREE
     show(ONE | TWO | THREE);
     printf("------------------\n");
     //打印ONE TWO THREE
     show(ONE | TWO | THREE | FOUR);
     printf("------------------\n");

	 return 0;
}

运行结果:

------------------
ONE
------------------
TWO
------------------
ONE
TWO
------------------
ONE
TWO
THREE
------------------
ONE
TWO
THREE
FOUR
------------------

2. 接口介绍

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 flags, mode_t mode);


pathname: 要打开或创建的目标文件
flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags,这里的传参方式采用的就是上面讲的标志位传参。
参数:
	O_RDONLY: 只读打开
	O_WRONLY: 只写打开
	O_RDWR : 读,写打开
	         这三个常量,必须指定一个且只能指定一个
	O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
	O_APPEND: 追加写
	O_TRUNC:清空文件之前的内容

返回值:
	成功:新打开的文件描述符
	失败:-1

在当前目录没有创建log.txt文件的情况下运行下面的代码:

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

#define FILE_NAME "log.txt"

int main()                                                                                                                                                           
{
    int fd = open(FILE_NAME, O_WRONLY);
    if(fd < 0)
    {
        perror("open");
        return 1;
    }

    close(fd);
    return 0;
}

此时会报错:

open: No such file or directory

这是只以O_WRONLY方式打开文件,如果此时不存在这个文件,系统调用接口默认不会创建该文件,这与C语言有区别。
如果想要在不存在该文件的情况下,打开文件的时候让系统自动创建的话,需要加上O_CREAT

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

#define FILE_NAME "log.txt"

int main()                                                                                                                                                           
{
    int fd = open(FILE_NAME, O_WRONLY | O_CREAT);
    if(fd < 0)
    {
        perror("open");
        return 1;
    }

    close(fd);
    return 0;
}

运行程序,查看创建的log.txt文件,但是发现log.txt文件的权限是乱码,这是因为我们在创建的时候没有给它指明默认权限的话,这时的默认权限就是乱的
在这里插入图片描述
因此创建文件的时候需要使用open的第三个参数,mode_t mode

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

#define FILE_NAME "log.txt"

int main()                                                                                                                                                           
{
    int fd = open(FILE_NAME, O_WRONLY | O_CREAT, 0666);
    if(fd < 0)
    {
        perror("open");
        return 1;
    }

    close(fd);
    return 0;
}

运行程序后,此时创建的log.txt文件的权限就是正确的啦!
在这里插入图片描述
为什么是664呢?这是因为默认权限还要与umask进行运算后才得到最终创建时文件的权限。默认用的是系统提供的umask文件掩码。如果在创建文件的时候改变umask,可以在文件创建之前使用umask(num),将umask暂时修改为num,程序运行完成后umask又恢复为系统默认的值。
实例:

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

#define FILE_NAME "log.txt"

int main()                                                                                                                                                           
{
	umask(0);
    int fd = open(FILE_NAME, O_WRONLY | O_CREAT, 0666);
    if(fd < 0)
    {
        perror("open");
        return 1;
    }

    close(fd);
    return 0;
}

umask修改为0后,创建的文件的权限就是我们所给的权限660
在这里插入图片描述
总结:
open函数具体使用哪个,和具体应用场景相关,如目标文件不存在,需要open创建,则第三个参数表示创建文件的默认权限,否则,使用两个参数的open

2.2 write

  • 头文件:#include <unistd.h>
  • ssize_t write(int fd, const void *buf, size_t count);
  • 其中ssize_t是一种有符号的整数
  • 返回值:如果写入成功,返回成功写入的字节数;失败则返回-1
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>

#define FILE_NAME "log.txt"

int main()                                                                                                                                                           
{
    int fd = open(FILE_NAME, O_WRONLY | O_CREAT, 0666);
    if(fd < 0)
    {
        perror("open");
        return 1;
    }
    int cnt = 5;

    char outBuffer[64];
    while(cnt)
    {
        sprintf(outBuffer, "%s:%d\n", "hello world", cnt--);

        write(fd, outBuffer, strlen(outBuffer) + 1); //向文件当中写入string的时候,要不要+1?                                                                                                                
    }

    close(fd);
    return 0;
}

加1后运行代码,使用vim查看log.txt文件,发现文件中出现了一些乱码,多写了一些东西
在这里插入图片描述
这是因为以\0作为字符串的结尾,是C语言的规定,和文件没有关系!
所以向文件中写入的时候,不要加1,除非呢就想把\0写进去

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

#define FILE_NAME "log.txt"

int main()                                                                                                                                                           
{
    int fd = open(FILE_NAME, O_WRONLY | O_CREAT, 0666);
    if(fd < 0)
    {
        perror("open");
        return 1;
    }
    int cnt = 5;

    char outBuffer[64];
    while(cnt)
    {
        sprintf(outBuffer, "%s:%d\n", "hello world", cnt--);

        write(fd, outBuffer, strlen(outBuffer)); //向文件当中写入string的时候,要不要+1?                                                                                                                
    }

    close(fd);
    return 0;
}

此时运行代码后生成的log.txt就是我们想要的了
在这里插入图片描述
以写方式打开在已经存在的log,txt,重新写入

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

#define FILE_NAME "log.txt"

int main()                                                                                                                                                           
{
    int fd = open(FILE_NAME, O_WRONLY | O_CREAT, 0666);
    if(fd < 0)
    {
        perror("open");
        return 1;
    }
    int cnt = 5;

    char outBuffer[64];
    while(cnt)
    {
        sprintf(outBuffer, "%s:%d\n", "aaaa", cnt--);

        write(fd, outBuffer, strlen(outBuffer));                                                                           
    }

    close(fd);
    return 0;
}

运行代码后查看log.txt文件,发现还有一些前面写入的内容。
在这里插入图片描述
这是因为系统调用以写方式打开已经存在的文件,默认不会清空文件的内容。由于之前的一些内容没有被新写入的内容完全覆盖,所以导致了还有一些之前的内容残留。这与C语言也有区别!
如果想在以写方式代开文件的时候自动清空之前的内容的话,需要加上选项O_TRUNC

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

#define FILE_NAME "log.txt"

int main()                                                                                                                                                           
{
    int fd = open(FILE_NAME, O_WRONLY | O_CREAT | O_TRUNC, 0666);
    if(fd < 0)
    {
        perror("open");
        return 1;
    }
    int cnt = 5;

    char outBuffer[64];
    while(cnt)
    {
        sprintf(outBuffer, "%s:%d\n", "aaaa", cnt--);

        write(fd, outBuffer, strlen(outBuffer));                                                                          
    }

    close(fd);
    return 0;
}

运行代码后查看log.txt文件的内容,这是之前的内容全部被清空了,只有我们新写入的内容。
在这里插入图片描述
使用O_APPEND可以追加写入文件

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

#define FILE_NAME "log.txt"

int main()                                                                                                                                                           
{
    int fd = open(FILE_NAME, O_WRONLY | O_CREAT | O_APPEND, 0666);
    if(fd < 0)
    {
        perror("open");
        return 1;
    }
    int cnt = 5;

    char outBuffer[64];
    while(cnt)
    {
        sprintf(outBuffer, "%s:%d\n", "aaaa", cnt--);

        write(fd, outBuffer, strlen(outBuffer));                                                                          
    }

    close(fd);
    return 0;
}

运行代码,可以发现log.txt文件的内容在本次执行完代码后,在之前的内容的后面又增加我们这次写入的内容。
在这里插入图片描述

2.3 read

  • 头文件:#include <unistd.h>
  • ssize_t read(int fd, void *buf, size_t count);
  • 返回值:如果写入成功,返回成功读取的字节数;失败则返回-1
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include《string.h>

#define FILE_NAME "log.txt"

int main()                                                                                                                                                           
{
    int fd = open(FILE_NAME, O_RDONLY);
    if(fd < 0)
    {
        perror("open");
        return 1;
    }

    char buffer[1024];
    ssize_t num = read(fd, buffer, sizeof(buffer)-1);
    if(num > 0) 
    	buffer[num] = 0;//在最后加上\0,使buffer成为字符串
    printf("%s", buffer);

    close(fd);
    return 0;
}

运行代码,读取文件
在这里插入图片描述

4. 文件描述符fd

4.1 open函数返回值

在认识返回值之前,先来认识一下两个概念: 系统调用库函数

  • 上面的fopenfclosefreadfwrite都是C标准库当中的函数,我们称之为库函数(libc)。
  • openclosereadwritelseek都属于系统提供的接口,称之为系统调用接口
  • 回忆一下我们讲操作系统概念时,画的一张图
    在这里插入图片描述

系统调用接口和库函数的关系,一目了然。
所以,可以认为,f#系列的函数,都是对系统调用的封装,方便二次开发。

文件操作的本质:进程与被打开文件的关系
进程可以打开多个文件,系统中一定会存在大量的被打开的文件,这些被打开的文件需要被OS管理起来。如何管理呢?先描述,再组织

操作系统为了管理对应的打开文件,必定要为文件创建对应的内核数据结构表示文件,这个数据结构就是struct file{ },其中包含了文件的大部分属性。

那么被打开的文件怎么和进程关联起来呢?

  • 通过对open函数的学习,我们知道了文件描述符就是一个小整数
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include《string.h>

#define FILE_NAME "log.txt"

int main()                                                                                                                                                           
{
    int fd0 = open(FILE_NAME(1), O_WRONLY | O_CREAT | O_APPEND, 0666);
    int fd1 = open(FILE_NAME(2), O_WRONLY | O_CREAT | O_APPEND, 0666);
    int fd2 = open(FILE_NAME(3), O_WRONLY | O_CREAT | O_APPEND, 0666);
    int fd3 = open(FILE_NAME(4), O_WRONLY | O_CREAT | O_APPEND, 0666);
    int fd4 = open(FILE_NAME(5), O_WRONLY | O_CREAT | O_APPEND, 0666);
    
    printf("fd: %d\n", fd0);
    printf("fd: %d\n", fd1);
    printf("fd: %d\n", fd2);
    printf("fd: %d\n", fd3);
    printf("fd: %d\n", fd4);
    
    close(fd0);
    close(fd1);
    close(fd2);
    close(fd3);
    close(fd4);
    return 0;
}

运行代码,生成了log.txt1log.txt2log.txt3log.txt4log.txt5五个文件
在这里插入图片描述
fd为什么从3开始的呢?0,1,2呢?

4.2 0 & 1 & 2

  • 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>
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系统调用,所以必须让进程和文件关联起来。
  • 每个进程都有一个指针*files,指向一张表files_struct,该表最重要的部分就是包含一个指针数组,每个元素都是一个指向打开文件的指针!
  • 所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件。
#include <stdio.h>

int main()
{
    printf("stdin->fd: %d\n", stdin->_fileno);
    printf("stdout->fd: %d\n", stdout->_fileno);
    printf("stderr->fd: %d\n", stderr->_fileno);
	
	return 0;
}

运行代码:
在这里插入图片描述

4.3 文件描述符的分配规则

直接看代码:

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

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

    printf("open fd: %d\n", fd);

    close(fd);

    return 0;
}

输出发现是open fd: 3

关闭0或者2,再看

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

int main()
{
    close(0); 
    //close(2);                                                                                                                                                         
    umask(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);

    close(fd);

    return 0;
}

发现是结果是:fd: 0或者fd 2

  • 可见,文件描述符的分配规则:在files_struct数组当中,找到当前没有被使用的 最小的一个下标,作为新的文件描述符.

4.4 重定向

那如果关闭1呢?看代码:

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

int main()
{
    close(1);                                                                                                                                                       
    umask(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);

    close(fd);

    return 0;
}

此时,我们发现,本来应该输出到显示器上的内容,输出到了文件log,txt当中,其中,fd=1
查看log.txt文件的内容,发现什么也没有。
在这里插入图片描述
这是因为向文件和显示器打印的时候,刷新策略不一样,与缓冲区有关,我们需要刷新一下缓冲区。

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

int main()
{
    close(1);                                                                                                                                                       
    umask(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);	
	fflush(stdout);
    close(fd);

    return 0;
}

运行代码:
在这里插入图片描述

  • 这种现象叫做输出重定向。常见的重定向有:>(输出), >>(追加), <(输入)
  • 重定向的本质是上层用的fd不变,在内核中更改fd对应的struct file*指向的地址
    在这里插入图片描述

4.5 使用dup2系统调用

函数原型如下:

#include <unistd.h>

int dup2(int oldfd, int newfd);

1. dup2()的功能:

dup2()将文件描述符oldfd复制到文件描述符newfd,如果newfd指向已打开的文件,则先关闭它。

2. 返回值:
返回值>=0:成功,返回一个新的文件描述符
返回值<0:失败,具体原因查看errno
3. 参数:
oldfd:要创建副本的文件描述符
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;
}

printf是C库当中的IO函数,一般往stdout中输出,但是stdout底层访问文件的时候,找的还是fd:1, 但此时,fd:1下标所表示内容,已经变成了myfile的地址,不再是显示器文件的地址,所以,输出的任何消息都会往文件中写入,进而完成输出重定向。

看看下面这段代码:

#include <stdio.h>              
#include <sys/types.h>          
#include <sys/stat.h>           
#include <fcntl.h>              
#include <unistd.h>             
#include <string.h>             
                               
int main()                      
{                               
	close(1);                     
    int fd=open("test.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);
                                
    printf("%d\n",fd);            
    printf("hello world\n");      
    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;
}

运行上面的程序确实不会输出到屏幕,但是我们查看创建的文件test.txt:

发现原来向屏幕输出的内容,输出到了创建的文件中,并且此文件的fd=1
在这里插入图片描述
上面的重定向程序中使用了fflush()强制刷新缓冲区,如果不用fflush()可以完成重定向吗?

运行后发现文件中并没有任何内容
在这里插入图片描述
因为向屏幕上打印时,缓存区的刷新机制是行刷新,但是重定向到文件中,缓存区的刷新机制是全缓冲,只有当缓冲区满了才刷新缓冲区,所以内容还在缓冲区中并没有刷新到文件中。进程退出,不是也会刷新缓冲区嘛?对的,但是在进程退出前,已经关闭了文件。

5. FILE

  • 因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过fd访问的。
  • 所以C库当中的FILE结构体内部,必定封装了fd

1.来段代码在研究一下:

#include <stdio.h>      
#include <unistd.h>      
#include <string.h>      
      
int main()      
{      
    // c接口      
    printf("hello printf\n");      
    fprintf(stdout, "hello fprintf\n");      
    const char *fputsString = "hello fputs\n";      
    fputs(fputsString, stdout);      
      
    //系统接口      
    const char *writeString = "hello write\n";      
    write(1, writeString, strlen(writeString));                                                                                                                                                                
      
    return 0;      
}      

运行结果:

hello printf
hello fprintf
hello fputs
hello write

2.在程序的最后加上fork(),创建子进程:

#include <stdio.h>      
#include <unistd.h>      
#include <string.h>      
      
int main()      
{      
    // c接口      
    printf("hello printf\n");      
    fprintf(stdout, "hello fprintf\n");      
    const char *fputsString = "hello fputs\n";      
    fputs(fputsString, stdout);      
      
    //系统接口      
    const char *writeString = "hello write\n";      
    write(1, writeString, strlen(writeString));                                                                                                                                                                
    
    fork();
    
    return 0;      
} 

运行结果:

hello printf
hello fprintf
hello fputs
hello write

3.但如果对进程实现输出重定向呢?./myfile > log.txt, 我们发现结果变成了:

hello write
hello printf
hello fprintf
hello fputs
hello printf
hello fprintf
hello fputs

我们发现printffprintffputs(库函数)都输出了2次,而write只输出了一次(系统调用)。为什么呢?肯定和fork有关!

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

总结一下上面两段代码输出结果不一样的原因:

  1. 如果我们没有进行输出重定向>,就看到了4条消息。这是因为stdout 默认使用的是行刷新,在进程fork之前,三条C函数已经将数据进行打印输出到显示器上(外设),FILE内部和进程内部不存在对应的数据啦。
  2. 如果我们进行输出重定向>, 写入文件不再是显示器,而是普通文件,采用的刷新策略是全缓冲,之前的3条c显示函数,虽然带了\n,但是不足以stdout缓冲区写满!数据并没有被刷新!执行fork的时候,stdout属于父进程,创建子进程时, 紧接着就是进程退出!谁先退出,一定要进行缓冲区刷新(就是修改),就会发生写时拷贝!!数据最终会显示两份。
  3. write为什么没有输出打印两次呢?上面的过程都和wirte无关,wirte没有FILE,而用的是fd,就没有C提供的缓冲区。

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

如果有兴趣,可以看看FILE结构体:

typedef struct _IO_FILE FILE;/usr/include/stdio.h
/usr/include/libio.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
};

6. 理解文件系统

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

[root@localhost linux]# ls -l
总用量 12
-rwxr-xr-x. 1 root root 7438 "9月 13 14:56" a.out
-rw-r--r--. 1 root root 654 "9月 13 14:56" test.c

每行包含7列:

  • 模式
  • 硬链接数
  • 文件所有者
  • 大小
  • 最后修改时间
  • 文件名

ls -l读取存储在磁盘上的文件信息,然后显示出来
在这里插入图片描述
其实这个信息除了通过这种方式来读取,还有一个stat命令能够看到更多信息

[root@localhost linux]# stat test.c
  File: "test.c"
  Size: 654 Blocks: 8 IO Block: 4096 普通文件
Device: 802h/2050d Inode: 263715 Links: 1
Access: (0644/-rw-r--r--) Uid: ( 0/ root) Gid: ( 0/ root)
Access: 2017-09-13 14:56:57.059012947 +0800
Modify: 2017-09-13 14:56:40.067012944 +0800
Change: 2017-09-13 14:56:40.069012948 +0800

上面的执行结果有几个信息需要解释清楚

6.1 inode初识

在Linux操作系统中,文件的元信息和内容是分离存储的,其中保存元信息的结构称之为inode,因为系统当中可能存在大量的文件,所以我们需要给每个文件的属性集起一个唯一的编号,即inode号。
也就是说,inode是一个文件的属性集合,Linux中几乎每个文件都有一个inode,为了区分系统当中大量的inode,我们为每个inode设置了inode编号。
在命令行当中输入ls -i,即可显示当前目录下各文件的inode编号。
在这里插入图片描述
注意: 无论是文件内容还是文件属性,它们都是存储在磁盘当中的。

6.2 磁盘的概念

  • 什么是磁盘?

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

  • 磁盘的基本概念

在这里插入图片描述
在这里插入图片描述

  • 磁盘的访问

对磁盘进行读写操作时,一般有以下几个步骤:

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

通过以上三个步骤,最终确定信息在磁盘的读写位置。
在这里插入图片描述

6.3 磁盘分区与格式化介绍

  • 线性存储介质

理解文件系统,首先我们必须将磁盘想象成一个线性的存储介质,想想磁带,当磁带被卷起来时,其就像磁盘一样是圆形的,但当我们把磁带拉直后,其就是线性的。
在这里插入图片描述

  • 磁盘分区

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

计算机为了更好的管理磁盘,于是对磁盘进行了分区。磁盘分区就是使用分区编辑器在磁盘上划分几个逻辑部分,盘片一旦划分成数个分区,不同的目录与文件就可以存储进不同的分区,分区越多,就可以将文件的性质区分得越细,按照更为细分的性质,存储在不同的地方以管理文件,例如在Windows下磁盘一般被分为C盘和D盘等区域。
注意:虽然分成了C盘,D盘等不同区域,但实际上这些盘都属于同一块磁盘,因为计算机上通常只安装了一块磁盘。

在Linux操作系统中,我们也可以通过以下命令查看我们磁盘的分区信息:

[hut@hecs-414761 ~]$ ls /dev/vda* -l

在这里插入图片描述

  • 磁盘格式化

当磁盘完成分区后,我们还需要对磁盘进行格式化。磁盘格式化就是对磁盘中的分区进行初始化的一种操作,这种操作通常会导致现有的磁盘或分区中所有的文件被清除。
简单来说,磁盘格式化就是对分区后的各个区域写入对应的管理信息。
在这里插入图片描述
其中,写入的管理信息是什么是由文件系统决定的,不同的文件系统格式化时写入的管理信息是不同的,常见的文件系统有EXT2、EXT3、XFS、NTFS等。

6.4 EXT2文件系统的存储方案

计算机为了更好的管理磁盘,会对磁盘进行分区。而对于每一个分区来说,分区的头部会包括一个启动块(Boot Block),对于该分区的其余区域,EXT2文件系统会根据分区的大小将其划分为一个个的块组(Block Group)。
在这里插入图片描述

注意: 启动块的大小是确定的,而块组的大小是由格式化的时候确定的,并且不可以更改。

其次,每个组块都有着相同的组成结构,每个组块都由超级块(Super Block)、块组描述符表(Group Descriptor Table)、块位图(Block Bitmap)、inode位图(inode Bitmap)、inode表(inode Table)以及数据表(Data Block)组成。
在这里插入图片描述

  1. Super Block: 存放文件系统本身的结构信息。记录的信息主要有:Data Block和inode的总量、未使用的Data Block和inode的数量、一个Data Block和inode的大小、最近一次挂载的时间、最近一次写入数据的时间、最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了。
  2. Group Descriptor Table: 块组描述符表,描述该分区当中块组的属性信息。
  3. Block Bitmap: 块位图当中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用。
  4. inode Bitmap: inode位图当中记录着每个inode是否空闲可用。
  5. inode Table: 存放文件属性,即每个文件的inode。
  6. Data Blocks: 存放文件内容。

注意

  1. 其他块组当中可能会存在冗余的Super Block,当某一Super Block被破坏后可以通过其他Super Block进行恢复。
  2. 磁盘分区并格式化后,每个分区的inode个数就确定了。
  • 如何理解创建一个空文件?

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

[root@localhost linux]# touch abc
[root@localhost linux]# ls -i abc
263466 abc

为了说明问题,我们将上图简化:
在这里插入图片描述
创建一个新文件主要有一下4个操作:

  1. 存储属性
    内核先找到一个空闲的i节点(这里是263466)。内核把文件信息记录到其中。
  2. 存储数据
    该文件需要存储在三个磁盘块,内核找到了三个空闲块:300,500,800。将内核缓冲区的第一块数据复制到300,下一块复制到500,以此类推。
  3. 记录分配情况
    文件内容按顺序300,500,800存放。内核在inode上的磁盘分布区记录了上述块列表。
  4. 添加文件名到目录
    新的文件名abc。linux如何在当前的目录中记录这个文件?内核将入口(263466,abc)添加到目录文件。文件名和inode之间的对应关系将文件名和文件的内容及属性连接起来。
  • 如何理解对文件写入信息?
  1. 通过文件的inode编号找到对应的inode结构。
  2. 通过inode结构找到存储该文件内容的数据块,并将数据写入数据块。
  3. 若不存在数据块或申请的数据块已被写满,则通过遍历块位图的方式找到一个空闲的块号,并在数据区当中找到对应的空闲块,再将数据写入数据块,最后还需要建立数据块和inode结构的对应关系。

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

  • 如何理解删除一个文件?
  1. 将该文件对应的inode在inode位图当中置为无效。
  2. 将该文件申请过的数据块在块位图当中置为无效。

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

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

因为拷贝文件需要先创建文件,然后再对该文件进行写入操作,该过程需要先申请inode号并填入文件的属性信息,之后还需要再申请数据块号,最后才能进行文件内容的数据拷贝,而删除文件只需将对应文件的inode号和数据块号置为无效即可,无需真正的删除文件,因此拷贝文件是很慢的,而删除文件是很快的。

这就像建楼一样,我们需要很长时间才能建好一栋楼,而我们若是想拆除一栋楼,只需在这栋楼上写上一个“拆”字即可。

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

注意
每个文件的文件名并没有存储在自己的inode结构当中,而是存储在该文件所处目录文件的文件内容当中。因为计算机并不关注文件的文件名,计算机只关注文件的inode号,而文件名和文件的inode指针存储在其目录文件的文件内容当中后,目录通过文件名和文件的inode指针即可将文件名和文件内容及其属性连接起来。

7. 软硬链接

7.1 软链接

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

[hut@hecs-414761 lesson22]$ ln -s hello hello-s

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

7.2 硬链接

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

[hut@hecs-414761 lesson22]$ ln hello hello-h

在这里插入图片描述
通过ls -i -l命令我们可以看到,硬链接文件的inode号与源文件的inode号是相同的,并且硬链接文件的大小与源文件的大小也是相同的,特别注意的是,当创建了一个硬链接文件后,该硬链接文件和源文件的硬链接数都变成了2。
在这里插入图片描述
我们看到,真正找到磁盘上文件的并不是文件名,而是inode。 其实在linux中可以让多个文件名对应于同一个inode。

hello和hello-h的链接状态完全相同,他们被称为指向文件的硬链接。内核记录了这个连接数,inode为144642的硬连接数为2。

我们在删除文件时干了两件事情:

  1. 在目录中将对应的记录删除;
  2. 将硬连接数-1,如果为0,则将对应的磁盘释放。

硬链接文件就是源文件的一个别名,一个文件有几个文件名,该文件的硬链接数就是几,这里inode号为1445642的文件有myproc和myproc-h两个文件名,因此该文件的硬链接数为2。

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

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

我们创建一个普通文件,该普通文件的硬链接数是1,因为此时该文件只有一个文件名。那为什么我们创建一个目录后,该目录的硬链接数是2?
在这里插入图片描述
因为每个目录创建后,该目录下默认会有两个隐含文件...,它们分别代表当前目录和上级目录,因此这里创建的目录有两个名字,一个是test另一个就是该目录下的.,所以刚创建的目录硬链接数是2。通过命令我们也可以看到test和该目录下的.的inode号是一样的,也就可以说明它们代表的实际上是同一个文件。
在这里插入图片描述
小技巧: 一个目录下相邻的子目录数等于该目录的硬链接数减2。

软硬链接的区别

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

8. 文件的三个时间

在Linux当中,我们可以使用命令stat 文件名来查看对应文件的信息。
在这里插入图片描述
这其中包含了文件的三个时间信息

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

当我们修改文件内容时,文件的大小一般也会随之改变,所以一般情况下Modify的改变会带动Change一起改变,但修改文件属性一般不会影响到文件内容,所以一般情况下Change的改变不会带动Modify的改变。
当我们访问文件时,文件的Access可能不会改变,只有当我们多时间内连续多次访问一个文件时,它的Access才会改变。

我们若是想将文件的这三个时间都更新到最新状态,可以使用命令touch 文件名来进行时间更新。

注意: 当某一文件存在时使用touch命令,此时touch命令的作用变为更新文件信息。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值