文章目录
在 Linux 系统中 “一切皆文件”
它意味着系统中的各种资源,无论是普通的文本文件、二进制文件这样的常规数据存储形式,还是硬件设备(如硬盘、键盘、鼠标、打印机等)、系统中的各种进程、网络套接字、管道等,都可以被抽象地看作是文件,并通过统一的文件操作接口(如 open、read、write、close 等函数)来进行访问和管理
一 文件是什么
普通数据文件
- 文本文件:
由一系列可被人类直接阅读的字符按照特定编码(如ASCII码、UTF-8等)组成,像.txt
文件、程序源代码文件(如.c
、.java
等)、配置文件(如.conf
、.ini
等)都属于此类。可以使用文本编辑器(如Vim
、gedit
等)打开查看并编辑其内容,在存储上是以字符对应的编码字节依次排列存储在磁盘等介质上。 - 二进制文件:
以二进制编码形式存储数据,内容无法直接通过文本形式读懂,像可执行程序(无扩展名或者有特定扩展名的可执行文件,如.out
等)、图像文件(如.jpg
、.png
等)、音频文件(如.mp3
、.wav
等)、视频文件(如.mp4
、.avi
等)都属于二进制文件。它们需要借助相应的软件来解析和处理,例如通过图像查看器查看图像文件内容,利用音频播放器播放音频文件等,其在磁盘上是按照相应的二进制数据格式规范进行存储,存储结构相对复杂,与具体的文件类型所要求的编码格式紧密相关。
二 文件是谁打开的
在Linux系统中,文件通常由以下几种主体来打开:
进程
- 原理:
进程是正在运行的程序的实例,当一个进程需要访问文件中的数据,或者要向文件写入数据等操作时,它会通过系统调用(如open
系统调用)来打开文件。在执行open
调用时,进程需要指定文件名以及打开的模式(例如只读模式O_RDONLY
、只写模式O_WRONLY
、读写模式O_RDWR
等,还可以结合一些其他标志位来指定更详细的打开特性,比如是否创建新文件、是否追加数据等)。内核收到进程的open
请求后,会根据指定的信息查找对应的文件(如果存在),进行权限验证等一系列操作,若一切顺利则返回一个文件描述符(一个非负整数)给进程,之后进程就可以凭借这个文件描述符通过其他系统调用(如read
、write
、close
等)来对文件进行读写操作以及最终关闭文件的操作。 - 示例:
比如一个简单的C语言程序,要读取一个文本文件中的内容,代码示例如下:
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
int fd;
char buffer[100];
// 使用open系统调用打开文件,这里假设文件名为test.txt,以只读模式打开
fd = open("test.txt", O_RDONLY);
if (fd == -1) {
perror("open error");
return 1;
}
// 使用read系统调用读取文件内容到buffer中
ssize_t bytesRead = read(fd, buffer, sizeof(buffer));
if (bytesRead == -1) {
perror("read error");
close(fd);
return 1;
}
// 对读取到的内容可以进行相应处理,这里简单打印出来
buffer[bytesRead] = '\0';
printf("Read content: %s", buffer);
// 使用close系统调用关闭文件
close(fd);
return 0;
}
在这个示例中,就是由 main
这个进程通过 open
系统调用打开名为 test.txt
的文件,后续进行读取和关闭等操作。
用户
- 命令行交互场景下:
当用户在终端输入命令来操作文件时,实际上也是间接通过相关进程来打开文件的。例如,用户输入cat test.txt
命令(cat
命令用于查看文件内容),此时系统会启动一个新的进程来执行cat
命令,这个进程内部会调用open
等相关系统调用去打开test.txt
文件,然后读取并输出文件内容到终端,最后关闭文件。同样,像用户使用vim
编辑器打开一个文件进行编辑时,vim
程序启动相应的进程,该进程会打开指定的文件,让用户可以在编辑器界面中对文件内容进行修改等操作,完成编辑后关闭文件。 - 图形界面环境下:
在Linux的图形界面中,当用户通过文件管理器(如Nautilus
等)双击打开一个文件时,文件管理器背后对应的进程会根据文件类型调用相应的程序来打开文件。比如双击打开一个.jpg
图像文件,文件管理器会启动一个图像查看程序对应的进程(如eog
等),这个图像查看程序的进程会打开该图像文件进行显示,完成查看后关闭文件。
三 进程打开文件的相关的接口
c语言标准库相关文件接口
1. fopen
函数
- 功能:
用于打开一个文件,并返回一个指向该文件的文件指针(FILE *
类型),后续可通过这个指针来调用其他函数对文件进行读写等操作。它相较于前面提到的系统调用层面的open
接口,在使用上更加方便、灵活,提供了一种更符合C语言风格的文件操作方式。 - 头文件及原型:
在<stdio.h>
头文件中定义,原型如下:
FILE *fopen(const char *filename, const char *mode);
其中,filename
是要打开的文件的名称(可以是相对路径或绝对路径),mode
是打开文件的模式字符串,常见的模式有:
- "r"
:以只读方式打开文件,文件必须存在,用于读取文件内容。
- "w"
:以只写方式打开文件,如果文件存在则清空文件内容,如果文件不存在则创建新文件,用于写入数据。
- "a"
:以追加方式打开文件,如果文件存在,则新写入的数据将追加到文件末尾,若文件不存在则创建新文件,常用于在已有文件后添加新内容。
- "r+"
:以读写方式打开文件,文件必须存在,可同时进行读取和写入操作。
- "w+"
:以读写方式打开文件,若文件存在则清空内容后可读写,若不存在则创建新文件,可先写入再读取或者反之。
- "a+"
:以读写方式打开文件,若文件存在则可在末尾追加内容并读取,若不存在则创建新文件,允许边追加边读取。
- 示例代码:
#include <stdio.h>
int main() {
FILE *fp;
// 以只读方式打开名为test.txt的文件
fp = fopen("test.txt", "r");
if (fp == NULL) {
perror("fopen error");
return 1;
}
// 后续可以使用fread、fwrite等函数对文件进行读写操作,这里暂不展示
// 使用fclose函数关闭文件
fclose(fp);
return 0;
}
2. fread
函数
- 功能:
从指定的文件指针所指向的文件中读取数据到给定的缓冲区中,常用于读取二进制文件或者文本文件中的数据块。 - 头文件及原型:
在<stdio.h>
头文件中定义,原型如下:
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
其中,ptr
是指向接收数据的缓冲区的指针(通常是一个数组等);size
是每个数据元素的大小(单位为字节);nmemb
是要读取的数据元素的数量;stream
就是通过 fopen
打开文件所获得的文件指针。函数返回实际读取的数据元素数量,如果返回值小于 nmemb
,可能表示读到文件末尾或者出现了读取错误等情况。
- 示例代码:
#include <stdio.h>
int main() {
FILE *fp;
char buffer[100];
// 以只读方式打开名为test.txt的文件
fp = fopen("test.txt", "r");
if (fp == NULL) {
perror("fopen error");
return 1;
}
// 从文件中读取数据,每次读取1个字节,最多读取100次(即最多读取100字节)
size_t elementsRead = fread(buffer, 1, 100, fp);
if (elementsRead < 100) {
if (feof(fp)) {
printf("Reached end of file.\n");
} else {
perror("fread error");
}
}
buffer[elementsRead] = '\0';
printf("Read content: %s", buffer);
fclose(fp);
return 0;
}
3. fwrite
函数
- 功能:
向指定的文件指针所指向的文件中写入数据,常用于将数据保存到二进制文件或者向文本文件中添加内容等情况。 - 头文件及原型:
在<stdio.h>
头文件中,原型如下:
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
其中,ptr
是指向要写入数据的缓冲区的指针(例如包含要写入数据的数组等);size
是每个数据元素的大小(单位为字节);nmemb
是要写入的数据元素的数量;stream
是文件指针。函数返回实际写入的数据元素数量,如果返回值小于 nmemb
,可能表示出现写入错误等情况。
- 示例代码:
#include <stdio.h>
int main() {
FILE *fp;
char data[] = "Hello, World!";
// 以只写方式打开名为test.txt的文件
fp = fopen("test.txt", "w");
if (fp == NULL) {
perror("fopen error");
return 1;
}
// 向文件中写入数据,每次写入1个字节,写入数据元素个数为数据长度
size_t elementsWritten = fwrite(data, 1, sizeof(data), fp);
if (elementsWritten < sizeof(data)) {
perror("fwrite error");
} else {
printf("Written %zu bytes to the file.\n", elementsWritten);
}
fclose(fp);
return 0;
}
4. fclose
函数
- 功能:
关闭由fopen
函数打开的文件,释放相关的文件资源,避免出现文件资源泄露等问题。 - 头文件及原型:
在<stdio.h>
头文件中定义,原型为:
int fclose(FILE *fp);
其中,fp
是要关闭的文件的文件指针,返回值为0表示关闭成功,返回非零值表示关闭出现错误,不过通常返回 EOF
(在 <stdio.h>
中定义,值通常为 -1)来表示关闭失败的情况。
- 示例代码:
在前面fopen
、fread
、fwrite
等函数使用示例中,最后都需要使用fclose
函数来关闭文件,如:
FILE *fp;
// 打开文件等操作......
// 关闭文件
fclose(fp);
5. fseek
函数
- 功能:
用于移动文件指针到指定的位置,以便从新的位置开始进行读写操作,可以实现对文件的随机读写。通过指定偏移量和相对位置(如相对于文件开头、当前位置、文件末尾等)来精准定位文件指针。 - 头文件及原型:
在<stdio.h>
头文件中定义,原型如下:
int fseek(FILE *stream, long offset, int whence);
其中,stream
是文件指针;offset
是要移动的偏移量(以字节为单位),正数表示向文件末尾方向移动,负数表示向文件开头方向移动;whence
规定了偏移量的相对位置,有以下几种取值:
- SEEK_SET
:表示相对于文件开头定位,此时 offset
就是绝对的偏移量,从文件开头开始计算移动的距离。
- SEEK_CUR
:表示相对于文件当前位置定位,移动后的位置是在当前文件指针位置基础上加上 offset
的值。
- SEEK_END
:表示相对于文件末尾定位,移动后的位置是在文件末尾基础上加上 offset
的值(通常 offset
为负数用于从文件末尾往前移动)。
返回值为0表示移动成功,返回非零值表示移动失败。
- 示例代码:
#include <stdio.h>
int main() {
FILE *fp;
char buffer[100];
// 以只读方式打开名为test.txt的文件
fp = fopen("test.txt", "r");
if (fp == NULL) {
perror("fopen error");
return 1;
}
// 先读取部分文件内容
fread(buffer, 1, 50, fp);
// 将文件指针移动到文件开头后20字节的位置
if (fseek(fp, 20, SEEK_SET) == 0) {
// 从新位置再读取部分内容
fread(buffer, 1, 30, fp);
buffer[30] = '\0';
printf("Read content from new position: %s", buffer);
} else {
perror("fseek error");
}
fclose(fp);
return 0;
}
linux_230">linux系统调用接口
1. open
系统调用
- 功能:用于打开或创建一个文件,并返回对应的文件描述符,后续进程便可基于这个文件描述符对文件进行读写等操作。
- 头文件及原型:
它位于<fcntl.h>
头文件中,原型如下:
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
其中,pathname
是要打开或创建的文件的路径名(可以是相对路径也可以是绝对路径);flags
是一组控制文件打开方式的标志位,常用的标志位如下:
- O_RDONLY
:以只读方式打开文件,进程只能从文件中读取数据。
- O_WRONLY
:以只写方式打开文件,进程只能向文件写入数据。
- O_RDWR
:以读写方式打开文件,进程既能读取又能写入文件数据。
- O_CREAT
:如果文件不存在,则创建该文件。需要和 mode
参数一起使用(当使用第二个函数原型时)来指定创建文件时的初始权限。
- O_APPEND
:以追加方式打开文件,每次写入操作都会将数据追加到文件末尾,而不会覆盖已有内容。
- O_TRUNC
:如果文件已经存在,并且以可写方式打开(如 O_WRONLY
或 O_RDWR
且配合 O_CREAT
等情况),则会截断文件,即将文件长度设置为0,清除原有内容。
mode
(仅在使用第二个函数原型且带 O_CREAT
标志时需要)用于指定创建文件时的权限模式,例如 S_IRUSR
(文件所有者可读权限)、S_IWUSR
(文件所有者可写权限)、S_IXUSR
(文件所有者可执行权限)等权限宏可以组合使用,常见的组合如 0644
(表示所有者可读可写,同组用户和其他用户可读)等。
- 示例代码:
以下是一个简单的C语言代码示例,展示如何使用open
系统调用打开文件:
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
int fd;
// 尝试打开名为test.txt的文件,以只读方式打开
fd = open("test.txt", O_RDONLY);
if (fd == -1) {
perror("open error");
return 1;
}
// 这里可以继续使用read等系统调用读取文件内容等操作,后续再关闭文件
close(fd);
return 0;
}
2. creat
系统调用
- 功能:专门用于创建一个新文件,如果文件已经存在,则会截断它(将文件内容清空),并返回对应的文件描述符。实际上,在现代Linux编程中,使用
open
系统调用结合O_CREAT
和O_TRUNC
标志位基本可以替代creat
的功能,但它依然存在于系统中供一些旧代码或特定场景使用。 - 头文件及原型:
位于<fcntl.h>
头文件中,原型为:
#include <fcntl.h>
int creat(const char *pathname, mode_t mode);
其中参数含义和 open
系统调用中相关参数类似,pathname
是文件路径名,mode
用于指定创建文件的初始权限。
- 示例代码:
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
int fd;
// 创建一个名为new_file.txt的新文件,初始权限设置为0644(所有者可读可写,同组用户和其他用户可读)
fd = creat("new_file.txt", 0644);
if (fd == -1) {
perror("creat error");
return 1;
}
close(fd);
return 0;
}
3. read
系统调用
- 功能:从已打开的文件中读取数据到指定的缓冲区中,返回实际读取到的字节数。
- 头文件及原型:
位于<unistd.h>
头文件中,原型如下:
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
其中,fd
是之前通过 open
等系统调用打开文件所获得的文件描述符;buf
是指向接收数据的缓冲区的指针(通常是一个字符数组等);count
是期望读取的字节数,但实际读取的字节数可能小于这个值,比如读到文件末尾等情况时,返回值为0,表示已读到文件末尾,如果返回 -1,则表示读取过程出现错误。
- 示例代码:
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
int fd;
char buffer[100];
ssize_t bytesRead;
// 打开名为test.txt的文件,以只读方式打开
fd = open("test.txt", O_RDONLY);
if (fd == -1) {
perror("open error");
return 1;
}
// 从文件中读取数据到buffer中,最多读取100字节
bytesRead = read(fd, buffer, sizeof(buffer));
if (bytesRead == -1) {
perror("read error");
close(fd);
return 1;
}
buffer[bytesRead] = '\0';
printf("Read content: %s", buffer);
close(fd);
return 0;
}
4. write
系统调用
- 功能:向已打开的文件中写入数据,返回实际写入文件的字节数。
- 头文件及原型:
位于<unistd.h>
头文件中,原型如下:
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
其中,fd
是文件描述符,buf
是指向要写入数据的缓冲区的指针(例如包含要写入的字符数组等),count
是期望写入的字节数,实际写入字节数可能小于 count
,比如磁盘空间不足等情况,返回 -1 表示写入过程出现错误。
- 示例代码:
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
int fd;
char data[] = "Hello, World!";
// 打开名为test.txt的文件,以只写方式打开
fd = open("test.txt", O_WRONLY);
if (fd == -1) {
perror("open error");
return 1;
}
// 向文件中写入数据
ssize_t bytesWritten = write(fd, data, sizeof(data));
if (bytesWritten == -1) {
perror("write error");
close(fd);
return 1;
}
printf("Written %zd bytes to the file.", bytesWritten);
close(fd);
return 0;
}
5. close
系统调用
- 功能:关闭之前由进程打开的文件,释放相关的系统资源,如文件描述符等。
- 头文件及原型:
位于<unistd.h>
头文件中,原型为:
#include <unistd.h>
int close(int fd);
其中,fd
是要关闭的文件的文件描述符,返回值为0表示关闭成功,返回 -1 表示关闭过程出现错误。
- 示例代码:
通常在使用open
等系统调用打开文件并完成相应读写操作后,就需要使用close
来关闭文件,示例代码可参考前面open
、read
、write
等系统调用示例中都包含的关闭文件操作部分,如:
int fd;
// 打开文件等操作......
// 关闭文件
close(fd);
系统调用接口和C标准库接口的关系:
联系
- 基于实现:C标准库中的许多接口(如文件操作相关的
fopen
、fread
等)底层基于系统调用接口(如open
、read
等)实现,对其进行封装和优化,使其更易用。 - 功能目的:都旨在方便程序员操作文件及其他系统资源,只是所处层次不同,常相互协作来完成复杂功能。
区别
- 抽象层次:系统调用接口接近底层,与内核直接交互,需关注更多细节;C标准库接口抽象程度更高,使用更简便,隐藏了底层机制。
- 可移植性:系统调用接口因操作系统差异导致可移植性差;C标准库接口基于C语言标准,在不同平台使用方式较一致,可移植性较好。
- 功能覆盖范围:系统调用接口涵盖操作系统内核全方位功能;C标准库接口侧重常用功能封装,范围相对窄些。
- 错误处理方式:系统调用接口返回特定错误码,需结合特定函数分析错误;C标准库接口多通过返回特殊值及全局变量辅助判断错误,更简化统一。
四 进程和文件的关系
在Linux操作系统中,task_struct
和 file_struct
这两个结构体对于理解进程和文件之间的关系起着关键作用,以下是对它们的详细介绍:
task_struct 结构体
- 含义与作用:
task_struct
是Linux内核中用于描述进程的核心结构体,它包含了进程运行过程中几乎所有的相关信息,相当于进程在内核中的“身份证”,从进程的基本属性(如进程标识符PID、进程的状态等)到资源使用情况(如内存使用情况、打开的文件描述符数量等),再到进程调度相关的信息(如优先级、调度策略等)都涵盖其中。可以说,内核通过对众多task_struct
结构体的管理来掌控系统中所有进程的运行。 - 与文件相关的体现:
在task_struct
结构体中,有一个成员指针指向files_struct
结构体(通常表示为files
成员),这建立起了进程与文件之间的一种间接关联。通过这个指针,进程可以访问到其对应的files_struct
,进而知晓自身打开了哪些文件以及相关的文件描述符等信息,它是连接进程与文件操作具体情况的一个重要纽带。
files_struct 结构体
- 含义与作用:
files_struct
主要用于管理进程所打开的文件相关信息,是对进程打开文件情况的一种集中描述。它包含了诸如文件描述符数组(用于存放进程打开文件后获取的文件描述符)、文件打开计数(记录每个文件被打开的次数,便于内核进行资源管理和共享控制等操作)等重要信息,是内核在处理进程文件操作时经常会涉及到的一个关键数据结构。 - 内部结构与文件关联细节:
-
文件描述符数组:在
files_struct
中有一个数组(通常命名类似fd_array
等)存放struct file 结构体的指针,进程每通过系统调用(如open
等)打开一个文件,内核就会在这个数组中分配一个空闲的位置(即一个有效的文件描述符,通常是一个较小的非负整数)来存放该文件对应的相关信息,后续进程就可以凭借这个文件描述符来对文件进行读写等操作(通过read
、write
等系统调用)。而且不同的进程可以通过各自的files_struct
中的文件描述符数组来独立地打开和操作同一个文件,内核会根据文件打开计数等机制来协调资源共享和访问控制等事宜。
数组元素为file*的指针,指向file结构体
file结构体如下
-
文件打开计数:对于每个被打开的文件,
files_struct
会记录其被打开的次数,这很重要。例如,当一个进程多次打开同一个文件时,打开计数会相应增加;而当进程关闭该文件(通过close
系统调用)时,打开计数会减少,只有当打开计数降为0时,内核才会真正释放该文件相关的一些底层资源(如内核缓冲区等),这种机制有助于合理地管理文件资源,避免资源的过早释放或错误释放等问题。
-
进程与文件关系的整体体现
- 文件打开过程关联:
进程在启动时默认会打开三个标准流文件,它们分别是标准输入(stdin)、标准输出(stdout)和标准错误输出(stderr),当一个进程想要打开一个文件时,它会通过系统调用(如open
)向内核发起请求,内核在执行open
操作时,一方面会根据请求查找对应的文件(验证文件是否存在、权限是否匹配等),另一方面会在该进程对应的files_struct
的文件描述符数组中找到一个空闲位置分配给这个新打开的文件,并返回对应的文件描述符给进程。这个过程中,task_struct
通过指向files_struct
的指针,使得进程能够“知晓”自己新打开了哪个文件以及后续如何操作它。 - 文件读写与共享体现:
在文件读写阶段,进程凭借从files_struct
中获取的文件描述符,通过read
、write
等系统调用对文件进行读写操作。而且多个进程可以通过各自的files_struct
中的文件描述符同时打开和读写同一个文件,这就涉及到文件资源的共享。例如,不同的进程可能同时读取一个配置文件,或者一个进程写入、另一个进程读取同一个数据文件等情况,内核借助files_struct
中的文件打开计数等机制来确保文件数据的一致性以及合理地分配文件读写的权限等资源,而这一切操作的源头都是基于各个进程对应的task_struct
结构体所关联的files_struct
来实现的。 - 文件关闭及资源释放关联:
当进程完成对文件的使用,通过close
系统调用关闭文件时,内核会根据files_struct
中记录的文件打开计数来进行相应处理。如果关闭操作使得文件打开计数减为0,那么内核会释放该文件对应的一些资源(如释放相关的内核缓冲区、更新文件的元数据等),并且在files_struct
中标记该文件描述符对应的位置为空闲状态,以便后续进程打开其他文件时可以再次使用这个位置。整个过程都是围绕着task_struct
所关联的files_struct
来有序进行的,确保了文件资源的合理回收和再利用。
总图