【Linux】文件基础IO(上)

目录

复习C文件操作接口

标准输入流、标准输出流、标准错误流

 系统文件I/O

open( ):

write( ):

read( )

 文件描述符

磁盘文件和内存文件

文件描述符分配规则

重定向

输出重定向

输入重定向

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

系统调用dup()和dup2()

Linux下一切皆文件



在开始之前我们需要建立一些共识
1.文件=内容+属性
2.文件的内容和属性都是具有数据属性,它们都占具一定内存空间
3.对文件的操作等于文件的内容或文件的属性操作
4.路径是一个文件的唯一标识
5.一个文件只有在打开时才能被访问  ,可执行文件存在磁盘里当,要执行该文件时才会将文件加载到内存中                                                                                               
文件操作就是进程打开文件进行读写删除等操作。

复习C文件操作接口

fopen( ):功能是某种方式打开一个文件,并对该文件进行I/O。

以上程序功能是打开一个名为t.txt的文件(如果该文件不存在,则在当前路径下创建该文件)并向文件写入5次hello world,执行效果如图:

fread()功能:打开文件1t.txt(该文件必须存在),并读取该文件的内容。

执行效果如图:

C语言文件操作:

文件的打开方式总结:

标准输入流、标准输出流、标准错误流

我们常说Linux下一切皆文件,那显示器和键盘也可以看作是文件。显示器显示数据是因为进程向显示器这个文件写入数据。再如,电脑要从键盘获取数据也就是读取键盘文件,那前面我不是说过文件要进行I/O操作,就要先打开相应的文件呢?那为什么我们向显示器写入数据,或向键盘读取数据时却没有进行打开操作呢?
这是因为一个进程一但运行起来,就会默打开三个文件即标准输入流(键盘文件)、标准输出流(显示器文件)以及标准错误流。 

也就是我们编写的C语言程序一运行起来,操作系统就会默认程序会使用这三个文件,直接就会帮进程打开这三个标准流。在我们使用键建盘或显示器时就不用进行打开操作,直接调用类似scanf( )和printf( )的函数向键盘读取数据和向显示器写入数据等操作。也就是说对 三个标准流的操作和我们打开某一交件进行操作是同一个概念,都会获取一个下指针。

例如:我们想要对显示器文件写入数据时 

#include <stdio.h>
int main()
{
	fputs("hello stdin \n", stdout);
	fputs("hello stdin \n", stdout);
	fputs("hello stdin \n", stdout);
	return 0;
}

 系统文件I/O


在C语言中fopen()其实是封装了系统调用open(),与C库函数和其他语言的库函数相比系统调用更接近底层,实际上开发语言的库函数也都是对系统调用行了封装。

 在Linux平台的C语言程序,C库函数封装的就是Linux系统调用,在Windows 平台下的C语言程序封装的就是Windows的系统调用接口。
 

open( ):

        系统接口中使用open函数打开文件,open函数的函数原型如下:

参数:pathname

表示要打开的文件或要创建的文件

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

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

参数:flogs

表示打开该文件的方式

可以传入多个以下参数选项进行或运算,构成flags

 例如:O_WRONLY |  O_CREAT

以只写的方式打开文件,但当目标文件不存在时自动创建文件。

参数:mode

表示创建的文件的默认权限。

例如,将mode设置为0666,则文件创建出来的权限为:-rw-rw-rw-

但实际上创建出来文件的权限值还会受到umask(文件默认掩码)的影响,实际创建出来文件的权限为:mode&(~umask)。umask的默认值一般为0002,当我们设置mode值为0666时实际创建出来文件的权限为0664,也就是-rw-rw-r--

若想创建出来文件的权限值不受umask的影响,则需要在创建文件前使用umask函数将文件默认掩码设置为0。

umask(0); //将文件默认掩码设置为0;

        当不需要创建文件时参数mode可以不用设置。

open()的返回值

open()的返回值是新打开的文件的描述符。

当我们打开多个文件时,再输出文件的描述符时发现,文件描述符是从3开始且连续递增的。 

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

当我们尝试打开一个不存在的文件时,也就是open()打开文件一定会失败。

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

 运行程序后发现,打开文件失败时open()的返回值是-1,也就是文件描述符是-1。

