Linux----IO(初级)

Posted 4nc414g0n

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux----IO(初级)相关的知识,希望对你有一定的参考价值。

Linux----IO(初级)

1)C文件I/O

复习: C语言----文件操作
补充:(输出到显示器的几种方式)

const char *msg = "hello fwrite\\n";
fwrite(msg, strlen(msg), 1, stdout);
printf("hello printf\\n");
fprintf(stdout, "hello fprintf\\n");

任何C程序,都默认会打开三个文件

  1. 标准输入(stdin), 键盘文件
  2. 标准输出(stdout)) ,显示器文件
  3. 标准错误(stderr),显示器文件

所有的外设硬件,本质对应的核心操作都是read和write,不同的硬件,对应的读写方式肯定是不一样的

2)系统文件I/O

①Linux一切皆文件

Linux一切皆文件理解:

每一个结构体里有两个函数指针,指向每个硬件的读写操作
OS(用结构体描述,再通过双链表组织起来),通过一层软件的虚拟,所以就都可以看作文件

②IO系统接口

头文件:
#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: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行"|"运算,构成flags
flags参数:

  1. O_RDONLY: 只读打开
  2. O_WRONLY: 只写打开
  3. O_RDWR : 读写打开
  4. O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
  5. O_APPEND: 追加写

注意:

  1. 前三个常量,必须指定一个且只能指定一个
  2. 系统喜欢用比特位不重叠(只有一个比特位是唯一的)的二进制序列来表示不同的标志(定义宏),系统内部可以通过按位与来检测,如: #define O_RDONLY 0x01;#define O_WRONLY 0x02;...
    这也解释了为什么flags参数可以进行"|"运算
    /usr/include/bits/fcntl-linux.h目录下可以查看此头文件

mode: (参考Linux权限8进制方式)
mode 参数指定创建新文件时应用的文件模式位。 在flags中指定 O_CREAT 或 O_TMPFILE 时必须提供此参数;如果既没有指定 O_CREAT 也没有指定 O_TMPFILE,则忽略mode


返回值:

  1. 成功:新打开的文件描述符(file descriptor)
  2. 失败:-1

头文件:
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
write() 将缓冲区中从 buf 开始的 count 个字节写入文件描述符 fd 所引用的文件
fd:open()返回的文件描述符
buf:buf指向要写入的内存
返回值:

  1. 成功时,返回写入的字节数(零表示未写入任何内容)。 如果此数字小于请求的字节数,则不是错误; 例如,这可能是因为磁盘设备已满
  2. 出错时,返回 -1

头文件:
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
read() 从 buf 开始,尝试将文件描述符 fd 中的 count 个字节读入缓冲区
如果 count 为零,read() 可能会检测到下面描述的错误。 在没有任何错误的情况下,或者如果 read() 不检查错误,则计数为 0 的 read() 返回零并且没有其他效果
返回值:

  1. 成功时,返回读取的字节数(零表示文件结束)可能会小于请求的字节数,例如因为现在实际可用的字节数较少
  2. 出错时,返回 -1

头文件:
#include <unistd.h>
int close(int fd);
返回值:

  1. 成功:0
  2. 失败:-1

头文件:
#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);

参考man 2 lseek

int main()

       int fd=open("log.txt",O_WRONLY|O_CREAT,0644);
//      int fd=open("log.txt",O_RDONLY);
       if(fd<0)
               perror("open");
               return 1;
       
//      char buffer[1024];
//      ssize_t s=read(fd,buffer,sizeof(buffer)-1);
//      if(s>0)
//              buffer[s]='\\0';
//              printf("%s\\n",buffer);
//      
       const char *msg="hello world\\n";
       write(fd,msg,strlen(msg));
       write(fd,msg,strlen(msg));
       write(fd,msg,strlen(msg));
       close(fd);
       return 0;

③站在系统角度理解

每个语言都有自己的库函数,自己的IO接口,因为系统调用成本较高,且不具备可以执行(Linux<->Windows)
以C语言为例,在我们调用fopen/fwrite等函数时,会自动根据平台选择自己底层对应的文件接口

④file descriptor(文件与进程)

先看下面一段代码:

int fd1=open("log1.txt",O_WRONLY|O_CREAT,0644);
int fd2=open("log2.txt",O_WRONLY|O_CREAT,0644);
int fd3=open("log3.txt",O_WRONLY|O_CREAT,0644);
int fd4=open("log4.txt",O_WRONLY|O_CREAT,0644);
int fd5=open("log5.txt",O_WRONLY|O_CREAT,0644);
printf("fd:%d",fd1);
printf("fd:%d",fd2);
printf("fd:%d",fd3);
printf("fd:%d",fd4);
printf("fd:%d",fd5);


可以发现是从3开始的连续数字,大胆猜测这就是数组的下标
而缺少的0 1 2 正好对应默认打开的标准输入,标准输出,标准错误

一个进程可以打开多个文件
同时操作系统为了管理这多个文件会用struct file结构体描述起来并组织在一起组织方式就是通过数组;

每一个task_struct里面都有一个struct file_struct*的指针指向struct file_struct

同时struct file_struct里会有一个struct file*类型的fd_array数组,每一个元素指向一个系统描述的文件也就是struct file(每新”描述“一个文件就会新增一个struct file,同时让fd_array末尾新增的指针指向它,再将它的下标fd返回给task_struct)


