Linux --- 高级IO

一、基本概念

五种IO模型

  • 阻塞IO:在内核将数据准备好之前,系统调用会一直等待,所有的套接字,默认都是阻塞方式
  • 非阻塞IO:如果内核还未将数据准备好,系统调用仍然会直接返回,并且返回EWOULDBLOCK错误码
  • 信号驱动IO:内核将数据准备好的时候,使用SIGIO信号通知应用程序进行IO操作
  • IO多路转接:虽然从流程图上看起来和阻塞IO类似,实际上最核心在于IO多路转接能够同时等待多个文件描述符的就绪状态
  • 异步IO:由内核在数据拷贝完成时,通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据)
任何IO过程中,都包含两个步骤:第一是等待,第二是拷贝。而且在实际的应用场景中, 等待消耗的时间往往都远高于拷贝的时间。让IO更高效,最核心的办法就是让等待的时间尽量少
上面五种IO方案可以用钓鱼的例子来理解:
  • 阻塞IO,即钓鱼时,就坐在那里盯着水面,看鱼是否上钩
  • 非阻塞IO,即钓鱼时,可以玩玩手机,聊聊天,只要时不时关注一下是否有鱼上钩即可
  • 信号驱动IO,即在鱼竿上放置一个铃铛,铃铛响了再去收杆,其他时间自由分配
  • IO多路转接,即一个人拿上几十个鱼竿在钓鱼
  • 异步IO,即我让其他人来钓鱼,最后钓完鱼,我来收鱼即可

同步通信 vs 异步通信

同步和异步关注的是消息通信机制

  • 所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回. 但是一旦调用返回,就得到返回值了; 换句话说,就是由调用者主动等待这个调用的结果
  • 异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果,换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果,而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用

注意:这里说的同步/异步和进程线程的同步/异步没有任何关系!!!

进程/线程的同步和异步是根据事件的先后顺序决定的,这里可以理解为在IO时,我们是否主动去接收数据,即最终进行IO的是自己还是其他人。

二、非阻塞IO

#include <unistd.h>
#include <fcntl.h>

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

fcntl的功能取决于cmd参数的值,包括但不限于以下几种:

  1. F_DUPFD:复制文件描述符

  2. F_GETFD/F_SETFD:获取文件描述符标志 / 设置文件描述符标志

  3. F_GETFL/F_SETFL:获取文件状态标志 / 设置文件状态标志

  4. F_GETOWN/F_SETOWN:获取/设置异步I/O所有权。

  5. F_GETLK/F_SETLK/F_SETLKW:获取/设置/等待记录锁。

我们只需要用第三个功能就能实现让一个文件描述符变为非阻塞。

#include <iostream>
#include <unistd.h>
#include <fcntl.h>

// 设置非阻塞
void SetNonBlock(int fd)
{
    int fl = fcntl(fd, F_GETFL);
    if (fl < 0)
    {
        std::cout << "fcntl err" << std::endl;
        exit(1);
    }
    fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}

int main()
{
    SetNonBlock(0);
    char buffer[1024];
    while (true)
    {
        ssize_t n = read(0, buffer, sizeof(buffer) - 1);
        if (n > 0)
        {
            buffer[n] = 0;
            std::cout << buffer << std::endl;
        }
        else if (n == 0)
        {
            std::cout << "end stdin" << std::endl;
            break;
        }
        else
        {
            // 非阻塞 --- 如果数据没有准备好,返回值会按照出错的方式返回 -1
            // 数据没有准备好 vs 真的出错了 如何分辨?
            // 如果数据没有准备好,read函数调用完成后,OS会将errno设置为EWOULDBLOCK / EAGAIN
            if (errno == EWOULDBLOCK) // 没有数据
            {
                std::cout << "缓冲区中没有数据,error:" << errno << std::endl;
            }
            else if (errno == EINTR) // 被中断了,也不算出错
            {
                std::cout << "读过程被中断" << std::endl;
            }
            else // 读出错
            {
                std::cout << "read err" << std::endl;
                break;
            }
        }

        sleep(1);
    }
    return 0;
}

 三、I/O多路转接

1、select

1)接口介绍

#include <sys/select.h>  
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

