(19)Linux系统下的文件操作 && 文件系统接口

前言:本篇主要讲解底层文件系统接口,详细介绍 open 接口和它的 flags 参数 (即系统传递标记位),重点讲解 O_RDWR, O_RDONLY, O_WRONLY, O_CREAT 和 O_APPEND 这些操作模式。

一、先来段代码回顾C文件接口 

hello.c写文件 

#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;
}

 hello.c读文件

#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;
}

 输出信息到显示器,你有哪些方法

#include <stdio.h>
#include <string.h>
int main()
{
 const char *msg = "hello fwrite\n";
 fwrite(msg, strlen(msg), 1, stdout);
 printf("hello printf\n");
 fprintf(stdout, "hello fprintf\n");
 return 0;
}

1、 关于文件操作的思考

我们曾经讲过:文件 = 文件内容 + 文件属性  

文件属性也是数据!这意味着,即便你创建一个空文件,也要占据磁盘空间!所以:

                                ② 文件操作 = 文件内容的操作 + 文件属性的操作 

 因此,在操作文件的过程中,既改变内容又改变属性的情况很正常,不要把它们割裂开来!

那么,所谓的 "打开" 文件,究竟在做什么? ③ 

"打开文件不是目的,访问文件才是目的!" 

访问文件时,都是要通过 fread,fwrite,fgets... 这样的代码来完成对文件的操作的,

如果通过这些方式,那么 "打开" 文件就需要 将文件的属性或内容加载到内存 (memory) 中!

因为这是由冯诺依曼体系结构决定的,将来 \textrm{CPU} 要执行 fread,fwrite 来对文件进行读写的。

 既然如此……  是不是所有的文件都会处于被打开的状态呢?并不是! 

那没有被打开的文件在哪里?

对于文件的理解,在宏观上我们可以区分成 内存文件 (打开的文件) 和 磁盘文件。  (存储在磁盘中) 

通常我们打开文件、访问文件和关闭文件,是谁在进行相关操作?

运行起来的时候,才会执行对应的代码,然后才是真正的对文件进行相关的操作。
实际上是  进程在对文件进行操作! 在系统角度理解是我们曾经写的代码变成了进程。

进程执行调度对应的代码到了 fopen,write 这样的接口,然后才完成了对文件的操作。
当我执行 fopen 时,对应地就把文件打开了,所以文件操作和进程之间是撇不开关系。

🔺 结论:学习文件操作,实际上就是学习 "进程" 与 "打开文件" 的关系。 ⑦
 

2、当前路径(Current Path)

文件的本质实际上是进程与打开文件之间的关系。

 因此文件操作和进程有关系,我们写一下我们的代码,获取进程 \textrm{pid},查一下进程信息:

#include <stdio.h>
#include <unistd.h>
 
int main(void)
{
    FILE* pf = fopen("log.txt", "w"); // 写入
    if (pf == NULL) {
        perror("fopen");
        return 1;
    }
 
    /* 获取进程 pid */
    printf("Mypid: %d\n", getpid());
 
    /* 打开文件后,等一等,方便查询 */
    while (1) {
        sleep(1);
    }
 
    const char* msg = "hello!";
    int count = 1;
    while (count <= 10) {
        fprintf(pf, "%s: %d\n", msg, count++);
    }
 
    fclose(pf);
}

getpid 拿到进程 \textrm{pid} 后,得益于 "昏睡指令" while(1){sleep(1);) 

我们的进程就一直的跑着,再打开一个窗口,通过 $ls proc 指令检视该进程信息:

 

我们重点关注 \textrm{exe} 和 \textrm{cwd}\textrm{exe} 后面链接指向的是可执行程序 mytest,即 路径 + 程序名。

而 \textrm{cwd} (current working directory),即 当前工作目录,记录着当前进程所处的路径!

每个进程都有一个工作路径,所以我们上一节实现的简单 \textrm{shell} 程序可以用 chdir 更改路径。

