操作系统 网络编程相关知识

注意:本文章整理于GPT,仅用于复习。

1、讲一下select,poll , epoll区别?epoll实现机制,为什么返回的都是就绪的,为什么select不能做到?epoll水平触发和边缘触发?epoll的边缘触发,触发没有处理,后面怎么办(事件是不是都在队列不处理了?)?如果后面又处理这个事件,会丢掉吗还是一直都在队列里面了?

selectpollepoll 的区别

select
  • 功能:监视多个文件描述符,等待其中的一个或多个变为可读、可写或有异常。
  • 限制:受文件描述符数量的限制,通常为 1024。
  • 效率:每次调用都需要重新设置文件描述符集合并线性扫描,效率较低。
  • 返回机制:返回文件描述符集合,需要遍历集合找到就绪的文件描述符。
poll
  • 功能:类似于 select,但没有文件描述符数量限制。
  • 限制:监视的文件描述符数量理论上没有限制,但受系统内存大小影响。
  • 效率:每次调用都需要重新设置文件描述符集合并线性扫描,但相较 select 稍微高效。
  • 返回机制:返回包含就绪状态的 pollfd 结构体数组,需要遍历数组找到就绪的文件描述符。
epoll
  • 功能:高效地监视大量文件描述符,仅返回就绪的文件描述符。
  • 限制:在 Linux 系统上,没有文件描述符数量限制。
  • 效率:基于事件驱动,只有状态变化时才会触发,不需要线性扫描。
  • 返回机制:直接返回就绪的文件描述符,避免了大量无效的检查。

epoll 的实现机制

epoll 通过内核空间维护一个红黑树来存储所有需要监视的文件描述符,以及一个双向链表来存储就绪的文件描述符。通过这种结构,epoll 可以高效地添加、删除和监视文件描述符。

  • 红黑树:存储所有监视的文件描述符,支持快速增删操作。
  • 就绪列表:存储就绪的文件描述符,当文件描述符状态发生变化时,会被加入到就绪列表中。

为什么 epoll 返回的都是就绪的?

epoll 只返回那些状态已经发生变化的文件描述符(即就绪的文件描述符),因为 epoll 内核实现中,会在文件描述符状态变化时,将其加入就绪列表,并在调用 epoll_wait 时,直接返回就绪列表中的文件描述符。因此,只返回就绪的文件描述符,避免了大量无效的检查和返回。

为什么 select 做不到?

select 每次调用都需要重新设置和扫描整个文件描述符集合,检查每个文件描述符的状态。由于 select 没有像 epoll 那样的事件驱动机制,因此每次都必须遍历所有监视的文件描述符,效率较低。

epoll 的水平触发(LT)和边缘触发(ET)

  • 水平触发(Level Triggered, LT)

    • 默认模式。
    • 只要文件描述符处于就绪状态,epoll_wait 会一直返回。
    • 简单易用,但可能会导致重复触发。
  • 边缘触发(Edge Triggered, ET)

    • 需要显式指定。
    • 只有在文件描述符状态发生变化时才会触发一次,之后即使文件描述符保持就绪,也不会再次触发,直到状态再次变化。
    • 高效,但需要确保每次处理所有数据,避免丢失事件。

边缘触发未处理事件的处理

边缘触发,事件未处理,后续怎么办?

在边缘触发模式下,如果一个事件未处理完成(例如,数据未全部读取),后续不会再次触发同一事件,直到状态再次变化。因此,必须确保每次处理所有数据,避免丢失事件。

事件是否都在队列中不处理了?

未处理的事件不会继续留在就绪队列中,因为边缘触发模式下,只有状态变化才会触发事件。如果事件未完全处理,必须手动再次检查文件描述符状态(例如,通过非阻塞读取或写入)。

后续处理事件会丢失吗?

如果一个事件在边缘触发模式下未完全处理,那么当再次调用 epoll_wait 时,事件不会再次返回,除非文件描述符状态再次变化。因此,必须确保每次处理所有数据,以避免事件丢失。

示例代码
#include <sys/epoll.h>
#include <fcntl.h>
#include <unistd.h>
#include <iostream>
#include <vector>