对应的一堆操作(读写等)

所以:用户层看到的fd,本质系统中维护进程和文件对应关系的数组的下标
所谓的默认打开文件,标准输入,标准输出,标准错误,其实是有底层系统支持的,默认在一个进程运行的时候,就会打开0,1,2


⑤分配文件描述符的规则

系统中分配文件描述符的规则:最小的,没有被使用的,进行分配

⑥C中的FILE

CentOS8好像没有<libio.h>头文件了,在\\VS2022\\Common7\\IDE\\VC\\Linux\\include\\usr\\include下可以找到
stdio.h中
libio.h中
这里看到的_fileno就是文件描述符
下面代码测试: (stdin stdout stderr默认对应0 1 2 )

printf("%d\\n",stdin->_fileno);
printf("%d\\n",stdout->_fileno);
printf("%d\\n",stderr->_fileno);

FILE *fp=fopen("log.txt","w");
printf("%d\\n",fp->_fileno);

FILE中包含的另外一个重要的就是缓冲区数据 (VS中将FILE typedef为了io_buf(io缓冲区))


当printf输出的时候其实并没有通过operation向fd_array[2]指向的显示器文件内核缓冲区写入,而是写入到了stdout结构体自己的缓冲区中,当遇到\\n或fflush(stdout)时,才会通过文件描述符找到对应的显示器文件,写入


引入重定向概念+代码验证1:

close(1);
int fd=open("log.txt",O_CREAT|O_WRONLY,0644);
if(fd<0)
	perror("open");
	return 1;

printf("fd:%d\\n",fd);
//fflush(stdout);//刷新缓冲区
close(fd);

没有fflush(stdout)时:

加上fflush(stdout):

重点解释:

  1. 这里的fd为1,是因为我们先close(1)关掉底层显示器输出文件,由于系统中分配文件描述符的规则,新打开的log.txt文件会找到最小的未被占用的位置也就是1(初步认识重定向)
  2. printf()末尾有’\\n’还是不能刷新到log.txt文件,因为一般C库函数写入文件时是全缓冲的(需要fflush),而写入显示器是行刷新(只用\\n),当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲

代码验证2:

printf("printf\\n");
fprintf(stdout,"fprintf\\n");
fputs("fputs\\n",stdout);

//system call
const char *p="write\\n";
write(1,p,strlen(p));

fork();

直接打印

输出重定向到log.txt

解释:(写时拷贝+数据刷新策略+文件差异)

  1. 当重定向到log.txt(普通文件)的时候,直到fork()完,进程退出的时候才会统一刷新,写入文件当中
  2. 但是fork的时候,父子数据会发生写时拷贝,所以当父进程准备刷新(二次缓冲区写入到内核缓冲区)的时候,子进程也就有了同样的一份数据,随即产生两份数据
  3. printf fputs fprintf库函数会自带用户级缓冲区,而 write 系统调用没有带用户级缓冲区(大多数情况下写时拷贝是拷贝属于进程的数据和用户级数据,内核级数据很少)
  4. 同时可以证明这些缓冲区是二次加上的,由C标准库提供

⑦重定向

unistd.h里提供了优雅的解决方案dup()库函数,而不用每次都close(fd)


#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);
#define _GNU_SOURCE / See feature_test_macros(7) /
#include <fcntl.h> / Obtain O_ constant definitions /
#include <unistd.h>
int dup3(int oldfd, int newfd, int flags);
这里的fd_array[newfd]是fd_array[oldfd]的拷贝
注意: 不一定会close(newfd)

  1. 如果 oldfd 不是有效的文件描述符,则调用失败,并且 newfd 不会关闭
  2. 如果 oldfd 是一个有效的文件描述符,并且 newfd 与 oldfd 具有相同的值,则 dup2() 什么都不做,并返回

返回值:成功后,这些系统调用会返回新的文件描述符。 出错时返回 -1


示例代码:

//      int fd= open("log.txt",O_CREAT|O_WRONLY,0644);
       int fd=open("log.txt",O_RDONLY);
       if(fd<0)
               perror("open");
               return 1;
       
//      dup2(fd,1);//输出重定向到显示器
       dup2(fd,0);//输入重定向键盘
       char buffer[1024];
       ssize_t s=read(0,buffer,sizeof(buffer)-1);
       if(s>0)
               buffer[s]=0;
               printf("echo:%s\\n",buffer);
       
//      const char*p="dup2\\n";
//      write(1,p,strlen(p));
//      write(1,p,strlen(p));
//      write(1,p,strlen(p));
//      write(1,p,strlen(p));
       return 0;

输出重定向>
输入重定向<

问题:程序替换的时候,会不变影响直定向对应的数据结构数据?
:不影响,task_struct里两个指针分别指向file_struct和mm_struct 互不干扰

简易shell添加重定向功能:Linux----50行简易shell(待补充)


3)文件系统

这里是引用

4)动态库和静态库


以上是关于Linux----IO(初级)的主要内容,如果未能解决你的问题,请参考以下文章

Java面试题—初级

缓冲区溢出攻击实践

从0到1开发一个初级DBMS(task2)数据库的存储结构

Linux--IO

block_dump观察Linux IO写入的具体文件(mysqld)

嵌入式Linux从入门到精通之第十四节:Linux IO控制技术