文件描述符

本文深入解析了C语言中的文件操作,包括打开、写入、读取和关闭文件。文件由内容和属性组成,打开文件是将文件加载到内存以便操作。文件操作如写入实际上是通过操作系统系统调用接口对磁盘硬件进行操作。文章还介绍了文件描述符的概念,它是进程管理打开文件的关键,以及其分配规则。缓冲区在C语言文件函数中起到重要作用,不同类型的输出有不同的缓冲策略。最后,文章讨论了标准输出和错误的重定向以及其实现方式。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

文件

  1. 文件=文件内容+文件属性(文件属性也是数据,即使创建一个空文件,也要占据磁盘空间)

  2. 文件操作 = 文件内容的操作+文件属性的操作(在操作文件时可能即改变了文件内容又改变文件属性)

  3. 所谓打开文件,究竟是干什么?

    打开文件是手段,目的是对文件进行操作,打开文件,就是将文件加载到内存,这是由冯诺依曼体系结构决定的(CPU只能对内存进行读写,不能访问其他外设,所以对文件操作,需要先将文件加载到内存)

  4. 当文件没有处于打开状态时文件储存在磁盘上

  5. 打开的文件(内存文件)和磁盘文件

  6. 当我们写的文件操作代码——>程序,程序运行的时候,执行相应的代码,才会真正的对文件进行相关操作

  7. 进程和打开文件

C语言文件操作

FILE* fp=fopen("log.txt","w");//以写的方式打开文件
if(fp==NULL)
{
    perror("fopen");
    return 1;
}
const char* msg="hello 104";
int cnt=1;
while(cnt<20)
{
    fprintf (fp,"%s:%d",msg,cnt++);//将%s:%d写入文件
}
fclose(fp);//关闭文件
  1. 默认这个文件在当前路径生成

当前路径:当前进程的工作路径

工作路径:进程在哪个路径运行,这个路径就是这个进程的工作路径

可以使用系统调用接口chdir()改变进程的当前路径

  1. 写入的方式打开文件(如果)
fopen("log.txt","w");  //用写的方式打开文件,会先将文件清空
fopen("log.txt","a");  //用写的方式打开文件,以追加写入

写入文件到底是怎么写入的?

写入文件是向磁盘中写入,磁盘是硬件,所以对硬件写入都是通过操作系统提供的系统调用接口完成的,我们C语言中使用的函数是对系统接口做了封装。

image-20221106215040704

进程和打开文件的关系(内存级)

文件系统级别的接口

open

int open(const char*pathname,int flags);
int open(const char*pathname,int flags,mode_t mode);

返回值:成功返回文件描述符,失败返回-1
参数:

  • patnname:打开的文件名

  • flags:O_RDONLY(只读),O_WRONLY(只写),O_RDWR(可读可写),O_APPEND(追加写入),O_CREAT(不存在创建),O_TRUNC(写入时先清空)(宏),系统传递标记位,用位图结构来进行传递的,每一个宏标记,一般只需要一个比特位是1,并且和其他宏对应的值,不能重叠

比如:O_RDONLY 0000 0001,O_WRONLY 0000 0010

  • mode:创建时文件的文件的读写权限(mask)//创建后真正的权限是mask&!unask
int main()
{
  int fd = open("test.txt",O_WRONLY|O_CREAT,0666);		 //不会清空,直接覆盖写入
  //int fd=open("test.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);//会清空后,再写入
  //int fd=open("test.txt",O_WRONLY|O_CREAT|O_APPEND,0666);//追加写入
  
  char buf[]="qwer";
 // write(fd,buf,sizeof(buf));			//写入文件

  read(fd,buf,4);			//从文件读取4个字节到buf
  printf("%s\n",buf);
  close(fd);
  
  return 0;
}

补充:命令行清空文件

> test.txt

write

