linux-高级IO(上)

发布于:2025-08-18 ⋅ 阅读:(13) ⋅ 点赞:(0)

目录

非阻塞IO

fcntl

多路转接之select

select


Io实际上就是inport&output,或者说是read & write

应用层中使用read的时候,本质是上是把数据从用户层写给操作系统,其本质是一个拷贝函数,

io=等+拷贝;

什么叫做高效的l呢?高效及单位时间内io过程中等待的比重越小IO的效率越高,几乎所有的提高io效率的策略本质上就是这个。

5种I/O模型:
 
1. 阻塞I/O:发起操作后,一直等数据处理完才返回,期间进程暂停。


2. 非阻塞I/O:操作发起后,数据没准备好就立刻返回,需不断检查是否就绪。


3. I/O多路复用:用一个进程监控多个I/O,哪个就绪就处理哪个,适合高并发。


4. 信号驱动I/O:注册信号后不用等,数据就绪时内核发信号通知处理。


5. 异步I/O:发起后直接返回,内核处理完所有步骤(包括数据复制)再通知。

前4种是同步o,最后一种是异步io。

非阻塞IO

fcntl

fcntl  是 Unix/Linux 系统中用于操作文件描述符属性的系统调用,功能强大,常用作设置文件描述符为非阻塞模式等。

fcntl  函数的原型为:

 int fcntl(int fd, int cmd, ... /* arg */); 

其参数含义如下:
 
1.  fd (第一个参数)
- 表示要操作的文件描述符(如通过  open  打开文件返回的整数)。
- 若  fd  无效(如未打开的描述符), fcntl  会执行失败并返回  -1 。
2.  cmd (第二个参数)
- 表示要执行的操作命令决定了  fcntl  的具体功能。
- 常见命令分类:
- 获取/设置文件状态标志(如  F_GETFL 、 F_SETFL );
- 复制文件描述符(如  F_DUPFD );
- 文件锁操作(如  F_SETLK 、 F_GETLK );
- 其他(如获取/设置文件描述符标志  F_GETFD 、 F_SETFD )。
3.  arg (可选参数,第三个参数)
- 是否需要该参数,取决于  cmd  的类型:
- 当  cmd  是 设置类命令(如  F_SETFL 、 F_DUPFD )时,需要传入  arg  作为设置的值(通常是整数或结构体指针,如文件锁相关命令需传入  struct flock* )。
- 当  cmd  是 获取类命令(如  F_GETFL 、 F_GETFD )时,无需传入  arg ,函数会忽略该参数。

 
例如:
 
- 用  fcntl(fd, F_SETFL, O_NONBLOCK)  设置非阻塞模式时, arg  为  O_NONBLOCK ;
- 用  fcntl(fd, F_GETFL)  获取状态时,无需  arg 。

 fcntl  的返回值取决于传入的 命令(第二个参数),主要分为以下几种情况:
 
1. 成功执行命令:
- 若命令是  F_DUPFD  或  F_DUPFD_CLOEXEC (复制文件描述符),返回新的文件描述符。
- 若命令是  F_GETFD 、 F_GETFL  等(获取标志/状态),返回对应的标志值(整数)。
- 若命令是  F_SETFD 、 F_SETFL  等(设置标志/状态),返回  0 。

2. 执行失败:
- 统一返回  -1 ,并设置全局变量  errno  来指示具体错误原因(如文件描述符无效、权限不足等)。
 
例如,用  fcntl(fd, F_GETFL)  获取标志时,成功返回标志值,失败返回  -1 。

示例

非阻塞轮巡。会不断的对缓冲区进行检测,如果发现缓冲区没有内容,他会返回17号错误码,资源不足问题。如此往复,如果用户向缓冲区内输入内容,他就会把。数据输出出来,然后进行下一次检测。

多路转接之select

select

io=等+拷贝;select只负责等。他可以等待多个文件描述符。

nfds

第1个参数之后的类型实际上是一个位图,假设该位图为0000 0000,将他设成3以后变为0000 0111假设第1号文件描述符就绪,那么位图会变成0000 0010。执行时比特位的位置表示的是文件描述符的编号,比特位的内容。表示是否需要内核关心,返回时比特位的位置仍表示文件描述符编号,位图的内容表示。哪些文件描述出需要被关心.Nfds实际上是这些位图内所存储的文件描述符的最大值加1