创建文件时,如果文件在当前目录下不存在,fopen 会默认在当前路径下自动创建文件。

 默认创建在当前路径,和源代码、可执行程序在同一个路径下,因为这取决于 \textrm{cwd}

cwd -> /home/ayf/lesson1

所以,当前路径更准确的说法应该是:在当前进程所处的工作路径。

" 当前路径指的是在当前进程所处的工作路径 "

只不过默认情况下 (默认路径) ,一个进程的工作路径在它当前所处的路径而已,这是可以改的。

所以我们在写文件操作代码时,不带路径默认是源代码所在的路径,注意是默认!而已!

3、文件操作模式(File Operation Mode) 

由文件操作符 (mode) 参数来指定,常用的模式包括:

man fopen

r:只读模式,打开一个已存在的文本文件,允许读取文件。
 
r+:读写模式,打开一个已存在的文本文件,允许读写文件。
 
w:只写模式,打开一个文本文件并清除其内容,如果文件不存在,则创建一个新文件。
 
w+:读写模式,打开一个文本文件并清除其内容,如果文件不存在,则创建一个新文件。
 
a:追加模式,打开一个文本文件并将数据追加到文件末尾,如果文件不存在,则创建一个新文件。
 
a+:读写模式,打开一个文本文件并将数据追加到文件末尾,如果文件不存在,则创建一个新文件。

 这里我们重点讲一下 a 和 a+

a 对应的是 appending 的首字母,意为 "追加" 。属于写入操作,不会覆盖源文件内容。

代码演示:测试追加效果

每次运行都会在 test.txt 里追加,我们多试几次看看:

a(append) 追加写入,可以不断地将文件中新增内容。(这让我联想到了追加重定向)

不同于 w,当我们以 w 方式打开文件准备写入的时候,其实文件已经先被清空了。

4、文件的读取(File Read) 

我们复习一下文本行输入函数 fgets,它可以从特定的文件流中,以行为单位读取特定的数据: 

char* fgets(char* s, int size, FILE* stream);_

 代码演示:fgets()

#include <stdio.h>
 
int main(void)
{
    FILE* pf = fopen("log.txt", "r"); // 读
    if (pf == NULL) {
        perror("fopen");//显示错误信息
        return 1;
    }
 
    char buffer[64];  // 用来存储
    while (fgets(buffer, sizeof(buffer), pf) != NULL) {
        // 打印读取到的内容
        printf("echo: %s", buffer);
    }
 
    fclose(pf);
}

运行结果如下:

 我们下面再来实现一个类似 $cat 的功能,输入文件名打印对应文件内容。

代码演示:实现一个自己的 cat

#include <stdio.h>
 
/* 读什么就打什么   mycat */
int main(int argc, char* argv[]) 
{
    if (argc != 2) {
        printf("Usage: %s filename\n", argv[0]);
        return 1;
    }
 
    FILE* pf = fopen(argv[1], "r");    // 读取
    if (pf == NULL) {
        perror("fopen");
        return 1;
    }
 
    char buffer[64];
    while (fgets(buffer, sizeof(buffer), pf) != NULL) {
        printf("%s", buffer);
    }
 
    fclose(pf);
}

读到什么就打印身边。

如果再把可执行程序 mytest 改名成 cat,$mv mytest cat ,

 我们就实现了一个自己的 cat 代码。

二、文件系统接口(Basic File IO)

操作文件,除了上述C接口(当然,C++也有接口,其他语言也有),我们还可以采用系统接口来进行文件访问, 先来直接以代码的形式,实现和上面一模一样的代码: 

