Linux网络编程常见I/O模型总结

【摘要】

Liunx网络编程中,经常会需要根据业务的不同,和性能要求的不同,来选择I/O处理方式,本文介绍几种常见的五种I/O模型,并重点讲解最常见最实用的I/O多路复用模型。

【关键词】

Linux,网络编程,I/O模型

【类别】

Linux编程类

一 五种I/O模型

下面我们简单的介绍一个各种I/O 操作模式。在Linux/UNIX 下,有下面这五种I/O 操作方式:

  • 阻塞I/O
  • 非阻塞I/O
  • I/O 多路复用
  • 信号驱动I/O(SIGIO)
  • 异步I/O

一般来说,程序进行输入操作有两步:

  1. 等待有数据可以读
  2. 将数据从系统内核中拷贝到程序的数据区。

对于一个对套接字的输入操作,第一步一般来说是等待数据从网络上传到本地。当数

据包到达的时候,数据将会从网络层拷贝到内核的缓存中;第二步是从内核中把数据拷贝到程序的数据区中。

 

 

1.1 阻塞IO模式

阻塞I/O 模式是最普遍使用的I/O 模式。大部分程序使用的都是阻塞模式的I/O 。缺

省的,一个套接字建立后所处于的模式就是阻塞I/O 模式。

对于一个UDP 套接字来说,数据就绪的标志比较简单:

  • 已经收到了一整个数据报
  •  没有收到。

一个进程调用recvfrom ,然后系统调用并不返回知道有数据报到达本地系统,然后系统将数据拷贝到进程的缓存中。

我们称这个进程在调用recvfrom 一直到从recvfrom 返回这段时间是阻塞的。当recvfrom

正常返回时,我们的进程继续它的操作。

 

图1-1 阻塞模式


1.2 非阻塞I/O模型

将socket变为非阻塞方法:

  int flags = fcntl(sockfd, F_GETFL, 0);

  fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);

 

当我们将一个套接字设置为非阻塞模式,我们相当于告诉了系统内核:“当我请求的

I/O 操作不能够马上完成,你想让我的进程进行休眠等待的时候,不要这么做,请马上返

回一个错误给我。”

我们可以参照图1-2 来描述非阻塞模式I/O 。

我们开始对recvfrom 的三次调用,因为系统还没有接收到网络数据,所以内核马上返

回一个EWOULDBLOCK的错误。第四次我们调用recvfrom 函数,一个数据报已经到达了,

内核将它拷贝到我们的应用程序的缓冲区中,然后recvfrom 正常返回,我们就可以对接收

到的数据进行处理了。

当一个应用程序使用了非阻塞模式的套接字,它需要使用一个循环来不听的测试是否一个文件描述符有数据可读(称做polling)。应用程序不停的polling 内核来检查是否I/O操作已经就绪。这将是一个极浪费CPU 资源的操作。这种模式使用中不是很普遍。

 

图1-2 非阻塞模式

 

1.3 I/O多路复用

                  在使用I/O 多路技术的时候,我们调用select()函数和poll()函数,在调用它们的时候阻塞,而不是我们来调用recvfrom(或recv)的时候阻塞。图1-3说明了它的工作方式。当我们调用select 函数阻塞的时候,select 函数等待数据报套接字进入读就绪状态。当select 函数返回的时候,也就是套接字可以读取数据的时候。这时候我们就可以调用recvfrom函数来将数据拷贝到我们的程序缓冲区中。

                   和阻塞模式相比较,select()和poll()并没有什么高级的地方,而且,在阻塞模式下只需要调用一个函数:读取或发送,在使用了多路复用技术后,我们需要调用两个函数了:先调用select()函数或poll()函数,然后才能进行真正的读写。

多路复用的高级之处在于,它能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()函数就可以返回。

 

图1-3 I/O多路复用

 

多路复用的方式是真正实用的服务器程序,非多路复用的网络程序只能作为学习或着陪测的角色。下面介绍一下多路复用函数:select/poll/epoll/port。select-->poll-->epoll/port的演化路线:

一、select模型
select原型:

int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

其中参数n表示监控的所有fd中最大值+1。和select模型紧密结合的四个宏,:

FD_CLR(int fd, fd_set *set);
  FD_ISSET(int fd, fd_set *set);
  FD_SET(int fd, fd_set *set);
  FD_ZERO(fd_set *set);

理解select模型的关键在于理解fd_set,为说明方便,取fd_set长度为1字节,fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd。
(1)执行fd_set set; FD_ZERO(&set);则set用位表示是0000,0000。
(2)若fd=5,执行FD_SET(fd,&set);后set变为0001,0000(第5位置为1)
(3)若再加入fd=2,fd=1,则set变为0001,0011
(4)执行select(6,&set,0,0,0)阻塞等待
(5)若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。注意:没有事件发生的fd=5被清空。

基于上面的讨论,可以得出select模型的特点:
(1) 可监控的文件描述符个数取决与sizeof(fd_set)的值。由于fd_set类型的长度在不同平台上不同。
(2)将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd,一是用于再select返回后,array作为源数据和fd_set进行FD_ISSET判断。二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数。
(3)可见select模型必须在select前循环array(加fd,取maxfd),select返回后循环array(FD_ISSET判断是否有事件发生)。

下面给一个伪码说明基本select模型的服务器模型:

· array[slect_len];
nSock=0;
array[nSock++]=listen_fd;(之前listen port已绑定并listen)
maxfd=listen_fd;
 while{

·   FD_ZERO(&set);

·    foreach (fd in array) 

·   {

·        fd大于maxfd,则maxfd=fd

·        FD_SET(fd,&set)

·    }

·    res=select(maxfd+1,&set,0,0,0);

·   //检测是否是一个新连接过来

·    if(FD_ISSET(listen_fd,&set))

·    {

·        newfd=accept(listen_fd);

·        array[nsock++]=newfd;
            if(--res<=0) continue

·    }

·    foreach 下标1开始 (fd in array) 

·   {

·        if(FD_ISSET(fd,&set))

·           执行读等相关操作

·           如果错误或者关闭,则要删除该fd,将array中相应位置和最后一个元素互换就好,nsock减一
             if(--res<=0) continue

·   }

· }

二、poll模型
poll原型:

· int poll(struct pollfd *ufds, unsigned int nfds, int timeout);
struct pollfd {

·                      int fd;           /* file descriptor */
                     short events;     /* requested events */

·                short revents;    /* returned events */

·              };

和select相比,两大改进:
(1)不再有fd个数的上限限制,可以将参数ufds想象成栈低指针,nfds是栈中元素个数,该栈可以无限制增长
(2)引入pollfd结构,将fd信息、需要监控的事件、返回的事件分开保存,则poll返回后不会丢失fd信息和需要监控的事件信息,也就省略了select模型中前面的循环操作,返回后的循环仍然不可避免。另每次poll阻塞操作都会自动把上次的revents清空。

(3)另外,poll() 函数不会受到socket描述符上的O_NDELAY标记和O_NONBLOCK标记的影响和制约,也就是说,不管socket是阻塞的还是非阻塞的,poll()函数都不会收到影响;而select()函数则不同,select()函数会受到O_NDELAY标记和O_NONBLOCK标记的影响,如果socket是阻塞的socket,则调用select()跟不调用select()时的效果是一样的,socket仍然是阻塞式TCP通讯,相反,如果socket是非阻塞的socket,那么调用select()时就可以实现非阻塞式TCP通讯。
poll的服务器模型伪码:

· struct pollfd fds[POLL_LEN];
unsigned int nfds=0;
fds[0].fd=server_sockfd;
fds[0].events=POLLIN|POLLPRI;
nfds++;

