计算机基础——FD,系统IO,网络IO

文件描述符

进程控制块PCB记录着进程的数据
文件描述符FD记录着进程使用文件的数据

在 Linux 的世界里,一切设备皆文件。我们可以系统调用中 I/O 的函数(I:input,输入;O:output,输出),对文件进行相应的操作( open()、close()、write() 、read() 等)。

打开现存文件或新建文件时,系统(内核)会返回一个文件描述符,文件描述符用来指定已打开的文件。这个文件描述符相当于这个已打开文件的标号,文件描述符是非负整数,是文件的标识,操作这个文件描述符相当于操作这个描述符所指定的文件

在这里插入图片描述
1 开启一个进程,默认打开文件描述符为0,1,2的三个文件用来存放标准输入,标准输出,error,
2 之后进程每打开一个文件,则在PCB文件描述符链表增加一个int型的文件描述符
在这里插入图片描述
文件描述符就是这个array的下标

在这里插入图片描述

通过文件描述符,可以找到文件指针,从而进入打开文件表。该表存储了以下信息:
1 文件偏移量,也就是文件内部指针偏移量。调用 read() 或者 write() 函数时,文件偏移量会自动更新,当然也可以使用 lseek() 直接修改。
2 状态标志,比如只读模式、读写模式、追加模式、覆盖模式等。
3 i-node 表指针。
在这里插入图片描述
如上图linux查看进程打开的文件,除了0u,1u,2u 还有文件8r,表示文件状态标志为只读,0t4表示offset,10227564表示inod指针

然而,要想真正读写文件,还得通过打开文件表的 i-node 指针进入 i-node 表,该表包含了诸如以下的信息:
1文件类型,例如常规文件、套接字或 FIFO。
2文件大小。
3时间戳,比如创建时间、更新时间。
4文件锁。

系统IO

在这里插入图片描述

在这里插入图片描述

设想自己是一个进程,就叫小进吧。小进需要接收一个输入,我们不管这个输入是从网络套接字来,还是键盘,鼠标来,输入的来源可以千千万万。但是,都必须由内核来帮小进完成,为啥内核这么霸道?因为计算机上运行的可不只是咱小进一个进程,还有很多进程。这些进程兄弟也可能需要从这些输入设备接收输入,没有内核居中协调,岂不是乱套。

从小进的角度看,内核帮助它完成输入,其实包括三个步骤:

1、内核替小进接收好数据,这些数据暂时存在内核的内存空间

2、内核将数据从自己的内存空间复制到小进的内存空间

3、告诉小进,输入数据来了,赶快读吧

这三步看似挺简单,其实在具体实现时,有很多地方需要考虑:

0、小进如何告诉内核自己要接收一个输入?

1、内核接到小进的请求,替小进接收好数据这段时间, 小进咋办?

2、内核在将数据复制到小进的内存空间这段时间,小进咋办?

3、到底什么时候告诉小进数据准备好了,是在内核接收好数据之后就告诉小进,还是在将数据复制到小进的内存空间之后再告诉他?

4、内核以什么样的方式告诉小进,数据准备好了?

IO模型

阻塞式 I/O 模型

进程调用系统io后,进程挂起等待直到系统将数据拷贝到进程的用户空间

非阻塞式 I/O 模型

进程调用系统io后,进程不是挂起等待而是隔一段时间来询问一次缓冲区是否有数据,缓冲区有数据,进程挂起等待系统将缓冲区数据拷贝到进程用户空间(只在数据拷贝阶段阻塞)

信号驱动式 I/O

进程调用系统io后,进程处理其他事务等待系统检测到缓冲区有数据,发送信号给进程,进程收到信号后挂起等待系统将数据拷贝到进程用户空间(只在数据拷贝阶段阻塞)

异步非阻塞模型

进程调用系统io后,离开直到内核接收到数据并将数据从内核空间复制到进程的用户空间后,内核才给小进程发送信号,全程没有阻塞

区别:

在这里插入图片描述

网络IO

在这里插入图片描述

读取数据时:数据从网卡缓冲区->内核缓冲区->用户缓冲区。
写入数据时:数据从用户缓冲区->内存缓冲区->网卡缓冲区。

1 网络IO的硬件基础是网卡,数据到达网卡之后,终点是应用程序的内存,但是,应用程序是无法直接访问网卡的,所以需要操作系统做中转。
2 所以,上面提到的两个数组,分别是操作系统的IO数组和应用程序内存中的数组
3 操作系统的IO数组,就是io buffer,程序的数组,就是我们熟悉的字节数组。

也就是说,网络IO的过程,就是操作系统接收到网卡的数据,缓存到一个buffer中,然后应用程序调用操作系统的函数,从对应的buffer中取出数据。

常见的IO模型,分别是BIO、NIO、Select、POLL、EPOLL,后三者又统称为多路复用器。

所有的网络模型解决的核心问题都是以下3个问题:

1有哪些网络连接?
2哪个连接有数据?
3如何读取数据?

下面,按照IO模型的发展顺序,以IO模型存在的问题为主线,简单系统地总结一下IO模型,建立起对IO模型的系统认识。

BIO

在这里插入图片描述
线程和内核长链接,没有读到数据一直挂起等待,有数据读到返回

NIO

然后随着内核的发展,进化成可以不阻塞了
在这里插入图片描述
在这里插入图片描述
于是Nio出现了,用户线程定时轮询fd,轮询一次,哪个fd有数据读哪个
在这里插入图片描述
因此也有新的问题产生,那就是如果有数据的fd很少或者一致没有数据,就造成空轮询浪费cpu资源
在这里插入图片描述

