【Linux】基础IO

目录

一、文件概括

二、C语言文件接口

1、打开和关闭文件

2、文件的读写

3、编写mycat

三、系统接口

四、理解Linux下一切皆文件

总结


一、文件概括

我们对文件的操作主要是两个方面

1、对文件的内容进行操作

2、对文件的属性进行操作

文件是在磁盘上放着的,我们要访问文件,是编写代码,然后经过编译,生成可执行程序,运行程序,访问文件。对文件的访问的本质是进程通过系统接口访问文件。

要向硬件写入文件,只能通过操作系统,普通用户也想要写入,就要调用文件类的系统调用接口

而我们平时写C语言/C++所使用的文件类的函数,本质上是语言对系统接口的封装,目的是为了让接口更加好用。这就导致了不同的语言具有不同的语言级别的文件访问接口,外在表现它们是不一样的,但是它们都在内部封装了系统接口,而这样的接口一款操作系统只有一套。

如果语言不提供对文件的系统接口的封装,就会导致所有的访问文件的操作,,都要我们直接使用系统接口,一旦使用系统接口,编写文件代码,就无法在其它平台上直接运行了,就失去了跨平台性。

二、C语言文件接口

1、打开和关闭文件

#include 
 
int main()
{
    FILE *fp = fopen("log.txt", "r");
    if (fp == NULL)
    {
        perror("fopen");
        return 1; /* 要返回错误代码 */
    }

    fclose(fp);
    fp = NULL; /* 需要指向空,否则会指向原打开文件地址 */
    return 0;
}

作用:用来打开一个文件

格式:FILE * fopen(const char * path, const char * mode);

它的第一个参数是文件路径,可以是绝对路径,也可以是相对路径

mode是它的选项

它主要常用的选项如上图

r是以只读的方式打开文件

w是只写的方式打开文件,如果文件不存在,它会自动创建,如果文件已经存在,它会在打开之前清空文件,然后再打开

a是append,追加,向文件的结尾追加内容

上面的特性与输出重定向和追加重定向类似 

返回值:打开文件成功返回一个文件指针,若打开文件失败则返回NULL

在例子中我们使用了perror,C语言内部有errno,当某些函数出错时errno会被设置,使用perror等函数可以捕获到。

我们再回到fopen的第一个参数我们说如果不填入绝对路径,只写文件名,它会在默认路径创建文件,那么什么是默认文件呢?

我们看一段代码

  1 #include 
  2 
  3 int main()
  4 {
  5     FILE* fp = fopen("log.txt", "w");
  6     if(fp == NULL)
  7     {
  8         perror("fopen");
  9         return 1;
 10     }
 11 
 12     fclose(fp);
 13 
 14     return 0;                                                                                                                                                                          
 15 }                  

这是最普通的文件打开方式

【Linux】基础IO_第1张图片

我们发现log.txt真的在当前路径创建了,在我们编译好可执行程序,并且没有运行可执行程序时,log.txt是没有被创建的。

当我们将可执行程序移动到其它目录

【Linux】基础IO_第2张图片 

 

我们将可执行程序移动到tmp目录下,tmp目录是一个只有可执行程序的目录,该目录是没有log。txt的,我们再运行

【Linux】基础IO_第3张图片

我们发现,生成的文件好像是在可执行程序的运行 目录下创建

我们前面了解过一种查看当前进程的方式,在/proc目录下是进程的相关信息,我们只要知道进程的pid就能够查看,所以我们稍加修改代码,在最后加上sleep死循环,并且打印他的pid

  1 #include   
  2 #include   
  3 int main()  
  4 {  
  5     FILE* fp = fopen("log.txt", "w");  
  6     if(fp == NULL)  
  7     {  
  8         perror("fopen");  
  9         return 1;  
 10     }  
 11   
 12     printf("pid: %d\n", getpid());                                                                                                                                                     
 13                                                                                                                                                        
 14     while(1)                                                                                                                                           
 15     {                                                                                                                                                  
 16         sleep(1);                                                                                                                                      
 17     }                                                                                                                                                  
 18                                                                                                                                                        
 19     fclose(fp);                                                                                                                                        
 20                                                                                                                                                        
 21     return 0;                                                                                                                                          
 22 }                                  

【Linux】基础IO_第4张图片

然后切换到/proc/27140目录下