参数

  • nfds:指定被监听的文件描述符的总数。通常设置为所有文件描述符中的最大值加1,因为文件描述符是从0开始计数的。
  • readfds:指向可读事件对应的文件描述符集合的指针。当select返回时,该集合将包含已准备好读取的文件描述符。
  • writefds:指向可写事件对应的文件描述符集合的指针。当select返回时,该集合将包含已准备好写入的文件描述符。
  • exceptfds:指向异常事件对应的文件描述符集合的指针。当select返回时,该集合将包含发生异常的文件描述符。
  • timeout:指定select的超时时间。如果设置为NULL,则select将一直阻塞,直到有文件描述符就绪;如果设置为0,则select将立即返回,无论文件描述符是否就绪;如果设置为一个非零的时间值,则select将在指定的时间内阻塞,直到有文件描述符就绪或超时。

返回值

  • 在正常情况下,select返回满足条件的文件描述符的个数
  • 如果超时,则返回0
  • 如果出错或被某个信号中断,则返回-1

2)代码 

// SelectServer.hpp
#pragma once
#include "Socket.hpp" // 我在https://blog.csdn.net/V_zjs/article/details/137426801?spm=1001.2014.3001.5501 中有写过,有需要可以看看
#include "Log.hpp" // 我在https://blog.csdn.net/V_zjs/article/details/138088701?spm=1001.2014.3001.5501 中有写过,有需要可以看看
#include <iostream>
#include <algorithm>

using namespace zxws;

const int defaultbackage = 5;
const int defaultport = 8080;
const int num = sizeof(fd_set) * 8;

class SelectServer
{
public:
    SelectServer(int port = defaultbacklog) : _port(port), _listensocketfd(new TcpSocket()), _isrunning(false)
    {
    }
    ~SelectServer()
    {
    }

    void Init()
    {
        _listensocketfd->BuildListenSocket(_port, defaultbackage);
        for (auto &sock : _rfds_array)
        {
            sock = nullptr;
        }
        _rfds_array[0] = _listensocketfd.get();
    }

    void Loop()
    {
        _isrunning = true;
        while (_isrunning)
        {
            fd_set rds; // 输入输出型参数,rds需要每次都被设置
            FD_ZERO(&rds);
            int max_fd = _listensocketfd->GetSockfd();
            for (int i = 0; i < num; i++)
            {
                if (_rfds_array[i] == nullptr)
                    continue;
                FD_SET(_rfds_array[i]->GetSockfd(), &rds);
                max_fd = std::max(max_fd, _rfds_array[i]->GetSockfd());
            }

            Debug();

            struct timeval time = {5, 0};
            int n = select(max_fd + 1, &rds, nullptr, nullptr, &time /*nullptr*/);
            switch (n)
            {
            case 0:
                lg(Info, "没有数据, time: %u.%u s", time.tv_sec, time.tv_usec);
                break;
            case -1:
                lg(Fatal, "select err");
                break;
            default:
                HandleEven(rds);
                break;
            }
        }
        _isrunning = false;
    }

    void Debug()
    {
        std::cout << "文件描述符有:";
        for (auto sock : _rfds_array)
        {
            if (!sock)
                continue;
            std::cout << sock->GetSockfd() << " ";
        }
        std::cout << "\n";
    }

private:
    void HandleEven(fd_set &rds)
    {
        for (int i = 0; i < num; i++)
        {
            if (_rfds_array[i] == nullptr)
                continue;
            int fd = _rfds_array[i]->GetSockfd();
            // fd 读事件就绪
            if (FD_ISSET(fd, &rds))
            {
                // 如果是监听套接字
                if (fd == _listensocketfd->GetSockfd())
                {
                    std::string ip;
                    uint16_t port;
                    Socket *sock = _listensocketfd->AcceptConnection(&ip, &port);
                    if (!sock)
                    {
                        lg(Fatal, "获取数据失败");
                        return;
                    }
                    lg(Info, "接收到客户端 ip: %s, port: %d, fd: %d", ip.c_str(), port, sock->GetSockfd());

                    int pos = 0;
                    for (; pos < num; pos++)
                    {
                        if (_rfds_array[pos] == nullptr)
                        {
                            _rfds_array[pos] = sock;
                            break;
                        }
                    }

                    if (pos == num)
                    {
                        sock->CloseSockfd();
                        delete sock;
                        lg(Info, "数组已满,无法在添加");
                    }
                }
                else // 如果是正常的套接字
                {
                    std::string message;
                    bool res = _rfds_array[i]->Recv(&message, 1024); // 这里的读取是有问题的,需要序列化和反序列化,定制协议的
                    if (res == false)                                // 读写失败
                    {
                        // 关闭当前的描述符
                        _rfds_array[i]->CloseSockfd();
                        delete _rfds_array[i];
                        _rfds_array[i] = nullptr;
                    }
                    else
                    {
                        lg(Info, "client # %s", message.c_str());
                    }
                }
            }
        }
    }

private:
    std::unique_ptr<Socket> _listensocketfd;
    bool _isrunning;
    uint16_t _port;
    Socket *_rfds_array[num];
};

