Linux---基础IO

关于文件的一些概念

  1. 文件包含文件内容和文件属性
  2. 对文件的操作包括对文件内容的操作和对文件属性的操作,也有可能同时进行对文件内容和文件属性的操作
  3. 打开文件这个动作是指的将文件的属性和内容加载到内存中
  4. 磁盘上并不是所有的文件都会被加载到内存中
  5. 对文件的理解可以分为内存文件和磁盘文件
  6. 通常当我们在对文件进行操作(打开文件,访问文件....)时,实质上是程序加载到内存中变成的进程对文件进行操作
  7. 这里讨论的就是进程和“打开文件”的关系

C语言中的文件操作函数实际上是对系统接口的封装,在向文件写入数据时是对磁盘内进行写入,只有操作系统有资格向硬件磁盘写入。上层(用户)如何调用操作系统呢?要通过相应的系统接口!平时我们没有用到系统接口是因为C语言的函数都经过了封装。

为什么要进行封装?因为如果在程序中直接进行系统调用(linux环境),将代码放入到windows环境下就不会兼容。不具有跨平台特性。

为了更好地了解底层原理,所以我们有必要学一学文件级别的系统接口

1.系统接口

 open

返回值小于0,打开失败。 

flags代表打开方式,就像C语言中的r,w等等。不过这里要特殊一些。

O_RDONLY(只读), O_WRONLY(只写), O_RDWR(读写),O_APPEND(读写),O_CREAT(没有文件创建文件),O_TRUNC(如果原来文件中有内容,将内容全部清空)等,他们实际上都是宏,是系统传递表计位,是用位图结构进行传递的,每一个宏标记,一般只需要一个比特位是1,并且和其他的宏对应的标记不能相同如下例。所以他们也可以是或的形式。

mode代表文件初始权限。如果被打开的文件是第一次出现,那么要给上初始权限。否则文件的显示权限部分会出现乱码(不等于最终权限)

int fd = open("text.txt",O_RDONLY)//只读
int fd = open("text.txt",O_WRONLY)//只写
int fd = open("text.txt",O_RDWR)//读写
int fd = open("text.txt",O_RDONLY|O_CREAT)//只读,如果没有文件创建文件
int fd = open("text.txt",O_RDONLY|O_WRONLY|O_CREAT)//读写,如果没有文件创建文件

int fd = open("text.txt",O_RDONLY|O_CREAT,0777);//初始权限给的是0777

 这种程序设计方法非常像:利用位运算来达到选择特定方法,十分的优雅。

看一个例子,他会向文件log打印str,用vim打开log文件,看看写进去了没有

如果是strlen(str)+1呢?用vim打开后,发现'\0'被识别成了其他符号,说明linux是会将C语言中'\0'识别成^@的!

open函数最后有一个权限参数。权限是什么意思?是给这个文件权限的初值!在文件权限那一节有详细讲过,但是给的值并不一定在操作系统就是这个权限,因为要和操作系统的umask进行运算,最终形成权限,即使我们给log的是0666,但是,实际权限却是0664,

如果非要0666呢!?之前说过可以在操作系统更改umask,今天来说一个可以在代码中修改umask,并且只对当前进程有效:

要先将log去除,在调用函数才能创建新的log,为其赋权限。

接下来说一说C语言的文件调用:


write 

#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main()
{
   int fd = open("log.txt",O_RDWR|O_CREAT,0666);
   if(fd<0)
   {
    perror("open");
    return 1;
   }

   const char* str = "hello world\n";
   write(fd,str,strlen(str));                                                                                                                                                         
   printf("%d\n",fd);

   close(fd);
   return 0;
}
//这里可以把字符串中的'\0'作为结束标志写入到文件中吗?答案是否定的,因为只有在C语言中
//'\0'是字符串结束的标志,在其他的打开方式下,'\0'可能会被识别成其他的字符!

write(fd, buf, count) 尝试从内存地址 buf 处向文件描述符 fd 写入 count 个字节的数据。

返回值说明:

  • 返回值 ≥ 0:表示实际写入的字节数,可能小于 count

  • 返回值 = -1:表示出错,并设置 errno

调用者必须保证 buf 指向的内存至少有 count 个字节可读,count应该小于等于你实际准备好的数据长度。

read

#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main()
{
    int fd = open("log.txt",O_RDWR|O_CREAT,0666);
    if(fd<0)
    {
        perror("open");
        return 1;
    }
    char str[128];
    ssize_t s= read(fd,str,127);
    if(s>0)
    {
        str[s] = '\0';
        printf("%s",str);                                                                                                                                                                
    }
    close(fd);
    return 0;
}

 2.文件描述符

 open函数的返回值fd就是文件描述符。每次打开函数后,fd都是从3开始的,那么0,1,2呢?

这里可以直接说明:0是标准输入(键盘),1是标准输出(显示器),2是标准错误(显示器),每次都是默认打开的。不禁联想到C语言中的FILE* stdin,FILE* stdout,FILE* stderr。

 验证:

 linux下一切皆文件

