- 主题:stackoverflow 有个比较 c++ stackful 和 stackless 协程的帖子
这个 SSL_do_handshake() 可能是个巨坑。楼上不一定有处理清楚:
1. 一般说接收连接,都是一个协程专门 accept(),成功之后 spawn 一个新协程处理这个连接。这里要特别注意 handshake() 的调用时机,必须在子协程里面,而不能是 accept() 的这个协程。不然会卡 accept() 协程。造成不能继续处理连接。
2. 要把 SSL 变成异步的,这个可不简单。需要反复处理 SSL_ERROR_WANT_READ 和 SSL_ERROR_WANT_WRITE 不容易写好。
3. 碰到 SSL_ERROR_SYSCALL 和 SSL_ERROR_SSL 要调用 ERR_get_error() 清理掉错误码,不然整个线程里面所有协程的 SSL 连接都会出错。所以处理 SSL 的协程也不要在线程之间调度来调度去的。
用好协程不是件容易的事。
【 在 wallyz 的大作中提到: 】
: 如果说"密钥协商,和登录认证"造成了“IO长时间占用线程”的话,那应该说明的是,之前“接续模式”的程序本身的IO方式有些问题
: 因为SSL_do_handshake本身支持以非阻塞方式进行,主要是适当的处理SL_ERROR_WANT_READ和SSL_ERROR_WANT_WRITE;
: 登录认证就更不用说了,肯定只是一些应用层的消息交互,如果设计没问题,这必然不可能造成IO长时间占用线程
: ...................
--
FROM 120.33.8.*
原来是阻塞IO,循环接收的方式。因为即使阻塞IO,也可能没有收完就出来了。
后来,基础IO的方式改变了,在非协程模式下是阻塞IO,协程模式下是非阻塞的。
【 在 wallyz 的大作中提到: 】
: 我有点纳闷,如果“IO会长时间占用线程”,这里我理解的IO肯定是非阻塞式的IO,那非阻塞IO怎么会长时间占用线程呢?
: 即使要发的数据包特别长,在socket buffer未满的情况下,本质上也还是内存拷贝(甚至零拷贝),也是不会长时间占用线程的
: 而如果socket buffer满了,遇见EAGAIN,这时候不应该就地重试send或者sleep或者就地poll,而是应该放弃这次send,转而处理别的连接,等待之前的socket的EPOLLOUT事件之后再重新send,所以没有道理会有一个连接长时间占用线程
: ...................
--
FROM 221.218.61.*
我说的密钥协商和登录认证,是举个例子,还有一些工作,细节很难切割成接续模式。
登录认证,在用户级是一个复杂的过程,其中会调用一些框架程序。我试图把它切成三段。结果解决了90%的问题,还是有一些边边角角的事会耽误一点点。
【 在 wallyz 的大作中提到: 】
: 如果说"密钥协商,和登录认证"造成了“IO长时间占用线程”的话,那应该说明的是,之前“接续模式”的程序本身的IO方式有些问题
: 因为SSL_do_handshake本身支持以非阻塞方式进行,主要是适当的处理SL_ERROR_WANT_READ和SSL_ERROR_WANT_WRITE;
: 登录认证就更不用说了,肯定只是一些应用层的消息交互,如果设计没问题,这必然不可能造成IO长时间占用线程
: ...................
--
FROM 221.218.61.*
关于协程栈的管理,我等会儿回答。
【 在 wallyz 的大作中提到: 】
: 如果说"密钥协商,和登录认证"造成了“IO长时间占用线程”的话,那应该说明的是,之前“接续模式”的程序本身的IO方式有些问题
: 因为SSL_do_handshake本身支持以非阻塞方式进行,主要是适当的处理SL_ERROR_WANT_READ和SSL_ERROR_WANT_WRITE;
: 登录认证就更不用说了,肯定只是一些应用层的消息交互,如果设计没问题,这必然不可能造成IO长时间占用线程
: ...................
--
FROM 221.218.61.*
例如:int RecvNet(int socket,char *buf,int n,int timeout)
接收,整个系统,包括客户端,管理器,服务器,及各种模式(多进程,多线程,线程池),都是通过这个函数进行的。
它基本就是read。多了个TIMEOUT。它原来是阻塞模式循环接收。
改成协程模式后,要与原来的兼容。
解决办法是,里面加一个:
static T_YIELD yield=NULL;
T_YIELD get_yield()
{
return yield;
}
T_YIELD set_yield(T_YIELD new_yield)
{
T_YIELD oldyield=yield;
yield=new_yield;
return oldyield;
}
如果在非协程环境,yield=NULL,就进行原来的IO.
协程环境设置yield函数,它就会变成非阻塞模式。
if(socket<0) return SYSERR;
if(!buf && n<0) return 0;
if(yield) {
fflag=fcntl(socket,F_GETFL,0);
if(fflag!=-1) fcntl(socket,F_SETFL,fflag|O_ASYNC|O_NONBLOCK); //异步操作
}
如果异步操作出现任何异常,就立即转换为同步模式,不会耽误事。
while(bcount<n){
if((br=read(socket,buf,n-bcount))>0){//如果能收到,就收,无需转入异步模式。
bcount+=br;
buf+=br;
repeat=0;
continue;
}
if(fflag==-1 && errno==EAGAIN) return TIMEOUTERR;
if(br<=0 && errno && errno != EAGAIN){
if(errno!=ECONNRESET)
ShowLog(1,"%s:br=%d,err=%d,%s",__FUNCTION__,br,errno,strerror(errno));
break;
}
if(bcount < n && fflag!=-1) { //切换任务
if(repeat++>3) return -errno;
ShowLog(5,"%s:tid=%lX,socket=%d,yield to schedle bcount=%d/%d",__FUNCTION__,pthread_self(),socket,bcount,n);
i=yield(socket,0,timeout);
if(i<0) {
if(timeout>0) {
struct timeval tmout;
tmout.tv_sec=timeout;
tmout.tv_usec=0;
ret=setsockopt(socket,SOL_SOCKET,SO_RCVTIMEO,(char *)&tmout,sizeof(tmout));
}
fcntl(socket,F_SETFL,fflag);
fflag=-1;
【 在 wallyz 的大作中提到: 】
: 我有点纳闷,如果“IO会长时间占用线程”,这里我理解的IO肯定是非阻塞式的IO,那非阻塞IO怎么会长时间占用线程呢?
: 即使要发的数据包特别长,在socket buffer未满的情况下,本质上也还是内存拷贝(甚至零拷贝),也是不会长时间占用线程的
: 而如果socket buffer满了,遇见EAGAIN,这时候不应该就地重试send或者sleep或者就地poll,而是应该放弃这次send,转而处理别的连接,等待之前的socket的EPOLLOUT事件之后再重新send,所以没有道理会有一个连接长时间占用线程
: ...................
--
修改:ylh0315 FROM 221.218.61.*
FROM 221.218.61.*
关于协程栈。
原来的线程池模式,只有有限个线程,栈空间不是问题。
改成协程模式后,10000个客户端就是10000个协程,每个2M(实际上用过6M)栈的话就是20G。这对于转发器,一个小服务器来说,成本有点高。
我的解决方案如下:
连接到达时,也就是accept之后,分配了context,但是没有栈。直到它的呼叫请求被一个线程抓住,发现这个context没有栈,才给它分配一个栈(向下自增型)。这时,线程一个栈,context一个栈,共两个。
通过makecontext为协程设置一个入口点,swapcontext切换进入工作流程。
此时线程还在抓着这个协程,直到它进入IO,进行yield,才放手。
这个协程,几经转折,完成了任务,结束这次业务请求后,栈被收回。寄存在线程的工作空间。这个线程在接收到空栈的context时可以把这个栈空间转给新的context。线程只寄存一个栈空间。
分配和释放之间会有冲突,如果冲突了,就不分配。线程就直接以同步方式执行这个context。
它所调用的各种IO也都不会执行yield。这个线程就退化成接续模式。在压力测试中发现了这种退化。
【 在 wallyz 的大作中提到:
: 如果说"密钥协商,和登录认证"造成了“IO长时间占用线程”的话,那应该说明的是,之前“接续模式”的程序本身的IO方式有些问题
: 因为SSL_do_handshake本身支持以非阻塞方式进行,主要是适当的处理SL_ERROR_WANT_READ和SSL_ERROR_WANT_WRITE;
: 登录认证就更不用说了,肯定只是一些应用层的消息交互,如果设计没问题,这必然不可能造成IO长时间占用线程
: ...................
--
修改:ylh0315 FROM 221.218.61.*
FROM 221.218.61.*
“如果异步操作出现任何异常,就立即转换为同步模式,不会耽误事。”
我对这里也感觉纳闷
异步出现非正常情况,除了连接断掉之外,无外乎是EAGAIN/EWOULDBLOCK,也就是read没数据或者write缓冲区满,这时候转成同步模式是不合适的,转成同步模式会阻塞当前线程,这样做和就地poll这个socket没啥两样,都会造成线程被阻塞
【 在 ylh0315 的大作中提到: 】
: 例如:int RecvNet(int socket,char *buf,int n,int timeout)
: 接收,整个系统,包括客户端,管理器,服务器,及各种模式(多进程,多线程,线程池),都是通过这个函数进行的。
: 它基本就是read。多了个TIMEOUT。它原来是阻塞模式循环接收。
: ...................
--
FROM 113.120.108.*
见84楼,yield之后判断返回状态,如果yield失败,就会自动转成同步模式。
见85楼,如果为context分配栈失败,该context就不是一个协程,它的IO走到这里就会自动转成同步模式。该任务的一切活动不受影响,只不过在大数据包的IO期间死占线程。
测试过从北京呼叫处于国外的管理器,一个64K的包被切成好几段。协程的任务会被调度好几次,非协程的一气呵成,但是期间抓死线程。
【 在 wallyz 的大作中提到: 】
: “如果异步操作出现任何异常,就立即转换为同步模式,不会耽误事。”
: 我对这里也感觉纳闷
: 异步出现非正常情况,除了连接断掉之外,无外乎是EAGAIN/EWOULDBLOCK,也就是read没数据或者write缓冲区满,这时候转成同步模式是不合适的,转成同步模式会阻塞当前线程,这样做和就地poll这个socket没啥两样,都会造成线程被阻塞
: ...................
--
修改:ylh0315 FROM 221.218.61.*
FROM 221.218.61.*
超时问题,同步比较简单,异步要在yield中处理,它的最后一个参数就是timeout。epoll对于单个fd的超时也没有什么好办法。
我的做法是每一步都在context中打时间戳。
记得主线程吗?它除了负责接生,另一个任务就是定期检查各个队列中超时的context。超时后会进行夭折处理。在IO中,yield也是进行TIMEOUT状态的返回,然后整个函数返回TIMEOUT状态。协程自行了断。
while(1) {
do {
FD_ZERO(&efds);
FD_SET(sock, &efds);
//健康检查周期
tm.tv_sec=15;
tm.tv_usec=0;
ret=select(sock+1,&efds,NULL,&efds,&tm);
if(ret==-1) {
ShowLog(1,"select error %s",strerror(errno));
close(sock);
quit(3);
}
if(ret==0) {
check_TCB_timeout();
if(poolchk) poolchk();
}
} while(ret<=0);
后边才是对select出来的进行accept。
超时之后,除了对交易进行夭折处理,还一个重要任务是对连接池的健康管理。对连接故障的判断,进行容错处理。
【 在 wallyz 的大作中提到: 】
: “如果异步操作出现任何异常,就立即转换为同步模式,不会耽误事。”
: 我对这里也感觉纳闷
: 异步出现非正常情况,除了连接断掉之外,无外乎是EAGAIN/EWOULDBLOCK,也就是read没数据或者write缓冲区满,这时候转成同步模式是不合适的,转成同步模式会阻塞当前线程,这样做和就地poll这个socket没啥两样,都会造成线程被阻塞
: ...................
--
修改:ylh0315 FROM 221.218.61.*
FROM 221.218.61.*
常规非阻塞模型下的超时处理很直接,不是什么难题,
每个线程维护一个multimap, key是超时时间戳(当前时间+比如5秒),value指向控制块(或者你说的context),每次io之后更新控制块在multimap时间戳
另外起一个timerfd让epoll一起监视,每隔比如100ms timerfd返回,检查multimap,以当前时间戳取upper_bound,从begin开始到upper_bound挨个清理(也就是你说的夭折处理)
【 在 ylh0315 的大作中提到: 】
: 超时问题,同步比较简单,异步要在yield中处理,它的最后一个参数就是timeout。epoll对于单个fd的超时也没有什么好办法。
: 我的做法是每一步都在context中打时间戳。
: 记得主线程吗?它除了负责接生,另一个任务就是定期检查各个队列中超时的context。超时后会进行夭折处理。在IO中,yield也是进行TIMEOUT状态的返回,然后整个函数返回TIMEOUT状态。协程自行了断。
: ...................
--
FROM 113.120.108.*