2015年11月

协程与libco

协程是一种轻量级的用户态线程。

协程的优点

首先看看同步和异步编程的优点
同步编程优缺点:

  • 优点:逻辑清晰,开发简单
  • 缺点:吞吐量低,时延大

异步编程优缺点:

  • 优点:吞吐量高,时延小
  • 缺点:逻辑分散,开发复杂

协程同时拥有同步和异步编程的优点,可以实现同步编程,异步调用

协程与事件驱动

协程和事件驱动对比其实没有性能上的提升,但是协程让我们用同步编程的方式来写事件驱动。

协程实现原理

很多语言都支持协程,比如python,go等,协程切换的时候只要变更上下文环境即可。 但是由于像C/C++这样的语言,使用的是传统的内存管理方式,本身语言层面很难实现,所以要借助一些库,比如linux的glibc库中就提供了一系列函数来实现上下文的环境的切换。 boost也自带了一个协程库。
但是无论哪一种库,它实现的原理无非就是切换栈的内存位置,保存并修改CPU寄存器的值,最后只要IP寄存器一概,指令的地址就跳到了新的地方。 总的来说C/C++实现协程主要由两方面的内存管理:

  • 栈,由于需要保存协程运行过程中数据,所以需要自己实现协程栈的管理
  • 寄存器数据
  • 还有一些管理逻辑,比如如何切换调用栈。比如如何保存寄存器数据,保存那些寄存器数据等等

libco通过给每一个线程分配一个数组(数组大小为128)作为协程栈,栈底就是线程本身。在协程让出CPU时,把协程从栈顶删除,恢复协程时,把协程压入栈顶。 libco切换协程时,把寄存器数据保存到内存中.协程切换是由一个叫coctx_swap函数完成。coctx_swap是基于glibc源码修改了一个swapcotext高性能版本,据说切换效率可以达到1000W+/s

协程与IO

由于操作系统并不知道协程的存在,因此在协程中直接执行IO操作的时候,还是会把协程阻塞。但是我们希望协程做到在执行IO操作的时候,它可以让出CPU,让别的协程继续执行,当IO完成之后再唤醒。那如何在现有代码在调用read/write时,自动切换出去呢?

libco解决方法是epoll+NonBlocking IO+ IO Hooking。libco提供了一份hook的IO函数,在协程真正调用系统的read/write函数前先把判断数据是否准备好,如果没有准备好,放入到epoll中监听,协程主动让出CPU。等epoll监听到相应的事件之后,在恢复协程,进行真正的IO读写。
以下是libco的IO Hooking中的recv函数,在文件co_hooks_sys_call.cpp文件中

ssize_t recv( int socket, void *buffer, size_t length, int flags )
{
    //宏展开之后,就是把系统的recv函数的赋值给函数指针g_sys_recv_func
    HOOK_SYS_FUNC( recv );
    // 判断该协程是否允许IO hooking
    if( !co_is_enable_sys_hook() )
    {
        return g_sys_recv_func( socket,buffer,length,flags );
    }
     // 从一个全局的数组中以fd为index得到一个rpchook_t
     // rpchook_t记录这个fd的一些信息,如超时等
    rpchook_t *lp = get_by_fd( socket );

    if( !lp || ( O_NONBLOCK & lp->user_flag ) ) 
    {
        return g_sys_recv_func( socket,buffer,length,flags );
    }
    int timeout = ( lp->read_timeout.tv_sec * 1000 ) 
                + ( lp->read_timeout.tv_usec / 1000 );

    struct pollfd pf = { 0 };
    pf.fd = socket;
    pf.events = ( POLLIN | POLLERR | POLLHUP );
    // !!调用poll,注册EventLoop事件,然后让出CPU
    int pollret = poll( &pf,1,timeout );
    // 已经被唤醒,继续读取
    ssize_t readret = g_sys_recv_func( socket,buffer,length,flags );
    return readret;
}

通过IO Hooking实现协程在IO的自动切换最大好处在于能把目前大部分以同步方式写的代码,以最小的代价改成异步操作。

libco源码解读

libco工作流程

在实际使用中,libco工作流程如下

  1. mainloop主循环负责监听连接请求,有请求则建立一个worker协程处理。如果timeout时间内没有请求,则处理就绪协程(即io操作已经返回)
  2. worker协程,如果遇到io操作则挂起,对fd添加监听操作,同时让出CPU。

