1.C语言文件操作函数fopen
文件写入
#include <stdio.h>
#include <string.h>
int main()
{
FILE* fp = fopen("myfile", "w");
if (!fp) {
printf("fopen error!\n");
}
const char* msg = "hello bit!\n";
int count = 5;
while (count--) {
fwrite(msg, strlen(msg), 1, fp);
}
fclose(fp);
return 0;
}
fopen
fwirte
fclose
打印内容到显示器的方式:
1.printf("%s");2.fprintf(stdout,"%s");
3.puts("%s")输出字符串,并自动在字符串末尾添加换行符.
4.fputs("%s",stdout)字符串输出到指定的文件流,不会自动添加换行符。
5.fwrite("%s", 一个元素大小, 个数, stdout);
文件读
#include <stdio.h>
#include <string.h>
int main()
{
FILE* fp = fopen("myfile", "r");
if (!fp) {
printf("fopen error!\n");
}
char buf[1024];
const char* msg = "hello bit!\n";
while (1) {
//注意返回值和参数,此处有坑,仔细查看man手册关于该函数的说明
ssize_t s = fread(buf, 1, strlen(msg), fp);
if (s > 0) {
buf[s] = 0;
printf("%s", buf);
}
if (feof(fp)) {
break;
}
}
fclose(fp);
return 0;
}
fread
2.系统文件io操作函数open
#include <unistd.h>
#include <string.h>
int main()
{
umask(0);
int fd = open("myfile", O_WRONLY | O_CREAT, 0644);
if (fd < 0) {
perror("open");
return 1;
}
int count = 5;
const char* msg = "hello bit!\n";
int len = strlen(msg);
while (count--) {
write(fd, msg, len);
}
close(fd);
return 0;
}
open
#include <unistd.h> // 包含 write() 和 close() 函数
#include <fcntl.h> // 包含 open() 和 O_* 常量
#include <stdio.h> // 包含 perror() 和 printf()
#include <sys/types.h> //头文件用于定义一些数据类型和宏 pid_t size_t
#include <fcntl.h>
#include <unistd.h>
int open(const char *pathname, int flags, mode_t mode);
const char *pathname
: 要打开的文件的路径名。int flags
: 文件打开的标志,可以是以下值的组合:
O_RDONLY
: 以只读方式打开文件。O_WRONLY
: 以只写方式打开文件。O_RDWR
: 以读写方式打开文件。O_CREAT
: 如果文件不存在,则创建该文件。需要提供第三个参数mode
。O_TRUNC
: 如果文件已存在并且以写入模式打开,并清空里面内容。O_APPEND
: 在文件末尾写入数据。mode_t mode
: 设置文件的权限.eg 0764 拥有者7wrx 所属组6wr 其他人4r。但实际权限还和unmask(权限掩码)有关。eg.open("test.c",O_CREAT,0666) unmask 0222 结果0666-0222=0444
返回值:
成功时,返回新的文件描述符(非负整数)。fd
失败时,返回
-1
,并设置errno
以指示错误类型。
close
wirte
read
3.文件描述符fd
open打开/创建一个文件时,它的返回值就是文件描述符,本质就是一个int变量。通过fd系统可以找到对应的文件。
当我们创建多个文件时,fd是从3开始增长。为什么?
因为进程启动时会打开三个流,stdin(标准输入)0 stdout(标准输出)1 stderr(标准输出错误)2
文件是存储在磁盘当中,当进程要访问文件时,文件才会从磁盘加载到内存。和进程一样,在内存中可以存在多个文件,这时候就进行管理。先描述,再组织。
每个加载到内存的文件都有struct file结构体,里面封装了与文件相关的信息,例如文件描述符、文件状态、指向文件内容的指针等。
struct file { int fd; // 文件描述符 const char *name; // 文件名 int flags; // 打开标志(如只读、只写等) off_t pos; // 当前读/写位置 size_t size; // 文件大小 // 其他可能的字段,如文件类型、权限等 };
这些结构体用指针链接起来,形参file list链表。进程和文件的关系就变成task list和file list的关系。
在这中间起映射作用的就是files_struct,每个进程都有一个files_struct.
task_struct中有*files指向files_struct的指针,files_struct有指向所有打开文件的
file
结构体的数组。files_struct就是文件描述符表。文件描述符当作数组下标,来查找对应的struct file。
找到对应的struct file之后呢?
对于外部设备的管理,也是先描述,再组织。struct device有外设的信息,不同外设信息种类一样,但数据不同,进行io操作的方式也不同。比如,read wirte只能在键盘上读,wirte函数就实现为空,显示器只能输出,read就实现为空。不同外设都有相同的函数,但实现不同。
struct file中就可以定义函数指针,指向对应外设的io实现函数。
这样底层是不同的外设,但上层都是统一的struct file。
struct file和struct device就像父类和子类的关系,file里面都是函数指针,device 函数实现各不相同。
4.重定向
> 输出重定向 >>追加重定向 <输入重定向
Linux下重定向是怎么实现的?
我们知道进程是通过文件描述表中的指针数组,来找到对应的文件的。数组中每一个指针都指向一个文件,如果把数组中元素改变,让它指向其它文件,就完成了重定向。
没有指针指向的文件会自动关闭。
文件描述符的分配规则是从小到大分配,当close(1)时,再新打开一个文件,此时指向该文件的指针处于下标为1的位置。这也变相的实现的输出重定向。
dup2
关于重定向的操作有dup2函数来实现,dup2将一个文件描述符复制到另一个文件描述符。如果目标文件描述符已打开,它将被关闭并重新分配。
#include <unistd.h>
int dup2(int oldfd, int newfd);
oldfd要赋值的文件描述符,newfd被赋值的文件描述符
最后两个下标都指向oldfd指向的文件。
返回值:
成功 返回newfd的下标失败 -1
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
int main() {
int fd;
// 打开文件以进行写入
fd = open("output.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
// 重定向标准输出到文件描述符 fd
if (dup2(fd, STDOUT_FILENO) == -1) {
perror("dup2");
exit(EXIT_FAILURE);
}
// 关闭原始文件描述符,因为不再需要
close(fd);
// 现在所有标准输出都会写入到 output.txt 文件
printf("This will go to the output.txt file instead of the console.\n");
return 0;
}
用户缓冲区和文件内核缓冲区
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
int main()
{
close(1);
int fd = open("myfile", O_WRONLY | O_CREAT, 00644);
if (fd < 0) {
perror("open");
return 1;
}
printf("fd: %d\n", fd);
//fflush(stdout);
close(fd);
exit(0);
}
上述代码,close(1)把stdout向显示器打印的输出流关闭了,再创建并打开了myfile文件。根据文件描述符从小到大分配,所以此时向向显示器输出就重定向为向myfile文件输出。
所以这段代码执行完后,理应创建myfile并且里面有原本向显示器打印的内容。
[wws@hcss-ecs-178e myshell]$ ll
total 48
-rw-rw-r-- 1 wws wws 82 Oct 12 16:51 makefile
-rwxrwxr-x 1 wws wws 20128 Oct 16 21:02 myshell
-rw-rw-r-- 1 wws wws 7627 Oct 16 21:16 myshell.cc
-rwxrwxr-x 1 wws wws 8560 Oct 17 16:28 test
-rw-rw-r-- 1 wws wws 300 Oct 17 16:28 test.c
[wws@hcss-ecs-178e myshell]$ ./test
[wws@hcss-ecs-178e myshell]$ ll
total 48
-rw-rw-r-- 1 wws wws 82 Oct 12 16:51 makefile
-rw-r--r-- 1 wws wws 0 Oct 17 16:29 myfile
-rwxrwxr-x 1 wws wws 20128 Oct 16 21:02 myshell
-rw-rw-r-- 1 wws wws 7627 Oct 16 21:16 myshell.cc
-rwxrwxr-x 1 wws wws 8560 Oct 17 16:28 test
-rw-rw-r-- 1 wws wws 300 Oct 17 16:28 test.c
[wws@hcss-ecs-178e myshell]$ cat myfile
[wws@hcss-ecs-178e myshell]$
可以看到myfile被创建了,但里面并没有内容。为什么?
首先我们要理解fwirte把数据写到磁盘的过程。
1.它首先将数据写入用户缓冲区。当缓冲区满或用户请求写操作时(一般向显示器写行刷新,向文件写是缓冲区满了刷新),数据会被复制到内核缓冲区。然后,内核会将内核缓冲区的数据写入磁盘(由OS决定什么时候能刷新)。
2.struct FILE
typedef struct { int fd; // 文件描述符 char *buffer; // 缓冲区指针 size_t buffer_size; // 缓冲区大小 size_t buffer_index; // 缓冲区的当前索引 int flags; // 文件状态标志 long offset; // 当前读写位置 int error; // 错误标志 int eof; // EOF 标志 // 可能还有其他字段 } FILE;
3.fwirte和wirte区别,fwirte会先把数据写到FILE结构体的缓冲区 (用户缓冲区),而wirte直接把数据写到文件的内核缓冲区。
4.fclose close区别,fclose在关闭文件时它会自动处理与该文件流相关的缓冲区,会把用户缓冲区的数据刷新到内核缓冲区。而close不会,只会把内核缓冲区的数据刷新到磁盘。
5.fflush(FILE *stream) 把用户缓冲区的数据刷新到内核缓冲区
fsync(int fd),把文件的内核缓冲区中的数据写入磁盘中
所以原因就在于
1.因为把向显示器写重定向为向文件内写,导致用户缓冲区的刷新规则从行刷新变为满刷新。
2.fwirte是把内容写到了用户缓冲区
3.close(fd)会把文件的内核缓冲区的内容刷新到磁盘,但不会把用户缓冲区的内容进行刷新。
4.程序结束自动刷新缓冲区,但文件已经关闭 文件的内核缓冲区也关闭,用户缓冲区的数据不能刷新到内核缓冲区。也就导致不能向磁盘中写入。
解决方法:只要把要写入的数据在文件的内核缓冲区关闭前写入就可以
1.在文件关闭前 fflush(FLIE*)把数据从用户缓冲区刷新到内核缓冲区2.不用fwirte,用wirte直接把数据写到文件的内核缓冲区中
3.不用close,用fclose文件关闭时,会把用户缓冲区的内容刷新
用户缓冲区的意义:fwirte把用户缓冲区写满才会刷新,对比wirte直接写到内核缓冲区这样的好处是1.减少了io交换 2.fwirte底层就是wirte,减少了系统调用 提高程序执行效率。
内核缓冲区也是为了提高io效率。
再来看一个例子
[wws@hcss-ecs-178e myshell]$ gcc test.c -o test
[wws@hcss-ecs-178e myshell]$ ./test
hello fwrite
hello fwrite
hello fprintf
hello write
[wws@hcss-ecs-178e myshell]$ ./test > file
[wws@hcss-ecs-178e myshell]$ cat file
hello write
hello fwrite
hello fwrite
hello fprintf
hello fwrite
hello fwrite
hello fprintf
为什么向显示器打印每个函数都打印一次,向文件打印只有wirte系统函数打印了一次呢?
1.重定向文件写入,导致行刷新变成满刷新。打印完后,缓冲区没满 不会进行刷新。
2.fork(),父进程的缓冲区会被复制到子进程中,但在子进程中进行的任何操作不会影响父进程的输出缓冲区。父子进程相互独立。
3.wirte直接写到内核缓冲区中,和FILE中的缓冲区没关系。
4.如果向显示器打印就是行刷新,等到fork后,用户缓冲区已经没有数据了。