细说Linux操作系统的网络I/O模型

【本文正在参加金石计划附加挑战赛------第四期命题】

前言

本篇文章给掘友们介绍 Linux 操作系统的几种网络 I/O 模型。

阻塞式I/O

  • 描述 : 在阻塞模式下,系统调用(如 readrecv)会阻塞进程,直到数据准备好。只有在数据准备好并被完全复制到用户空间后,调用才会返回。
  • 优点: 实现简单,代码逻辑直观。
  • 缺点: 阻塞会导致线程闲置等待,浪费资源,无法高效处理大量并发连接。
  • 使用场景: 小规模的网络应用或性能要求不高的程序。
go 复制代码
package main

import (
    "fmt"
    "net"
)

func main() {
    // 创建 TCP 监听器
    listener, err := net.Listen("tcp", ":8080")
    if err != nil {
        panic(err)
    }
    defer listener.Close()

    fmt.Println("Server listening on :8080")

    for {
        // 阻塞等待客户端连接
        conn, err := listener.Accept()
        if err != nil {
            fmt.Println("Connection error:", err)
            continue
        }

        // 处理连接(阻塞读取)
        go handleConnection(conn)
    }
}

func handleConnection(conn net.Conn) {
    defer conn.Close()
    buf := make([]byte, 1024)
    for {
        // 阻塞读取数据
        n, err := conn.Read(buf)
        if err != nil {
            fmt.Println("Read error:", err)
            return
        }
        fmt.Printf("Received: %s\n", string(buf[:n]))
        conn.Write([]byte("Echo: " + string(buf[:n])))
    }
}

非阻塞式I/O

  • 描述 : 非阻塞模式下,系统调用会立即返回,而不管数据是否准备好。如果数据尚未准备好,系统会返回错误(如 EAGAIN)。程序可以不断轮询检查数据是否可用。
  • 优点: 线程不会被阻塞,可以用于高性能的网络服务。
  • 缺点: 频繁轮询会导致高 CPU 占用。
  • 使用场景: 简单场景的非阻塞网络服务。
go 复制代码
package main

import (
    "fmt"
    "net"
    "syscall"
    "time"
)

func main() {
    listener, err := net.Listen("tcp", ":8080")
    if err != nil {
        panic(err)
    }
    defer listener.Close()

    fmt.Println("Server listening on :8080")

    for {
        conn, err := listener.Accept()
        if err != nil {
            fmt.Println("Connection error:", err)
            continue
        }

        // 设置非阻塞模式
        if tcpConn, ok := conn.(*net.TCPConn); ok {
            fd, _ := tcpConn.File()
            syscall.SetNonblock(int(fd.Fd()), true)
        }

        go handleNonBlocking(conn)
    }
}

func handleNonBlocking(conn net.Conn) {
    defer conn.Close()
    buf := make([]byte, 1024)

    for {
        // 非阻塞读取
        n, err := conn.Read(buf)
        if err != nil {
            time.Sleep(10 * time.Millisecond) // 避免忙轮询
            continue
        }
        fmt.Printf("Received: %s\n", string(buf[:n]))
        conn.Write([]byte("Echo: " + string(buf[:n])))
    }
}

多路复用I/O

  • 描述 : 使用系统调用(如 selectpollepoll)监听多个文件描述符。当某些文件描述符变为就绪状态时,调用返回,由程序处理对应的 I/O 操作。
  • 优点: 支持处理大量连接而无需为每个连接分配线程。
  • 缺点 : selectpoll 在大规模连接时效率较低,而 epoll 在这方面表现优异。
  • 使用场景 : 高并发的网络服务器,如 Nginx 使用的就是 epoll
go 复制代码
package main

import (
    "fmt"
    "net"
    "syscall"
)

