【Linux】基础IO——C语言与底层OS的输入输出实现

C语言层面的IO函数

在C语言中,我们用的打开文件的函数是:

FILE *fp = fopen("文件名", "r/w/a")
// 失败返回 NULL

        “ w ”、“ > ” :写入,是一种覆盖式;“ a ” 、“ >> ”:追加;“ r ” : 读取。

        如果第一个参数不带路径只写文件名,那就默认在当前路径下执行。什么是当前路径?不是指可执行程序当前所在路径,应当是当前进程运行时所处的路径。

        比如说,“ w ” 状态下,可执行程序在路径 /test/day/ 中,我们默认创建进程时(./ exe),会在当前路径下创建一个 log.txt 文件,但如果在 /test/ 中执行程序(./ day/exe),就会在 /test/ 路径下创建一个 log.txt 文件。

// 可以在当前目录下输入指令查看进程的执行路径 cmd
/proc/pid 

还有两种常见的读写函数:

// 写
fputs("hello\n", fp);

// 读
fgets(buffer, strlen(buffer), fp);

系统级别的IO函数:system call

umask(0)

int open("log.txt", O_WRONLY|O_CREAT, 0666);

        open 是打开一个磁盘文件的函数,它的返回值是 int 类型,一般我们喜欢叫 fd 。

        第一个参数是文件名,默认路径下打开;

        第二个参数是“状态”,一般在没有文件时打开需要 O_WRONLY 按位或上 O_CREAT,这里的“状态”都被定义成宏,这是一种系统函数传参标识位,一个 int 类型,有 32 个比特位,也就有 32 种标志位,其中 

#define X 0x1
#define Y 0x2

open(... , arg2 , ...){}
// 函数调用时,会传宏,会对 arg2 进行进行宏判断:

if(arg2 & x) { 真 }

if(arg2 & Y) { 真 }

// 以此来判断读写状态

// 通过该指令可查看更多宏:
grep -E 'O_CREAT|O_RDONLY|O_WRONLY' /usr/include/ -R

        第三个参数是权限,输入4位权限,为了不受系统原来的 umask 影响,在打开前需要 umask(0),指不受系统影响。

        open 的返回值如果 <0(-1),说明打开文件失败。

        在 open 后,应在最后调用 close(fd) 关闭文件。

        int open 打开多个,返回值 fd 是从 3 开始的,3、4、5、6,那 0、1、2 呢?其实 0、1、2 就是系统默认打开的 3 个文件,stdin、stdout、stderr。

        fopen() 做了什么?

1、给用户申请 struct FILE 结构体(对象),调用 open(),返回 FILE* 的地址。

2、在底层通过 open(),返回 fd,把 fd 填进 FILE 中的 int _fileno。

        其实 fopen、fclose 等语言层面上是跨平台的,底层封装的是 system call(open、close),因为这样更简易,且具有跨平台性。

文件描述符

内存文件:

        文件多了需要组织,和管理进程一样,也有文件管理,它的管理方式也是一种结构体:struct file,在内存当中,用双链表形式链接。

磁盘文件:

        文件有几个部分构成:内容 + 属性,1个G的电影其实在磁盘上不止1G,1G只是内容,各种属性信息的大小还没计算呢。

        一般加载到内存中的文件,加载的是文件的属性!等到需要时,再延后式慢慢加载数据。

接下来我们来认识一下一个重要结构体,它是 task_struct 里的 files_struct。

         图中 fd_array[] 的 0、1、2、3,就是文件描述符,创建文件时,从最小的且没有使用的文件描述符开始使用。

        “ w ” :先找到文件描述符,通过对应的文件流,写入到物理内存的缓冲区,再写入到磁盘中。

        所以文件都要通过进程打开,只是打开的进程不同。例如打开图片用的打开方式不同。

        “ r ” :找到文件描述符,通过文件中的属性在磁盘中找到,再读写到内存中,拷贝给进程。

        如果进程多了就要管理进程,就是管理 task_struct,那么文件多了,也需要管理。和进程一样,文件加载到内存也需要,先描述,在组织!!生成对应的 struct file 结构体,里面会包含文件的属性,且 OS 会给文件分配对应的文件描述符,然后 struct file 会用双链表连接起来,方便管理,而在 files_struct 结构体内的 fd_array[] 数组中每个文件描述符都分配对应了一个 file* 结构体指针,分配文件描述符就是 file* 分到对应的下标 [] 中。

        不同的是,fork() 时,tast_struct 会被拷贝一份,但 struct file 不拷贝,与父进程共用。

        在语言层面上,我们都知道C语言的函数其实封装了系统函数的接口,那语言层面上是如何找到对应的文件描述符的呢?

        我们都知道, FILE *fp = fopen(...) ,其实 FILE* 是一个对象,fopen 打开文件时,在C库上开辟一块空间,叫做 FILE 对象,fp 指针指向它,读写文件时通过 fp 找到 FILE 对象里的 int fd,找到进程中的 *files 指针,通过 fd 找到 file_struct 中对应的下标,再找到对应的 file 文件,进行读写。 

重定向

        重定向的本质是修改文件描述符 fd 的下标对应的 struct file* 的内容。

        当我们要重定向时,会关闭对应的文件描述符,通过改变下标的指向的 file* 覆盖到关闭的文件描述符上,一般创建新的文件是从最小的且没有使用的文件描述符开始。

        stdout 在 files_struct 中永远只认1号下标,关闭了也只认1,就算1已经是别人了。所以就会有一个现象:

// 包含对应头文件

int main{
    close(1);
    int fd = open("log.txt", O_WRONLY|O_CREAT, 0666);
    if (fd < 0){
        // error
    }
     
    printf("hello printf\n");
    fprintf(stdout, "hello fprintf\n");
    fflush(stdout); // 这里需要刷新,为什么刷新我下一街讲
    
    close(fd);
}

        明明我已经关闭了标准输出的 fd,为什么结果却写到了 log.txt 文件中呢?因为一生要强的 stdout 只认1,先关闭了1,后来新开的文件就分配给了1,所以这时候输出的不是显示器,而是文件中,这就算重定向!! 

        不过这些都是老方法,现在最新的方法是,把原来的文件描述符拷贝给新的要分配的描述符,其实是 fd_array 里的内容,简单点说就是文件描述符,对应的指令是:

close(1) // 可加可不加
dup2(old fd, new fd); // fd -> 1

        dup不常用,一般用 dup2。

小知识

用户层访问文件时的顺序是:

        FILE —> struct FILE { int fileno(fd) }  —> task_struct —> *file —> files_struct —> fd_array[] —> file*(fd);

        重定向 " > " 改变的是1号文件 stdout,而不是2号文件 stderr,虽然它们都是输出到显示器当中。

fprintf(stdout, "hello\n");
fprintf(stderr, "hello\n");

        所有的外设都有自己的一套 read、write 方法,怎么做到统一呢?

struct file
{
    void(*read)();
    viod(*write)();
}

        指向各个外设的 read() 和 write(),做到了多态,只需调用应用层的 read () 和 write()。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值