【Linux】基础IO_第5张图片 

显示更多信息

【Linux】基础IO_第6张图片 

我们重点关注cwd和exe这两个文件

cwd是当前进程的工作目录

exe是可执行程序的位置

所谓的当前路径:当一个进程运行起来的时候,每一个进程都会记录自己当前所处的工作路径

我们调用fopen时的log.txt的路径是操作系统将cwd和文件名拼接而成的。

同时要注意:一个进程启动之后,cwd就一般不会改变了

fclose 函数说明:

作用:关闭一个文件流,释放文件指针

格式:int fclose( FILE *fp );

返回值:如果流成功关闭,fclose 返回 0,否则返回EOF

参数说明:

*fp:需要关闭的文件指针
注:在文件操作完成后我们应该调用该函数来关闭文件,如果不关闭文件将可能会丢失数据。因为在向文件写入数据时会先将数据输出到缓冲区,待缓冲区充满后才正式输出给文件。
 

 

 

2、文件的读写

文件的写入主要就三种

fwrite

fputs

fprintf

  1 #include   
  2 #include   
  3 #include   
  4   
  5 int main()  
  6 {  
  7     FILE* fp = fopen("log.txt", "w");  
  8     if(fp == NULL)  
  9     {  
 10         perror("fopen");  
 11         return 1;  
 12     }  
 13   
 14     const char* s1 = "hello fwrite";  
 15     fwrite(s1, strlen(s1), 1, fp);  
 16   
 17     const char* s2 = "hello fprintf";
 18     fprintf(fp, "%s\n", s2);
 19 
 20    const char* s3 = "hello fputs";
 21    fputs(s3, fp);
 22 
 23     fclose(fp);                                                                                                                                                                        
 24                                                                                                                                                  
 25     return 0;                                                                                                                                    
 26 }                              

【Linux】基础IO_第7张图片

成功将三个字符串写入到log.txt中,不过这里有一个细节,strlen(s1)到底要不要加上1来使C语言字符串最后的'\0'也写入到文件中?

我们可以测试一下,上面的代码只是将strlen(s1)后面加1

【Linux】基础IO_第8张图片 

然后我们就发现出现了一些乱码。

这是因为文件只会保存有效的数据,C语言字符串结尾的'\0'不是有效数据,所以不能+1,如果+1就会出现乱码。

这是在系统层面上的说法:文件读取的是有效数据,如果有需要'\0'可以自行加上

3、编写mycat

 基于上面的认识,我们可以自己实现一个mycat

#include       
#include       
#include       
      
int main()      
{      
    FILE* fp = fopen("log.txt", "r");      
    if(fp == NULL)      
    {      
        perror("fopen");      
        return 1;      
    }      
      
        
        
   char buffer[64];    
   memset(buffer, '\0', sizeof buffer);    
   while(fgets(buffer, sizeof buffer, fp) != NULL)    
   {    
       fprintf(stdout, "%s", buffer);                                                                                                                                                      
   }                                                                                                                                                                      
                                                                                                                                                                                                                                                                                                                               
                                                                                                                                                                          
    fclose(fp);                                                                                                                                                           
                                                                                                                                            
    return 0;                                                                                                                               
}                            

【Linux】基础IO_第9张图片

 

三、系统接口

在一个C语言编写的程序运行时,系统会默认打开三个标准输入输出流

stdin

stdout

stderr

【Linux】基础IO_第10张图片

 它们三个与键盘显示器有关,在C语言看来,它们都被统一看作FILE*

FILE是一个结构体,它包含文件相关内容


我们前面使用了很多C语言接口

fopen         fclose        fread         fwrite

open          close         read          write

上面一行是C语言接口,下面一行是与之所对应的系统接口

【Linux】基础IO_第11张图片

它的第一个参数与fopen一致,就不多说了

重点是第二个参数

在官方文档中第二个选项有很多

【Linux】基础IO_第12张图片 

The argument flags must include one of the following access modes: O_RDONLY, O_WRONLY, or O_RDWR.  These request opening the file read-only, write-only, or read/write, respec?

这么多选项,我们重点关注几个,先来说上面的至少包含的选项

这时我们就会有疑问,他说至少包含一个选项,可是它的flags参数就只有一个,根本无法传其它选项?