func main() {
    ln, err := net.Listen("tcp", ":8080")
    if err != nil {
        panic(err)
    }
    defer ln.Close()

    listenerFd := int(ln.(*net.TCPListener).File().Fd())

    // 设置监听 socket 为非阻塞
    syscall.SetNonblock(listenerFd, true)

    pollFds := []syscall.PollFd{
        {Fd: int32(listenerFd), Events: syscall.POLLIN},
    }

    fmt.Println("Server listening on :8080")

    connections := make(map[int]net.Conn)

    for {
        // 等待文件描述符就绪
        _, err := syscall.Poll(pollFds, -1)
        if err != nil {
            panic(err)
        }

        // 检查监听 socket 是否有新连接
        if pollFds[0].Revents&syscall.POLLIN != 0 {
            conn, err := ln.Accept()
            if err == nil {
                fd := int(conn.(*net.TCPConn).File().Fd())
                syscall.SetNonblock(fd, true)
                connections[fd] = conn
                pollFds = append(pollFds, syscall.PollFd{Fd: int32(fd), Events: syscall.POLLIN})
            }
        }

        // 处理现有连接
        for i := 1; i < len(pollFds); i++ {
            fd := pollFds[i].Fd
            if pollFds[i].Revents&syscall.POLLIN != 0 {
                buf := make([]byte, 1024)
                n, err := connections[int(fd)].Read(buf)
                if err == nil {
                    fmt.Printf("Received: %s\n", string(buf[:n]))
                    connections[int(fd)].Write([]byte("Echo: " + string(buf[:n])))
                } else {
                    // 关闭连接并移除
                    connections[int(fd)].Close()
                    pollFds = append(pollFds[:i], pollFds[i+1:]...)
                    delete(connections, int(fd))
                    i--
                }
            }
        }
    }
}

信号驱动I/O

  • 描述 : 通过为文件描述符设置信号(如 SIGIO),当数据准备好时内核发送信号通知应用程序进行处理。
  • 优点: 无需主动轮询,资源占用较低。
  • 缺点: 实现复杂,信号处理可能引入额外的复杂性和延迟。
  • 使用场景: 适合对响应时间要求高的应用,但实际使用不多。
go 复制代码
package main

import (
    "fmt"
    "os"
    "os/signal"
    "syscall"
)

func main() {
    // 打开一个文件描述符(可以是 socket 文件描述符)
    fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_STREAM, syscall.IPPROTO_TCP)
    if err != nil {
        panic(err)
    }
    defer syscall.Close(fd)

    // 绑定地址
    addr := &syscall.SockaddrInet4{Port: 8080}
    copy(addr.Addr[:], []byte{127, 0, 0, 1})
    if err := syscall.Bind(fd, addr); err != nil {
        panic(err)
    }

    // 设置非阻塞模式
    if err := syscall.SetNonblock(fd, true); err != nil {
        panic(err)
    }

    // 设置信号驱动模式
    if err := syscall.FcntlInt(fd, syscall.F_SETFL, syscall.O_ASYNC); err != nil {
        panic(err)
    }

    // 设置当前进程为信号目标
    if err := syscall.FcntlInt(fd, syscall.F_SETOWN, syscall.Getpid()); err != nil {
        panic(err)
    }

    // 监听
    if err := syscall.Listen(fd, 10); err != nil {
        panic(err)
    }

    // 捕获 SIGIO 信号
    sigCh := make(chan os.Signal, 1)
    signal.Notify(sigCh, syscall.SIGIO)

    fmt.Println("Server listening on :8080")
    for {
        select {
        case sig := <-sigCh:
            if sig == syscall.SIGIO {
                fmt.Println("SIGIO received: Ready for I/O")
                connFd, _, err := syscall.Accept(fd)
                if err == nil {
                    fmt.Println("Connection accepted:", connFd)
                    go handleConnection(connFd)
                } else {
                        fmt.Println("Accept error:", err)
                }
            }
        }
    }
}

func handleConnection(connFd int) {
    defer syscall.Close(connFd)
    buf := make([]byte, 1024)
    for {
        n, err := syscall.Read(connFd, buf)
        if err != nil {
            fmt.Println("Read error:", err)
            return
        }
        fmt.Printf("Received: %s\n", string(buf[:n]))
        syscall.Write(connFd, []byte("Echo: "+string(buf[:n])))
    }
}

