链接前文:GuoQi_HttpServe 设计
1. too many open files
一般来说是因为 进程打开的得文件句柄太多,已经达到了上限,从而已经无法再打开了,一般认为可以通过增加句柄上限来解决问题。
首先 ulimit -a 来查看当前系统所支持的句柄上限。一般默认为1024。
以及可以用 lsof -p 进程id;来查看进程所打开文件的详情, lsof -p 进程号 id | wc -l 可以统计进程打开了多少文件。
如果你对你的程序有一定的解的话,应该对程序打开文件数(链接数)上限有一定的估算,如果感觉数字异常,请使用第一步的lsof -p 进程id > openfiles.log命令,获得当前占用句柄的全部详情进行分析,
1)打开的这些文件是不是都是必要的?
2)定位到打开这些文件的代码
3)是否程序操作了文件写入,但是没有进行正常关闭
4)是否程序进行了通讯,但是没有正常关闭(也就是没有超时结束的机制)
2. core dump
Linux下的程序常常会因为内存访问错误等原因造成segment fault(段错误),此时如果系统core dump功能是打开的,那么将会有内存映像转储到硬盘上来,之后可以用gdb对core文件进行分析,还原系统发生段错误时刻的堆栈情况。这对于我们发现程序bug很有帮助。
通过core文件调试步骤:
- ulimit -c unlimted(打开core,默认没有打开)
- 运行./a.out(编译的时候加调试选项-g) 死锁阻塞,Ctrl+\ 产生core dump
- gdb ./a.out core.xxx
- thread apply all bt 查看死锁位置
最有可能的原因:指针未初始化,数组访问越界,栈空间太小导致溢出。在本项目产生这个原因就是线程池未正确的初始化,导致程序down掉。
3.SIGPIPE 问题
SIGPIPE信号产生的原因:
简单来说,就是客户端程序向服务器端程序发送了消息,然后关闭客户端,服务器端返回消息的时候就会收到内核给的SIGPIPE信号。
TCP的全双工信道其实是两条单工信道,client端调用close的时候,虽然本意是关闭两条信道,但是其实只能关闭它发送的那一条单工信道,还是可以接受数据,处于半连接的状态,此时server端还是可以发送数据,并不知道client端已经完全关闭了。所以这时候,对一个已经收到FIN包的socket调用read方法, 如果接收缓冲已空, 则返回0, 这就是常说的表示连接关闭. 但第一次对其调用write方法时, 如果发送缓冲没问题, 会返回正确写入(发送). 但发送的报文会导致对端发送RST报文, 因为对端的socket已经调用了close, 完全关闭, 既不发送, 也不接收数据。 所以, 第二次调用write方法(假设在收到RST之后), 会生成SIGPIPE信号, 导致进程退出。
解决办法:
设置系统忽略SIGPIPIE消息,从结果解决SIGPIPE错误导致程序崩溃问题;
将信号句柄设置为 SIG_IGN 忽略信号,那么就不会因为SIGPIPE信号终止程序,从而导致退出了。
void handle_for_sigpipe()//安全屏蔽掉SIGPIPE
{
struct sigaction sa;
memset(&sa, '\0', sizeof(sa));
sa.sa_handler = SIG_IGN;//忽略该信号
sa.sa_flags = 0;
if (sigaction(SIGPIPE, &sa, NULL))
return;
}
4.非阻塞设置问题
阻塞式I/O 在并发服务当中会成为拖慢服务器响应的累赘,会降低CPU的处理效率,非阻塞式I/O就能在并发服务中及时返回响应需求,从而使得加快服务器响应速度。
常见的设置方法就是使用 fcntl 函数将 flag 标志位置为 O_NONBLOCK;
int flag = fcntl(server_sock_fd, F_GETFL, 0); //设置套接字为非阻塞模式
if (flag == -1) return -1;
flag |= O_NONBLOCK;
if (fcntl(server_sock_fd, F_SETFL, flag) == -1)
perror("Set non block failed!");
5. epoll LT 和 ET 模式导致传输数据快慢的原因。
水平触发(level-triggered,也被称为条件触发)
LT: 只要满足条件,就触发一个事件(只要有数据没有被获取,内核就不断通知你)
边缘触发(edge-triggered)ET: 每当状态变化时,触发一个事件
LT 支持block 以及 no_nblock;
ET 仅支持 no_nblock;
所以两者的效率和上一条的原因其实是一样的。
tep.events = EPOLLIN | EPOLLET; //定义事件 ET边沿触发模式
6.epoll + 多线程带线程池 和 epoll + 多线程不带线程池 在任务属性的选择
实际测试中,I/O多路复用 + 多线程的选择传输数据的速度远远超过了I/O多路复用 + 多线程带线程池的选择。究其原因还是线程池的选用需要有适合它的地方才能发挥重要作用。
线程池的创建主要是在节省了线程创建和销毁的资源消耗,但是在I/O事件中没有什么作用,所以当创建线程和销毁线程的资源消耗占据了比较大的比重的时候,线程池的优势就会比较明显的突出来。
所以在该测试中,大量短连接在极短时间内访问服务器,单核心的处理器和单线程处理其实没什么差别。所以cpu还得去维护线程池所多花费的开销,综合下来速率并没有传统多线程模型高。
7.进程、线程退出时资源没有及时的得到释放。
子进程执行完任务退出之后,父进程如果不进行及时的回收,那么就是造成子进程成为僵尸进程。
这个时候需要运用 waitpid() 函数对子进程进行回收,也提高程序的吞吐量。
线程和进程一样,在结束的时候需要进行回收,利用 pthread_detach(newpthread) 函数。该函数为线程一旦结束,立马释放掉其拥有的资源。
延伸
Redis、Nginx采用的都是单线程模型,但是都可以处理高并发请求。
Redis 原因是基于多路复用的非阻塞IO,基于NIO(non_blocking_io);
Redis 为什么这么快?
完全基于内存,绝大部分请求是纯粹的内存操作;
数据结构简单,对数据操作也简单,redis中的数据结构是专门进行设计的;
采用单线程,避免了不必要的上下文切换和竞争条件,不用考虑加锁释放锁和死锁的问题;