答案是,它使用了位图这种数据结构,一个int具有32个比特位,并且每个选项就包含了两种,选中或者是为选中,正好与二进制相同,所以就定义int的每个比特位代表不同的选项,从而实现了一次可传递多个选项。

我们也可以写类似的代码

#include     
#include     
#include     
    
#define ONE 0x1    
#define TWO 0x2    
#define THREE 0x4     
    
void show(int flags)    
{    
    if(flags & ONE) printf("Hello ONE\n");    
    if(flags & TWO) printf("Hello TWO\n");    
    if(flags & THREE) printf("Hello THREE\n");    
}    
    
int main()    
{    
    
    show(ONE);
    show(ONE | TWO);
    show(ONE | TWO | THREE);
   
    return 0;
}


说会正题,open的第二个参数

O_APPEND  追加写入内容,与fopen的a方式打开类似

O_CREAT    如果文件不存在就创建它

O_TRUNC  每次打开文件之前都清空文件

O_RDONLY 以只读的方式打开文件

O_WRONLY  以只写的方式打开文件

O_RDWR 以读写的方式打开文件

这证明了,在应用层看到的一个很简单的动作,在系统接口和OS层面,可能要做非常多的动作。


  我们可以尝试使用这些选项

#include 
#include 
#include 
#include 
#include 
#include 


int main()    
{    
    
    int fd = open("log.txt", O_RDONLY | O_CREAT);    
    if(fd < 0)    
    {    
        perror("open");    
        return 1;    
    }    
    
    const char* s1 = "Hello write\n";    
    
    close(fd);
    return 0;
}

   我们的选项是创建log.txt文件

【Linux】基础IO_第13张图片                                                                                                                                                                                                                                                                   

 我们创建出了log.txt可是却发现它的权限不对,它是乱的,并不像我们touch出来的文件权限那样

这时就要用到open的带三个参数的函数了,带两个参数的open函数主要是用来read的

三个参数的用来write,不过因为umask的存在实际的权限与我们设定的权限可能并不会相同,这时就要使用umaks函数,来手动设置当前进程的umask值

【Linux】基础IO_第14张图片

【Linux】基础IO_第15张图片 

这样就成功打开了一个文件

【Linux】基础IO_第16张图片 

接下来是使用write来写入文件

#include     
#include     
#include     
    
int main()    
{    
    umask(0);    
    int fd = open("log.txt", O_RDONLY | O_CREAT, 0666);    
    if(fd < 0)    
    {    
        perror("open");    
        return 1;    
    }    
    
    fprintf(stdout, "open log.txt success fd: %d\n", fd);                                                                                                                                  
                                                                                                                                                                              
    const char* s = "Hello Write";                                                                                                                                            
    write(fd, s, strlen(s));                                                                                                                                                  
                                                                                                                                                                              
    close(fd);        
    return 0;                                                       
}

 

【Linux】基础IO_第17张图片

【Linux】基础IO_第18张图片 

最后一个接口是read,read是从一个文件中读取数据,不过这时候就需要我们手动添加'\0',因为它是系统接口它可以不考虑'\0',而调用C语言接口fgets等,他都是默认在字符串结尾添加'\0'的

#include       
#include       
#include       
#include       
#include       
#include       
      
int main()      
{      
    umask(0);      
    int fd = open("log.txt", O_RDWR | O_CREAT, 0666);      
    if(fd < 0)      
    {      
        perror("open");      
        return 1;      
    }      
      
    fprintf(stdout, "open log.txt success fd: %d\n", fd);      
      
    char buffer[64];      
    memset(buffer, '\0', sizeof buffer);      
    read(fd, buffer, sizeof buffer);    
    
    fprintf(stdout, "%s\n", buffer);                                                                                                                                                       
                                                                                                                                                                  
    close(fd);                               
}

 【Linux】基础IO_第19张图片


#include       
#include       
#include       
#include       
#include       
#include       
      
int main()      
{      
    umask(0);      
    int fd = open("log.txt", O_RDWR | O_CREAT, 0666);      
    if(fd < 0)      
    {      
        perror("open");      
        return 1;      
    }      
        
    int fd1 = open("log1.txt", O_CREAT | O_RDWR);      
    int fd2 = open("log1.txt", O_CREAT | O_RDWR);      
    int fd3 = open("log1.txt", O_CREAT | O_RDWR);      
    int fd4 = open("log1.txt", O_CREAT | O_RDWR);      
      
    printf("%d\n", fd);                                                                                                                                                                    
    printf("%d\n", fd1);      
    printf("%d\n", fd2);      
    printf("%d\n", fd3);      
    printf("%d\n", fd4);      
    close(fd);                                                             
}

 