实际上所谓的文件描述符本质上是一个指针数组的下标,指针数组当中的每一个指针都指向一个被打开文件的文件信息,通过对应文件的文件描述符就可以找到对应的文件信息。当使用open()打开文件成功时数组当中的指针个数增加,然后将该指针在数组当中的下标进行返回,而当文件打开失败时直接返回-1,因此,成功打开多个文件时所获得的文件描述符就是连续且递增的。而进程默认情况下会有3个缺省打开的文件描述符,分别就是标准输入0、标准输出1、标准错误2,这就是为什么成功打开文件时所得到的文件描述符是从3开始进程分配的。
 

 而现在知道,文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件。

close():

 关闭文件,参数是文件描述符。

传入相应文件的文件描述符,关闭相应的文件。关闭文件成功则返回0,关闭文件失败则返回-1。

write( ):

函数原型:

 使用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");
		return 1;
	}
	const char* msg = "hello syscall\n";
	for (int i = 0; i < 5; i++){
		write(fd, msg, strlen(msg));
	}
	close(fd);
	return 0;
}

read( )

从文件中读取信息,函数原型:

 使用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");
		return 1;
	}
	char ch;
	while (1){
		ssize_t s = read(fd, &ch, 1);
		if (s <= 0){
			break;
		}
		write(1, &ch, 1); //向文件描述符为1的文件写入数据,即向显示器写入数据
	}
	close(fd);
	return 0;
}

当执行可执行文件时,我们可以看到显示器打印出文件内容。 

 文件描述符


进程调用系统接口open( )时,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);
	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;
}

一个进程可以打开多个文件,系统中又会同时存在多个进程,打开着多个文件,操统要管理这些已经打开的文件,操作系统会为每个被打开的文件创建一个用于描述这个文件的struct_file结构体,之后将这些结构体以双链表这一数据结构连接起来,对文件的管理就变成了对这一双链表的增删查改。而为了区分哪一个文件是由哪一个进程打开的,还需要建立进程与进程打开的文件的对应关系。


那进程与进程打开的文件的关系如何构建呢?
通过前面的学习我们知道,当一个程序在操作系统运行起来时,操作系统已经将该程序的代码和数据加载到内存中了、并创建相应的task_struct,mm_struct、映射表等相关的数据结构。
在进程stask_struct中有一个指针指向files_struct的结构体,该结构体中有一个指针数组fd_array,该数组的下标就是文件描述符,该数组是指针数组,存放的是FILE * 类型的指针,该指针又指向文件在内存中的位置。

 

所以我们只需要知道某一文件的文件描述符,就能找到该文件的相关信息,从而对该文件进行I/O操作等。

在前面我们知道一个进程创建出来时会默认打开三个文件分别是:标准输入流(键盘文件)、标准输出流(显示器文件)以及标准错误流。他们都有各自的struct_file结构体,进程管理文件的数组的下标为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的位置已经被准输入流、标准输出流和标准错误流占用了,所以只能从3开始进行分配。这三个文件和普通文件一样可以被关闭,如close(0)关闭标准输入流。