由于用户并不了解第1个参数之后的类型的位图的结构,因此操作系统提供了相应的接口,帮助用户进行操作

readfds 

- 指向可读事件监控集合的指针。

- 若某文件描述符被加入该集合, select  会检测其是否有数据可读(如网络数据到达、管道有数据等)。

- 可设为  NULL ,表示不监控可读事件。

writefds 

- 指向可写事件监控集合的指针。

- 用于检测文件描述符是否可写(如缓冲区空闲,可写入数据)。

- 可设为  NULL ,表示不监控可写事件。

exceptfds 

- 指向异常事件监控集合的指针。

- 用于检测文件描述符的异常情况(如带外数据到达)。

- 可设为  NULL ,表示不监控异常事件。

struct timeval

当最后一个参数为零时,其含义是每过0秒返回一次及非阻塞轮询,如果最后一个参数为NULL,极为阻塞等待。

我们需要注意的是,select的最后一个参数及struct timeval是一个输入输出型参数,他的意思是假设我们这最后一个参数设置为5.0,当过两秒以后有一个文件描述符就绪,他返回时会返回为3.0。他的意思是距离超时还剩三秒。

select服务器的构建过程中,如果想采用多路复用。来对文件进行处理的话,我们不能直接accept。  accept它本身就是检测并获取listen socket上面的事件,如果用accept,那么我们初始目的想要等待多个文件描述符就不可能了,因此应该用select.

对于新链接的到来于select而言是读事件的就绪,

对于select而言,位图我们需要提前进行构建,

之后我们需要清理其内部的残余数据。

Select接收到某一文件描述符就绪以后,它会向应用层发送信号。如果应用层没有处理该信号的话,那么select会持续不断地向应用层发送信号。

在处理函数中,我们已知。文件描述符的读程序已经就绪,那么接下来我们就需要建立用户ip及用户端口 ,通过accept来返回一个新的文件描述符,而这个文件描述符即是用户发送数据的一个缓存区间,是,我们是不能够直接读取该文件描述符的,在我们以前的代码。场景中之所以能够直接读取,是因为在多进程多线程场景中有其他的进程或线程来以进行等待处理工作,而当下的场景是一个单进程场景我们不能够直接读取,如果直接读取,而该文件描述内部是为空的话,我们的程序就会阻塞在这里

因此我们需要将这个新的文件描述符传递给select,但是 select在下文,而我们的处理函数在上能二者不在一个代码段中,此时我们就需要一个新的工具-辅助数组。他能够帮助我们将文件描述符值在不同的函数当中进行传递

我们将Hander函数更名为资源分配函数,因为我们并不知道哪一个文件描述符已经就绪,因此我们需要遍历这个辅助数组。当我们发现辅助数组。其内部值为默认值时我们返回上层i++。当它不是默认值此时它有两种可能,第1种是 Listen socket所发送的文件描述,第2种可能是客户端发送的,对于两者,我们有着不同的处理要求

如果发送信号的是listen socket,那么说明有新的客户端想要进行连接,此时我们需要保存该客户端的ip以及端口号将该文件描述符添加到辅助数组当中。

如果信号来自客户端,那么我们直接可以读取该数据,如果数据读取成功,我们进行打印输出,当数据读取完毕,我们退出并关闭文件描述符,如果读取失败。报错的同时关闭该文件描述符。

完整代码selectserver

#pragma once

#include <iostream>
#include <sys/select.h>
#include <sys/time.h>
#include "Socket.hpp"

using namespace std;

static const uint16_t defaultport = 8888;
static const int fd_num_max = (sizeof(fd_set) * 8);
int defaultfd = -1;