while{

·  res=poll(fds,nfds,-1);

·  if(fds[0].revents&(POLLIN|POLLPRI))

· {

·  执行accept并加入fds中,if(--res<=0)continue

· }

·   循环之后的fds,

· if(fds[i].revents&(POLLIN|POLLERR ))

· {操作略if(--res<=0)continue}
}

注意select和poll中res的检测,可有效减少循环的次数,这也是大量死连接存在时,select和poll性能下降厉害的原因。

 

 

1.4信号驱动I/O模式

我们可以使用信号,让内核在文件描述符就绪的时候使用SIGIO 信号来通知我们。我

们将这种模式称为信号驱动I/O 模式。

使用这种模式,我们首先需要允许套接字使用信号驱动I/O ,还要安装一个SIGIO 的

回调处理函数。在这种模式下,系统调用将会立即返回,然后我们的程序可以继续做其他的事情。当数据就绪的时候,系统会向我们的进程发送一个SIGIO 信号。这样我们就可以在SIGIO信号的处理函数中进行I/O 操作(或是我们在函数中通知主函数有数据可读)。

对于信号驱动I/O 模式,它的先进之处在于它在等待数据的时候不会阻塞,程序可以做自己的事情。当有数据到达的时候,系统内核会向程序发送一个SIGIO 信号进行通知,这样我们的程序就可以获得更大的灵活性,因为我们不必为等待数据进行额外的编码。

 

图1-4 信号驱动I/O模式

 

信号I/O 可以使内核在某个文件描述符发生改变的时候发信号通知我们的程序。异步

I/O 可以提高我们程序进行I/O 读写的效率。通过使用它,当我们的程序进行I/O 操作的时候,内核可以在初始化I/O 操作后立即返回,在进行I/O 操作的同时,我们的程序可以做自己的事情,直到I/O 操作结束,系统内核给我们的程序发消息通知。

基于Berkeley 接口的Socket 信号驱动I/O 使用信号SIGIO。有的系统SIGPOLL 信号,它也是相当于SIGIO 的。

为了在一个套接字上使用信号驱动I/O 操作,下面这三步是所必须的。

(1)一个和SIGIO 信号的处理函数必须设定。

(2)套接字的拥有者必须被设定。一般来说是使用fcntl 函数的F_SETOWN 参数来

进行设定拥有者。

(3)套接字必须被允许使用异步I/O。一般是通过调用fcntl 函数的F_SETFL 命令,

O_ASYNC 为参数来实现。

 

虽然设定套接字为异步I/O 非常简单,但是使用起来困难的部分是怎样在程序中断定

产生SIGIO 信号发送给套接字属主的时候,程序处在什么状态。

1.UDP 套接字的SIGIO 信号

在UDP 协议上使用异步I/O 非常简单.这个信号将会在这个时候产生:

l 套接字收到了一个数据报的数据包。

l 套接字发生了异步错误。

当我们在使用UDP 套接字异步I/O 的时候,我们使用recvfrom()函数来读取数据报数据或是异步I/O 错误信息。

2.TCP 套接字的SIGIO 信号

不幸的是,异步I/O 几乎对TCP 套接字而言没有什么作用。因为对于一个TCP 套接

字来说, SIGIO 信号发生的几率太高了,所以SIGIO 信号并不能告诉我们究竟发生了什

么事情。在TCP 连接中, SIGIO 信号将会在这个时候产生:

l 在一个监听某个端口的套接字上成功的建立了一个新连接。

l 一个断线的请求被成功的初始化。

l 一个断线的请求成功的结束。

l 套接字的某一个通道(发送通道或是接收通道)被关闭。

l 套接字接收到新数据。

l 套接字将数据发送出去。

l 发生了一个异步I/O 的错误。

 

 

 

1.5异步I/O模式

当我们运行在异步I/O 模式下时,我们如果想进行I/O 操作,只需要告诉内核我们要进行I/O 操作,然后内核会马上返回。具体的I/O 和数据的拷贝全部由内核来完成,我们的程序可以继续向下执行。当内核完成所有的I/O 操作和数据拷贝后,内核将通知我们的程序。

