目录
- C语言文件IO
- fwrite
- fread
- 默认打开的三个流
- 系统文件IO
- open
- close
- write
- read
- 文件描述符fd
- 文件描述符的分配规则
- 重定向
- 重定向原理
- 输出重定向
- 追加重定向
- 输入重定向
- 使用 dup2 系统调用
在学习C语言时,就接触过文件相关的操作,通过
fwrite
,
fread
等这样的读写接口将信息写入或读出文件。但这只是在用法上进行了学习,也就是只停留在了语言层面;那么C++,JAVA这些语言又是如何进行读写的呢?
如何让使用硬件资源这个话题曾在操作系统一文提到过,今天通过基础IO一文进行讲解。
IO:IO通常指的是数据的输入和输出过程。这包括从外部设备(如键盘、鼠标、显示器、打印机等)读取数据以及向这些设备写入数据
C语言文件IO
fwrite
fwrite是C语言标准库中的一个函数:用于将数据从内存中的缓冲区写入到文件流中。这个函数通常用于二进制文件的写操作,因为它不会对写入的数据进行任何形式的转换或格式化。
参数:
ptr
:指向要写入文件的内存缓冲区的指针。size
:每个数据项的大小(以字节为单位)。count
:要写入的数据项的个数。stream
:指向 FILE 对象的指针,表示目标文件流。
返回值: fwrite函数返回成功写入的数据项个数。如果返回值小于count,则表示发生了错误或到达了文件末尾(EOF)。
fwrite使用示例
int main()
{//把a中的数据写到文件中FILE* fp = fopen("test.txt", "w");if (NULL == fp){perror("fopen::test.txt");return 1;}//二进制的写文件const char*msg="hello Linux\n";int cnt=5;while(cnt--){fwrite(msg, strlen(msg), 1, fp);}fclose(fp);fp = NULL;return 0;
}
fread
在C语言中,fread函数被用于从文件流中读取数据。它适用于二进制文件的读取,因为可以确保数据按照原始格式(即字节流)被读取,而不会受到任何格式转换的影响。
参数:
ptr
:这是一个指向内存的指针,用于存储从文件中读取的数据。size
:这代表每个数据单元的大小,单位是字节。count
:这表示你想要读取的数据单元的数量。stream
:这是一个指向FILE对象的指针,它代表了你要从中读取数据的文件流。
返回值:
fread返回成功读取的元素数量(不是字节数)。如果返回值小于count,则可能发生了错误或到达了文件末尾。
fread使用实示例
int main()
{FILE* fp = fopen("test.txt", "r");if (NULL == fp){perror("fopen::test.txt");return 1;}char buffer[5][64];//存放从文件中读取的内容。int i=0;while(1){size_t rd = fread(buffer[i++], sizeof(buffer), 1, fp);//存放到二维数组中if (rd<1){break;}}while(i--){printf("%s",buffer[i]);//打印}fclose(fp);fp = NULL;return 0;
}
除此之外还有很多的读写及文件操作接口,这里简单进行汇总,具体使用请参考——C语言文件操作
文件操作函数 | 功能 |
---|---|
fopen | 打开文件 |
fclose | 关闭文件 |
fputc | 写入一个字符 |
fgetc | 读取一个字符 |
fputs | 写入一个字符串 |
fgets | 读取一个字符串 |
fprintf | 格式化写入数据 |
fscanf | 格式化读取数据 |
fwrite | 向二进制文件写入数据 |
fread | 从二进制文件读取数据 |
fseek | 设置文件指针的位置 |
ftell | 计算当前文件指针相对于起始位置的偏移量 |
rewind | 设置文件指针到文件的起始位置 |
ferror | 判断文件操作过程中是否发生错误 |
feof | 判断文件指针是否读取到文件末尾 |
默认打开的三个流
在C语言中,程序一运行起来,就会默认打开三个流:
stdin
- 标准输⼊流,在大多数的环境中从键盘输入stdout
- 标准输出流,大多数的环境中输出至显示器界面stderr
- 标准错误流,大多数环境中输出到显示器界面
正是默认打开了这三个流,使用scanf
、printf
等函数就可以直接进行输入输出操作的
scanf,fgets使用示例
int main()
{char buffer[64];scanf("%s",buffer);printf("%s\n",buffer);getchar();//scanf不读取\n,使用getchar清空一下缓冲区fgets(buffer,sizeof(buffer),stdin);printf("%s\n",buffer);return 0;
}
可以看到scanf
,fgets
都是从stdin
标准输入读取数据的。
输出信息到显示器,你有哪些方法?
int main()
{const char* msg="hello world\n";printf("%s",msg);fprintf(stdout,"%s",msg);fwrite(msg,strlen(msg),1,stdout);return 0;
}
stdout
标准输出流将信息输出到屏幕上。
系统文件IO
操作文件,除了上述C语言接口(当然,C++也有接口,其他语言也有),我们还可以采用系统接口来进行文件访问。
此时我们可以回顾一下操作系统的定位:
在整个计算机软硬件架构中,操作系统的定位是:一款纯正的“搞管理”的软件。目的就是为了帮助用户管理好软硬件资源。也是计算机开机后第一个被加载到内存的软件
再加上之前进程部分的学习可以得出:操作系统是计算机软硬资源的管理者,不管你是什么语言,都是在操作系统上跑的;而且操作系统是不会相信任何人的,对于软硬件资源的操作,操作系统只能对外提供系统调用接口供上层使用。也就是说,实际上C语言(所有语言)的所有文件操作函数都是对对应系统调用接口的封装。
lib代表函数库,涉及硬件资源的函数基本都是对底层系统调用的封装。
下面对文件操作的系统调用进行介绍
open
open
函数是一个核心的系统调用函数,用于打开或创建一个文件,并返回一个文件描述符。
man手册查看open:
NAMEopen, openat, creat - open and possibly create a fileSYNOPSIS//头文件#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:如果文件存在并且以写模式打开,写入前会将文件内容清空。
-
mode
(可选):文件的权限位,当flags中包含O_CREAT时需要指定,用于设置新创建文件的权限。- 设定的权限受到umask影响
返回值:
- 成功:新打开的文件描述符
- 失败:-1
使用演示
int main()
{int fd=open("log.txt",O_CREAT|O_WRONLY|O_TRUNC,0666);//注意搭配使用if(fd<0){perror("open failed\n");return -1;}close(fd);return 0;
}
第一次创建该文件时,一定要自己设置第三个参数。不了解权限设置的请参考——Linux权限问题
close
使用close
函数关闭文件
NAMEclose - close a file descriptorSYNOPSIS#include <unistd.h>int close(int fd);
使用close函数时传入需要关闭文件的文件描述符即可,若关闭文件成功则返回0,若关闭文件失败则返回-1。
write
write
函数是Linux系统编程中用于向文件描述符写入数据的系统调用。它广泛应用于文件、管道、套接字等文件描述符类型的写操作。
NAMEwrite - write to a file descriptorSYNOPSIS#include <unistd.h>ssize_t write(int fd, const void *buf, size_t count);
参数:
fd
:文件描述符,标识要写入的文件、管道、套接字等。它通常是通过 open、pipe、socket 等系统调用获得的。buf
:指向要写入数据的缓冲区的指针。缓冲区中的数据将被写入到文件描述符所指向的文件或设备中。count
:要写入的数据的字节数。
返回值:
- 成功时,返回写入的字节数。这个值可能小于 count,表示只写入了部分数据。
- 失败时,返回 -1,并设置 errno 以指示错误原因。
使用示例
int main()
{//int fd=open("sys_text",O_CREAT|O_WRONLY);int fd=open("sys_text",O_CREAT|O_WRONLY|O_TRUNC,0666);//第一次创建记得加上权限,注意umaskif(fd<0){perror("open failed\n");return -1;}const char*msg="hello wrold\n";int cnt=5;ssize_t sz;while(cnt--){sz= write(fd,msg,strlen(msg));}if(sz<0){perror("write failed\n");return -1;}close(fd);return 0;
}
注意flags参数的设置,一般O_TRUNC
,O_APPEND
二选一,清空再写或者在尾部追加写。
read
read
函数是最常用的系统调用之一,用于从文件或其他输入设备读取数据。
NAMEread - read from a file descriptorSYNOPSIS#include <unistd.h>ssize_t read(int fd, void *buf, size_t count);
参数:
fd
:文件描述符,代表了需要读取的文件或设备。文件描述符可以通过调用open
或其他文件操作函数获取。buf
:一个指向用户分配的缓冲区的指针,read
函数将把读取到的数据写入该缓冲区。count
:需要读取的字节数,表示最多读取count
字节数据。
返回值:
- 若成功,read函数返回读取的字节数(0表示已到达文件末尾)
- 若失败,返回-1,并设置errno来指示具体错误
使用示例
int main()
{//int fd=open("sys_text",O_CREAT|O_WRONLY);int fd=open("sys_text",O_RDONLY);//第一次创建记得加上权限,注意umaskif(fd<0){perror("open failed\n");return -1;}char buffer [64];while(1){ssize_t sz= read(fd,buffer,sizeof(buffer));if(sz>0){printf("%s", buffer);}else if(sz==0){break;}else{printf("errno:%d", errno);}}close(fd);return 0;
}
使用errno,记得包含头文件#include <errno.h>
文件描述符fd
使用open打开文件,其返回值即打开文件的标识符;那么为什么其返回值是一个int
类型呢?
使用open这个系统调用去打开一个文件,操作系统就必然要对打开的文件进行管理,而系统当中又存在大量进程,也就是说,在系统中任何时刻都可能存在大量已经打开的文件。所以为了进行高效的管理,依旧是六字真言——先描述,后组织;
操作系统会为每个已经打开的文件创建各自的struct file
结构体——也就是将文件进行描述,然后将这些结构体以双链表的形式连接起来——组织,之后操作系统对文件的管理也就变成了对这张双链表的增删查改等操作。而为了区分已经打开的文件哪些属于特定的某一个进程,我们就还需要建立进程和文件之间的对应关系。
每当用户空间进程打开一个文件时,内核就会创建一个struct file
对象来跟踪对该文件的所有操作,并以双链表的形式组织起来;而进程的内核PCB(task_struct)当中有一个指针,该指针指向一个名为files_struct
的结构体,在该结构体当中就有一个名为fd_array
的指针数组,该指针数组的成员为指向struct file
对象的指针,所以通过该指针数组,就可以找到所打开的文件。而文件描述符fd
为什么是int类型就是因为使用open后,返回的就是该指针数组的下标,有了这下标,就可以通过指针数组找到对应打开的文件。
文件描述符的分配规则
在一个进程当中,正常使用使用open打开的第一个文件的返回值永远都是3
//文件描述符的分配规则
int main()
{//默认会打开0 1 2三个流int fd=open("sys_text",O_RDONLY);printf("fd:%d\n",fd);//文件描述符的分配规则:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符close(fd);return 0;
}
还记得C语言时,一个程序打开会默认打开三个流:stdin
,stdout
,stderr
,现在我们也知道了C的文件接口都是调用了系统的接口;所以C使用FILE*
作为返回值来找到对应的文件最终也一定是转化为通过下标来找到对应文件的。
三个标准流的类型也为FILE *
extern FILE *stdin;extern FILE *stdout;extern FILE *stderr;
而在Linux下一切皆文件,C层面的三个默认打开的标准输入,输出,错误流本质就是在Linux中提前打开了三个对应的文件,而且是进程一启动就默认打开的,所以stdin
,stdout
,stderr
对应的下标就是0,1,2
。所以此后打开的文件其下标一定是从3开始的(不关闭前面三个的状况下)
参考下面演示代码:
//fd本质
int main()
{//0,1,2,一般指键盘,显示器,显示器//in out errchar buf[64];ssize_t s = read(0, buf, sizeof(buf));//从fd为0号下标开始读:键盘if (s > 0){buf[s] = 0;//转为字符串write(1, buf, strlen(buf));//写入fd为1的文件:显示器write(2, buf, strlen(buf));//写入fd为2的文件:显示器}return 0;
}
如果关闭了0号文件再打开一个文件呢?
//文件描述符的分配规则
int main()
{//默认会打开0 1 2三个流close(0);//关闭0号文件int fd=open("sys_text",O_RDONLY);printf("fd:%d\n",fd);//文件描述符的分配规则:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符close(fd);return 0;
}
可以看到此时打开文件的fd变为0。
文件描述符的分配规则: 在files_struct数组
当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。
重定向
重定向原理
输出重定向就是,将我们本应该输出到一个文件的数据重定向输出到另一个文件中。
输出重定向
当关闭1号文件后再打开一个文件会发生什么?
int main()
{//理应显示到屏幕,但却输出到我们指定的文件中。close(1);//outint fd=open("sys_text",O_WRONLY|O_TRUNC);if(fd<0){ perror("open failed\n");return -1;}printf("hello world\n");printf("hello linux\n");printf("hello csdn\n");//stdout->_fileno=2;//可以看出private的用处//printf("stdout->_fileno:%d\n",stdout->_fileno);fflush(stdout);//对于不同类型文件,刷新方式有所区别close(fd);return 0;
}
此时,我们发现,本来应该输出到显示器上的内容,输出到了文件sys_text
当中,其中,fd=1。这种现象叫做输出重定向。常见的重定向有>, >>, <
追加重定向
所谓追加重定向,就是在原有输出重定向的基础上,将open的flags的O_TRUNC
变为O_APPEND
int main()
{//理应显示到屏幕,但却输出到我们指定的文件中。close(1);//outint fd=open("sys_text",O_WRONLY|O_APPEND);if(fd<0){ perror("open failed\n");return -1;}printf("APPEND\n");fflush(stdout);//对于不同类型文件,刷新方式有所区别close(fd);return 0;
}
输入重定向
输入重定向就是从0号文件输入变成从其他文件输入。
如:从sys_text输入
int main()
{//理应显示到屏幕,但却输出到我们指定的文件中。close(0);//inint fd=open("sys_text",O_RDONLY);if(fd<0){ perror("open failed\n");return -1;}char str[40];while (scanf("%s", str) != EOF){printf("%s\n", str);}close(fd);return 0;
}
scanf默认从stdin读取,stdin默认是0号文件,如今文件sys_text变为了0号文件,所以scanf读取时会从文件sys_text中读取。
使用 dup2 系统调用
#include <unistd.h>int dup2(int oldfd, int newfd);
参数:
oldfd
:要复制的文件描述符。newfd
:目标文件描述符。如果 newfd 已经打开,它会被关闭,除非newfd和oldfd相同
注意:
- 如果oldfd是一个有效的文件描述符,但是newfd和oldfd具有相同的值,则dup2不做任何操作,并返回newfd
- 如果oldfd不是有效的文件描述符,则dup2调用失败,并且此时文件描述符为newfd的文件没有被关闭
返回值:
- 成功时,返回新的文件描述符(通常与newfd 相同)。
- 失败时,返回 -1,并设置 errno 以指示错误原因。
dup2原理:
dup2进行重定向只需进行fd_array数组当中元素的拷贝即可。例如,我们若是将fd_array[3]当中的内容拷贝到fd_array[1]当中,因为C语言当中的stdout就是向文件描述符为1文件输出数据,那么此时我们就将输出重定向到了文件sys_text
- 此时下标1,3都指向新打开的文件。
//使用dup2重定向
int main()
{int fd=open("sys_text",O_WRONLY|O_TRUNC);//清空再写if(fd<0){perror("open failed\n");return -1;}dup2(fd,1);//old cover new ->printf("fd:%d\n",fd);printf("stdout->_fileno:%d\n",stdout->_fileno);printf("i hate u\n");printf("i love u\n");close(fd);return 0;
}
原本应该输出到标准输出,但是输出到了新打开的文件中