异步I/O

  • 描述 : 使用异步系统调用(如 aio_read),内核会在后台处理 I/O。当 I/O 操作完成后,通过回调或信号通知应用程序。
  • 优点: 应用程序完全非阻塞,效率高。
  • 缺点: 实现复杂,Linux 的异步 I/O 支持并不成熟,使用较少。
  • 使用场景: 需要极高性能的系统中。
go 复制代码
package main

import (
    "fmt"
    "net"
)

func main() {
    listener, err := net.Listen("tcp", ":8080")
    if err != nil {
        panic(err)
    }
    defer listener.Close()

    fmt.Println("Server listening on :8080")

    for {
        conn, err := listener.Accept()
        if err != nil {
            fmt.Println("Connection error:", err)
            continue
        }
        go handleAsync(conn)
    }
}

func handleAsync(conn net.Conn) {
    defer conn.Close()
    buf := make([]byte, 1024)

    // 模拟异步读写
    readCh := make(chan []byte)
    go func() {
        for {
            n, err := conn.Read(buf)
            if err != nil {
                close(readCh)
                return
            }
            readCh <- buf[:n]
        }
    }()

    for data := range readCh {
        fmt.Printf("Received: %s\n", string(data))
        conn.Write([]byte("Echo: " + string(data)))
    }
}

直接I/O

  • 描述 : 跳过内核缓存,直接在用户空间和设备之间传输数据。通过设置特定标志(如 O_DIRECT)实现。
  • 优点: 减少数据拷贝,适合 I/O 密集型场景。
  • 缺点: 实现复杂,对应用程序要求较高。
  • 使用场景: 数据库、存储服务等高性能系统。
go 复制代码
package main

import (
    "fmt"
    "os"
    "syscall"
)

func main() {
    // 打开文件,使用 O_DIRECT 标志
    file, err := syscall.Open("/tmp/testfile", syscall.O_RDWR|syscall.O_DIRECT|syscall.O_CREAT, 0666)
    if err != nil {
        panic(err)
    }
    defer syscall.Close(file)

    // 数据必须是对齐的(例如 512 字节对齐)
    buf := make([]byte, 512)
    copy(buf, []byte("Hello, Direct I/O!"))

    // 写入文件
    n, err := syscall.Write(file, buf)
    if err != nil {
        panic(err)
    }
    fmt.Printf("Written %d bytes\n", n)

    // 将文件指针移回开头
    if _, err := syscall.Seek(file, 0, 0); err != nil {
        panic(err)
    }

    // 读取文件
    readBuf := make([]byte, 512)
    n, err = syscall.Read(file, readBuf)
    if err != nil {
        panic(err)
    }
    fmt.Printf("Read %d bytes: %s\n", n, string(readBuf[:n]))
}

总结

  • 单线程或少量连接 : 使用 阻塞 I/O非阻塞 I/O
  • 高并发服务器 : 首选 I/O 多路复用 (推荐 epoll)。
  • 对性能极高要求 : 尝试 异步 I/O直接 I/O

每种模型的选择依赖于具体的应用场景和性能需求。

相关推荐
海绵波波1074 小时前
flask后端开发(10):问答平台项目结构搭建
后端·python·flask
007php0075 小时前
linux服务器上CentOS的yum和Ubuntu包管理工具apt区别与使用实战
linux·运维·服务器·ubuntu·centos·php·ai编程
网络风云5 小时前
【魅力golang】之-反射
开发语言·后端·golang
Q_19284999065 小时前
基于Spring Boot的电影售票系统
java·spring boot·后端
djykkkkkk5 小时前
ubuntu编译遇到的问题
linux·运维·ubuntu
qq_429856575 小时前
linux 查看服务是否开机自启动
linux·运维·服务器
百事可乐☆6 小时前
全局webSocket 单个页面进行监听并移除单页面监听
网络·websocket·网络协议
运维&陈同学6 小时前
【Kibana01】企业级日志分析系统ELK之Kibana的安装与介绍
运维·后端·elk·elasticsearch·云原生·自动化·kibana·日志收集
7yewh7 小时前
Linux驱动开发 IIC I2C驱动 编写APP访问EEPROM AT24C02
linux·arm开发·驱动开发·嵌入式硬件·嵌入式
dessler7 小时前
Docker-Dockerfile讲解(三)
linux·运维·docker