上次介绍了:Linux:进程控制(二.详细讲解进程程序替换)
文章目录
- 1.基础认识
- 2.再识c语言中文件接口
- 3.三个默认打开的文件流
- 4.相关系统接口
- 5.文件描述符
-
- [5.1 0、1、2](#5.1 0、1、2)
- 5.2底层
- 6.文件描述符的分配规则
- 7.Linux中一切皆文件
1.基础认识
-
文件是存储在磁盘或其他存储介质上的数据集合,包括数据内容和文件属性。
-
在操作系统中,文件的操作通常需要通过进程来打开文件才进行,进程在打开文件时会创建一个文件描述符,用于标识这个文件。
-
在访问文件之前,通常需要先打开文件。通过打开文件,进程可以获取文件的句柄或文件描述符,然后可以通过读取、写入、修改文件内容来进行文件操作。文件的修改通常是通过执行相应的代码来实现的,比如写入数据、修改文件属性等。
-
在操作系统中,进程在打开文件时会获得一个文件描述符,这个文件描述符是进程访问该文件的标识符。一个进程可以打开多个文件,每个打开的文件都会有一个对应的文件描述符。这意味着一个进程可以同时访问多个文件,进行读取、写入等操作。
-
当一个文件被打开时,通常会将文件的部分或全部内容加载到内存中,以便进程可以直接访问和操作文件内容。这样可以提高文件的访问速度和效率。
-
在系统中,一个进程能打开多个文件。还有可能会存在多个进程同时打开多个文件
所以,打开的文件OS一定要对其进行管理,怎么管理------先描述再组织:那么内核中一定有用来描述被打开文件的结构体,并用它来定义一个个对象
-
操作系统中,并不是所有的文件都会被进程打开。实际上,系统中可能存在大量的文件,但并不是所有的文件都会被进程打开并加载到内存中进行操作。有些文件可能处于未打开状态,即它们仅存在于磁盘中,没有被任何进程打开
2.再识c语言中文件接口
2.1fopen()与fclose()
当在 C 语言中进行文件操作时,fopen() 和 fclose() 是两个非常重要的函数。下面我将详细讲解它们的作用和用法:
fopen()
:该函数用于打开一个文件,并返回一个指向 FILE 结构体的指针,该指针用于后续的文件操作。- 语法:FILE *fopen(const char *filename, const char *mode);
- 参数 :
- filename:要打开的文件的路径和名称。
- mode:打开文件的模式,包括只读、只写、读写等不同选项。
- 返回值 :
- 如果成功打开文件,则返回指向 FILE 结构体的指针。
- 如果打开文件失败,则返回 NULL。
mode 参数。mode 参数控制文件的打开方式,包括读取、写入、追加等不同选项。下面是各种模式的含义和用法:
- "r":只读模式
- 打开文件以供读取。如果文件不存在,打开操作将失败。
- 如果文件不存在,则返回 NULL。
- "w":只写模式
- 打开文件以供写入。如果文件存在,则会被截断(即文件内容会被清空);如果文件不存在,则会创建一个新文件。
- 如果文件打开成功,则返回指向文件的指针。
- "a":追加模式
- 打开文件以供写入,但是不会截断文件。新的数据会被追加到文件末尾。
- 如果文件打开成功,则返回指向文件的指针。
- "r+":读写模式(文件必须存在)
- 打开文件以供读取和写入。文件必须存在,否则打开操作将失败。
- 如果文件打开成功,则返回指向文件的指针。
- "w+":读写模式(文件不存在则创建)
- 打开文件以供读取和写入。如果文件存在,则会被截断;如果文件不存在,则会创建一个新文件。
- 如果文件打开成功,则返回指向文件的指针。
- "a+":读写模式(追加模式,文件不存在则创建)
- 打开文件以供读取和写入,不会截断文件。新的数据会被追加到文件末尾。
- 如果文件打开成功,则返回指向文件的指针。
c
#include<stdio.h>
int main()
{
FILE* f = fopen("./test.txt", "w");//打开
if (f == NULL)
{
perror("fopen");
return 1;
}
//在打开关闭之间,我们能进行文件操作
fclose(f);//关闭
return 0;
}
- fclose() 函数:
- fclose() 函数用于关闭一个已打开的文件,释放文件资源并刷新缓冲区。
- 语法:int fclose(FILE *stream);
- 参数:
- stream:指向已打开文件的 FILE 结构体指针。
- 返回值:
- 如果成功关闭文件,则返回 0。
- 如果关闭文件失败,则返回 EOF。
2.2文件操作函数
不带路径时,都默认是当前路径。因为进程在启动的时候,会自动记录自己启动时所在的路径
如果使用chdir()函数的话,就会改变
chdir()
函数用于更改当前工作目录:
- 函数原型:int chdir(const char *path);
- 功能:将当前工作目录更改为指定的目录。
- 参数:
path
是一个字符串,表示要更改到的目录路径。- 返回值:如果成功,则返回 0;如果失败,则返回 -1。
-
fprintf()
:向文件写入格式化数据- 函数原型:int fprintf(FILE *stream, const char *format, ...);
- 功能:将格式化的数据写入到指定文件中。
- 示例:fprintf(file, "Hello, World!");
-
fscanf()
:从文件读取格式化数据- 函数原型:int fscanf(FILE *stream, const char *format, ...);
- 功能:从指定文件中读取格式化的数据。
- 示例:fscanf(file, "%d %s", &num, str);
-
fputc()
:向文件写入一个字符- 函数原型:int fputc(int c, FILE *stream);
- 功能:将一个字符写入到指定文件中。
- 示例:fputc('A', file);
-
fgetc()
:从文件读取一个字符- 函数原型:int fgetc(FILE *stream);
- 功能:从指定文件中读取一个字符。
- 示例:char ch = fgetc(file);
-
fgets()
:用于从指定文件中读取一行数据,并将其存储到指定的缓冲区中- 函数原型:char *fgets(char *str, int num, FILE *stream);
- 功能:从指定文件中读取一行数据。
- 示例:fgets(buffer, sizeof(buffer), file);
-
fputs()
:向文件写入一行数据- 函数原型:int fputs(const char *str, FILE *stream);
- 功能:将一行数据写入到指定文件中。
- 示例:fputs("Hello, World!", file);
-
fwrite()
是 C 语言标准库中用于将数据块写入文件的函数。csize_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
ptr
:指向要写入的数据的指针。size
:要写入的每个数据项的大小(以字节为单位)。nmemb
:要写入的数据项的数量。stream
:指向要写入的文件的指针。
c
#include<stdio.h>
int main()
{
FILE* f = fopen("./test.txt", "w");//打开
if (f == NULL)
{
perror("fopen");
return 1;
}
//在打开关闭之间,我们能进行文件操作
const char* str = "this's test.txt";
fputs(str, f);
fclose(f);//关闭
return 0;
}
以w方式打开时,文件首先会被清空,这也就是为什么我们看不到新的一个
this's test.txt
,相当于覆盖了
- 我们之前看到的输出重定向
>
:也是直接覆盖(默认也是以w方式打开的)
使用a方式打开(append)
c
int main()
{
FILE* f = fopen("./test.txt", "a");//打开
if (f == NULL)
{
perror("fopen");
return 1;
}
//在打开关闭之间,我们能进行文件操作
const char* str = "this's test.txt\n";
fputs(str, f);
fclose(f);//关闭
return 0;
}
这次我们使用a方式来打开文件,会在文件末尾处开始写入,不会覆盖而是追加
- 使用
>>
符号进行输出重定向时,会以追加模式打开文件,新的内容会被追加到文件末尾而不会清空原有内容
3.三个默认打开的文件流
在标准C库中,有三个默认打开的文件流,它们分别是:
stdin
:标准输入流,通常用于从键盘设备读取输入。stdout
:标准输出流,通常用于向显示器设备输出信息。stderr
:标准错误流,通常用于向控制台输出错误信息。这三个文件流在程序启动时会自动打开,不需要显式地打开或关闭
stdin
、stdout
和 stderr
是标准C库中定义的全局变量,它们分别代表标准输入流、标准输出流和标准错误流。这些变量通常在 <stdio.h>
头文件中声明,可以直接使用。
-
stdin
:stdin
是标准输入流,通常用于从用户输入设备(如键盘)读取数据。- 在程序启动时,
stdin
会自动关联到标准输入设备,通常是键盘。
-
stdout
:stdout
是标准输出流,通常用于向用户输出设备(如屏幕)输出数据。- 在程序启动时,
stdout
会自动关联到标准输出设备,通常是屏幕。
c#include <stdio.h> #include <string.h> #include <sys/types.h> #include <unistd.h> #define FILENAME "log.txt" int main() { printf("hello printf\n"); // 使用 printf 函数向标准输出流输出字符串 fputs("hello fputs\n", stdout); // 使用 fputs 函数向标准输出流输出字符串 const char* msg = "hello fwrite\n"; fwrite(msg, 1, strlen(msg), stdout); // 使用 fwrite 函数向标准输出流输出字符串 fprintf(stdout, "hello fprintf\n"); // 使用 fprintf 函数向标准输出流输出格式化字符串 return 0; }
printf("hello printf\n");
(我们经常用):
- 函数原型:
int printf(const char *format, ...);
printf
是标准C库中的函数,用于向标准输出流(stdout
)输出格式化字符串。- 在这里,
printf
输出了字符串 "hello printf" 到标准输出流,并在末尾添加一个换行符。
fputs("hello fputs\n", stdout);
:
- 函数原型:
int fputs(const char *str, FILE *stream);
fputs
是标准C库中的函数,用于向指定文件流(这里是stdout
,即标准输出流)输出字符串。- 在这里,
fputs
输出了字符串 "hello fputs" 到标准输出流,并在末尾添加一个换行符。
fwrite(msg, 1, strlen(msg), stdout);
:
- 函数原型:
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
fwrite
是标准C库中的函数,用于向指定文件流输出指定数量的字节。- 在这里,
fwrite
输出了msg
指向的字符串 "hello fwrite" 到标准输出流,并指定输出字符串的长度。
fprintf(stdout, "hello fprintf\n");
:、
- 函数原型:
int fprintf(FILE *stream, const char *format, ...);
fprintf
是标准C库中的函数,用于向指定文件流输出格式化字符串。- 在这里,
fprintf
输出了格式化字符串 "hello fprintf" 到标准输出流。
-
stderr
:stderr
是标准错误流,通常用于向用户输出设备输出错误信息。- 在程序启动时,
stderr
会自动关联到标准错误设备,通常也是屏幕。 - 您可以使用
fprintf(stderr, ...)
等函数向stderr
输出错误信息。
我们上面在进行相关操作时,会发现中间必然要访问硬件。那这就说明OS一定提供了相关的系统调用接口
4.相关系统接口
4.1open()
在2号手册,说明是系统调用接口
open
函数是用于打开文件的系统调用函数。它的原型如下:
c
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
pathname
是要打开的文件的路径名,flags
是打开文件的标志,mode
是文件的权限。open
函数==返回一个文件描述符(file descriptor)==用于后续对文件的读写操作。打开失败的话返回-1(不需要创建文件时,就使用两个参数就好了)
flags
参数可以是以下标志的组合(都是一个个宏):
O_RDONLY
:只读O_WRONLY
:只写O_RDWR
:读写O_CREAT
:如果文件不存在则创建O_TRUNC
:如果文件存在则截断为0长度,就像之前的wO_APPEND
:追加写入,就像之前的a这些宏都只有一个比特位为1,其余为0
mode
参数指定了文件的权限,通常与 S_IRUSR
、S_IWUSR
等宏一起使用。如果创建文件时不加上权限,那么创建出来的文件权限是乱码。这就需要我们如果创建了文件,就要给上文件的权限。经常使用格式:0666(与我们之前讲的umask一样,0可以看成一个格式要求),但是这样创建后的权限不是最终权限,我们使用0666
后,还要收到掩码的修改
c
#include <stdio.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 == -1)
{
perror("open");
return 1;
}
return 0;
}
如果想要创造一个自己指明权限的文件,可以使用umask()
函数
4.2umask()函数
umask
函数是一个系统调用,用于设置进程的文件创建屏蔽字(file mode creation mask)。文件创建屏蔽字是一个权限掩码,用于确定新建文件的默认权限。在创建新文件时,系统会根据进程的文件创建屏蔽字来屏蔽一些权限位,以确保新建文件不会拥有过于宽松的权限。
umask
函数的原型:
c
#include <sys/stat.h>
mode_t umask(mode_t mask);
umask
函数接受一个参数 mask
,该参数是一个权限掩码,用于指定要屏蔽的权限位。umask
函数会返回之前的文件创建屏蔽字。通常,umask
函数会在创建文件之前调用,以确保新建文件不会拥有不必要的权限。
umask
函数的作用是进程级别的,它只影响调用该函数的进程及其子进程,不会对其他进程产生影响。
4.3close()、write()、read()
- close()
close()
函数用于关闭一个已打开的文件描述符。文件描述符是一个非负整数,用于在程序中唯一标识打开的文件、设备或其他输入/输出资源。当你打开一个文件时,系统会分配一个文件描述符给你,你可以通过这个描述符来读写文件。当你完成对文件的操作后,应该使用 close()
函数来关闭它,以释放系统资源。
函数原型如下:
c
#include <unistd.h>
int close(int fd);
其中 fd
是要关闭的文件描述符。如果成功,close()
返回 0;如果失败,返回 -1 并设置全局变量 errno
以指示错误原因。
- write()
write()
函数用于向打开的文件描述符写入数据。你可以使用它向普通文件、设备文件或套接字写入数据。
也是从文件的开头写
函数原型如下:
c
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
fd
是要写入的文件描述符。buf
是一个指向要写入数据的缓冲区的指针。count
是要写入的数据的字节数。
write()
函数返回实际写入的字节数。在成功时,返回值通常等于 count
,除非到达文件的末尾或发生其他错误。如果发生错误,write()
返回 -1 并设置 errno
。
c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include<string.h>
int main()
{
int fd = open("test.txt", O_WRONLY | O_CREAT, 0666);
if (fd == -1)
{
perror("open");
return 1;
}
const char* str = "hellow write\n";
write(fd, str, strlen(str));
close(fd);
return 0;
}
- read()
read()
函数用于从文件描述符中读取数据。它的函数原型如下:
c
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
fd
是要读取的文件描述符。buf
是一个指向存储读取数据的缓冲区的指针。count
是要读取的字节数。
read()
函数会尝试从文件描述符 fd
对应的文件中读取 count
个字节的数据,并将读取的数据存储到 buf
指向的内存缓冲区中。函数返回值是实际读取的字节数。如果返回值为 0,则表示已经到达文件末尾;如果返回值为 -1,则表示读取出现错误。
以下是一个简单的示例,演示如何使用 read()
函数从文件中读取数据:
c
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main() {
int fd = open("example.txt", O_RDONLY);
if (fd == -1) {
perror("open");
return 1;
}
char buffer[100];
ssize_t bytes_read = read(fd, buffer, sizeof(buffer));
if (bytes_read == -1) {
perror("read");
close(fd);
return 1;
}
printf("Read %zd bytes: %s\n", bytes_read, buffer);
close(fd);
return 0;
}
5.文件描述符
方才我们使用的open()会返回一个整数fd,就是文件描述符
后面write()与close()也都要使用fd,也就是说,我们的操作系统是只认识文件描述符的
文件描述符是一个整数,用于在操作系统中唯一标识一个被打开的文件、设备或其他I/O资源
5.1 0、1、2
Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2.
0、1、2对应的物理设备一般是:键盘,显示器,显示器
所以输入输出还可以采用如下方式:
c
write(1, buf, strlen(buf));
这是大家可能会问? 刚才我们才说才c语言里stdin
、stdout
与stderr
,这三个FILE*
是标准输入, 标准输出,与标准错误
那上面这0、1、2是怎么回事?
我们可以知道,一定是FILE 这个类型的结构体里面一定封装了文件描述符。三者里一定分别封装了0、1、2*
c
int main()
{
printf("%d", stdin->_fileno);
printf("%d", stdout->_fileno);
printf("%d", stderr->_fileno);
return 0;
}
5.2底层
在 Linux 内核中,已经打开的文件结构体通常被称为文件描述符表(File Descriptor Table)中的表项,每个表项对应一个已经打开的文件。这些表项存储在内核内存中,而不是用户进程的内存空间中。
当进程打开文件时,内核会在文件描述符表中为该文件分配一个表项,并将相应的信息存储在表项中。当进程需要读取或写入文件时,内核会根据文件描述符找到对应的文件描述符表项,然后进行相应的操作。
文件描述符的本质:就是数组下标。我们使用open来得到下标,后续也是使用下标来进行write和close操作
进程你怎么知道你打开了哪些文件呢?
每打开一个文件,数组便要指向一个
文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file
结构体。表示一个已经打开的文件对象。而进程执行open
系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*files
, 指向一张表files_struct
,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件
结构体里缓冲区通常用于存储文件数据的临时缓冲区,用于读取或写入文件内容。这个缓冲区不是指操作系统的缓冲区,也不是C语言标准库中的缓冲区
6.文件描述符的分配规则
fd的分配规则:最小的没有被使用的数组下标,会分配给最新打开的文件!
c
int main()
{
close(1);
int fd = open("test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
printf("%d\n", fd);
return 0;
}
printf()函数默认就是向标准输出流打印了,但是现在我们关闭了,而且新打开的test.txt文件的fd为1,代替了标准输出流的位置,printf就向test.txt中写入了
重定向---dup2()系统调用
dup2()
是一个系统调用,用于复制文件描述符。它的原型如下:
c
#include <unistd.h>
int dup2(int oldfd, int newfd);
dup2()
系统调用的作用是将oldfd
文件描述符复制到newfd
文件描述符处。如果newfd
已经打开,则会先关闭newfd
,然后将oldfd
复制到newfd
处;如果newfd
等于oldfd
,则dup2()
不会关闭oldfd
,但会返回newfd
。- 这个系统调用通常用于重定向标准输入、标准输出和标准错误流,例如将一个文件描述符复制到标准输出流(文件描述符 1)或标准错误流(文件描述符 2)。
c
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
int fd = open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
dup2(fd, 1);
printf("hellow log.txt\n");
return 0;
}
7.Linux中一切皆文件
在用户层面上,我们直接调用read函数后,函数内部是会调用结构体里面的函数指针,所以就大用了各种设备的函数了
在Linux中,"一切皆文件"(Everything is a file)是一个重要的概念,它体现了Linux操作系统的设计哲学。这个概念的核心思想是,Linux将所有设备、进程、网络连接、管道等抽象概念都视为文件,并通过文件系统的方式来管理和访问它们。
虚拟文件系统:Linux中的虚拟文件系统(Virtual File System,VFS)将不同类型的文件系统(如ext4、NTFS、procfs等)抽象成统一的文件接口,使得用户和应用程序可以以统一的方式访问不同的文件系统。
通过将所有这些不同的概念都视为文件,Linux提供了一种统一的接口和一致的操作方式,使得用户和开发者可以更加方便地管理和操作系统中的各种资源。
- 虚拟文件系统(VFS) :
- Linux 内核中有一个虚拟文件系统(VFS),它提供了一个抽象层,使得不同类型的文件系统(如 ext4、NTFS 等)能够以统一的方式被内核访问。
- VFS 为所有文件提供了统一的接口,包括打开文件、读写文件、关闭文件等操作。
- 文件描述符 :
- Linux 中每个进程都有一个文件描述符表,用于跟踪打开的文件和设备。
- 标准输入流
stdin
、标准输出流stdout
、标准错误流stderr
分别对应文件描述符 0、1、2。
- 系统调用 :
- Linux 提供了一系列系统调用(如
open()
、read()
、write()
、close()
等),用于在用户空间和内核空间之间进行文件操作。 - 用户程序可以通过系统调用来打开设备文件、读取设备数据、写入设备数据等。
- Linux 提供了一系列系统调用(如
好啦大家,今天就到这里啦。感谢大家支持!!!