#include <iostream>
#include "SelectServer.hpp"
int main(int argc, char* argv[])
{
    if(argc!=2)
    {
        std::cout << " Usrage: " << argv[0] << " port\n" << std::endl;
        return 1;
    }
    uint16_t port = std::stoi(argv[1]);
    std::unique_ptr<SelectServer> svr(new SelectServer(port));
    svr->Init();
    svr->Loop();
    return 0;
}

从上面的代码和测试来看,多路转接能够用单进程实现与多个客户端的通信

3)优缺点

优点

  • slect只负责等待,可以等待多个fd,IO的时候效率高

缺点

  • 每次都需要对select的参数进行重置
  • 编写代码的时候,select要使用数据结构对与客户端通信的fd进行记录,所以很多时候都需要遍历,影响select效率
  • 用户到内核,内核到用户,每次select调用和返回,都要对位图进行重新设置,用户和内核之间,要一直进行数据拷贝
  • select让OS在底层关注所有被置为1的fd时,需要对位图进行遍历,这就是select的第一个参数是max_fd+1的原因,OS需要轮询检测被置为1的fd是否有事件就绪
  • fd_set是一个系统提供的类型,fd_set大小是固定的为128字节,即select能够检测的fd总数有上限

总的来说:select的优点是所有多路转接都会有的优点,缺点则主要是select的输入输出型参数导致的。

2、poll

1)接口介绍

#include <poll.h>  
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

参数

  • fds:该结构体数组用于指定程序关心的文件描述符集,每个struct pollfd代表一个被监视的文件描述符。
  • nfds:指定fds数组的长度,即要监视的文件描述符的数量。
  • timeout:指定poll函数等待事件发生的超时时间(以毫秒为单位)。
    • 特定时间值:表示poll调用将在指定的时间内进行阻塞等待,如果超时时间内没有文件描述符上的事件就绪,则poll将超时返回。
    • 0:表示poll调用将进行非阻塞等待,无论是否有文件描述符上的事件就绪,都会立即返回。
    • -1:表示poll调用后将阻塞等待,直到至少有一个文件描述符上的事件就绪。

返回值

  • 如果函数调用成功,则返回有事件就绪的文件描述符个数。
  • 如果timeout时间耗尽且没有文件描述符上的事件就绪,则返回0
  • 如果函数调用失败,则返回-1,并设置相应的错误码。
struct pollfd {  
    int fd;          /* 文件描述符 */  
    short events;    /* 等待的事件集合,例如POLLIN(读就绪)、POLLOUT(写就绪)等 */  
    short revents;   /* 实际发生的事件集合,由内核在调用返回时设置 */  
};
// 将输入、输出分离,不用每次都取重新设置参数

这些事件类型由一组位掩码(bitmask)表示,可以使用按位或运算符组合多个事件。以下是一些常用的事件选项:

  1. POLLIN:数据可读。当指定的文件描述符上有数据可读时,此事件被触发。这通常用于非阻塞套接字,以检测何时可以读取新数据。

  2. POLLOUT 或 POLLWRNORM:数据可写。当指定的文件描述符准备好写数据时,此事件被触发。这可以用于检查套接字是否准备好发送数据。

  3. POLLRDNORM:等价于POLLIN。这是POSIX规范中定义的别名,与POLLIN完全相同。

  4. POLLWRBAND:优先带外数据可写。这个选项用于带外数据(out-of-band data)的写操作,但现代系统中这个特性已经很少使用。

  5. POLLPRI:优先数据可读。当指定的文件描述符上有优先(例如带外)数据可读时,此事件被触发。这通常用于处理如SIGURG信号等紧急数据。

  6. POLLERR:错误发生。当指定的文件描述符发生错误时,此事件被触发。这可以用于检测套接字错误,例如连接被对方关闭。

  7. POLLHUP:挂起(hang up)。当指定的文件描述符被关闭,或者远程连接关闭时,此事件被触发。这可以用于检测连接断开。

  8. POLLNVAL:无效请求。当指定的文件描述符无效时,此事件被触发。这通常意味着fd的值超出了文件描述符的范围,或者文件描述符已经被关闭。

