不同操作系统所暴露出的接口是不同的,因此Linux下的一些系统调用接口是无法移植到Windows下的。


文章目录

  • 一、C语言中的文件接口
  • 二、系统文件I/O
    • 2.1.系统调用接口
      • open
    • 2.2.文件描述符fd(file descriptor)
    • 2.3.补充内容--函数指针访问硬件
    • 2.4.重定向的实现原理
  • 三、FILE
    • 3.1. 行缓冲和全缓冲
    • 3.2. 在刷新前关闭文件描述符
  • 四、dup 重定向
    • 4.1.使用dup2 完成重定向
    • 4.2.重定向恢复
    • 4.3.在my_shell中添加重定向功能
  • 总结

一、C语言中的文件接口

在C语言文件操作时学过文件接口C语言中的文件接口

这里补充一些内容:

文件=内容+属性
stdin:标准输入(键盘)
stdout:标准输出(显示器)
stderr:标准错误(显示器)

Linux下一切皆文件,所以stdin,stdout,stderr也是可以当做文件看待,被fopen打开。
这是因为语言是人和计算机交互的载体,所以任何一门语言都需要标准输入、输出和错误

之所以能直接使用printf和scanf这种输出到显示器和从键盘输入的函数,是因为C语言默认帮助我们打开了这三个标准输入和输出的设备。

用下面的代码验证:

可以看到普通文件和显示器文件在代码层面上是没有差别的。


二、系统文件I/O

2.1.系统调用接口

操作文件,除了上述C接口(当然,C++也有接口,其他语言也有),我们还可以采用系统接口来进行文件访问,使用man手册可以查看他们:

fd:要打开文件的描述符
buf:写入/读入的字符串指针
count:写入/读入字符的个数(字节数)

实际上,我们上面用的C语言文件接口,在底层用的就是系统调用的接口

系统调用的接口只有一套,和系统有关,而库函数可以有多个(C语言文件接口,C++文件接口等),语言层的库函数文件接口都是基于系统调用接口进行的封装。

由于显示器是硬件,硬盘也是硬件,所以任何语言的文件操作最终都要通过操作系统进行对硬件的写入。

open

通过mani手册可以查看用法:

要包含的头文件:
#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:打开文件的方式O_RDONLY :只读方式O_WRONLY :只写方式O_RDWR :读写方式
可选项:O_TRUNC :截断文件(清空文件内容)O_CREAT :文件不存在则创建文件O_APPEND :追加的方式O_EXCL | O_CREAT :如果文件存在,则打开文件失败mode:创建文件时的权限(八进制,比如0664等)返回值:
成功:新打开的文件描述符
失败:-1

可选项的原理:

所有选项对应的数在转成二进制后只有一个比特位为1且为1的二进制位是不同的,所以传多个选项的时候实际上是对传入的数进行按位或处理。最后传给flags的是一个数,这个数有一个或多个比特位为1。

以下面的代码为例:

2.2.文件描述符fd(file descriptor)

可以看到打开成功后各自的返回值是3 4 5。。。
打开失败返回值则为-1
这些返回值叫做文件描述符,在系统层面是一个整数,从0开始。
其中:0为标准输入,1为标准输出,2为标准错误,这三个默认被打开

文件描述符的本质是数组下标,操作系统为每一个进程维护了一个文件描述符表,该表的索引值都从从0开始的,索引值都有一个指针指向对应的文件:

一个进程如何通过文件描述符找到文件:当进程执行write(4,"hello",5)时,进程先找到自己的PCB,PCB中包含文件描述符表指针,通过这个指针找到文件描述符表(struct files_struct),然后通过索引下标4找到对应的文件。

所以如果将文件描述符为0(标准输入)的关掉,则分配的下标为0:

可以看出文件描述符的分配规则为:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符

另外,要记得关闭文件描述符,否则会造成文件描述符泄漏,因为文件描述符表(数组)是有上限的。

2.3.补充内容–函数指针访问硬件

不同的硬件访问方式是不一样的,对于外设访问的方式一般是读和写,但是实现代码是不一样的,此时进程就可以通过函数指针的方式来指向它们各自的实现方法:

2.4.重定向的实现原理