异步I/O 和 信号驱动I/O 的区别是:

l 信号驱动I/O 模式下,内核在操作可以被操作的时候通知给我们的应用程序发送

SIGIO 消息。

l 异步I/O 模式下,内核在所有的操作都已经被内核操作结束之后才会通知我们的

应用程序。

如下图,当我们进行一个IO 操作的时候,我们传递给内核我们的文件描述符,我们

的缓存区指针和缓存区的大小,一个偏移量offset,以及在内核结束所有操作后和我们联

系的方法。这种调用也是立即返回的,我们的程序不需要阻塞住来等待数据的就绪。我们可以要求系统内核在所有的操作结束后(包括从网络上读取信息,然后拷贝到我们提供给内核的缓存区中)给我们发一个消息。

 

图1-5  异步I/O模式

 

二、实践情况

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Go语言(也称为Golang)是由Google开发的一种静态强类型、编译型的编程语言。它旨在成为一门简单、高效、安全和并发的编程语言,特别适用于构建高性能的服务器和分布式系统。以下是Go语言的一些主要特点和优势: 简洁性:Go语言的语法简单直观,易于学习和使用。它避免了复杂的语法特性,如继承、重载等,转而采用组合和接口来实现代码的复用和扩展。 高性能:Go语言具有出色的性能,可以媲美C和C++。它使用静态类型系统和编译型语言的优势,能够生成高效的机器码。 并发性:Go语言内置了对并发的支持,通过轻量级的goroutine和channel机制,可以轻松实现并发编程。这使得Go语言在构建高性能的服务器和分布式系统时具有天然的优势。 安全性:Go语言具有强大的类型系统和内存管理机制,能够减少运行时错误和内存泄漏等问题。它还支持编译时检查,可以在编译阶段就发现潜在的问题。 标准库:Go语言的标准库非常丰富,包含了大量的实用功能和工具,如网络编程、文件操作、加密解密等。这使得开发者可以更加专注于业务逻辑的实现,而无需花费太多时间在底层功能的实现上。 跨平台:Go语言支持多种操作系统和平台,包括Windows、Linux、macOS等。它使用统一的构建系统(如Go Modules),可以轻松地跨平台编译和运行代码。 开源和社区支持:Go语言是开源的,具有庞大的社区支持和丰富的资源。开发者可以通过社区获取帮助、分享经验和学习资料。 总之,Go语言是一种简单、高效、安全、并发的编程语言,特别适用于构建高性能的服务器和分布式系统。如果你正在寻找一种易于学习和使用的编程语言,并且需要处理大量的并发请求和数据,那么Go语言可能是一个不错的选择。
### 回答1: Linux多线程服务端编程是指使用Muduo C网络库在Linux操作系统中进行多线程的服务端编程。Muduo C网络库是一个基于事件驱动的网络库,采用了Reactor模式,并且在底层使用了epoll来实现高效的I/O复用。 使用Muduo C网络库进行多线程服务端编程有以下几个步骤: 1. 引入Muduo C网络库:首先需要下载并引入Muduo C网络库的源代码,然后在编写代码时包含相应的头文件。 2. 创建并初始化EventLoop:首先需要创建一个EventLoop对象,它用于接收和分发事件。通过初始化函数进行初始化,并在主线程中调用它的loop()函数来运行事件循环。 3. 创建TcpServer:然后创建一个TcpServer对象,它负责监听客户端的连接,并管理多个TcpConnection对象。通过设置回调函数,可以在特定事件发生时处理相应的逻辑。 4. 创建多个EventLoopThread:为了提高并发性能,可以创建多个EventLoopThread对象,每个对象负责一个EventLoop,从而实现多线程处理客户端的连接和请求。 5. 处理事件:在回调函数中处理特定事件,例如有新的连接到来时会调用onConnection()函数,可以在该函数中进行一些初始化操作。当有数据到来时会调用onMessage()函数,可以在该函数中处理接收和发送数据的逻辑。 6. 运行服务端:在主线程中调用TcpServer的start()函数来运行服务端,等待客户端的连接和请求。 总的来说,使用Muduo C网络库进行Linux多线程服务端编程可以更好地利用多核处理器的性能优势。每个线程负责处理特定事件,通过事件驱动模式实现高效的网络编程。这样可以提高服务器的并发能力,提高系统的整体性能。 ### 回答2: Linux多线程服务端编程是指在Linux平台上使用多线程的方式来编写网络服务器程序。而使用muduo C网络库是一种常见的方法,它提供了高效的网络编程接口,可以简化多线程服务器的开发过程。 muduo C网络库基于Reactor模式,利用多线程实现了高并发的网络通信。在使用muduo C进行多线程服务端编程时,我们可以按照以下步骤进行: 1. 引入muduo库:首先需要导入muduo C网络库的头文件,并链接对应的库文件,以供程序调用。 2. 创建线程池:利用muduo C中的ThreadPool类创建一个线程池,用于管理和调度处理网络请求的多个线程。 3. 创建TcpServer对象:使用muduo C中的TcpServer类创建一个服务器对象,监听指定的端口,并设置好Acceptor、TcpConnectionCallback等相关回调函数。 4. 定义业务逻辑:根据具体的业务需求,编写处理网络请求的业务逻辑代码,如接收客户端的请求、处理请求、发送响应等。 5. 注册业务逻辑函数:将定义好的业务逻辑函数注册到TcpServer对象中,以便在处理网络请求时调用。 6. 启动服务器:调用TcpServer对象的start函数,启动服务器,开始监听端口并接收客户端请求。 7. 处理网络请求:当有客户端连接到服务器时,muduo C会自动分配一个线程去处理该连接,执行注册的业务逻辑函数来处理网络请求。 8. 释放资源:在程序结束时,需要调用相应的函数来释放使用的资源,如关闭服务器、销毁线程池等。 通过使用muduo C网络库,我们可以简化多线程服务端编程的过程,提高服务器的并发处理能力。因为muduo C网络库已经实现了底层的网络通信细节,我们只需要专注于编写业务逻辑代码,从而减少开发的工作量。同时,muduo C的多线程模型可以有效地提高服务器的并发性能,满足高并发网络服务的需求。 ### 回答3: Linux多线程服务端编程是指在Linux操作系统上开发多线程的服务器应用程序。使用muduo C网络库有助于简化开发过程,提供高效的网络通信能力。 muduo C网络库是一个基于Reactor模式的网络库,适用于C++语言,由Douglas Schmidt的ACE网络库演化而来。它提供了高度并发的网络编程能力,封装了许多底层细节,使得开发者能够更加专注于业务逻辑的实现。 在开发过程中,首先需要创建一个muduo C的EventLoop对象来管理事件循环。然后,可以利用TcpServer类来创建服务器并监听指定的端口。当有新的客户端请求到达时,muduo C会自动调用用户定义的回调函数处理请求。 在处理请求时,可以使用muduo C提供的ThreadPool来创建多个工作线程。这些工作线程将负责处理具体的业务逻辑。通过将工作任务分配给不同的线程,可以充分利用多核服务器的计算资源,提高服务器的处理能力。 在具体的业务逻辑中,可以使用muduo C提供的Buffer类来处理网络数据。Buffer类提供了高效的数据读写操作,可以方便地进行数据解析与封装。 此外,muduo C还提供了TimerQueue类来处理定时任务,可以用于实现定时事件的调度与管理。这对于一些需要定期执行的任务非常有用,如心跳检测、定时备份等。 总之,使用muduo C网络库可以简化Linux多线程服务端编程的开发过程,提供高效的并发能力。通过合理地利用多线程和其他的相关组件,可以实现高性能、稳定可靠的网络服务端应用程序。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值