2)代码

// PollServer.hpp
#pragma once
#include "Socket.hpp"
#include "Log.hpp"
#include <poll.h>
#include <iostream>
#include <algorithm>
using namespace zxws;

const int defaultbackage = 5;
const int defaultport = 8080;
const int gnum = 1024;

class PollServer
{
public:
    PollServer(int port = defaultbacklog) : _port(port), _listensocketfd(new TcpSocket()), _isrunning(false), _num(gnum),_rfds(nullptr)
    {
    }
    ~PollServer()
    {
        delete []_rfds;
    }

    void Init()
    {
        _listensocketfd->BuildListenSocket(_port, defaultbackage);
        _rfds = new struct pollfd[_num];
        for (int i = 0; i < _num; i++)
        {
            _rfds[i].fd = -1;
            _rfds[i].events = 0;
            _rfds[i].revents = 0;
        }
        _rfds[0].fd = _listensocketfd->GetSockfd();
        _rfds[0].events |= POLLIN;
    }

    void Loop()
    {
        _isrunning = true;
        while (_isrunning)
        {
            int time = 1000; // 1s
            int n = poll(_rfds, _num, time);
            switch (n)
            {
            case 0:
                lg(Info, "没有数据");
                break;
            case -1:
                lg(Fatal, "poll err");
                break;
            default:
                HandleEven();
                break;
            }
        }
        _isrunning = false;
    }

private:
    void HandleEven()
    {
        for (int i = 0; i < _num; i++)
        {
            if (_rfds[i].fd == -1)
                continue;
            int fd = _rfds[i].fd;
            short event = _rfds[i].revents; // 注意这里要的是 输出型参数 revents 不是 events,别写错了!!!
            // 读事件就绪
            if (event & POLLIN)
            {
                // 如果是监听套接字
                if (fd == _listensocketfd->GetSockfd())
                {
                    std::string ip;
                    uint16_t port;
                    int sock = _listensocketfd->AcceptConnection(&ip, &port);
                    if (sock == -1)
                    {
                        lg(Fatal, "获取数据失败");
                        return;
                    }
                    lg(Info, "接收到客户端 ip: %s, port: %d, fd: %d", ip.c_str(), port, sock);

                    int pos = 0;
                    for (; pos < _num; pos++)
                    {
                        if (_rfds[pos].fd == -1)
                        {
                            _rfds[pos].fd = sock;
                            _rfds[pos].events = POLLIN;
                            break;
                        }
                    }

                    if (pos == _num)
                    {
                        lg(Info, "数组已满,无法在添加");
                    }
                }
                else // 如果是正常的套接字
                {
                    char buffer[1024]{};
                    ssize_t n = recv(fd, buffer, 1023, 0); // 这里的读取是有问题的,需要序列化和反序列化,定制协议的
                    if (n <= 0)                            // 读写失败
                    {
                        // 关闭当前的描述符
                        close(fd);
                        _rfds[i].fd = -1;
                        _rfds[i].events = _rfds[i].revents = 0;
                    }
                    else
                    {
                        buffer[n] = 0;
                        lg(Info, "client # %s", buffer);
                    }
                }
            }
        }
    }

private:
    std::unique_ptr<Socket> _listensocketfd;
    bool _isrunning;
    uint16_t _port;
    struct pollfd *_rfds;
    int _num;
};


#include <iostream>
#include "PollServer.hpp"
int main(int argc, char* argv[])
{
    if(argc!=2)
    {
        std::cout << " Usrage: " << argv[0] << " port\n" << std::endl;
        return 1;
    }
    uint16_t port = std::stoi(argv[1]);
    std::unique_ptr<PollServer> svr(new PollServer(port));
    svr->Init();
    svr->Loop();
    return 0;
}

3)优缺点

优点

  • 可以等待多个fd,效率高
  • 输入,输出参数分离(events和revents),不用对poll参数进行重置
  • poll关心的fd没有上限,可以根据需要创建数组大小

缺点

  • 用户到内核空间,要有数据拷贝 --- 必要开销
  • poll应用层,要遍历。在内核层面,要遍历检测,关心的fd是否有对应的事件就绪---OS需要轮询检测被置为1的fd是否有事件就绪

3、epoll