class SelectServer
{
public:
    SelectServer(uint16_t port = defaultport) : _port(port)
    {
        for (int i = 0; i < fd_num_max; i++)
        {
            fd_array[i] = defaultfd;
            // std::cout << "fd_array[" << i << "]" << " : " << fd_array[i] << std::endl;
        }
    }
    bool Init()
    {
        _listensock.Socket();
        _listensock.Bind(_port);
        _listensock.Listen();

        return true;
    }
    void Accepter()
    {
        // 我们的连接事件就绪了
        std::string clientip;
        uint16_t clientport = 0;
        int sock = _listensock.Accept(&clientip, &clientport); // 会不会阻塞在这里?不会
        if (sock < 0) return;
        lg(Info, "accept success, %s: %d, sock fd: %d", clientip.c_str(), clientport, sock);

        // sock -> fd_array[]
        int pos = 1;
        for (; pos < fd_num_max; pos++) // 第二个循环
        {
            if (fd_array[pos] != defaultfd)
                continue;
            else
                break;
        }
        if (pos == fd_num_max)
        {
            lg(Warning, "server is full, close %d now!", sock);
            close(sock);
        }
        else
        {
            fd_array[pos] = sock;
            PrintFd();
            // TODO
        }
    }
    void Recver(int fd, int pos)
    {
        // demo
        char buffer[1024];
        ssize_t n = read(fd, buffer, sizeof(buffer) - 1); // bug?
        if (n > 0)
        {
            buffer[n] = 0;
            cout << "get a messge: " << buffer << endl;
        }
        else if (n == 0)
        {
            lg(Info, "client quit, me too, close fd is : %d", fd);
            close(fd);
            fd_array[pos] = defaultfd; // 这里本质是从select中移除
        }
        else
        {
            lg(Warning, "recv error: fd is : %d", fd);
            close(fd);
            fd_array[pos] = defaultfd; // 这里本质是从select中移除
        }
    }
    void Dispatcher(fd_set &rfds)
    {
        for (int i = 0; i < fd_num_max; i++) // 这是第三个循环
        {
            int fd = fd_array[i];
            if (fd == defaultfd)
                continue;

            if (FD_ISSET(fd, &rfds))
            {
                if (fd == _listensock.Fd())
                {
                    Accepter(); // 连接管理器
                }
                else // non listenfd
                {
                    Recver(fd, i);
                }
            }
        }
    }
    void Start()
    {
        int listensock = _listensock.Fd();
        fd_array[0] = listensock;
        for (;;)
        {
            fd_set rfds;
            FD_ZERO(&rfds);

            int maxfd = fd_array[0];
            for (int i = 0; i < fd_num_max; i++) // 第一次循环
            {
                if (fd_array[i] == defaultfd)
                    continue;
                FD_SET(fd_array[i], &rfds);
                if (maxfd < fd_array[i])
                {
                    maxfd = fd_array[i];
                    lg(Info, "max fd update, max fd is: %d", maxfd);
                }
            }

            // accept?不能直接accept!检测并获取listensock上面的事件,新连接到来,等价于读事件就绪

            // struct timeval timeout = {1, 0}; // 输入输出,可能要进行周期的重复设置
            struct timeval timeout = {0, 0}; // 输入输出,可能要进行周期的重复设置
            // 如果事件就绪,上层不处理,select会一直通知你!
            // select告诉你就绪了,接下来的一次读取,我们读取fd的时候,不会被阻塞
            // rfds: 输入输出型参数。 1111 1111 -> 0000 0000
            int n = select(maxfd + 1, &rfds, nullptr, nullptr, /*&timeout*/ nullptr);
            switch (n)
            {
            case 0:
                cout << "time out, timeout: " << timeout.tv_sec << "." << timeout.tv_usec << endl;
                break;
            case -1:
                cerr << "select error" << endl;
                break;
            default:
                // 有事件就绪了,TODO
                cout << "get a new link!!!!!" << endl;
                Dispatcher(rfds); // 就绪的事件和fd你怎么知道只有一个呢???
                break;
            }
        }
    }
    void PrintFd()
    {
        cout << "online fd list: ";
        for (int i = 0; i < fd_num_max; i++)
        {
            if (fd_array[i] == defaultfd)
                continue;
            cout << fd_array[i] << " ";
        }
        cout << endl;
    }
    ~SelectServer()
    {
        _listensock.Close();
    }

private:
    Sock _listensock;
    uint16_t _port;
    int fd_array[fd_num_max];   // 数组, 用户维护的!
    // int wfd_array[fd_num_max];
};


网站公告

今日签到

点亮在社区的每一天
去签到