Linux下IO操作

在系统角度理解文件

你不是说你Linux下一切皆文件吗,这样的话,键盘、显示器自然也可以被看成是文件,但是既然是文件的话,我们为什么从来没有打开过这些文件就能够进行scanf、fgets、printf、cout呢?
就像我们写c语言的时候肯定要先打开这个文件,然后再对文件进行读写,但我们对键盘或者显示器读写的时候却没有打开,这是为什么呢?
其实C/C++程序,默认就会打开三个文件流
在这里插入图片描述
上面三个分别是

  1. 标准输入
  2. 标准输出
  3. 标准错误
    标准输入对应键盘 标准输出和标准错误都对应显示器
    正是因为这三个文件流默认被打开了,所以我们对其进行读写的时候才不需要打开

基本知识

  1. 文件=内容+属性
  2. 文件的所有操作,无外乎:a. 对内容 b. 对属性
  3. 文件在磁盘上放着,我们访问文件,先写代码->编译->exe->运行->访问文件:本质是进程在访问文件
    对文件的读写,就是向硬件写入,只有操作系统才有这个权利,那么普通用户也想向硬件写入,所以就必须让OS提供接口,这就是文件的系统调用接口。
    我们之前学过的语言层面的文件操作,其实是语言对这些接口做了封装,让接口更好地使用,这就导致了不同的语言有不同语言级别的文件访问接口,但是他们都是封装的系统接口
    语言提供自己的文件操作接口,这也为跨平台提供了便利,只需要把不同平台的代码都实现一遍,然后在不同的平台上条件编译,动态裁剪就可以。
    如果语言不提供对文件的系统接口封装,那么所有访问文件的操作都必须直接使用OS的接口,一旦使用系统接口,编写的代码就不具备了跨平台性。
  4. 显示器也是硬件,printf向显示器打印也是一种写入,这和写入到磁盘上的文件没有本质区别。
  5. Linux认为,一切皆文件。
    感性理解:
    我们曾经理解的文件:磁盘上能进行read,write的文件
    显示器:printf / cout也是一种write啊
    键盘:scanf / cin也是一种read啊
    站在你写的程序的角度来看,加载到内存变成进程后,也就是站在内存的角度,对键盘和普通文件的read就是把数据input到内存,而对显示器和普通文件的write就是把数据从内存output到上面。
    总结上面的过程:
    在这里插入图片描述
    综上:什么叫做文件呢?
    站在系统的角度,能够被input读取,或者能够被output写出的设备就叫做文件!
    狭义文件:普通的磁盘文件
    广义上的文件:显示器,键盘,网卡,声卡,显卡,磁盘,几乎所有的外设,都可以称之为文件。

C语言文件操作函数

fopen

FILE *fopen(const char *path, const char *mode);

mode:就是文件打开的方式,有以下几种方式

r :Open text file for reading. The stream is positioned at the
beginning of the file.

r+ :Open for reading and writing. The stream is positioned at the
beginning of the file.

w :Truncate(缩短) file to zero length or create text file for writing.
The stream is positioned at the beginning of the file.
w+ :Open for reading and writing.The file is created if it does not exist, otherwise it is truncated.
The stream is positioned at the beginning of the file.
a :Open for appending (writing at end of file).
The file is created if it does not exist.
The stream is positioned at the end of the file.
a+: Open for reading and appending (writing at end of file).
The file is created if it does not exist. The initial file position
for reading is at the beginning of the file,
but output is always appended to the end of the file.

w方式和a方式打开文件的区别就像,输入重定向与追加重定向
以w或者w+的方式打开文件的时候,如果文件存在,那么文件中的内容将会被清空,然后再写入,如果文件不存在,那么文件将会被创建

path

你说fopen创建文件的时候,默认是在哪里创建的呢?你可能会说在当前路径下创建,欸嘿,那什么叫做当前路径呢?
是源文件所在的路径吗?这个我们可以验证一下,我发现源文件在一个目录下,程序也在这个目录下,执行程序的时候,在这个目录下创建了一个文件,当我把程序cp到上一级目录下的时候,在上一级目录执行程序,发现文件在上一级目录下被创建了,显然当前路径并不是源文件所在的路径,那么是程序文件所在的路径吗?
可以说大部分情况下都是这样,但是这样说并不准确。
我们写下面这段代码:

#include <stdio.h>
  2 #include <unistd.h>
  3                                                                      
  4 int main()
  5 {
  6     FILE* fp=fopen("file.txt","w");
  7     if(fp==NULL)
  8     {
  9         perror("fopen");
 10         return 1;
 11     }
 12 
 13     fclose(fp);
 14 
 15     while(1)
 16     {
 17         sleep(1);
 18     }
 19 
 20     return 0;
 21 }

死循环是为了不让进程结束,方便我们查看进程信息,我们获取进程PID
在这里插入图片描述
然后就可以在/proc/PID目录下查看进程信息了
在这里插入图片描述
我们可以看到这两条信息,exe的意思就是指进程所对应的可执行程序的路径,cwd就是进程的当前工作目录,当前路径的秘密就是这东西。
当一个程序运行起来的时候,每个进程都会记录自己当前所处的工作路径,我们创建文件的时候把默认当前路径就是,把这个当前工作路径与文件名拼接就形成了最终的路径!

写函数

fwrite、fprintf、fputs

上代码:

 const char* s1="hello fwrite\n";
 15     fwrite(s1,strlen(s1),1,fp);                                                                                                                 
 16 
 17     const char* s2="hello fprintf\n";
 18     fprintf(fp,"%s",s2);
 19 
 20     const char* s3="hello fputs\n";
 21     fputs(s3,fp);
 22     fclose(fp);

问题1:fwrite里面的strlen结果需要+1吗?需要把字符串结尾的\0也写到文件里面吗?
答案是不需要,因为你\0是C语言的规定,文件是不需要遵守的,文件里面只保存有效数据,大不了你读的时候把\0加上就行了。
问题2:当你要写入一个文件的时候,以w方式打开,文件是在fopen的时候就被清空了呢?还是在fwrite的时候被清空了呢?
是在fopen的时候就被清空了
小tips:如果我们在命令行想清空一个文件,那么我们只需要"> 文件名" 就可以,这就像c语言以w方式fopen一样,>就指明了是写,但是没有具体的写操作,就只是将文件以写方式打开了一样。

读函数

这里只讲fgets

上代码:

char line[64];
 15     //fgets返回值不为NULL时,没读取完毕继续读取,返回NULL读取完毕
 16    while(fgets(line,sizeof(line),fp))
 17    {
 18        //将读到的结果输出到屏幕上
 19        fprintf(stdout,"%s",line);
 20    }

这里sizeof(line)也不用-1,fgets不是读字符串吗,它会自动在结尾添加\0
实现一个小程序,cat:

1 #include <stdio.h>
  2 #include <unistd.h>
  3 #include <string.h>
  4 
  5 int main(int argc,char* argv[])
  6 {
  7     if(argc!=2)
  8     {
  9         printf("argv error\n");
 10         return 2;
 11     }
 12     FILE* fp=fopen(argv[1],"r");                                                                                                                
 13     if(fp==NULL)
 14     {
 15         perror("fopen");
 16         return 1;
 17     }
 18 
 19     char line[64];
 20     //fgets返回值不为NULL时,没读取完毕继续读取,返回NULL读取完毕
 21    while(fgets(line,sizeof(line),fp))
 22    {
 23        //将读到的结果输出到屏幕上
 24        fprintf(stdout,"%s",line);
 25    }

第一个命令行参数是程序名,第二个就是你要显示的文件名。

文件操作的系统调用接口

open

#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_TRUNC:若文件存在,并且以读方式打开的时候,清空文件
O_APPEND: 追加写
返回值:
成功:新打开的文件描述符
失败:-1

write

#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
 12     //umask(0);       
 13     //fopen("log.txt","w");
 14     //int fd=open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
 15     //fopen("log.txt","a") 
 16     //int fd=open("log.txt",O_WRONLY|O_CREAT|O_APPEND,0666);
 18     if(fd<0)          
 19     {                 
 20         perror("open");
 21         return 1;     
 22     }                 
 23                       
 28     const char* s="hello IO\n";
 29     //const char* s="csy\n";
 30     write(fd,s,strlen(s));

read

#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);

用法示例:

17     int fd=open("log.txt",O_RDONLY);
 18     if(fd<0)
 19     {
 20         perror("open");
 21         return 1;
 22     }
 23 
 24     char buffer[10];
 25     memset(buffer,'\0',sizeof(buffer));                                                                                                         
 26     read(fd,buffer,sizeof(buffer));
 27     printf("%s",buffer);

文件描述符

上面我们看到了,C语言fopen函数返回的是一个FILE*,我们进行读写操作都要传入这个FILE*,而系统接口函数open返回的是一个文件描述符fd,还是一个整型,那么既然C库函数底层封装的是系统接口函数,那么FILE*是什么呢?fd又是什么东西,这俩之间有啥关系呢?
先看一段代码:

 16		int fd1=open("log.txt",O_WRONLY|O_CREAT|O_APPEND,0666);
 17     printf("open success,fd:%d\n",fd1);
 18     int fd2=open("log.txt",O_WRONLY|O_CREAT|O_APPEND,0666);
 19     printf("open success,fd:%d\n",fd2);
 20     int fd3=open("log.txt",O_WRONLY|O_CREAT|O_APPEND,0666);
 21     printf("open success,fd:%d\n",fd3);
 22     int fd4=open("log.txt",O_WRONLY|O_CREAT|O_APPEND,0666);
 23     printf("open success,fd:%d\n",fd4);

结果如下:
在这里插入图片描述
这里的fd为何是从3开始的呢,0、1、2为什么没用
0,1,2刚好是三个,这不由得让我想起C语言不是会默认打开三个标准文件流吗:stdin,stdout,stderr
你想如果是默认打开了,那么我们后面再打开别的文件的时候,自然要排在他们的后面,所以就从3开始了,显然fd的0、1、2是和这三个标准文件流是有关系的。
验证一下:

 13     int a=0;
 14     fscanf(stdin,"%d",&a);
 15     printf("%d\n",a);
 16 
 17     char input[16];
 18     read(0,input,sizeof(input));
 19     printf("%s\n",input);                                                                                                                       
 20 
 21     //fprintf(stdout,"hello world\n");
 22     //const char* s="hello fd\n";
 23     //write(1,s,strlen(s));

我发现,写的时候用stdout或1,都写到了显示器上,而读的时候用stdin或者0,都读取了键盘内容,那显然,3就对应stderr
其实FILE是C标准库提供的一个结构体,那么我们知道,C库函数内部是一定要调用系统调用的,在系统角度,操作系统只认识fd,所以FILE结构体中一定保存着fd的值。
所以,fd是什么呢
进程要访问文件,那必然要先打开文件,把文件加载到内存中,一个进程可以打开多个文件,那么多个进程呢?
每个进程都会有自己的文件,所以在操作系统中会存在大量的被打开的文件,那么操作系统就需要对这些文件进行管理,怎么管理呢:先描述,后组织
在内核中·,操作系统为了管理每个被打开的文件,需要为每个文件构建struct file结构体

struct file
{
	···//包含了一个被打开文件的几乎所有的内容(不仅仅包含属性)
	struct file* prev;
	struct file* next;
};

很多的文件共同构成了一个双链表。
fd是一个个连续的小数字,这让我们想起来了数组的下标,实际就是,在进程PCB task_struct 里面有一个struct file* 数组,每个fd就对应数组下标,数组内容指向每个被打开的文件的struct file
这样我们每打开一个文件其实就是向struct file链表中添加一个节点,然后把节点指针push进数组尾部,把下标返回给fd,这就是文件描述符。
在这里插入图片描述

FILE结构体中就保存着fd,名为_fileno
eg:

 12     printf("%d\n",stdin->_fileno);
 13     printf("%d\n",stdout->_fileno);
 14     printf("%d\n",stderr->_fileno); 

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值