我们连续打开多个文件,查看fd

【Linux】基础IO_第20张图片

发现它的fd是连续增加的可是,0,1,2去哪里了?

其实0,1,2是系统默认打开的三个文件stdin,stdout, stderr

我们可以尝试向1中写入,1是stdout,查看情况

#include     
#include     
#include     
#include     
#include     
#include     
    
int main()    
{    
    umask(0);    
    int fd = open("log.txt", O_RDWR | O_CREAT, 0666);    
    if(fd < 0)    
    {    
        perror("open");    
        return 1;    
    }    
    
    int fd1 = open("log1.txt", O_CREAT | O_RDWR);    
    int fd2 = open("log1.txt", O_CREAT | O_RDWR);    
    int fd3 = open("log1.txt", O_CREAT | O_RDWR);    
    int fd4 = open("log1.txt", O_CREAT | O_RDWR);    
    
    printf("%d\n", fd);    
    printf("%d\n", fd1);    
    printf("%d\n", fd2);    
    printf("%d\n", fd3);    
    printf("%d\n", fd4);    
    const char* s = "Hello World";                                                                                                                                                         
    write(1, s, strlen(s));                                                                                                                        
    close(fd);                                                          

 

#include     
#include     
#include     
#include     
#include     
#include     
    
int main()    
{    
    umask(0);    
    int fd = open("log.txt", O_RDWR | O_CREAT, 0666);    
    if(fd < 0)    
    {    
        perror("open");    
        return 1;    
    }    
    
   
    const char* s = "Hello World\n";                                                                                                                                                       
    write(1, s, strlen(s));    
    //close(fd);    

我们先将close屏蔽掉,因为这里涉及缓冲区。

我们发现向1中写入就是向stdout写入,也就是显示器。

我们当然也可以从0中读取

#include     
#include     
#include     
#include     
#include     
#include     
    
int main()    
{    
    umask(0);    
    int fd = open("log.txt", O_RDWR | O_CREAT, 0666);    
    if(fd < 0)    
    {    
        perror("open");    
        return 1;    
    }    
                                                                                                                         
                                                                                                                                                                    
    char input[16];                                                                                                                                                 
    ssize_t s = read(0, input, sizeof input);                                                                                                                       
    if(s > 0)                                                                                                                                                                              
    {    
        input[s] = '\0';    
        printf("%s\n", input);    
    } 
}   

 

【Linux】基础IO_第21张图片


stdin, stdout,stderr内部是有fd的

#include       
#include       
#include       
      
int main()      
{      
      
      
    printf("stdin: %d\n", stdin->_fileno);      
    printf("stdout: %d\n", stdout->_fileno);      
    printf("stderr: %d\n", stderr->_fileno);                    
}

【Linux】基础IO_第22张图片


 进程要访问文件,必须要先打开文件

一般而言 进程:打开的文件 = 1 : n

文件要被访问,前提是加载到内存中才能被直接访问

进程打开的文件是有多个的,如果是多个进程都打开自己的文件呢?

系统中会存在大量的被打开的文件,所以OS要把如此之多的文件管理起来,需要先描述在组织

在内核中,OS内部要为了管理每一个被打开的文件,需要创建struct file结构体

struct file

{

        struct file* next;

        struct file* prev;

        //包含了一个被打开的文件的几乎所有的内容(不仅仅包含属性)

}

创建struct file对象,充当一个被打开的文件,如果有很多呢?

就使用双链表组织起来


我们调用open系统接口时

task_struct中的struct file* 指针指向struct_file,然后struct_file中的struct file* array[]这个指针数组的下标就是文件描述符fd,指针数组中存的是该进程打开的文件的被操作系统所创建的struct file结构体。struct file 结构体中包含了文件的所有信息


因此C库函数一定封装了系统调用接口,FILE*中的FILE是C语言提供的,通过FILE*找到文件描述符,将数据向文件描述符中写入


文件在Linux中被分成两种,一种是被打开的文件(被进程所打开,struct file)被叫做内存文件

另一种是没有被打开的文件(在磁盘上  文件 = 内容 + 属性)被叫做磁盘文件

文件被加载到内存,加载的内容,还是属性是分情况的


接下来重点说内存文件

struct file是一个内核的数据结构,包含了文件的所有内容(不仅仅是包含了属性)

【Linux】基础IO_第23张图片

对于图片左半部分属于进程管理

图片右半部分属于文件管理

打开的文件与进程一样都是需要被管理的struct file 然后将该进程的struct file地址填入struct file

到该进程PCB的struct file_struct中的指针数组,同时返回该位置的下标(文件描述符)

fwrite->FILE*->write->write(fd,……)->自己执行操作系统内部的write方法->能找到进程的tsk_struct->fs->files_struct->fd_array[]->struct file->内存文件


前面我们只是简单的说明了文件描述符的分配规则

最小的没有被占用的文件描述符

#include     
#include     
#include     
#include     
#include     
#include     
    
int main()    
{    
    close(1);    
    int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);    
    if(fd < 0)    
    {    
        perror("open");    
        return 1;    
    }    
    printf("fd: %d\n", fd);    
    printf("fd: %d\n", fd);    
    printf("fd: %d\n", fd);    
    printf("fd: %d\n", fd);    
    fprintf(stdout, "Hello World\n");    
    const char* s = "hello";    
    fwrite(s, strlen(s), 1, stdout);    
    fflush(stdout);    
    close(fd);                                                                                                                                                                             
    
    return 0;    
}    
                                                                                          

 我们将1关闭,然后向stdout中打印一系列字符串

【Linux】基础IO_第24张图片

然后我们就发现,他没有打印到显示器,而是写入到log.txt中了

原因是,C语言接口十分的单纯,它将stdout写死成了数组下表为1的内容了

这样我们就实现了输出重定向


输出重定向原理

我们创建进程时,系统会给进程默认关联打开的三个文件

stdin,stdout,stderr,我们关闭了1,在C语言的FILE中1还存在,不过被设定为NULL,关闭1是将该进程与显示器的关联去掉,files_struct数组下标的内容被置为NULL,当我打开log.txt时,将1这个fd给了log.txt,将log.txt的struct file地址填入到1下标中


重定向的本质是:在OS内部更改fd对应的内容的指向

不过我们前面写的太垃圾了,系统是提供了更改关联关系的系统接口

【Linux】基础IO_第25张图片 

我们只看dup2,dup2有两个参数一个oldfd,另一个是newfd

oldfd copy to newfd

换句话说最后是将newfd的内容更改为oldfd的内容

所以我们要实现输出重定向要传入的参数是dup2(3,1)

#include       
#include       
#include       
#include       
#include       
#include       
      
int main()      
{      
    //close(1);      
    int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);      
    if(fd < 0)      
    {      
        perror("open");      
        return 1;      
    }      
      
    dup2(fd, 1);    
    printf("fd: %d\n", fd);    
    printf("fd: %d\n", fd);    
    printf("fd: %d\n", fd);                                                                                                                                                                
    printf("fd: %d\n", fd);                                                                                                           
    fprintf(stdout, "Hello World\n");                                                                                                 
    const char* s = "hello";                                                                                                          
    fwrite(s, strlen(s), 1, stdout);                                                                                                  
    fflush(stdout);                                                                                                                   
    close(fd);                                                                                                                        
                                                                                                                                      
    return 0;                                                                                                                         
}                                                               

 【Linux】基础IO_第26张图片

同样也实现了

 

四、理解Linux下一切皆文件

Linux的设计哲学体现在操作系统的软件设计层面

如何使用C语言来实现面向对象和多态呢?

C语言可以使用struct来保存成员变量,C语言是无法保存成员方法的,但是我们可以使用函数指针来调用成员函数


底层不同的硬件,一定是对应不同的操作方法的,我们所接触的都是外设,所以每一个设备的核心代码都可以是read,write,I/O操作

所有的设备,都可以有自己的read和write但是代码的实现一定是不一样的。也就是说Linux从驱动向上的全部都是以面向对象的方式实现的


总结


例如:以上就是今天要讲的内容,本文仅仅简单介绍了Linux的基础IO

你可能感兴趣的:(Linux,linux,c语言)