重定向有两个操作符分别是>(格式化重定向)和>>(追加重定向),其实现原理就是操作系统内核把标准输出(1)关掉(注意:标准错误没有关闭),然后打开对应的文件,此时该文件的下标就是1,应用层默认输入到下标为1的文件中,所以就输入到该文件中了。至于追加重定向则是在打开的时候加上选项O_APPEND

如果想把标准输出和标准错误都重定向,可以这样写./文件名 > 文件 2>&12>&1的作用是把文件描述符1的内容拷贝到文件描述符2里面,1已经指向了新文件,所以2也指向这个新文件。


三、FILE

文件指针中那个FILE本质是一个结构体,这个结构体中一定是包含文件描述符(fd)的,因为访问文件都是通过fd访问的

下面是FILE的代码:
typedef struct _IO_FILE FILE; 在/usr/include/stdio.h

struct _IO_FILE {int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags//缓冲区相关/* The following pointers correspond to the C++ streambuf protocol. *//* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */char* _IO_read_ptr; /* Current read pointer */char* _IO_read_end; /* End of get area. */char* _IO_read_base; /* Start of putback+get area. */char* _IO_write_base; /* Start of put area. */char* _IO_write_ptr; /* Current put pointer. */char* _IO_write_end; /* End of put area. */char* _IO_buf_base; /* Start of reserve area. */char* _IO_buf_end; /* End of reserve area. *//* The following fields are used to support backing up and undo. */char *_IO_save_base; /* Pointer to start of non-current get area. */char *_IO_backup_base; /* Pointer to first valid character of backup area */char *_IO_save_end; /* Pointer to end of non-current get area. */struct _IO_marker *_markers;struct _IO_FILE *_chain;int _fileno; //封装的文件描述符
#if 0int _blksize;
#elseint _flags2;
#endif_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary *//* 1+column number of pbase(); 0 is unknown. */unsigned short _cur_column;signed char _vtable_offset;char _shortbuf[1];/* char* _save_gptr; char* _save_egptr; */_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

3.1. 行缓冲和全缓冲

对于下面的代码:

#include<stdio.h>
#include<string.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
#include<unistd.h>
int main()
{const char *msg0="hello printf\n";const char *msg1="hello fwrite\n";const char *msg2="hello write\n";printf("%s", msg0);fwrite(msg1, strlen(msg0), 1, stdout);write(1, msg2, strlen(msg2));fork();return 0;
}

如果对结果重定向,printffwrite(库函数)都输出了2次,而write 只输出了一次(系统调用)。

刷新一般有三种方式: 第一个是对应系统调用而言,后两个是对应库函数而言

  • 无缓冲: 对数据进行操作时,不需要经过缓冲区,直接刷新在文件或显示器上。系统调用(wirte)没有附带缓冲区。

  • 行缓冲: 遇到\n就对缓冲区的数据进行刷新,这是对显示器而言。

  • 全缓冲: 缓冲区满了或者进程退出时才对数据进行刷新,这是对文件采取的刷新策略。

重定向影响了缓冲方式
显示器采用的是行缓冲(遇到\n就刷新)
文件采用的则是全缓冲(缓冲区数据写满才刷新)

没有重定向之前是往显示器写,而重定向则是往文件中写。