1)接口介绍

#include <sys/epoll.h>
int epoll_create(int size);

参数

  • size:这是 epoll 实例的大小,即可以监听的文件描述符数量的一个估计值。然而,在实际使用中,这个参数在很多系统上已经不再具有实际的含义,可以将其设置为一个大于0的任意值。

返回值

  • 如果成功,epoll_create 返回一个非负整数,代表 epoll 实例的文件描述符。这个描述符将在后续的 epoll 操作中使用,如添加、删除、修改监听的文件描述符,以及等待 I/O 事件的发生。
  • 如果失败,epoll_create 返回 -1,并设置 errno 表示错误原因。

 int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

参数

  1. epfd:这是通过epoll_create创建的epoll实例的文件描述符。
  2. op:操作类型,用于指定要进行的操作,有以下三种取值:
    • EPOLL_CTL_ADD:向epoll实例中添加一个文件描述符及其关联的事件。
    • EPOLL_CTL_MOD:修改已经存在于epoll实例中的文件描述符的监听事件内容。
    • EPOLL_CTL_DEL:从epoll实例中删除一个文件描述符。
  3. fd:这是要进行操作的目标文件描述符,即要注册、修改或删除的文件描述符。
  4. event:这是一个指向epoll_event结构体的指针,用于描述所监听事件的相关信息。如果是删除操作(即opEPOLL_CTL_DEL),该参数可以为NULL。
typedef union epoll_data {
    void        *ptr;
    int          fd;
    uint32_t     u32;
    uint64_t     u64;
} epoll_data_t;


struct epoll_event {  
    __uint32_t events;      /* Epoll events */ 
    epoll_data_t data;      /* User data variable */
};

返回值

  • 如果操作成功,epoll_ctl函数返回0。
  • 如果操作失败,epoll_ctl函数返回-1,并设置errno表示错误原因。

 
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);

参数

  1. epfd:这是通过 epoll_create 或 epoll_create1 创建的 epoll 实例的文件描述符。
  2. events:这是一个指向 epoll_event 结构体数组的指针,用于存储 epoll_wait 返回的就绪事件。
  3. maxevents:告诉内核这个 events 数组有多大,即最大可以返回多少个就绪事件。
  4. timeout:指定等待事件的超时时间(毫秒)。如果 timeout 是 -1,那么 epoll_wait 将一直阻塞,直到有文件描述符就绪。如果 timeout 是 0,epoll_wait 将立即返回,不管是否有文件描述符就绪。

返回值

  • 如果成功,epoll_wait 返回就绪事件的数量。这些事件被存储在 events 数组中。方便用户在遍历时,只会遍历需要处理的事件。
  • 如果调用被中断(比如收到了一个信号),epoll_wait 返回 -1,并设置 errno 为 EINTR。
  • 如果出错,epoll_wait 返回 -1,并设置 errno 为其他值(比如 EBADF 表示 epfd 不是一个有效的文件描述符)。

2)原理

OS如何知道网卡上的数据是否就绪?硬件中断

OS是如何把外设上面的数据,拿到内存中?根据中断号,执行中断向量表中的方法

所以我们根本没必要像select和poll一样,让OS去轮询的检查是否有文件描述符的事件就绪,我们可以让硬件去通知OS关心的文件描述符上的事件就绪,这样就大大提高了效率。

epoll和poll,select的主要区别在于epoll对于事件的维护是由OS帮助用户完成的,不需要用户进行维护,而poll和select则需要用户对事件手动进行维护,所以epoll原理复杂,但是代码写起来比较简单,同时效率也更高

3)epoll的两种工作模式 --- LT模式 & ET模式

  • 1、水平触发Level Triggered 工作模式,epoll默认状态下就是LT工作模式
  • 当文件描述符上的事件就绪时,上层可以选择不处理,也可以只处理一部分,epoll会再次通知你,直到数据被处理完
  • 2、边缘触发Edge Triggered工作模式
  • 当文件描述符上的事件就绪时,上层必须要全部处理完,否则只能等待下次该文件描述符上有新的事件就绪,epoll才会通知你(简单来说就是只有当前文件描述符关心的事件出现变化时---有新的数据到来,epoll才会通知你处理),不然你将无法处理之前剩余的没有处理的数据

其中ET模式的效率会更高,为什么?