被打开的文件在内存中,进程也在内存中。所以系统在运行过程中,会存在大量被打开的文件。操作系统要对这些文件进行管理。一个进程可以打开多个文件,那么操作系统如何让进程和其打开的文件映射起来呢?

在PCB中存放一个管理该进程打开的文件的结构体,其中有一部分是数组,专门存放struct_file指针,指向在内存中的文件结构体。而操作系统通过对文件描述,组织,进行管理,就是对sruct_file结构体进行管理。其中有文件属性等内容。其中一个文件可能会被多个进程使用,所以stuct_file结构体中会有一个计数器,有一个进程使用它就+1。当一个进程要关闭一个文件时,将自己对应的struct files_struct中的文件数组位置置为NULL,对应文件struct_file中的计数器减1,当为0时,可以将对应的struct_file从队列中删除。

 不要有疑问,0,1,2->stdin,stdout,stderr,键盘,显示屏,虽然都是硬件,但是在linux中也可以作为文件管理。是怎样作为文件使用的呢?

可以看到在linux源码中struct_file存在operations指针,系统可以通过基类struct_file调用不同设备的read和write函数,是一种“多态”!

文件描述符的分配规则

遍历files_struct的数组,找到一个最小的且未被使用的下标,分配给新的文件。

重定向

fprintf和fscanf

函数名功能方向默认文件流底层系统调用示例输出/输入去向
printf输出(写)stdout(标准输出)write(1, ...)printf("Hello\n");输出到终端
fprintf输出(写)任意 FILE *(文件、stderr 等)write(fd, ...)fprintf(fp, "data=%d\n", x);输出到文件或指定流
scanf输入(读)stdin(标准输入)read(0, ...)scanf("%d", &x);键盘读取
fscanf输入(读)任意 FILE *read(fd, ...)fscanf(fp, "%d", &x);文件读取

引入

先看一个有趣的代码:

#include<stdio.h>
#include<sys/types.h>
#include<fcntl.h>
#include<sys/stat.h>
#include<unistd.h>
int main()
{
    close(1);

    int fd = open("log.txt",O_RDWR|O_CREAT,0666);
    if(fd < 0)
    {
        perror("open");
        return 0;
    }
    fprintf(stdout,"这是一个进程");
    fflush(stdout);                                                                                                                                                                      
    close(fd);
}

最后它并不会在显示器上打印“这是一个进程”,而是将其写入到log.txt文件中。 那么为什么会出现这种现象?那是因为发生了重定向,本来应该是向某个文件输入,但是输入到另一个文件。

dup函数 

上图中的所有数据都是内核数据结构,只有(系统调用)操作系统才有权限提供接口修改内核数据结构。最常用的就是dup函数。

比较常用的就是dup2函数,oldfd为原来文件的描述符,newfd为重定向输出的文件描述符。

#include<stdio.h>
#include<sys/types.h>
#include<fcntl.h>
#include<sys/stat.h>
#include<unistd.h>
int main()
{

    int fd = open("log.txt",O_RDWR|O_CREAT,0666);
    if(fd < 0)
    {
        perror("open");
        return 0;
    }

    //和上面的代码是等效的
    dup2(fd,1);

    fprintf(stdout,"这是一个进程");
    fflush(stdout);                                                                                                                                                                      
    close(fd);
}

>(1>)、>>(1>>)、2>、2>>、2>&1

符号含义默认操作的文件描述符
>输出重定向(覆盖)1>
>>输出重定向(追加)1>>
<输入重定向0<
2>错误输出重定向(覆盖)2>
2>>错误输出重定向(追加)2>>
2>&1将错误输出重定向到标准输出相同的位置

perror输出成这是perror: Success,是因为 perror 不像 printf 那样单纯打印字符串,它的作用是:输出参数字符串 + 当前 errno 对应的错误信息。在这段程序中,前面没有出错操作,所以全局变量 errno = 0,对应的错误信息是 "Success"。因此输出为:这是perror: Success

如果有系统调用出错(如 fopen 失败),errno 会被设置为错误码,比如 2,则输出会变成:这是perror: No such file or directory

 >1>

作用:
将标准输出(fd=1)重定向到文件(覆盖写)。

  • stdout(正常输出)写入到 out.txtstderr(错误信息)仍显示在屏幕

>>1>>

作用:
将标准输出追加到文件末尾。

效果:

  • 输出追加到 out.txt;错误仍在屏幕显示

2>2>>

作用:
把标准错误输出(fd=2)重定向到文件。

效果:

  • 错误信息进入 err.txt;正常输出仍显示在屏幕

2>&1

作用:
stderr(fd=2)重定向到 stdout(fd=1) 当前指向的地方。

执行顺序(从左到右解析)

 > out.txt:让标准输出(1)指向 out.txt
 2>&1:让标准错误(2)也指向与 1 相同的文件