  • 一般C库函数写入文件时是全缓冲的,而写入显示器是行缓冲。
  • printf、fwrite库函数会自带缓冲区,当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲
  • 全缓冲进程退出之后,会统一刷新,写入文件当中
  • fork的时候,父子数据会发生写时拷贝,数据被暂存在用户级缓冲区中,因此子进程也就有了同样的一份数据,即产生两份数据。
  • 由于父子进程的缓冲区中有两份一样的数据,所以会刷新两份。
  • write 没有刷新两份,说明没有所谓的用户级缓冲区。

我们这里所说的缓冲区,都是C标准库提供的用户级缓冲区printffwrite是库函数, write是系统调用,库函数在系统调用的“上层”, 是对系统调用的“封装”,因此write 没有用户级缓冲区,而 printffwrite有。

另外fflush函数也在C标准库中,将用户级的缓冲区往系统中刷新:

3.2. 在刷新前关闭文件描述符

前面说过,文件采用的是全缓冲,全缓冲进程退出之后,会统一刷新,写入文件当中。所以,在程序退出之前,数据一直都在用户级缓冲区中,如果在文件退出前关闭文件描述符,则不会写入文件中。

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>int main()
{close(1);int fd = open("log.txt", O_CREAT|O_WRONLY);if (fd < 0){perror("open file fail");exit(-1);}const char* msg3 = "hello fwrite\n";printf("hello printf\n");fprintf(stdout,"hello fprintf\n");fwrite(msg3, strlen(msg3), 1, stdout);close(fd);return 0;
}


四、dup 重定向

上面的重定向是将标准输出的fd关掉,然后打开重定向的文件。
而使用dup2函数就不需要关掉标准输出:

dup函数的作用是,返回一个新的文件描述符(可用文件描述符的最小值)newfd,并且新的文件描述符newfd指向oldfd所指向的文件表项。
比如:

文件描述符为1的文件通过dup重定向到文件描述符1上,那么也就相当于文件描述符3对应的文件也是显示器文件,那么向文件描述符3进行write,最终结果也会打印在显示器上。

4.1.使用dup2 完成重定向

dup2有两个参数oldfdnewfdnewfdoldfd的拷贝,将newfd重定向到oldfd,当整个函数调用成功后,会将newfd关掉,然后让newfd指向oldfd。总之,dup2函数的作用就是让newfd重定向到oldfd所指的文件表项上,如果出错就返回-1,否则返回的就是newfd。所以如果想让原本输出到显示器上的数据重定向到文件中,可以这样写:

4.2.重定向恢复

在进行重定向后,如果想要恢复到重定向之前的状态,可以在重定向之前用dup函数保留该文件描述符对应的文件表项,然后在需要恢复重定向的时候使用dup2重定向到原来的文件表项,以重定向后恢复标准输出为例,如下所示:

4.3.在my_shell中添加重定向功能

其实现原理是在子进程进行程序替换之前将标准输出(1)重定向到打开的文件中。

#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<stdlib.h>
#include<ctype.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#define SIZE 256
#define NUM 16 //命令行参数的个数void redirect(char* cmd)
{int fd=-1;int redirect_count=0;//记录>的个数char*file=NULL;char*ptr=cmd;while(*ptr){if(*ptr=='>'){*ptr++='\0';redirect_count++;if(*ptr=='>'){*ptr++='\0';redirect_count++;}while(*ptr!='\0'&&isspace(*ptr))//跳过空格{ptr++;}file=ptr;//找到文件名while(*ptr!='\0'&&!isspace(*ptr))//清空文件名后面的空格{ptr++;}*ptr='\0';if(redirect_count==1){//>fd=open(file,O_CREAT|O_TRUNC|O_WRONLY,0644);}else if(redirect_count==2){//>>fd=open(file,O_CREAT|O_APPEND|O_WRONLY,0644);   }else{//do nothing!}//文件已经打开,用dup2重定向dup2(fd,1);close(fd);}//end ifelse if(*ptr=='<'){ //和重定向>类似}ptr++;}}
int main()
{char cmd[SIZE];const char* cmd_line="[my_shell@VM-0-16-centos ~]# ";while(1){cmd[0]=0;printf("%s",cmd_line);fgets(cmd,SIZE,stdin);cmd[strlen(cmd)-1]='\0'; pid_t id=fork();if(id<0){perror("fork error!\n");continue;}if(id==0)//子进程{redirect(cmd);char*args[NUM];//将命令字符串分割args[0]=strtok(cmd," ");int i=1;do    {    args[i]=strtok(NULL," ");    if(args[i]==NULL)    {    break;    }    i++;    }while(1);   execvp(args[0],args);exit(1);}int status=0;pid_t ret=waitpid(id,&status,0);if(ret>0){printf("status code:%d\n",(status>>8)&0xFF);}}return 0;
}


总结

  1. 操作系统提供系统调用接口(open,close,read,write),C语言对系统调用接口进行封装
  2. FILE*是个结构体指针,FILE是个结构体,有两个比较重要的方面:文件描述符和缓冲区(C语言提供)
  3. fd是系统调用,fd是文件描述符表(数组)的下标,里面的内容是文件指针指向文件。系统默认打开0 1 2(标准输入,标准输出,标准错误)
  4. 重定向的底层是将fd的指向改变

13 Linux下的基础IO相关推荐

  1. Linux中的基础IO(二)

    Linux中的基础IO(二) 文章目录 Linux中的基础IO(二) 一.基本接口 二.文件描述符 三.文件描述符的分配规则 四.重定向 五.dup2系统调用 六.minishell 一.基本接口 i ...

  2. Linux中的基础IO(一)

    Linux中的基础IO 文章目录 Linux中的基础IO 一.C语言中的文件接口 二.随机读写数据文件 三.文件读写的出错检测 一.C语言中的文件接口 写在前面 计算机文件是以计算机硬盘为载体存储在计 ...

  3. Linux系统进阶-基础IO

    Linux系统进阶-基础IO 文章目录 Linux系统进阶-基础IO C语言中的文件接口 对文件进行写入 对文件进行读取 什么是当前路径 默认打开的三个流 stdout & stderr 系统 ...

  4. 开发笔记 —— Linux 下的基础指令

    Linux 下的基础指令 基本使用 远程客户端 注销&关机命令 注销 重启系统 关闭系统 文件系统指令 文件和目录 练习 文件归档(tar) 软件安装 文件下载 手动下载 命令下载 使用 TA ...

  5. 一文弄懂Linux下五种IO模型

    Linux下主要的IO主要分为:阻塞IO(Blocking IO),非阻塞IO(Non-blocking IO),同步IO(Sync IO)和异步IO(Async IO). 同步:调用端会一直等待服务 ...

  6. Linux下C编程-----IO/文件操作 模拟linux ls程序显示文件系统树形结构(2)

    Linux下的IO/文件操作练习,知识虽然简单 但是往往基础容易被忽略,偶尔的练习是有必要的. 练习printf /****************************************** ...

  7. 【Linux练习生】基础IO(详细)

    本节我们讲解基础IO的部分,将围绕以下内容进行梳理讲解: 复习C文件IO相关操作 认识文件相关系统调用接口 认识文件描述符,理解重定向 对比fd和FILE,理解系统调用和库函数的关系 理解文件系统中i ...

  8. 如何提高Linux下块设备IO的整体性能?

    编辑手记:本文主要讲解Linux IO调度层的三种模式:cfp.deadline和noop,并给出各自的优化和适用场景建议. 作者简介: 邹立巍 Linux系统技术专家.目前在腾讯SNG社交网络运营部 ...

  9. Linux系统:基础IO

    基础IO 1. 内存文件 1.1 理解内存文件 文件是内容和属性的集合,文件不仅有内容,还有各种属性.任何文件操作都可以分为对文件内容的操作和对文件属性的操作. 文件没被打开时是放在磁盘上的,想要打开 ...

最新文章

  1. 无线路由器与无线AP的区别
  2. linux下多线程的调试
  3. 【回文自动机】bzoj3676 [Apio2014]回文串
  4. 2018冬令营模拟测试赛(九)
  5. 转:c/c++ 运行库
  6. MySQL in语句内参数个数限制
  7. STL之Iterator(迭代器)
  8. VMware下主机与虚拟机通信问题
  9. Python 抓取数据存储到Mysql中
  10. 【渝粤题库】陕西师范大学151204 中级财务会计作业(笔试题型)
  11. 两个list取交集_利用jieba计算两个句子的相似度
  12. 关于 C 的 arithmetic conversion (进行 算术运算 时的 强制转换规则)
  13. python菜鸟教程100例-Python 基础教程 | 菜鸟教程
  14. JAVA基础【刘意】27天全集【Day02小结】
  15. 万物互联时代,有一款好设计你需要知道
  16. 搜索引擎优化有哪些方法?分享SEO搜索引擎优化的12点经验
  17. 从苏炳添的学术论文中,看看如何写论文
  18. 服务器上的服务一直自动关闭,服务器会话连接自动关闭怎么办?
  19. error: redefinition of ‘xxx’问题的解决
  20. ffmpeg录制桌面,麦克风和系统声音独立成2路音轨

热门文章

  1. Apache POI 中文使用指南
  2. Fragment懒加载——最简方案(LazyBread)
  3. MFC下好用的高速绘图控件-(Hight-Speed Charting)
  4. C# BeginInvoke / Invoke
  5. 上海交通大学安泰经济与管理学院:大疫当前谈供应链思维
  6. mfc中IDC、IDD等资源标识的含义
  7. trace32 python rcl 研究
  8. 关于H264,X264,DivX,Xvid的小知识 fr net
  9. 【软件测试】金三银四,我不允许测试工程师不知道这个刷题工具
  10. Switf的基本语法