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程序,都默认会打开三个文件
:
- 标准输入(stdin), 键盘文件
- 标准输出(stdout)) ,显示器文件
- 标准错误(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参数:
- O_RDONLY: 只读打开
- O_WRONLY: 只写打开
- O_RDWR : 读写打开
- O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
- O_APPEND: 追加写
注意:
- 前三个常量,必须指定一个且只能指定一个
- 系统喜欢用比特位不重叠(
只有一个比特位是唯一的
)的二进制序列来表示不同的标志(定义宏),系统内部可以通过按位与来检测,如:#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
返回值:
- 成功:新打开的文件描述符(file descriptor)
- 失败:-1
头文件:
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
write() 将缓冲区中从 buf 开始的 count 个字节写入文件描述符 fd 所引用的文件
fd
:open()返回的文件描述符
buf
:buf指向要写入的内存
返回值:
- 成功时,返回写入的字节数(零表示未写入任何内容)。 如果此数字小于请求的字节数,则不是错误; 例如,这可能是因为磁盘设备已满
- 出错时,返回 -1
头文件:
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
read() 从 buf 开始,尝试将文件描述符 fd 中的 count 个字节读入缓冲区
如果 count 为零,read() 可能会检测到下面描述的错误。 在没有任何错误的情况下,或者如果 read() 不检查错误,则计数为 0 的 read() 返回零并且没有其他效果
返回值:
- 成功时,返回读取的字节数(零表示文件结束)可能会小于请求的字节数,例如因为现在实际可用的字节数较少
- 出错时,返回 -1
头文件:
#include <unistd.h>
int close(int fd);
返回值:
- 成功:0
- 失败:-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):
重点解释:
- 这里的fd为1,是因为我们先close(1)关掉底层显示器输出文件,由于系统中分配文件描述符的规则,新打开的log.txt文件会找到最小的未被占用的位置也就是1(初步认识重定向)
- 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
解释:(写时拷贝+数据刷新策略+文件差异)
- 当重定向到log.txt(普通文件)的时候,直到fork()完,进程退出的时候才会统一刷新,写入文件当中
- 但是fork的时候,父子数据会发生写时拷贝,所以当父进程准备刷新(二次缓冲区
写入到内核缓冲区
)的时候,子进程也就有了同样的一份数据,随即产生两份数据- printf fputs fprintf库函数会自带用户级缓冲区,而 write 系统调用没有带用户级缓冲区(
大多数情况下写时拷贝是拷贝属于进程的数据和用户级数据,内核级数据很少
)- 同时可以证明这些缓冲区是二次加上的,由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)
- 如果 oldfd 不是有效的文件描述符,则调用失败,并且 newfd 不会关闭
- 如果 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(初级)的主要内容,如果未能解决你的问题,请参考以下文章