int main() {
    int epfd = epoll_create1(0);
    if (epfd == -1) {
        perror("epoll_create1");
        return -1;
    }

    int fd = open("test.txt", O_RDONLY | O_NONBLOCK);
    if (fd == -1) {
        perror("open");
        return -1;
    }

    epoll_event event;
    event.events = EPOLLIN | EPOLLET; // 边缘触发
    event.data.fd = fd;
    if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event) == -1) {
        perror("epoll_ctl");
        return -1;
    }

    std::vector<epoll_event> events(10);
    while (true) {
        int n = epoll_wait(epfd, events.data(), events.size(), -1);
        if (n == -1) {
            perror("epoll_wait");
            break;
        }
        for (int i = 0; i < n; ++i) {
            if (events[i].events & EPOLLIN) {
                char buf[1024];
                ssize_t count;
                while ((count = read(fd, buf, sizeof(buf))) > 0) {
                    // 处理读取的数据
                    std::cout.write(buf, count);
                }
                if (count == -1 && errno != EAGAIN) {
                    perror("read");
                    break;
                }
            }
        }
    }

    close(fd);
    close(epfd);
    return 0;
}

在此代码中,边缘触发模式确保只有在文件描述符状态变化时才会触发事件。确保每次读取所有数据,避免事件丢失。

2、socket.accept()函数对应着TCP三次握手中哪一次握手,哪个函数代表连接建立完毕,四次挥手的过程?一台机器最多可以建立多少个tcp连接,client端,server端,超过了怎么办?socket编程服务端和客户端调用函数的流程?网络编程如果不用io多路复用该用什么?

TCP三次握手与四次挥手

TCP三次握手
  • 第一次握手:客户端发送一个SYN包,表明希望建立连接,并且随机产生一个序列号。
  • 第二次握手:服务器接收到SYN包后,回复一个SYN-ACK包,表示同意连接,并且自己也随机产生一个序列号。
  • 第三次握手:客户端收到SYN-ACK包后,再回复一个ACK包,表示确认连接。
TCP四次挥手
  • 第一次挥手:客户端发送一个FIN包,表示请求关闭连接。
  • 第二次挥手:服务器接收到FIN包后,回复一个ACK包,表示同意关闭连接。
  • 第三次挥手:服务器发送一个FIN包,表示服务器也请求关闭连接。
  • 第四次挥手:客户端接收到FIN包后,回复一个ACK包,表示确认关闭连接。
socket.accept()函数
  • 对应握手socket.accept() 函数对应三次握手中的第三次握手。它在第三次握手完成后返回,表明连接已经建立。
  • 连接建立完毕的函数socket.accept() 返回时,连接已经建立完毕。返回的值是一个新的套接字对象,用于与客户端进行通信。

TCP连接数限制

  • 服务器端:理论上,一个服务器可以处理65535个并发连接(端口号范围)。但实际上,受限于系统资源(如内存、文件描述符数等),并发连接数会更少。可以通过调整系统参数(如ulimit)增加允许的文件描述符数。

  • 客户端:每个客户端的单个源IP地址和端口号组合成一个唯一的套接字,每个IP地址可以有多个连接,但同样受限于端口号范围和系统资源。

  • 超过连接数怎么办

    • 负载均衡:使用负载均衡器分担负载到多个服务器上。
    • 水平扩展:增加更多的服务器来处理更多的连接。
    • 优化资源使用:通过合理的资源管理和连接池技术,减少资源消耗。

Socket编程服务端和客户端调用函数流程

服务端流程
  1. 创建套接字socket()
  2. 绑定地址bind()
  3. 监听连接listen()
  4. 接受连接accept()
  5. 处理连接:读写数据,recv()send()read()write()
  6. 关闭连接close()
客户端流程
  1. 创建套接字socket()
  2. 连接服务器connect()
  3. 通信:读写数据,recv()send()read()write()
  4. 关闭连接close()

不用IO多路复用的替代方案

  • 多线程:为每个连接创建一个线程,适合处理少量并发连接,但线程数目多时开销大。
  • 多进程:为每个连接创建一个进程,适合在高隔离要求的场景使用,但进程切换开销大。
  • 异步IO:使用非阻塞IO和事件通知机制(如Linux的aio,Windows的IOCP),在单个线程内处理多个连接。
  • 协程:使用轻量级线程(如Python的asyncio,Go语言的goroutine),提供类似多线程的编程模型,但资源消耗更低。

小结

  • socket.accept() 对应TCP三次握手的第三次握手。
  • 连接建立完毕的标志是 socket.accept() 返回。
  • TCP四次挥手包含了两个FIN包和两个ACK包,确保双方都能优雅地关闭连接。
  • 一台机器的TCP连接数受系统资源限制,通过负载均衡和优化资源管理可以扩展处理能力。
  • 不使用IO多路复用时,可以采用多线程、多进程、异步IO或协程来处理并发连接。

