文章目录
硬件设备性能/读写速率,严格影响应用层的设置,所以了解IO很有必要!
小知识:
Windows/Linux对标准输入终止读取按键不同 Linux是ctrl+d
1.再识 IO
调用系统接口 write、read ,本质是把数据从用户层/语言级别缓冲区拷贝给操作系统/内核缓冲区,(写入到 OS 的发送缓冲区中 / 从 OS 的接收缓冲区中读取数据)–=-- 本质是拷贝函数!
调用 write 的时候只有当发送缓冲区中有足够的空间才能进行拷贝,当发送缓冲区没有空间了,此时 write 只能阻塞等待,不能继续拷贝。
调用 read 的时候,只有当接收缓冲区有数据才能进行读取拷贝,当接收缓冲区没有数据了,此时 read 也只能阻塞等待。
IO 的过程被分为两个部分:等待数据就绪和拷贝数据。
在 IO 的过程中,要进行拷贝,必须先判断条件成立 --=-- 读写事件是否就绪。
高效的 IO
在单位时间内/一次读|写过程,等待占时的比重越小,IO 的效率越高!几乎所有的提高IO效率的策略,都是以这个为依据!
2.IO模型【钓鱼举例】
计算机在应用中不可避免的操作是IO,随着时间的推移,人们在探索IO模型的路上不断改进,试图寻求更高效/更实用的IO策略!
2.1-2.4为同步IO:【参与IO,等待方式不同】
2.1阻塞 IO:鱼竿落下,老实等待
在内核将数据准备好之前,系统调用会一直等待。**所有的套接字/read/write/recv,默认都是阻塞方式。**阻塞IO是最常见的IO模型。
2.2非阻塞IO:鱼竿落下,游戏电影,时不时观察
非阻塞 IO 就是,如果内核未将数据准备好,系统调用直接返回,并返回 EWOULDBLOCK 错误码。
非阻塞 IO 往往需要程序员以循环的方式反复尝试读写文件描述符,这个过程称为轮询。这对 CPU 来说是较大的浪费,一般只有特定场景下才使用。
2.3信号驱动 IO:鱼竿挂铃铛,有鱼会通知
内核将数据准备好的时候,使用 SIGIO 信号通知应用程序进行 IO 操作。
2.4多路转接/复用:100个鱼竿下水,轮询。
阻塞IO
IO 多路转接看起来和阻塞 IO 类似,核心在于 IO 多路转接能够同时等待多个文件描述符的就绪状态。
2.5异步IO:老板(用户)让秘书(OS)钓鱼,有鱼电话通知老板【不参与钓鱼/IO,只是发起IO,最后拿结果】
由内核在数据拷贝完成时,通知应用程序。和信号驱动 IO 的区别在于,信号驱动是告诉应用程序何时可以开始拷贝数据。而异步IO是在数据拷贝完成时才通知应用程序。
任何 IO 过程中,都包含两个步骤:等待 + 拷贝。在实际的应用场景中,等待占比时间一般较大。让 IO 更高效==》让等待的时间尽量少。
异步IO的实现逻辑较同步来说相对混乱,有些新技术逐渐取代这种混乱:协程。
协程(Coroutines):
协程是一种比线程更轻量级的并发模型,它允许在单个线程中执行多个任务,并且能够在任务之间高效地切换。协程可以与异步IO结合使用,以实现非阻塞的IO操作。许多现代编程语言都支持协程,如Python、Go等。
3.阻塞IO vs 非阻塞IO
阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态。
阻塞调用:调用结果返回之前,当前进程/线程会被挂起。调用线程只有在得到结果之后才会返回。
非阻塞调用:在不能立刻得到结果之前,该进程不会被挂起而是立即返回。
非阻塞IO在等待的时候可以做其他事情,它们之间等的方式不一样,导致非阻塞IO在效率上稍微高一点。(等待的时候可以做其他事情,等待占比减少,效率提高,但是拷贝动作耗时相同)。
open:打开的时候就可以设置为非阻塞–通过flags参数。只不过fcntl比较通用!
回顾fd
fd:文件描述符–数组下标–指向文件结构体–文件结构体含flag字段
回顾缓冲区知识
这两种都能让信息在执行这行代码时就输出到显示器(行刷新) 只不过第一种能让打印后的光标紧跟信息,更美观。
int fcntl(int fd, int cmd, … /* arg */ );
recv()三个参数是和 read() 一样,最后一个参数 flag 设为 0 默认是阻塞等待。将这个参数设为 MSG_DONTWAIT,变为非阻塞IO
通用做法:使用 fcntl() 接口。文件描述符是一个数组下标,所有的网络通信、文件等,都是读写文件描述符,每一个文件描述符指向的是内核中的文件对象,文件对象有关于这个文件的 flags 标记位。通过 fcntl() 接口来设置一个文件描述符的属性!–=-- 设置其文件对象中的 flags 标志位,告诉内核这个指定的文件描述符以非阻塞的方式来操作。系统接口如下:
按照指定的 cmd 对指定的文件描述符进行可变参数部分的设置。
传入的 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)
详细了解
Linux中的fcntl接口是一个功能强大的系统调用,它提供了对文件描述符(file descriptor)进行底层控制的接口。通过fcntl,开发者可以精细地管理文件或套接字的行为,实现多种控制功能。以下是关于fcntl接口的简要叙述:
- 函数原型
在C语言中,fcntl函数的原型定义在<fcntl.h>头文件中,其基本形式如下:
c
#include <fcntl.h>
int fcntl(int fd, int cmd, …);
fd:是要操作的文件描述符。
cmd:是控制操作的命令,决定了fcntl函数执行的具体操作类型。
…:是与cmd相关联的可选参数,根据cmd的不同,参数的数量和类型也会有所不同。
2. 常见操作
fcntl支持多种操作,以下是一些常见的操作类型:
F_GETFL:获取文件描述符的状态标志,如是否设置了非阻塞模式等。
F_SETFL:设置文件描述符的状态标志,如将文件描述符设置为非阻塞模式。
F_GETFD:获取文件描述符的标志,如是否设置了FD_CLOEXEC标志(表示在exec调用时关闭该文件描述符)。
F_SETFD:设置文件描述符的标志。
F_GETLK、F_SETLK、F_SETLKW:用于获取、设置或阻塞地设置文件锁。这些操作允许进程对文件的特定部分进行加锁,以防止其他进程同时访问。
3. 非阻塞IO
在网络编程或文件操作中,非阻塞IO是一种重要的技术。通过fcntl的F_SETFL命令和O_NONBLOCK标志,可以轻松地将文件描述符设置为非阻塞模式。在非阻塞模式下,如果IO操作不能立即完成,系统调用会立即返回一个错误(如EAGAIN或EWOULDBLOCK),而不是让调用线程等待。
-
文件锁
fcntl还提供了文件锁的功能,通过F_GETLK、F_SETLK和F_SETLKW命令,进程可以对文件的特定部分进行加锁,以防止其他进程同时读写该部分。这对于实现进程间的同步和互斥访问非常有用。 -
示例
以下是一个使用fcntl将文件描述符设置为非阻塞模式的简单示例:
c
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
int main() {
int fd, flags;
fd = open(“file.txt”, O_RDWR);
if (fd == -1) {
perror(“open”);
exit(EXIT_FAILURE);
}
// 获取当前文件描述符标志
if ((flags = fcntl(fd, F_GETFL)) == -1) {
perror("fcntl - F_GETFL");
close(fd);
exit(EXIT_FAILURE);
}
// 设置非阻塞标志
flags |= O_NONBLOCK;
if (fcntl(fd, F_SETFL, flags) == -1) {
perror("fcntl - F_SETFL");
close(fd);
exit(EXIT_FAILURE);
}
printf("文件描述符已设置为非阻塞模式。\n");
close(fd);
return 0;
}
6. 总结
fcntl接口是Linux系统中一个非常重要的系统调用,它提供了对文件描述符的精细控制,包括状态标志的设置、文件锁的获取与释放等。通过fcntl,开发者可以更加灵活地处理文件和网络IO操作,提高程序的性能和可靠性。
代码一:阻塞式等待
代码二:非阻塞等待
由于设置对stdin设置了非阻塞等待 所以一旦调用read就会直接返回 由于read没有调用成功(那一瞬间用户没有输入数据)所以read返回-1。
修改:读取错误的原因
由于设置对stdin设置了非阻塞等待 所以一旦调用read就会直接返回 由于read没有调用成功(那一瞬间用户没有输入数据)所以read返回-1。
修改:n<0后不break
由于设置对stdin设置了非阻塞等待 所以一旦调用read就会直接返回 由于read没有调用成功(那一瞬间用户没有输入数据)所以read返回-1。如果不break,会继续执行while的printf和fflush语句,接着调用read,同样的没有输入,故有这样的输出结果。并且此时输入数据是可以被读取的!
区分真的出错还是没有数据:errno
完整代码
#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <cstdio>
#include <cerrno>
#include <cstring>
using namespace std;
void SetNonBlock(int fd)
{
int cur_flag = fcntl(fd, F_GETFL);
if (cur_flag < 0)
{
perror("fcntl");
return;
}
fcntl(fd, F_SETFL, cur_flag | O_NONBLOCK);
cout << " set fd " << fd << " nonblock done" << endl;
}
int main()
{
char buffer[1024];
SetNonBlock(0);
sleep(1);
while (true)
{
printf("Please Enter# ");
fflush(stdout);
sleep(1);
ssize_t n = read(0, buffer, sizeof(buffer) - 1);
if (n > 0)
{
buffer[n - 1] = 0;
cout << "echo : " << buffer << endl;
}
else if (n == 0)
{
cout << "read done" << endl;
break;
}
else
{
// 设置为非阻塞 底层fd数据没有就绪 recv/read/write/send返回值以出错形式返回
if (errno == EWOULDBLOCK)
{
cout << "0 fd data not ready, try again!" << endl;
sleep(1);
// do_other_thing();
}
else
{
// real 读取错误
cerr << "read error, n = " << n << "errno code: "
<< errno << ", error str: " << strerror(errno) << endl;
}
// TODO 信号中断IO?
}
}
return 0;
}
4.同步IO vs 异步IO
同步和异步关注:消息通信机制。
同步IO:发出一个调用,在没有得到结果之前,该调用不返回。一旦调用返回,就得到返回值了;由调用者主动等待这个调用的结果–=–参与了IO中的等待或者拷贝的过程,就是同步IO;
异步IO:调用在发出之后,这个调用就直接返回了,没有返回结果;当一个异步过程调用发出后,调用者不会立刻得到结果;在调用发出后,被调用者通过状态、通知来通知调用者,或者通过回调函数处理这个调用。异步IO不参与IO,只是发起IO,最后拿结果。
跟线程同步/异步无关 回顾线程同步/异步
同步并不是“同时进行”的意思,而是“协同步调”–协调指令运行的先后顺序。
线程资源竞争而导致的数据不一致问题,解决这个问题,==》线程同步机制:让原先异步的操作依次有序地执行。线程同步机制:互斥锁Mutex/读写锁Readers-Writer Lock/信号量Semaphore/条件变量Conditional Variables
锁是一种线程同步机制:
锁带来的问题:死锁(在考虑并发程度上单锁单临界区)/频繁上锁解锁效率低(尽可能的降低锁的范围)。
在讲多进程多线程的时候, 也提到同步和互斥. 这里的同步通信和进程之间的同步是完全不想干的概
念.
进程/线程同步也是进程/线程之间直接的制约关系。是为完成某种任务而建立的两个或多个线程,这个线程需要在某些位置上协调他们的工作次序而等待、传递信息所产生的制约关系. 尤其是在访问临界资源的时候.
5.其他高级IO
非阻塞IO,纪录锁,系统V流机制,I/O多路转接(也叫I/O多路复用),readv和writev函数以及存储映射IO(mmap),这些统称为高级IO