首先介绍一下物联网系统的挂载硬件服务器开发

这次我们的服务器是自己开发,从socket 模块一直到顶层的应用模块,自己实现了普通的tcp 和http服务器。

Python socket 简介:
这是一个很简单的Python实现的socket服务器的代码:
#!/usr/bin/python
# -*- coding: utf-8 -*-
import socket
import sys
from thread import *

HOST = ''
PORT = 9000

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print 'Socket created'

#Bind socket to local host and port
try:
    s.bind((HOST,PORT))
except socket.error,msg:
    print 'Bind failed. Error code: %s, Message: %s'
    sys.exit()

print 'Socket bind complete'

#start listening on socket
s.listen(10)
print 'Socket now listening'

#Function for handling connections. This will be used to create threads
def clientthread(conn):
    #sending message to connected client
    conn.send('Welecome to the server.Type something and hit enter\n')

    #define loop so that function do not terminate and thread do not end.
    while True:
        #receiving from client
        data = conn.recv(1024)
        file = open("/sys/class/thermal/thermal_zone0/temp")
        temp = float(file.read()) / 1000   
        file.close() 
        reply = str(temp)
        print reply
        if not data:
            break
        conn.sendall(reply)
    conn.close()

while 1:
    conn,addr = s.accept()
    print 'Connected with %s:%s'%(addr[0],str(addr[1]))
    start_new_thread(clientthread,(conn,))

s.close()

当然了我们的服务器要是用这个的话是无法处理太多连接的所以我们需要在我们的服务器上用到 非阻塞,io复用等概念, 我们自制服务器是一个异步非阻塞的服务器, 那么来介绍几个概念:

阻塞:

阻塞是个什么概念呢?比如某个时候你在等快递,但是你不知道快递什么时候过来,而且你没有别的事可以干(或者说接下来的事要等快递来了才能做);那么你可以去睡觉了,因为你知道快递把货送来时一定会给你打个电话(假定一定能叫醒你)。

非阻塞忙轮询:

接着上面等快递的例子,如果用忙轮询的方法,那么你需要知道快递员的手机号,然后每分钟给他挂个电话:“你到了没?”我们后面要介绍的select 复用模型就是用的这种概念。

为了解释阻塞是如何进行的,我们来讨论缓冲区

假设有一个管道,进程A为管道的写入方,B为管道的读出方。

  1. 假设一开始内核缓冲区是空的,B作为读出方,被阻塞着。然后首先A往管道写入,这时候内核缓冲区由空的状态变到非空状态,内核就会产生一个事件告诉B该醒来了,这个事件姑且称之为“缓冲区非空”。
  2. 但是“缓冲区非空”事件通知B后,B却还没有读出数据;且内核许诺了不能把写入管道中的数据丢掉这个时候,A写入的数据会滞留在内核缓冲区中,如果内核也缓冲区满了,B仍未开始读数据,最终内核缓冲区会被填满,这个时候会产生一个I/O事件,告诉进程A,你该等等(阻塞)了,我们把这个事件定义为“缓冲区满”。
  3. 假设后来B终于开始读数据了,于是内核的缓冲区空了出来,这时候内核会告诉A,内核缓冲区有空位了,你可以从长眠中醒来了,继续写数据了,我们把这个事件叫做“缓冲区非满”
  4. 也许事件Y1已经通知了A,但是A也没有数据写入了,而B继续读出数据,知道内核缓冲区空了。这个时候内核就告诉B,你需要阻塞了!,我们把这个时间定为“缓冲区空”。

这四个情形涵盖了四个I/O事件,缓冲区满,缓冲区空,缓冲区非空,缓冲区非满。这四个I/O事件是进行阻塞同步的根本。(如果不能理解“同步”是什么概念,请学习操作系统的锁,信号量,条件变量等任务同步方面的相关知识)。

然后我们来说说阻塞I/O的缺点

(本文最开始贴的socket代码就是阻塞的io)。但是阻塞I/O模式下,一个线程只能处理一个流的I/O事件。如果想要同时处理多个流,要么多进程(fork),要么多线程(pthread_create),很不幸这两种方法效率都不高。

再来考虑非阻塞忙轮询的I/O方式

我们发现我们可以同时处理多个流了(把一个流从阻塞模式切换到非阻塞模式再此不予讨论):

while true {
    for i in stream[]; {
        if i has data
            read until unavailable
    }
}

我们只要不停的把所有流从头到尾问一遍,又从头开始。这样就可以处理多个流了,但这样的做法显然不好,因为如果所有的流都没有数据,那么只会白白浪费CPU。这里要补充一点,阻塞模式下,内核对于I/O事件的处理是阻塞或者唤醒,而非阻塞模式下则把I/O事件交给其他对象(后文介绍的 select 以及 epoll)处理甚至直接忽略。

这里要简单介绍一下网络编程中的io复用:

select,poll,epoll都是IO多路复用的机制。所谓I/O多路复用机制,就是说通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。 我们将在下面详细讨论关于io复用的内容

为了避免CPU空转,可以引进了一个代理(一开始有一位叫做select的代理,后来又有一位叫做poll的代理,不过两者的本质是一样的)。这个代理比较厉害,可以同时观察许多流的I/O事件,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有I/O事件时,就从阻塞态中醒来,于是我们的程序就会轮询一遍所有的流(于是我们可以把“忙”字去掉了)。代码长这样:

while true {
    select(streams[])
    for i in streams[] {
        if i has data
            read until unavailable
    }
}

于是,如果没有I/O事件产生,我们的程序就会阻塞在select处。但是依然有个问题,我们从select那里仅仅知道了,有I/O事件发生了,但却并不知道是那几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。

但是使用select,我们有O(n)的无差别轮询复杂度,同时处理的流越多,每一次无差别轮询时间就越长。

前面的select和poll都是要从头到尾遍历所有的fd(文件描述符),每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大,同时select支持的文件描述符数量太小了,默认是1024,而poll在支持文件描述符上面没有限制。 现在可以来介绍一下我们自制服务器使用的epoll模型啦, epoll 可以理解为event poll,不同于忙轮询和无差别轮询,epoll之会把哪个流发生了怎样的I/O事件通知我们。此时我们对这些流的操作都是有意义的。(复杂度降低到了O(k),k为产生I/O事件的流的个数,也有认为O(1)的[原文为O(1),但实际上O(k)更为准确])

epoll的主要模型流程:
  1. epoll_create 创建一个epoll对象,一般epollfd = epoll_create()
  2. epoll_ctl (epoll_add/epoll_del的合体),往epoll对象中增加/删除某一个流的某一个事件。比如 epoll_ctl(epollfd, EPOLL_CTL_ADD, socket, EPOLLIN);//有缓冲区内有数据时epoll_wait返回,epoll_ctl(epollfd, EPOLL_CTL_DEL, socket, EPOLLOUT);缓冲区可写入时epoll_wait返回
  3. epoll_wait(epollfd,...)等待直到注册的事件发生

epoll和select,poll最大的区别是它是它有一个就绪链表来储存就绪的fd,当设备就绪,唤醒等待队列上的等待者时,就会调用一个回调函数,而这个回调函数会把就绪的fd加入一个就绪链表)select和poll在“唤醒”的时候要遍历整个fd集合,而epoll在“醒着”的时候只要判断一下就绪链表有没有就绪的fd就好了,这样极大的节省了cpu的时间。

具体的epoll 模型在我们的项目中如何使用我们在下一篇文章里面详细了解



Copyright © 2019 outshineamaze.
All rights reserved.

results matching ""

    No results matching ""