写文件:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#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 world!\n";
 int len = strlen(msg);
 while(count--){
 write(fd, msg, len);//fd: 后面讲, msg:缓冲区首地址, len: 本次读取,期望写入多少个字节的数
据。 返回值:实际写了多少字节数据
 }
 close(fd);
 return 0;
}

 读文件:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
 int fd = open("myfile", O_RDONLY);
 if(fd < 0){
 perror("open");
 return 1;
 }
 const char *msg = "hello world!\n";
 char buf[1024];
 while(1){
 ssize_t s = read(fd, buf, strlen(msg));//类比write
 if(s > 0){
 printf("%s", buf);
 }else{
 break;
 }
 }
 close(fd);
 return 0;
}

1、系统调用与封装(Syscall and Wrapper) 

当我们向文件写入时,最终是不是向磁盘写入?是!磁盘是硬件吗?就是硬件。

当我们像文件写入时,最后是向磁盘写入。磁盘是硬件,那么谁有资格向磁盘写入呢?

只能是操作系统!

既然是操作系统在写入,那我们自然不能绕开操作系统对磁盘硬件进行访问。

因为操作系统是对软硬件资源进行管理的大手子,你的任何操作都不能越过操作系统!

 所有的上层访问文件的操作,都必须贯穿操作系统。

想要被上层使用,必须使用操作系统的相关的 系统调用 (syscall) !

回顾:: 如何理解 printf?我们怎么从来没有见过这些系统调用接口呢?

显示器是硬件,我们 printf 的消息打印到了硬件上,是你自己调用的 printf 打印到硬件上的,

但并不是你的程序显示到了显示器上,因为显示器是硬件,它的管理者只能是操作系统,

你不能绕过操作系统,而必须使用对应的接口来访问显示器,我们看到的 printf 一打,

内容就出现在屏幕上,实际上在函数的内部,一定是 调用了系统调用接口 的。

 结论:printf 函数内部一定封装了系统调用接口。

所有的语言提供的接口,之所以你没有见到系统调用,因为所有的语言都被系统接口做了 封装

所以你看不到对应的底层的系统接口的差别。为什么要封装?原生系统接口,使用成本比较高。

系统接口是 OS 提供的,就会带来一个问题:如果使用原生接口,你的代码只能在一个平台上跑。

直接使用原生系统接口,必然导致语言不具备 跨平台性 (Cross-platform) !

如果语言直接使用操作系统接口,那么它就不具备跨平台性,可是为什么采用封装就能解决?

封装是如何解决跨平台问题的呢?很简单:

                                " 穷举所有的底层接口 + 条件编译 "

结论:我们学习的接口,C 库提供的文件访问接口,系统调用。它们两具有上下层关系,C 底层一定会调用这些系统调用接口。

  • 解释:不同的语言,有不同的文件访问接口。
  • 系统调用:这就是我什么我们必须学习文件级别的系统接口!

 2、文件打开:open()

打开文件,在 C 语言上是 fopen,在系统层面上是 open。

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);

我们看到,这个 open 接口一个是两参数的,一个是三参数的,这个我们放到后面解释。

  •  open 接口的 pathname 参数表示要打开的文件名,和 C 语言的 fopen 一样,是要带路径的。
  •  flags 参数是打开文件要传递的选项,即 系统传递标记位,我们下面会重点讲解。
  •  mode 参数,就是 "文件操作模式" 了。
#if (defined _CRT_DECLARE_NONSTDC_NAMES && _CRT_DECLARE_NONSTDC_NAMES) || (!defined _CRT_DECLARE_NONSTDC_NAMES && !__STDC__)
    #define O_RDONLY     _O_RDONLY
    #define O_WRONLY     _O_WRONLY
    #define O_RDWR       _O_RDWR
    #define O_APPEND     _O_APPEND
    #define O_CREAT      _O_CREAT
    #define O_TRUNC      _O_TRUNC
    #define O_EXCL       _O_EXCL
    #define O_TEXT       _O_TEXT
    #define O_BINARY     _O_BINARY
    #define O_RAW        _O_BINARY
    #define O_TEMPORARY  _O_TEMPORARY
    #define O_NOINHERIT  _O_NOINHERIT
    #define O_SEQUENTIAL _O_SEQUENTIAL
    #define O_RANDOM     _O_RANDOM