3、介绍五大 IO 模型?reactor 和 proactor 的区别?为什么要使用Reactor模式, 为什么不直接用多线程?epoll_wait 是怎么处理的和time_out?如果客户端崩溃服务器会怎么样?Eventloop是怎么实现的?一个线程会发生死锁吗(比如:多次尝试同一个锁、递归获取锁)?

五大 IO 模型

1. 阻塞 I/O (Blocking I/O)

在阻塞 I/O 模型中,系统调用(如 readrecv)会阻塞进程,直到数据就绪。这种模式简单易用,但效率低下,因为进程在等待数据时无法进行其他操作。

2. 非阻塞 I/O (Non-blocking I/O)

在非阻塞 I/O 模型中,系统调用不会阻塞进程,而是立即返回一个状态值。如果数据未就绪,进程可以继续执行其他操作,并在稍后再次检查数据状态。这种模式减少了阻塞时间,但需要不断轮询,增加了系统开销。

3. I/O 多路复用 (I/O Multiplexing)

I/O 多路复用使用系统调用(如 selectpollepoll)来同时监视多个文件描述符。一旦有文件描述符变为就绪状态,进程可以对其进行 I/O 操作。这种模式适用于处理大量并发连接。

4. 信号驱动 I/O (Signal-driven I/O)

在信号驱动 I/O 模型中,当文件描述符变为就绪状态时,内核会发送一个信号给进程。进程在接收到信号后,可以进行 I/O 操作。这种模式减少了轮询开销,但信号处理的实现和管理较为复杂。

5. 异步 I/O (Asynchronous I/O)

在异步 I/O 模型中,进程发起 I/O 操作后,立即返回,内核在操作完成后通知进程。进程在等待期间可以继续执行其他操作。这种模式最为高效,但实现复杂,要求操作系统提供良好的支持。

Reactor 和 Proactor 的区别

Reactor 模式
  • 原理:Reactor 模式由事件循环驱动,监听事件的发生(如 I/O 事件),并在事件发生时调用相应的事件处理器(回调函数)。
  • 流程
    1. 事件发生时,事件循环通知应用程序。
    2. 应用程序处理事件并执行 I/O 操作。
  • 特点:I/O 操作由应用程序执行,适用于同步 I/O。
Proactor 模式
  • 原理:Proactor 模式也是事件驱动,但 I/O 操作由操作系统或底层库完成,完成后通知应用程序。
  • 流程
    1. 应用程序发起 I/O 操作。
    2. 操作系统或底层库完成 I/O 操作后,通知应用程序。
    3. 应用程序处理已完成的操作。
  • 特点:I/O 操作由操作系统完成,适用于异步 I/O。

为什么要使用 Reactor 模式,为什么不直接用多线程?

  • 资源效率:Reactor 模式通过事件循环处理多个连接,避免了线程上下文切换和调度的开销,资源效率更高。
  • 编程简洁性:Reactor 模式简化了并发编程,避免了多线程中的竞态条件和死锁问题。
  • 可扩展性:Reactor 模式在高并发场景下具有更好的可扩展性,能够更高效地处理大量并发连接。

epoll_wait 是怎么处理的和 time_out

  • 处理epoll_wait 在调用时,会检查内核中的事件队列。如果有事件就绪,立即返回就绪的事件列表;如果没有事件就绪,则阻塞等待,直到有事件发生或超时时间到。
  • time_outtime_out 参数指定 epoll_wait 阻塞等待的最长时间(以毫秒为单位)。如果 time_out 为 -1,epoll_wait 会无限期阻塞;如果为 0,epoll_wait 会立即返回,即使没有事件就绪。

如果客户端崩溃服务器会怎么样?

  • TCP连接:如果客户端崩溃,服务器端的 TCP 连接可能会检测到连接的关闭(通过 FIN 包或 RST 包)。服务器可以通过处理 recv 返回 0 或处理异常来检测到这一情况。
  • 资源释放:服务器应当正确处理客户端断开连接的情况,释放相应的资源(如文件描述符和内存)。

EventLoop 是怎么实现的?

EventLoop 通常包含以下几个步骤:

  1. 初始化:创建并初始化事件循环对象(如 epoll 实例)。
  2. 注册事件:将需要监视的文件描述符和事件类型注册到事件循环中。
  3. 事件循环:循环等待事件发生(如调用 epoll_wait)。
  4. 事件处理:事件发生时,调用相应的事件处理器(回调函数)。
  5. 清理资源:事件循环结束后,清理所有资源。