libco提供的协程接口:

  • co_create: 创建协程
  • co_yield:协程主动让出CPU
  • co_resume: 恢复协程

数据结构篇

协程上下文环境参数

struct coctx_param_t
{
    coctx_pfn_t f;//函数指针
    coctx_pfn_t f_link;//函数指针
    const void *s1;
    const void *s2;
}

协程上下文环境
主要是用来保存协程的栈位置和寄存器内容

struct coctx_t
{
    void *regs[5];//用于保存寄存器的值。
    coctx_param_t *param; //函数参数
    coctx_pfn_t routine; //这个函数是stCoRoutine_t中的pfn统一封装,在pfn函数运行完成之后,主动让出CPU
    const void *s1;//rontine
    const void *s2;//routine的参数
    size_t ss_size;//栈的大小,一般等于stCoRoutine_t的sRunStac大小,即128K
    char *ss_sp;//栈指针,指向stRoutine_t的sRunStack
}

coctx_t相关函数:

coctx_swap( coctx_t *, coctx_t * ) //实现协程切换的关键函数,通过汇编实现
coctx_init( coctx_t *ctx) //初始化ctx(将ctx置0)
coctx_make( coctx_t *ctx, coctx_pfn_t pfn, const void *s, const void *s1) //把pfn函数指针赋值给routine
coctx_swap( coctx_t *cur, coctx *ctx) //把当前协程运行环境保存到cur中,然后切换到ctx所指的协程

协程线程结构体

struct stCoRoutine_t
{
    stCoRoutineEnv_t *env; //协程所属线程环境
    pfn_co_routine_t pfn; //协程绑定的函数
    void *arg;         //函数的参数
    coctx_t ctx;       //上下文环境
    char cStart;       //该协程是否已经开始运行
    char cEnd;         //该协程是否已经运行结束
    stCoSpec_t aSpec[1024]; //协程的本地变量数组
    char cIsMain;      //是否是线程本身,libco将线程本身也看作一个协程
    char cEnableSysHook;//是否开启hook系统调用
    char sRunStack[1024 * 128];//协程运行栈大小为128K
}

协程线程环境结构体
用于记录当前线程中里

struct stCoRoutineEnv_t
{
    stCoRoutine_t *pCallStack[128]; //协程调用栈
    int iCallStackSize;             //栈的大小,指的是栈顶位置
    stEpoll_t *pEpoll;      
}

线程epoll结构体
每个线程都会有一个独立epoll fd来监听事件

struct stCoEpoll_t
{
    int iEpollFd;
    static const int _EPOLL_SIZE = 1024 * 10;
    struct stTimeout_t *pTimeout;        //超时处理
    struct stTimeoutItemLink_t *pstTimeoutList;
    struct stTimeoutItemLink_t *pstActiveList;
};

目前还没搞明白libco的超时机制,这部分以后有机会再补上

libco多线程

libco本身是支持多线程。libco有一个全局数组保存着每个线程的环境,这个全局数据是由线程的id来作为索引.
```c
static stCoRoutineEnv_t* g_arrCoEnvPerThread[ 102400 ]
````
同一个线程内的线程读写线程的变量不需要加锁,但是读写线程外的公共变量需要加锁

利用状态机解决问题

概念

状态机是软件编程中的一个重要概念。比这个概念更重要的是对它的灵活应用。在一个思路清晰而且高效的程序中,必然有状态机的身影浮现。

比如说一个按键命令解析程序,就可以被看做状态机:本来在A状态下,触发一个按键后切换到了B状态;再触发另一个键后切换到C状态,或者返回到A状态。这就是最简单的按键状态机例子。进一步看,击键动作本身也可以看做一个状态机。一个细小的击键动作包含了:释放、抖动、闭合、抖动和重新释放等状态。

程序其实就是状态机

- 阅读剩余部分 -

C/C++头文件和库文件查找规则

头文件

#include “headfile.h”

搜索顺序为:

  1. 先搜索当前目录
  2. 然后搜索-I指定的目录
  3. 再搜索gcc的环境变量CPLUS_INCLUDE_PATH(C语言使用的是C_INCLUDE_PATH,Object-C文件是OBJC_INCLUDE_PATH
  4. 最后搜索gcc的内定目录,一般会包含下面这些目录:

- 阅读剩余部分 -