#endif

思考:在 Linux 下,C 语言中文件不存在,就直接创建它,创建是不是需要权限?

当然是需要的,我们需要给文件设置初始权限,这个 mode 参数就是干这个活的。

我们再来看看这个接口的返回值,居然是个 int,而不是我们 fopen 的 FILE* 

  •  open 的返回值是个 int,返回 -1 表示 error,并设置 errno。

3、flags 系统传递标记位

我们可以输入 man 2 open  看看如何设置 flags 参数,实际上就是设置文件操作模式的。

我们重点关注下面这几个文件操作模式,它们被定义在 <fcntl.h> 头文件中:

 O 实际上就是 Open 的意思,它们的用途通过名字不难猜:

  • O_RDONLY: 只读打开
  • O_WRONLY: 只写打开
  • O_RDWR : 读,写打开 这三个常量,必须指定一个且只能指定一个
  • O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
  • O_APPEND: 追加写

返回值:

  • 成功:新打开的文件描述符
  • 失败:-1 
int open(const char* pathname, int flags);

我们称 flags 标记位,并且它是个整数类型(C99 标准之前没有 bool 类型) 

标记位实际上我们造就用过了,比如定义 flag 变量,设 flag=0,设 flag=1,传的都是单个的。

 思考:但如果我想一次传递多个标志位呢?定义多个标记位?flag1, flag2, flag3...

方案:系统传递标记位是通过 位图 来进行传递的。

想必大家已经对位图不陌生了,在前几章我们讲解 waitpid 的 status 参数时就介绍过了:

status 参数也是整型,也是被当作一个 "位图结构" 看待的,这里的 flags 也是如此!

当成位图,就是一串整数0000\, 0000

我们可以让不同的位表示,是否只读,是否只写,是否读写…… 等等等等: 

每个 宏标记一般只需要满足有一个比特位是 1,并且和其它宏对应的值不重叠 即可。 

代码演示:我们创建一个 test.c 

#include <stdio.h>
 
#define PRINT_A   0x1     // 0000 0001
#define PRINT_B   0x2     // 0000 0010
#define PRINT_C   0x4     // 0000 0100
#define PRINT_D   0x8     // 0000 1000
#define PRINT_DFL 0x0
 
// open
void Show (
    int flags  /* 传递标志位 */
    )
{
    if (flags & PRINT_A)     printf("Hello A\n");
    if (flags & PRINT_B)     printf("Hello B\n");
    if (flags & PRINT_C)     printf("Hello C\n");
    if (flags & PRINT_D)     printf("Hello D\n");
    if (flags == PRINT_DFL)  printf("Hello Default\n");
}
 
int main(void)
{
    /* 我想打谁,只需要传对应的标记位即可 */
    printf("# PRINT_DFL: \n");
    Show(PRINT_DFL);
 
    printf("# PRINT_A: \n");
    Show(PRINT_A);
 
    printf("# PRINT_B: \n");
    Show(PRINT_B);
 
    printf("# PRINT_A AND PRINT_B: \n");
    Show(PRINT_A | PRINT_B);
 
    printf("# PRINT_C AND PRINT_D: \n");
    Show(PRINT_C | PRINT_D);
 
    printf("# PRINT_A AND PRINT_B AND PRINT_C AND PRINT_D: \n");
    Show(PRINT_A | PRINT_B | PRINT_C | PRINT_D);
 
    return 0;
}

4、open 接口用法演示  

讲完了 flags 标记位,现在我们可以演示 open 接口的用法了。

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

 代码演示:是用 open() 打开 log.txt 文件没有就创建。

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
 
int main(void)
{
    int fd = open("log.txt", O_WRONLY | O_CREAT);
    if (fd < 0) {         // 打开失败
        perror("open"); 
        return 1;
    }
 
    printf("fd: %d\n", fd);   // 把 fd 打出来看看
 
    return 0;
}