write(fd,buffer,sizeof(buffer)-1;
//-1是因为把数据写入到文件里,文件不认识'\0',所以在文件中会出现乱码(编译器认识'\0')

read

read(fd,buffer,sizeof(buffer)-1;

文件描述符

int main()
{
  int fd1=open("test1.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
  printf("fd1:%d\n",fd1);
  int fd2=open("test2.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
  int fd3=open("test3.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
  printf("fd2:%d\n",fd2);
  printf("fd3:%d\n",fd3);
  close(fd1);
  close(fd2);
  close(fd3);
  
  return 0;
}

image-20221107200144337

从上述代码可以看出文件描述符从3开始

那文件描述符的这个数字到底是什么以及为什从3开始,0,1,2呢?

我们首先了解文件的原理

一个进程可能会打开多个文件,打开的文件会被加载到内存中,那么如何管理这些文件呢?

管理文件就是将文件先描述,在组织

一个文件被打开,在内存中就会创建文件的数据结构(结构体),再通过链表的方式链接起来

struct file
{
    //包含了文件的大部分内容+属性
    
    struct file* next;
    struct file* prev;
}

一个进程可能会打开多个文件,进程的pcb(task_struct)中会有一个结构体指针,指向一个结构体,这个结构会有一个结构体指针数组成员,这个数组中的结构体指针就指向描述文件的数据结构,这么一个数组下标就对应指向结构体对应文件的描述符。

从3开始是因为,0,1,2一开始就打开了,0,1,2——>stdin(标准输入,键盘),stdout(标准输出,显示器),stdree(标准错误,显示器)

补充:C语言中的FILE*是一个结构体指针,因为C语言文件函数是封装系统接口,那么C语言中的FILE中一定封装了fd

FILE* f=fopen("bite","r");
printf("%d",f->_fileno);	//fd

文件描述符的分配规则

从头遍历这个结构体指针数组,找到一个最小的且没有被使用的下标,分配给新的文件

重定向的本质

把1对应的文件关掉,打开一个文件

本来向显示器打印,最后向一个文件打印(重定向)

stdout标准输出,只往1指向的文件输出,不关心1到底指向谁

文件操作只认fd,不关心fd到底指向那个文件

如果我们将来要进行重定向,上层只认0,1,2,3,4这样的fd,我们可以在os内部,通过一定的方式调整数组特定下标的内容(指向),我们就可以完成重定向操作

具体操作

上面的一堆数据,都是os内核,只能使用os提供的系统调用

系统调用

int dup2(int oldfd,int newfd);

是把fd_array数组上的内容拷贝,而不是拷贝数组下标

把数组oldfd下标得内容拷贝到数组下标newfd

比如:

dup2(fd,1;		//把fd内容拷贝到1中
//这样在fd_array[]数组中下标1中的内容和下标fd的内容一样,这时,我们向标准输出打印,就不会像显示器打印,而是向文件中打印

返回值:

失败返回-1,成功返回一个文件描述符

理解缓冲区

缓冲区的本质

就是一段内存

缓冲区在哪里?

先看一段代码

int main()
{
  printf("hello write1");
  fputs("hello write2",stdout);
  fprintf(stdout,"hello write3");
  write(1,"hello write4",12);
  sleep(5);
  return 0;
}

image-20221112191701300

可以看出,"hello write4"马上刷新出来,而其余的是sleep5秒之后,进程退出,刷新缓冲区,才输出的。

由此可得,系统并没有为我们提供缓冲区,而C语言的函数,却有缓冲区,说明缓冲区是由语言提供的

C语言接口封装了write接口,C语言接口是数据是先放进缓冲区的,等到要刷新缓冲区时,再调用write函数

那么缓冲区到底在那?

缓冲区是语言提供的,这几个C语言接口有一个共同的参数:stdoutstdout是FILE*类型,FILE是C语言的文件类型,它是一个结构体,结构体中除了包含fd(文件描述符),还有一部分就是缓冲区。

那么我们的代码在执行时,会把数据先写入到FILE中的的缓冲区中,等待缓冲区积累到一定量时,在把缓冲区中的数据通过write(fd,……)刷新到内核空间中(因为打开文件,文件就会加载到内核空间中),再通过synfcs系统接口,把数据再刷新到硬件(这才真正把数据写入到磁盘中)

1

在刚刚说过,缓冲区在积累一定量时刷新,刷新策略是什么?

  1. 常规

无缓冲(立即刷新)

行缓冲(逐行缓冲)显示器文件

全缓冲(缓冲区满,刷新)磁盘文件

  1. 特殊

进程退出

用户强制刷新(fflush

标准错误

标准错误的输出和标准输出的输出都为显示器,但是标准输出和标准错误的文件描述符不同,所以将标准输出重定向后,标准错误的输出依旧在显示器上

int main()
{
    //fopen: C库函数
    int fd = open("log.txt", O_RDONLY);//必定失败的
    //stdout
    printf("hello printf 1\n");
    fprintf(stdout, "hello fprintf 1\n");
    fputs("hello fputs 1\n", stdout);
    //stderr
    fprintf(stderr, "hello fprintf 2\n");
    fputs("hello fputs 2\n", stderr);
    perror("hello perror 2");
    //cout
    std::cout << "hello cout 1" << std::endl;
    //cerr
    std::cerr << "hello cerr 2" << std::endl;
    return 0;
}

重定向标准输出

image-20221113190131082

重定向标准输出,标准错误到两个文件

image-20221113185755352

重定向标准输出,标准错误到一个文件

./test > all.txt 2>&1		//先标准输出重定向,再把fd_array[]下标1中的内容拷贝到下标2中
    						//重定向 > 前不加数字,默认标准输出重定向

image-20221113190630518

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值