正常输出 + 错误输出 全部进 out.txt,终端上不再显示任何输出

等价写法:

总结表格
命令标准输出(stdout) 去向错误输出(stderr) 去向文件写入方式
./a.out > out.txtout.txt屏幕覆盖
./a.out >> out.txtout.txt屏幕追加
./a.out 2> err.txt屏幕err.txt覆盖
./a.out 2>> err.txt屏幕err.txt追加
./a.out > all.txt 2>&1all.txtall.txt覆盖
./a.out >> all.txt 2>&1all.txtall.txt追加
<

在 Linux 中,重定向符号 < 表示 把文件的内容作为标准输入(stdin)提供给程序

把in.txt绑定到文件描述符 0(stdin)上,让程序从文件中读输入,而不是从键盘读。

上面所写的全部重定向命令中,2>&1>out.txt<in.txt 都是 Shell 重定向子句,它们是 Shell 的语法结构,用来告诉 Shell:

“在执行命令前,修改某个文件描述符(fd)的来源或去向。”

比如./test < input.txt 2>&1 > output.txt,命令是./test,< input.txt 是它的输入重定向,2>&1把 stderr(fd=2)重定向到 stdout(fd=1) 当前指向的地方,> output.txt是他的输出重定向。

底层原理:dup2

这些符号本质上都是在 执行 dup2 系统调用。

./test > out.txt 2>&1 < in.txt

并不是 test 程序在做 dup2(),而是 Shell 调用了 dup2() 来修改文件描述符表

也就是说:

  • 你在键盘上敲的重定向符号 → 由 Shell 解释;

  • Shell 用 open() 打开或创建文件;

  • Shell 用 dup2() 改变文件描述符的指向;

  • 然后才执行 execve("./test", ...) 启动程序;

  • 启动后的程序就“天然地”从这些文件中读写。

示例 1:./test > out.txt

Shell 做的事大致等价于:

int fd = open("out.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
dup2(fd, 1);  // 把stdout重定向到文件
close(fd);
execve("./test", argv, envp);

所以程序内部的:

printf("Hello\n");

其实是系统调用:但是此时1已经被fd夺舍了,所以再往1中写就会往out.txt写入。

write(1, "Hello\n", 6);

理解缓冲区

在操作系统有一份缓冲区,在语言级别也会有一份缓冲区。

系统缓冲区

下面的图是在系统级别上的缓冲区,每一个sruct_file会对应一个缓冲区。

语言缓冲区

C语言中会将文件描述符还有其它一些文件信息封装成FILE,在这个FILE中,其实还存在语言缓冲区:

struct _IO_FILE {
    unsigned char *_IO_buf_base;   // 缓冲区起始地址
    unsigned char *_IO_buf_end;    // 缓冲区结束地址
    unsigned char *_IO_write_ptr;  // 当前写指针
    int _flags;                    // 缓冲模式 (行缓冲、全缓冲、无缓冲)
    int _fileno;                   // 文件描述符(如stdout=1)
    ...
};

所以当在调用fopen时,会返回一个FILE*类型的指针,因为fopen中会malloc一个FILE类型的结构体。因此,语言层面上的缓冲区是存在内存中的堆上。

当在语言层面上往文件写内容时,会先往缓冲区中写,然后等待刷新后会被刷新到系统上的缓冲区中。这里有两点,语言层面上的缓冲区的刷新策略是什么?被刷新到系统的缓冲区上时就一定会被刷新到硬件中吗?

模式说明典型场景
行缓冲输出遇到 \n 自动刷新stdout(连接终端时)
全缓冲缓冲区满才刷新文件 I/O
无缓冲每次写立即刷新

stderr(错误输出)

其次是当进程结束时,缓冲区的内容会被刷新到系统中。

其实并不会立即刷新到硬件上,除非显式刷新,调用fsync(fd);或者当关闭fd时,会刷新。

看几个例子加深对缓冲区的理解:

1.

动态图理解过程:

第一个例子的实际运行时内存的动态图,正常执行带\n的printf和fprintf,write:

当不带\n时:

最后进程退出时:

2.

printf内,先解析格式字符串,取变长参数(如有必要),格式化成字符串,再向FILE中的缓冲区写入,如果缓冲区满 / 遇到 '\n' / fflush() / 程序结束,会调用write,向系统写入。fprintf也是如此。

标准错误和标准输出的区别

标准错误和标准输出都是向显示器打印,有什么区别呢?标准错误和标准输出是通过不同的文件描述符打印到显示器上的!

  

在发生重定向时,没有说明默认就是重定向标准输出,如果要重定向标准错误则要加上2:

 意义在哪里?可以区分程序中的日常信息和错误信息!方便查找错误。cout和cerr,printf和perror要分开使用。

 如果不想分开这两个内容,想重定向到一个文件中:

 先将1重定向到all.txt中,再把1的地址给2,那么2也指向1所指的文件,2也完成了重定向。

hh

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_w_z_j_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值