此时,我们的log.txt是原本就存在的,

如果你要创建这个文件,该文件是要受到Linux 权限的约束的!

创建一个文件,你需要告诉操作系统默认权限是什么。

当我们要打开一个曾经不存在的文件,不能使用两个参数的 open,而要使用三个参数的 open!

也就是带 mode_t mode 的 open,这里的 mode 代表创建文件的权限:

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

代码演示:

int main()
{
    int fd = open("log.txt", O_APPEND | O_CREAT, 0666);  // 八进制表示
    if (fd < 0) {
        perror("open");
        return 1;
    }
 
    printf("fd: %d\n", fd);
 
    return 0;
}

因为你要创建的文件,所以要听操作系统!我们来看看 umask: 

你要 666,操作系统不能给你,因为 umask 是 0002,所以最多只能给你 664。 

我们现在就是要 666,我们只需要调用 umask(),然后传 0:umask(0) 

就可以让权限掩码暂时不听按操作系统的默认权限掩码,而用你设置的!

 

 此时权限就变成了我们的666.

实际上,umask 命令就是调用这个接口的。

umask 设为 0,可以让我们以确定的权限打开文件,比如服务器要打开一个日志文件,权限就必须要按照它对应的权限设置好,不要采用系统的默认权限,可能会出问题。

5、文件关闭:close() 

在 C 语言中,关闭文件可以调用 fclose,在系统接口中我们可以调用 close 来关闭: 

#include <unistd.h>
 
int close(int fd);

 该接口相对 open 相对来说比较简单,只有一个 fd 参数,我们直接看代码:

  1 #include <stdio.h>  
  2 #include <sys/types.h>  
  3 #include <sys/stat.h>  
  4 #include <fcntl.h>  
  5 #include <unistd.h>  // 需引入头文件                                  
  6 int main()  
  7 {                                                                   
  8       umask(0);   // umask现在就为0,听我的,别听操作系统的umask了   
  9     int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);  // 八进制表示 
 10 if (fd < 0) {  
 11         perror("open");  
 12         return 1;        
 13     }              
 14        
 15     printf("fd: %d\n", fd);  
 16     close(fd);//关闭文件     
 17                                              
 18     return 0;                                
 19 }  

 

没啥问题,就。。。。。没了。

6、文件写入:write()

文件打开和文件关闭都有了,我们总要干点事吧?现在我们来做文件写入!

在 C 语言中我们用的是 fprintf, fputs, fwrite 等接口,而在系统中,我们可以调用 write 接口:

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

write 接口有三个参数:

  • fd:文件描述符
  • buf:要写入的缓冲区的起始地址(如果是字符串,那么就是字符串的起始地址)
  • count:要写入的缓冲区的大小

代码演示:向文件写入 6行信息


  1 #include <stdio.h>  
  2 #include<string.h>
  3 #include <sys/types.h>
  4 #include <sys/stat.h>
  5 #include <fcntl.h>
  6 #include <unistd.h>  // 需引入头文件                                  
  7 int main()                             
  8 {                                                                   
  9       umask(0);   // umask现在就为0,听我的,别听操作系统的umask了   
 10     int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);  // 八进制表示 
 11     if (fd < 0) {                      
 12         perror("open");                
 13         return 1;                      
 14     }                                  
 15     int cnt=6;                         
 16     const char* str="666666\n";        
 17     while(cnt--){                      
 18       write(fd,str,strlen(str));       
 19     }                                  
 20                                        
 21     close(fd);//关闭文件               
 22                                        
 23     return 0;                          
 24 }                                      

 运行结果:

 > 文件名 ,前面什么都不写,直接重定向 + 文件名: 

$ > log.txt

 

这算是一个小技巧吧

感谢阅读!!!!!!!!!!!

  • 27
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值