示例代码(简化版):

#include <sys/epoll.h>
#include <unistd.h>
#include <vector>
#include <functional>
#include <iostream>

class EventLoop {
public:
    EventLoop() : epfd(epoll_create1(0)) {}

    ~EventLoop() {
        close(epfd);
    }

    void addEvent(int fd, uint32_t events, std::function<void()> callback) {
        epoll_event event;
        event.data.fd = fd;
        event.events = events;
        callbacks[fd] = callback;
        epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);
    }

    void run() {
        std::vector<epoll_event> events(10);
        while (true) {
            int n = epoll_wait(epfd, events.data(), events.size(), -1);
            for (int i = 0; i < n; ++i) {
                int fd = events[i].data.fd;
                if (callbacks.find(fd) != callbacks.end()) {
                    callbacks[fd]();
                }
            }
        }
    }

private:
    int epfd;
    std::unordered_map<int, std::function<void()>> callbacks;
};

int main() {
    EventLoop loop;
    loop.addEvent(STDIN_FILENO, EPOLLIN, []() {
        char buf[1024];
        ssize_t n = read(STDIN_FILENO, buf, sizeof(buf));
        if (n > 0) {
            std::cout << "Read: " << std::string(buf, n) << std::endl;
        }
    });
    loop.run();
    return 0;
}

一个线程会发生死锁吗?

一个线程可以在以下情况下发生死锁:

  • 递归锁:如果同一个线程多次尝试获取同一个非递归锁,会导致死锁。
  • 递归获取锁:如果使用递归锁(如 std::recursive_mutex),允许同一个线程多次获取同一个锁,不会导致死锁。

示例代码(递归锁):

#include <mutex>
#include <iostream>

std::recursive_mutex mtx;

void recursiveFunction(int n) {
    if (n <= 0) return;
    mtx.lock();
    std::cout << "Locked: " << n << std::endl;
    recursiveFunction(n - 1);
    mtx.unlock();
}

int main() {
    recursiveFunction(5);
    return 0;
}

总结

  • 五大 IO 模型:阻塞 I/O、非阻塞 I/O、I/O 多路复用、信号驱动 I/O 和异步 I/O。
  • Reactor 和 Proactor 的区别:Reactor 由应用程序处理 I/O 操作,Proactor 由操作系统处理 I/O 操作。
  • 为什么使用 Reactor 模式:资源效率高,编程简洁,可扩展性好。
  • epoll_wait 的处理和 time_out:检查内核事件队列,根据 time_out 参数决定阻塞等待时间。
  • 客户端崩溃的处理:服务器通过 recv 返回 0 或异常检测客户端断开,释放资源。
  • EventLoop 实现:初始化、注册事件、事件循环和事件处理。
  • 单线程死锁:递归锁允许同一个线程多次获取锁,避免死锁。

4、NIO、BIO、同步IO、异步IO,以及它们与IO多路复用的区别和联系?介绍一下字节序?网卡是干啥的,网卡收发数据是通过什么实现的? accept 这个用 ET 模式你怎么实现一次性建立完连接?我们写的服务能否拿到用户端的ip,为什么可以拿到?网络字节序是大端还是小端,本地字节序是大端还是小端?讲讲阻塞IO和非阻塞IO,讲讲有哪些常用实现?

NIO、BIO、同步 I/O、异步 I/O 及其与 I/O 多路复用的区别和联系

BIO(Blocking I/O)
  • 特点:阻塞 I/O,在进行 I/O 操作时,调用线程会被阻塞,直到操作完成。
  • 使用场景:适用于并发连接数较少的应用。
  • 实现方式:传统的 readwrite 调用。
NIO(Non-blocking I/O)
  • 特点:非阻塞 I/O,I/O 操作不会阻塞调用线程,而是立即返回状态。
  • 使用场景:适用于并发连接数较多的应用。
  • 实现方式:使用 selectpollepoll 等 I/O 多路复用机制。
同步 I/O
  • 特点:调用发起后,调用方等待 I/O 操作完成。线程会被阻塞,直至操作完成。
  • 实现方式:传统的阻塞 I/O、使用 I/O 多路复用的非阻塞 I/O 都是同步 I/O。