#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
int main()
{
	umask(0);
    closu(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;
}

这时空出的数组下标就给别的文件用,也就是说下标为0的位置的元素指向了另一个文件。

 

close(1):

通过以上程序可以知道,文件描述符是从0往上递增的,若关闭标准输出流则在程序执行时看不到显示器输出文件描述符。 

 结论: 文件描述符是从0开始的,没有被使用的fd_array数组下标开始进行分配的。其增删和我们常见的数组是一样的,是连续的。

重定向

在了解文件描述符的概念及分配规则后,我们可以来学习重定向操作了。

重定向原理:
输出重定向是指将程序的输出结果从标准输出流(通常指显示器)重定向到其他文件,如文本文件、管道或其他程序的输入流。在Linux中输出重定向使用大于号(>)和双大于号(>>)来实现。将程序的输出结果写入到文本文件中。

输出重定向

        例如以下程序,当我们直接执行程序的时,执行结果会直接输出到显示器,我们创建一个空文件,文件里没有什么内容,把执行结果重定向到t12.txt,重新执行时显示器并没有输出结果,再查看文件t12.txt,这时会看到程序的执行结果。

 使用单大于号>重定向的内容会覆盖原文件的内容(就是覆盖重定向到的文件),如果想要将重定向的内容附加到文件末尾,可以使用双大于号>>,即追加重定向。

输入重定向

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

 例如下面的程序,我们将文件描述符为0的文件关闭,再打开t12.txt文件,这时文件t12.txt就分配到0这个文件描述符,这时本应该从键盘文件读取数据的scanf函数,改为从t12.txt文件中读取数据。而scanf()是默认从stdin读取数据的,而stdin指向的FILE结构体中存储的文件描述符是0,因此scanf()实际上就是向文件描述符为0的文件读取数据。

 

 运行结果后,我们发现scanf()将t12.txt文件当中的数据都读取出来了。

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

标准输出流和标准错误流都是用于向屏幕显示器输出信息的流,但它们有以下区别:标准输出流(stdout)用于输出普通的程序运行结果,而标准错误流(stderr)用于输出程序运行中的错误信息。标准输出流通常是缓冲的,也就是说程序在输出内容时会先把内容存放在缓冲区中,等到缓冲区满了或者遇到换行符时才会输出到屏幕。而标准错误流不会被缓冲,程序在输出错误信息时会立即显示出来。标准输出流和标准错误流可以分别被重定向到不同的地方。比如,可以把标准输出流重定向到一个文件中,而把标准错误流输出到屏幕上。

 在以上程序的执行结果看起来标准输出流和标准错误流并没有区别,都是向显示器输出数据。但我们若是将程序运行结果重定向输出到文件t13.txt当中,我们会发现标准错误流的信息仍然会向显示器写入,而标准输出流的信息则是写入了t13.txt文件中。

 总的来说,标准输出流和标准错误流都是用于向屏幕显示器输出信息的流,但它们输出的内容和输出的方式有所不同。


系统调用dup()和dup2()


在实际开发中我们很少以上面的方去实现数据的重定向而是通过调用dup()和dup2()实现。
 

dup ()和dup2()

函数原型:

 其中dup()分配一个新的最小的系统中未使用的描述符,而dup2(oldfd,newfd),则会将旧的描述符拷贝到新的描述符,也就是newfd和oldfd会共享文件的偏移和状态值,要注意的是;dup2()会先执行flose(newfd),切断newfd与原文件的联系,然后映射到oldfd所打开的文件的文件上,但当oldfd是一个无效的文件描述符时,dup2()调用失败,不会执行close(newfd)。

返回值:

调用成功返回newfd,否则返回-1。
使用用dup2()时
1、如果oldfd不是有效的文件描述符、则dup2()调失败,且不会关闭文件描述符为newfd的文件。
2、如果oldfd是一个有效的文件描述符,但newfd和oldfd具相同的值 ,不会执行close(newfd),并返返回newfd。
例如,我们在程序中创建一个空文件dup2.txt,获取该文件的文件描述符fp,这时调用dup2(fd,1),1为标准输出流的文件描述符,这时就实现了将程序的执行结果重定向到dup2.txt文中。

Linux下一切皆文件

"一切皆文件"是Linux操作系统的一个核心理念,它意味着在Linux系统中,所有输入/输出设备、进程等都可以以文件的方式表示和访问。这个概念的好处是,它使得对于不同类型的资源,我们可以使用相同的操作方式进行操作,从而使得系统变得更加简洁和易于管理。例如,如果要读取一个文件,我们可以使用标准的文件读取函数,而如果要读取输入设备(如键盘),我们可以读取标准输入文件。同样,如果要向终端打印输出,我们可以使用标准的输出文件,如果要将输出重定向到文件,则可以将输出文件设置为目标文件。

        首先,在windows中的文件,它们在linux中也是文件,其次在windows中不是文件的, 比如进程, 磁盘, 显示器等硬件在Linux中都被抽象成了文件,甚至一些很离谱的东西,比如管道,也是文件。不管是我们常说的文本文件,还是硬件文件操作系统都会用一个struct file类型的结构件去描述相应的文件,这个类型的结构体非常复杂里面包含文件的各种属性,如是否可以被执行、是否可以被写入等各种信息还有对应的读写函数指针,对硬件的操作主要是读写操作,如在我们用户角度看键盘是用来向操作系统写入输入数据的,在操作系统角度是无法向键盘写入数据的,操作系统只能读取键盘。站在操作系统的角度来看下层,驱动层和硬件层在它看来就是一个struct file结构体,操作系统通过维护这个结构体来控制各种硬件。站在操作系统的角度来看上层,用户层以及系统调用在它看来就是一个个的进程,是一个个的task_struct结构体,操作系统也是通过维护这个结构体来调度各个进程的。
真正的文件在操作系统中的体现也是结构体,操作系统维护的同样是被打开文件的结构体而不是文件本身。
作者只是一个进阶的新手,在这个知识点上无站在很高的高角度去讲解。
 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值