目录
一、C语言的文件IO操作
二、系统调用的文件IO操作
1.open打开文件
2.close关闭文件
3.write写入文件
4.read读取文件
三、文件描述符
四、文件描述符的分配规则
五、终端文件
六、C语言中的文件IO对比系统调用文件IO
文件I/O(Input/Output,输入/输出)是指计算机程序与文件系统之间的数据交换过程。
文件I/O通常包括以下几种操作:打开文件、读取文件、写入文件、关闭文件、文件定位、文件错误处理
一、C语言的文件IO操作
以w方式打开文件会默认清空文件再写入,以a方式打开文件会在文件末尾追加写入
#include <stdio.h>
#include <string.h>
int main() {
FILE* fp = fopen("log.txt", "w"); // 打开log.txt文件,不存在就创建
if (fp == NULL) {
perror("fopen"); // perror函数会自动根据错误码报告错误
return 1;
} else {
const char* msg = "hello world\n";
fwrite(msg, strlen(msg), 1, fp);
}
fclose(fp);
return 0;
}
二、系统调用的文件IO操作
1.open打开文件
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
pathname:要打开或创建的文件名
flags:打开文件的参数选项,用或运算使用多个选项(包含在头文件<fcntl.h>中)
O_RDONLY: 只读打开
O_WRONLY: 只写打开
O_RDWR : 读,写打开
(以上三个常量,必须指定一个且只能指定一个)
O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
O_APPEND: 追加写
O_TRUNC:如果文件已存在且成功打开,则将文件长度截断至0(清空文件再写入,如果不带 O_TRUNC选项,文件将被打开但不会自动被截断。这意味着,如果文件已经存在,它的内容不会被清空,文件指针会放在文件的开始位置,接下来的写操作会从文件的开始处覆盖现有数据。)
mode:权限位,要考虑umask权限掩码的存在
返回值:打开成功返回文件描述符,打开失败返回-1
第一个open指令通常用于打开已经存在的文件,第二个open通常用于打开并创建不存在的文件(如果使用第一个open指令打开并创建不存在的文件,该文件的权限将会是乱码)
2.close关闭文件
int close(int fd);
fd:文件描述符
返回值:关闭成功返回0,关闭失败返回-1并可以设置全局变量errno指示错误原因
3.write写入文件
ssize_t write(int fd, const void *buf, size_t count);
fd:文件描述符
buf:要写入数据的缓冲区指针
count:写入数据的字节数
返回值:ssize_t是有符号整型数,写入成功返回写入字节数,写入失败返回-1并可以设置全局变量errno指示错误原因
4.read读取文件
ssize_t read(int fd, void *buf, size_t count);
fd:文件描述符
buf:指向缓冲区的指针,用于存储从文件中读取的数据
count:要读取的最大字节数
返回值:读取成功返回读取到的字节数,若读取到文件末尾返回0,读取失败返回-1并可以设置全局变量errno指示错误原因
代码演示:
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
int main() {
int fd = open("log.txt", O_WRONLY | O_CREAT, 0666); // 只写方式打开log.txt文件,不存在就创建,并设置文件权限为0666
if (fd == -1) { // 打开文件失败
perror("open");
return 1;
}
const char* msg = "hello Linux File\n";
write(fd, msg, strlen(msg)); // 写入文件
close(fd); // 关闭文件
return 0;
}
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
int main() {
int fd = open("log.txt", O_RDONLY); // 只读方式打开文件
if (fd == -1) { // 打开失败
perror("open");
return 1;
}
char buf[1024]; // 用于存储从文件中读取数据的缓冲区
ssize_t read_bytes = read(fd, buf, sizeof(buf) - 1);
if (read_bytes == -1) { // 读取失败
perror("read");
close(fd);
return 1;
}
buf[read_bytes] = '\0'; // 确保读取的数据以空字符结尾
printf("Read from file: %s\n", buf);
close(fd);
return 0;
}
三、文件描述符
当进程打开文件时,操作系统要在内存中创建内核数据结构file来描述被打开的文件,该数据结构中有文件属性,文件内核级缓冲区等
描述进程的内核数据结构task_struct中都有一个指向文件描述符表的指针files,该指针指向一张文件描述符表files_struct,文件描述符表中最重要的部分就是一个指针数组,每一个元素就是一个被打开文件的指针file*,而文件描述符fd就是文件在指针数组中的位置下标。
Linux系统中,每个进程都会默认打开三个文件描述符为0、1、2的文件,分别为标准输入、标准输出、标准错误,对应的物理设备一般为键盘、显示器、显示器
所以我们还可以通过如下的方式进行输入输出:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main() {
char buf[1024];
ssize_t read_bytes = read(0, buf, sizeof(buf)); // 从键盘中读取数据
if (read_bytes == -1)
{
perror("read");
return -1;
}
buf[read_bytes] = '\0';
write(1, buf, strlen(buf)); // 向显示器写入数据
write(2, buf, strlen(buf)); // 向显示器写入数据
return 0;
}
四、文件描述符的分配规则
在文件描述符表files_struct的指针数组当中,找到当前没有被使用的 最小的一个下标,作为新的文件描述符。
五、终端文件
创建一个进程,在/proc路径下会有一个与进程pid同名的目录文件来保存进程信息,该目录文件下存在名为fd目录文件,用于存储该进程打开文件的信息
以进程pid为8301的进程为例,在/proc/8301/fd路径下存放着如下信息:链接文件0、1、2分别对应着标准输入、标准输出和标准错误。
这三个文件指向的位置是/dev/pts/1,查看路径/dev/pts发现其中有三个文件属性以c开头的字符设备文件。上述进程默认打开的0、1、2文件指向的就是名为1的字符设备文件。
其实该目录下存放的就是终端文件,终端设备名为0,1的文件,名为ptmx用于创建伪终端主设备和从设备对的特殊文件。
终端文件其实就是Linux系统的终端,我们可以同时打开多个终端,/dev/pts同时也会多出相应的终端文件。既然终端也是一个文件,那么我们就可以从一个终端向另一个终端进行输入
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
int main()
{
//当前终端为1
int fd = open("/dev/pts/3", O_WRONLY | O_APPEND);//向终端3中输入
if (fd == -1)
{
perror("open");
return 1;
}
const char* msg = "hello linux file!";
write(fd, msg, strlen(msg));
close(fd);
return 0;
}
总结:当打开Linux系统时,都会由shell进程打开标准输入,标准输出和标准错误对应的终端文件,后续进程只会shell进程已经打开的终端文件,这样就可以让多个进程都在同一终端输入输出
六、C语言中的文件IO对比系统调用文件IO
C语言中打开文件使用fopen函数,并用FILE*类型接收其返回值。
FILE*是一个指向FILE结构体的指针,该结构体中封装了文件IO的相关的信息,例如文件描述符fd就被封装在其中。因此可以使用FILE*访问文件描述符(C语言中文件描述符叫做_fileno)
#include <stdio.h>
int main()
{
FILE* fp = fopen("log.txt", "w"); // 以w方式打开文件
if (fp == NULL)
{
perror("fopen");
return 1;
}
printf("fd:%d\n", fp->_fileno); // 查看文件描述符
fwrite("hello", 1, 5, fp); // 写入文件
fclose(fp); // 关闭文件
return 0;
}
C语言程序默认也会打开标准输入stdin、标准输出stdout和标准错误stderr,通过代码查看其对应的文件描述符,发现恰好也是0、1、2
#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;
}
总结,不难发现其实C语言中的文件IO操作斗士队系统调用文件IO操作的封装
为什么要进行封装呢?
因为不同操作系统的系统调用指令不同,在语言层面对其进行封装就可以根据操作系统的不同调用对应的系统调用指令,实现跨平台性