从整体上来说,通知一次就处理好数据,和通知多次才把数据处理好,显然前者更优,而ET模式就是倒逼上层要一次性将数据全部处理完,具体到原理,上层将数据接收完,使得接收方的缓冲区会更大,让tcp在通信时,返回给发送方更大的滑动窗口大小,更有可能让发送方发送更多的数据(这里之所以说可能,是因为发送的数据大小还和拥塞窗口有关),从而提高效率

当然LT模式我们也可以让上层一次性将数据读完,但是相较于ET模式的"倒逼"策略,终归是有一定的选择空间的,就跟我们写作业一样,有deadline和没deadline那是两种完全不同的情况。

那么采用ET模式,我们如何保证一次性将数据全部取完呢?我们肯定就需要循环读取数据了,所以阻塞式IO肯定是不行的,一旦它阻塞住,我们将无法往下执行,所以我们只能选择非阻塞式等待,一旦读取的数据量小于缓冲区的大小,就说明数据被读取完了 --- ET模式只能采用非阻塞IO。

4)代码

namespace zxws
{
    const static int defaultepfd = -1;
    const static int size = 1024;
    class Epoller
    {
    public:
        Epoller() : _epfd(defaultepfd) {}
        void Init()
        {
            _epfd = epoll_create(size);
            if (defaultepfd == _epfd)
            {
                lg(Fatal, "epoll_create error, %s : %d", strerror(errno), errno);
                exit(-1);
            }
            lg(Info, "create epoll success , epfd :%d", _epfd);
        }

        void AddEvent(int sockfd, int events)
        {
            struct epoll_event ev;
            ev.events = events;
            ev.data.fd = sockfd;
            int n = ::epoll_ctl(_epfd, EPOLL_CTL_ADD, sockfd, &ev);
            if (n < 0)
            {
                lg(Error, "epoll_ctl add error, %s : %d", strerror(errno), errno);
            }
        }
        void ModifyEvent(int sockfd, int events)
        {
            struct epoll_event ev;
            ev.data.fd = sockfd;
            ev.events = events;
            int n = ::epoll_ctl(_epfd, EPOLL_CTL_MOD, sockfd, &ev);
            if (n < 0)
            {
                lg(Error, "epoll_ctl modify error, %s : %d", strerror(errno), errno);
            }
        }
        void DelEvent(int sockfd)
        {
            int n = ::epoll_ctl(_epfd, EPOLL_CTL_DEL, sockfd, nullptr);
            if (n < 0)
            {
                lg(Error, "epoll_ctl delete error, %s : %d", strerror(errno), errno);
            }
        }

        int Wait(struct epoll_event *rev, int maxevents, int timeout)
        {
            int n = ::epoll_wait(_epfd, rev, maxevents, timeout);
            return n;
        }

        ~Epoller()
        {
            if (_epfd >= 0)
                close(_epfd);
        }

    private:
        int _epfd;
    };

    // 服务端TcpServer的写法和之前的select和epoll类似,这里就不赘诉了,有兴趣的可以自己实现一下
}
  • 9
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Mac Rust io-uring是一种在Mac操作系统上使用Rust语言进行开发的io-uring库。 io-uring是Linux内核中的一个新特性,它为应用程序提供了一种高性能、高效率的异步I/O操作方式。它通过使用事件驱动和无锁技术,实现了在高并发环境下进行文件操作的优化。io-uring提供了更低的系统开销和更高的吞吐量,特别适用于需要大量I/O操作的应用程序。 虽然io-uring最初是为Linux内核设计的,但由于其高性能的特性,一些开发者试图将其移植到其他操作系统上。其中,Mac Rust io-uring就是一个在Mac操作系统上使用Rust语言实现io-uring的库。 使用Mac Rust io-uring,开发者可以在Mac环境下利用io-uring的特性来提高文件操作的性能。这对于需要进行大量I/O操作的应用程序来说,是一个很有价值的工具。例如,对于数据库、Web服务器或文件传输等应用,通过使用Mac Rust io-uring,可以显著提高其性能和吞吐量。 Mac Rust io-uring不仅提供了对io-uring的封装,还提供了一些更高级别的功能和接口,以方便开发者使用。开发者可以使用Mac Rust io-uring来实现一些高级的文件操作,例如批量读取或写入文件,提高数据处理的效率。 总之,Mac Rust io-uring是一个在Mac操作系统上使用Rust语言开发的io-uring库,它能够为开发者提供高性能的异步I/O操作方式,从而提高应用程序的性能和吞吐量。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值