🌈欢迎来到Linux专栏 ~~ 网络编程
- 🌍博客主页 :张小姐的猫~江湖背景
- 🔥所属专栏 :Linux ~ 不破不立
- 作者水平很有限,如果发现错误,可在评论区指正,感谢🙏

网络编程
- [🌈欢迎来到Linux专栏 ~~ 网络编程](#🌈欢迎来到Linux专栏 ~~ 网络编程)
- 🌏关于IO的理解
-
- 🔥IO是什么?本质是什么?
- [🔥什么叫做高效的IO? 如何设计高效IO](#🔥什么叫做高效的IO? 如何设计高效IO)
- 🌏五种IO模型
- [🌏探究 非阻塞IO](#🌏探究 非阻塞IO)
- 📢写在最后

🌏关于IO的理解
🔥IO是什么?本质是什么?
Input / Output:外设 ------ 内存 :网络通信本质属于IO范畴!

所以IO的本质:等 + 拷贝 。如果我们感觉到IO慢,其实是在单位时间内等待的时间占比太大了!因为拷贝的效率是和硬件强相关的,如果我们要提高IO效率,不能从拷贝出下功夫👊
🔥什么叫做高效的IO? 如何设计高效IO
所谓高效的IO ,就是在单位时间内,IO中等待的比重越低,那么IO的效率就高了
🌏五种IO模型
我们以钓鱼为例 :钓鱼 = 等待 + 钓;那么是钓鱼效率高是钓的快还是等待的少呢?
张三:一直关注鱼漂,鱼上钩立马识别,完成钓鱼。如果鱼漂不动,什么都不做
李四:轮询式的关注鱼漂,不会因为鱼漂没有鱼咬钩而卡住自己,边玩手机哈哈
王五:把铃铛🔔挂在鱼竿上,自己做别的事,铃铛来通知自己
赵六: 把一百根鱼缸都插到海边,同时检测多个鱼竿的鱼漂事件
田七: 让秘书小王帮他去钓鱼,自己高高挂起
以上就是五种钓鱼方式,那么上面哪个人的钓鱼效率最高呢?
因为钓鱼 = 等待 + 钓起。但是赵六有100根鱼竿,其他人就一根
- 鱼咬赵六的概率就是
100/104,其他人就通通是1/104也就是赵六等待上钩的时间永远比其他人要低,所以赵六的钓鱼概率是最高的(赵六一次追100个女孩哈哈)
张三 = 阻塞IO
李四 = 非阻塞IO
王五 = 信号驱动IO
赵六 = 多路复用 or 多路转接
田七 = 异步IO
就是对应的五种IO模型,最常使用的是阻塞、非阻塞、多路复用
🔥阻塞IO
阻塞IO :在内核将数据准备好之前,系统调用会⼀直等待。所有的套接字,默认都是阻塞方式

阻塞IO和非阻塞IO:差异在于等待事件就绪的方式不同 ------ 不就是二者都要等待吗 ------ 💥二者效率对于钓鱼这件事来说 效率是一样的
但是我们怎么感觉非阻塞IO 可以做更多的事情呢?阻塞和非阻塞在钓鱼(IO)上效率是一样的没错,非阻塞可以利用等待的时间去做其他事情啊 ,那么这个其他事情可以是读取另外一个文件描述符呢? 这样说非阻塞的效率高是可以的
- 🔥非阻塞在单位时间内可以做更多事情!
但是我们现在只讨论一个文件描述符的情况下讨论效率高低 阻塞和非阻塞效率是一样的
🔥非阻塞IO
非阻塞IO :如果内核还未将数据准备好,系统调用仍然会直接返回,并且返回EWOULDBLOCK错误码
非阻塞IO往往需要程序员循环的方式反复尝试读写无件描述符,这个过程称为轮询。这对CPU来说是较大的浪费,一般只有特定场景下才使用

🔥信号驱动IO
信号驱动IO :内核将数据准备好的时候,使用SIGIO信号通知应用程序进行IO操作
cpp
signal(SIGIO, handler);
① 注册 SIGIO
应用程序 -----------------> 内核
|
| 做其他事情
|
| socket 数据到达
| |
| v
| <------------------- ② SIGIO 信号
|
| ③ 信号处理函数执行
v
recv()/read()

🔥多路复用
IO多路转接 :虽然从流程图上看起来和阻塞IO类似。实际上最核心在于IO多路转接能够同时等待多个多件描述符的就绪状态
select去等待数据,同时等待多个文件描述符,一旦一个数据就绪了就拷贝数据recvfrom拷贝数据

💥同步IO 🆚 异步IO
IO分为两种:同步IO 和异步IO,上述的都是同步IO
同步和异步关注的是消息通信机制
-
同步IO :不仅要参与发起IO,还要参与IO的过程,其中等待和钓鱼参与其中一个就是同步IO
-
异步IO :只参与发起IO,但是不参与IO的任何细节
-
所谓同步 ,就是在发出一个调用时,在没有得到结果之前,该调用就不返回 。但是一旦调用返回,就得到返回值了;换句话说,就是由调用者主动等待这个调用的结果;
-
异步 则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果 ;换句话说,当⼀个异步过程调用发出后,调用者不会立刻得到结果;而是在调用发出后,被调用者通过状态、通知来通知调用者 ,或通过回调函数处理这个调用。田七给小王发布钓鱼任务后就不管了,直接回家,后续有结果也是小王打电话回来告诉田七
🔥异步IO
只参与发起IO,但是不参与IO的任何细节
由内核在数据拷贝完成时,通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据)
OS负责等待数据与拷贝

🌏探究 非阻塞IO
c
int recv(int sockfd, void *buf, size_t len, int flags);
对于recv来说,flags参数代表的是接收方式
| flags | 作用 |
|---|---|
0 |
默认接收 |
MSG_PEEK |
偷看数据,但不把数据从接收缓冲区移走 |
MSG_DONTWAIT |
本次 recv() 使用非阻塞方式 |
MSG_WAITALL |
尽量等到指定长度的数据全部到齐再返回 |
MSG_OOB |
接收带外数据 |
也就是说recv可以设置成非阻塞解锁,同理open文件也可以用O_NDELAY来设置
这里想说明的意思是设置成非阻塞的方式可以有很多! 但有没有一种通用的方式来设置呢??
🔥fcntl
一个文件描述符,默认都是阻塞IO ,后续无论是read、write操作此文件都是非阻塞的
cpp
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, .../* arg */ );
传入的cmd的值不同,后面追加的参数也不相同
fcntl函数有5种功能:
- 复制⼀个现有的描述符(cmd=F_DUPFD)
- 获得/设置文件描述符标记 (cmd=F_GETFD或F_SETFD)
- 💥获得/设置文件状态标记 (cmd=
F_GETFL或F_SETFL) - 获得/设置异步I/O所有权 (cmd=F_GETOWN或F_SETOWN)
- 获得/设置记录锁 (cmd=F_GETLK,F_SETLK或F_SETLKW)
我们此处只是用第三种功能,获取/设置文件状态标记,就可以将一个文件描述符设置为非阻塞
实现函数SetNoBlock
基于fcntl,我们实现一个SetNoBlock函数,将文件描述符设置为非阻塞
cpp
void SetNoBlock(int fd) {
int fl = fcntl(fd, F_GETFL);
if (fl < 0) {
perror("fcntl");
return;
}
fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}
使用 F_GETFL 将当前的文件描述符的属性取出来(这是一个位图)
然后再使用 F_SETFL 将文件描述符设置回去。设置回去的同时,加上一个O_NONBLOCK参数
轮询方式读取标准输入
cpp
#include <iostream>
#include <sys/types.h>
#include <system_error>
#include <unistd.h>
#include <fcntl.h>
void SetNoBlock(int fd) {
int fl = fcntl(fd, F_GETFL);
if (fl < 0) {
perror("fcntl");
return;
}
fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}
int main()
{
char inbuffer[1024];
SetNoBlock(0);
while(1)
{
ssize_t n = read(0, inbuffer, sizeof(inbuffer) - 1);
if(n > 0)
{
inbuffer[n-1] = 0;
std::cout << "echo # " << inbuffer << std::endl;
}
else {
//可以干其他事情
std::cerr << "error ..." << std::endl;
}
sleep(1);
}
}
其实上面代码有一个逻辑上错误:当我们读到文件结尾 的时候呢?if(n == 0)
当我们 读取失败 和 读取文件fd时,数据还没有就绪的时候都是以出错形式来进行返回的
- 所以我们必须区分开这两种情况,总不可能说数据没有就绪也算出错吧!
怎么区分出示读取出错 还是数据没就绪呢?
库函数、系统调用(本质也是库函数,C封装)会有一个全局变量errno:错误码
也有可能出现,某个进程接受到了某个信号导致提前唤醒!EINTR:系统调用被信号中断
cpp
进程阻塞在 read()
↓
突然收到信号
↓
执行信号处理函数
↓
read() 被提前打断
↓
返回 -1
errno = EINTR
cpp
else {
//数据还没就绪
if(errno == EAGAIN)
{
//操作被阻塞
std::cout << "data is not ready" << std::endl;
}
else if (errno == EINTR)
{
std::cout << "read 被信号中断" << std::endl;
continue;
}
else
{
//读取失败
std::cout << "read error" << errno << std::endl;
std::cout << "read error" << strerror(errno) << std::endl;
}
}
📢写在最后
接下来登场的是 epoll