异步 I/O
  • 特点:调用发起后,调用方无需等待 I/O 操作完成,I/O 操作由操作系统或底层实现完成后通知调用方。
  • 使用场景:适用于高性能、高并发应用。
  • 实现方式:如 Linux 下的 aio 接口。
I/O 多路复用
  • 特点:通过一个系统调用(如 selectpollepoll)同时监控多个文件描述符的 I/O 事件。
  • 使用场景:适用于需要处理大量并发连接的场景。
  • 实现方式selectpollepoll

字节序

  • 大端字节序(Big Endian):高字节存储在低地址。
  • 小端字节序(Little Endian):低字节存储在低地址。
  • 网络字节序:大端字节序,确保不同系统间的数据传输一致性。

网卡(Network Interface Card, NIC)

  • 功能:提供计算机与网络之间的数据传输接口,实现数据的发送和接收。
  • 收发数据的实现:网卡硬件通过 DMA(直接内存访问)技术将数据直接传输到系统内存,并通过中断通知操作系统。

在 ET 模式下实现一次性建立完连接

使用边缘触发(ET)模式下,accept 的实现需要注意以下几点:

  1. 非阻塞模式:将监听套接字设为非阻塞模式。
  2. 循环处理:在事件触发时,使用循环来处理所有就绪的连接请求,直到没有新的连接为止。

示例代码:

#include <sys/epoll.h>
#include <fcntl.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <iostream>

int setNonBlocking(int fd) {
    int flags = fcntl(fd, F_GETFL, 0);
    return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}

int main() {
    int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    setNonBlocking(listen_fd);

    sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(8080);
    bind(listen_fd, (sockaddr*)&server_addr, sizeof(server_addr));
    listen(listen_fd, 5);

    int epfd = epoll_create1(0);
    epoll_event event;
    event.events = EPOLLIN | EPOLLET;
    event.data.fd = listen_fd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &event);

    while (true) {
        epoll_event events[10];
        int nfds = epoll_wait(epfd, events, 10, -1);
        for (int i = 0; i < nfds; ++i) {
            if (events[i].data.fd == listen_fd) {
                while (true) {
                    sockaddr_in client_addr;
                    socklen_t client_len = sizeof(client_addr);
                    int conn_fd = accept(listen_fd, (sockaddr*)&client_addr, &client_len);
                    if (conn_fd < 0) {
                        break;
                    }
                    setNonBlocking(conn_fd);
                    // 处理新的连接...
                }
            }
        }
    }

    close(listen_fd);
    close(epfd);
    return 0;
}

获取客户端 IP

  • 方法:使用 getpeername 函数获取连接的对端地址。
  • 原因:每个连接在建立后,内核会维护连接的相关信息,包括对端 IP 地址和端口。

示例代码:

sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
getpeername(conn_fd, (sockaddr*)&client_addr, &client_len);
char *client_ip = inet_ntoa(client_addr.sin_addr);
int client_port = ntohs(client_addr.sin_port);
std::cout << "Client IP: " << client_ip << ", Port: " << client_port << std::endl;

字节序

  • 网络字节序:大端字节序。
  • 本地字节序:取决于具体平台,Intel x86 架构是小端字节序。

阻塞 I/O 和非阻塞 I/O

阻塞 I/O
  • 特点:调用 I/O 函数时,调用线程被阻塞,直到 I/O 操作完成。
  • 实现:传统的 readwrite 调用。
非阻塞 I/O
  • 特点:调用 I/O 函数时,如果操作无法立即完成,立即返回错误,调用线程继续执行其他操作。
  • 实现:通过设置文件描述符的 O_NONBLOCK 标志。

常用实现

  • select:水平触发,适用于小规模文件描述符集合。
  • poll:水平触发,无文件描述符数量限制,但效率低下。
  • epoll:水平触发和边缘触发,高效处理大规模文件描述符集合。

总结

  • NIO、BIO:分别为非阻塞 I/O 和阻塞 I/O。
  • 同步 I/O、异步 I/O:同步 I/O 会阻塞调用线程,异步 I/O 立即返回。
  • I/O 多路复用:通过 selectpollepoll 实现。
  • 字节序:大端和小端字节序。
  • 网卡功能:实现数据收发,通过 DMA 传输数据。
  • ET 模式的 accept:需要非阻塞模式和循环处理。
  • 获取客户端 IP:通过 getpeername 函数。
  • 网络字节序和本地字节序:网络字节序为大端,本地字节序取决于平台。
  • 12
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值