再然后,出现了select,select的作用在于monitor multiple fd waiting until one or more fd become ready,内核监控网卡数据,有数据就返回有数据的fd,用户线程再去读数据
在这里插入图片描述
在这里插入图片描述
select传参:
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
select虽然解决了空轮询的问题,但是数据频繁的从网卡拷贝到内核,再拷贝到用户空间

epoll

接下来发展到使用共享空间的epoll

在这里插入图片描述
用户需要监控哪些fd,添加到红黑树中,红黑树相当于epoll用缓存记录要监控的fd,监控到IO事件发生的fd,epoll将其放到链表,
在这里插入图片描述

epoll create:
在这里插入图片描述
epoll_create()创建一个epoll实例。其中nfd为epoll句柄,参数max_size标识这个监听的数目最大有多大,从Linux 2.6.8开始,max_size参数将被忽略,但必须大于零。epoll_create()返回引用新epoll实例的文件描述符。该文件描述符用于随后的所有对epoll的调用接口。每创建一个epoll句柄,会占用一个fd,因此当不再需要时,应使用close关闭epoll_create()返回的文件描述符,否则可能导致fd被耗尽。当所有文件描述符引用已关闭的epoll实例,内核将销毁该实例并释放关联的资源以供重用。

epoll ctl:
在这里插入图片描述

epoll wait():
在这里插入图片描述
epoll wait监控fd的event发生,

epoll的工作方式
epoll的两种工作方式:1.水平触发(LT)2.边缘触发(ET)
LT模式:若就绪的事件一次没有处理完要做的事件,就会一直去处理。即就会将没有处理完的事件继续放回到就绪队列之中(即那个内核中的链表),一直进行处理。
ET模式:就绪的事件只能处理一次,若没有处理完会在下次的其它事件就绪时再进行处理。而若以后再也没有就绪的事件,那么剩余的那部分数据也会随之而丢失。
由此可见:ET模式的效率比LT模式的效率要高很多。只是如果使用ET模式,就要保证每次进行数据处理时,要将其处理完,不能造成数据丢失,这样对编写代码的人要求就比较高。
注意:ET模式只支持非阻塞的读写:为了保证数据的完整性。

查看redis的系统调用:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

1 epoll_create 初始化了1024个fd,返回epoll fd为5
2 调用epoll_ctl ADD了文件描述符6,7,3,对应socket fd 6,7,3
3 调用epoll_wait epoll fd=5 的文件 返回0没有数据,一直监控

mmap:
1)进程调用mmap()时,只是在进程空间内新增了一块相应大小的缓冲区,并设置了相应的访问标识,但并没有建立进程空间到物理页面的映射。因此,第一次访问该空间时,会引发一个缺页异常。

2)一个共享内存区域可以看作是特殊文件系统shm中的一个文件,shm的安装点在交换区上。

3)mmap()系统调用使得进程之间通过映射同一个普通文件实现共享内存。普通文件被映射到进程地址空间后,进程可以向访问普通内存一样对文件进行访问,不必再调用read(),write()等操作。

4)最终被映射文件的内容的长度不会超过文件本身的初始大小,即映射不能改变文件的大小。文件被映射部分而不是整个文件决定了进程能够访问的空间大小,另外,如果指定文件的偏移部分,一定要注意为页面大小的整数倍。

AIO

目前只有windows实现了AIO,linux由于内核的先知还不能实现AIO

redis支持的类型:
在这里插入图片描述
redis文件描述符:

在这里插入图片描述

mmap–DMA–sendfile

epool讲解

GOLANG网络IO模型

在这里插入图片描述

GOLANG里的网络IO模型是:创建多个goroutine,每个goroutine的网络IO都是阻塞的,这样的代码非常直观

但低层,所有的网络IO实际上都是非阻塞的

1 在Listen完该socket后,还必须将其添加到监听队列中,以后该socket有事件到来时能够及时通知到。golang使用的就是epoll机制来实现socket事件通知。那我们看对一个监听socket,是如何将其添加到epoll的监听队列中呢

2 netpollOpen则在socket被创建出来后将其添加到epoll队列中,对于epoll,该函数被实例化为netpollopen。

3 netpollWait的主要作用是:等待关心的socket是否有事件(其实后面我们知道只是等待一个标记位是否发生改变),如果没有事件,那么就将当前的协程挂起,直到有通知事件发生,

4 accept新的connection,如果返回值为EAGAIN(没有数据可读,稍后再试),则调用WaitRead来阻塞当前协程,后续在该socket有事件到来时被唤醒

5 read()和write() 当系统调用返回EAGAIN时,会调用WaitRead/WaitWrite来阻塞当前协程,

6 wait调用的是 netpollWait,主要作用是:等待关心的socket是否有事件(其实后面我们知道只是等待一个标记位是否发生改变),如果没有事件,那么就将当前的协程挂起,直到有通知事件发生

7golang运行库在系统运行过程中存在socket事件检查点,目前,该检查点主要位于以下几个地方:

runtime·startTheWorldWithSema(void):在完成gc后;

findrunnable():这个暂时不知道何时会触发?

sysmon:golang中的监控协程,会周期性检查就绪socket

8 socket就绪事件,在socket就绪后又是如何唤醒被挂起的协程?主要调用函数runtime-netpoll(),这个函数主要调用epoll_wait(当然,golang封装了系统调用)来获取就绪socket fd,对每个就绪的fd,调用netpollready()作进一步处理。这个函数的最终返回值就是一个已经就绪的协程(g)链表。netpollready主要是将该socket fd标记为IOReady,并唤醒等待在该fd上的协程g,将其添加到传入的g链表中。

详见:https://www.bianchengquan.com/article/546655.html

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值