1.TCP并发之多线程模型
多线程是实现TCP并发最基础的方案。
- 主线程阻塞
accept获取新连接,每来一个客户端就创建子线程专门处理该连接读写,以此实现并发。 - 缺点:每个线程拥有独立栈空间,系统可创建线程总数有限。客户端并发量很大时,大量线程会带来内存占用、上下文切换开销,严重时会导致系统崩溃。
- 优化方案 :实际开发一般搭配线程池使用,复用线程,避免频繁创建销毁线程。
Linux下IO模型一共分为4类:
- 阻塞IO
- 非阻塞IO
- 信号驱动IO
- IO多路复用
2.非阻塞IO
普通socket默认是阻塞模式。调用recv、accept时,如果没有数据,函数会挂起等待。 非阻塞模式下,没有就绪数据时函数不会阻塞,会立刻返回,同时设置errno为EAGAIN / EWOULDBLOCK。
设置非阻塞三种写法:
#include <fcntl.h>
//方式1:fcntl修改已有fd,追加非阻塞标志
int flags = fcntl(sockfd, F_GETFL, 0);
fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
//方式2:创建socket直接指定非阻塞
int sockfd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0);
//方式3:Linux特有 accept4,直接得到非阻塞客户端fd
int client_fd = accept4(listen_fd, NULL, NULL, SOCK_NONBLOCK);
注意:单纯非阻塞IO需要自己循环轮询,CPU空耗很高,一般不会单独使用,大多配合多路复用一起工作。
3.信号驱动IO
核心思路:文件描述符IO就绪之后,内核给进程发送SIGIO信号,程序在信号处理函数中完成读写,主线程可以做别的业务,不需要阻塞等待IO。
使用三步:
- 使用
signal()注册SIGIO信号捕获函数; - fcntl设置fd开启异步标志
O_ASYNC; F_SETOWN告诉内核:IO就绪时把信号发给当前进程。
关键示例代码 read.c
#include "head.h"
int fd = 0;
void handler(int signo)
{
char tmpbuff[4096] = {0};
memset(tmpbuff, 0, sizeof(tmpbuff));
read(fd, tmpbuff, sizeof(tmpbuff));
printf("FIFO:%s\n", tmpbuff);
return;
}
int main(void)
{
int flags = 0;
char tmpbuff[4096] = {0};
signal(SIGIO, handler);
mkfifo("/tmp/myfifo", 0664);
fd = open("/tmp/myfifo", O_RDONLY);
if (-1 == fd)
{
perror("fail to open");
return -1;
}
//开启异步IO O_ASYNC
flags = fcntl(fd, F_GETFL);
flags |= O_ASYNC;
fcntl(fd, F_SETFL, flags);
//指定内核将SIGIO发送给当前进程
fcntl(fd, F_SETOWN, getpid());
while (1)
{
memset(tmpbuff, 0, sizeof(tmpbuff));
gets(tmpbuff);
printf("STDIN:%s\n", tmpbuff);
}
close(fd);
return 0;
}
write.c
#include "head.h"
int main(void)
{
int fd = 0;
char tmpbuff[4096] = {0};
mkfifo("/tmp/myfifo", 0664);
fd = open("/tmp/myfifo", O_WRONLY);
if (-1 == fd)
{
perror("fail to open");
return -1;
}
while (1)
{
gets(tmpbuff);
write(fd, tmpbuff, strlen(tmpbuff));
if (0 == strcmp(tmpbuff, ".quit"))
{
break;
}
}
close(fd);
return 0;
}
局限:信号驱动IO在网络编程中使用较少。信号处理函数内部有很多函数调用限制,高并发场景下信号会大量频发,容易丢失事件。
4.IO多路复用
Linux主流实现:select、poll、epoll。 一个线程可以同时监视大量文件描述符;当任意fd可读、可写、异常就绪,函数返回,用户层再去处理对应fd。是TCP高并发服务器最核心技术。
select
头文件
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
函数原型
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
功能:同时监视多个文件描述符。阻塞直到至少一个fd就绪,或者超时。
重要特性:
readfds/writefds/exceptfds属于传入传出参数。内核会把没有事件发生的fd从集合清除,下一轮循环需要重新复原fd集合。
参数说明:
nfds:要监视最大fd数值 + 1readfds:监听可读事件集合writefds:监听可写事件集合,一般传NULLexceptfds:监听异常事件集合,一般传NULLtimeout:超时时间;传NULL代表永久阻塞;0代表不阻塞
返回值:
-
0:就绪fd总数量
- 0:超时,没有事件就绪
- -1:出错
fd集合操作宏:
void FD_ZERO(fd_set *set); //清空集合
void FD_SET(int fd, fd_set *set); //fd加入集合
void FD_CLR(int fd, fd_set *set); //fd移除集合
void FD_ISSET(int fd, fd_set *set);//判断fd是否就绪
select缺点:fd集合大小有上限;每次调用需要把fd集合从用户态拷贝内核态;返回后需要遍历全部fd找就绪事件。
epoll
epoll是Linux独有的多路复用方案,解决select/poll性能短板,高并发场景首选。
epoll_create
int epoll_create(int size);
功能:创建epoll实例,在内核维护事件监听表,返回epoll专用fd。 参数:size在新版本Linux已经被忽略,但参数必须大于0。 返回值:成功返回epfd;失败返回‑1。
epoll_ctl
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
功能:对epoll内核事件表执行增、删、改操作。
参数:
-
epfd:epoll_create返回的句柄 -
op操作类型EPOLL_CTL_ADD:添加fd到内核监听表EPOLL_CTL_MOD:修改fd注册事件EPOLL_CTL_DEL:从内核移除fd
-
fd:要监控的目标文件描述符 -
event:事件结构体指针struct epoll_event {
uint32_t events; //关注事件掩码
epoll_data_t data; //用户自定义数据
};
typedef union epoll_data {
void *ptr;
int fd; //最常用,保存待监控fd
uint32_t u32;
uint64_t u64;
} epoll_data_t;
events常用掩码:
EPOLLIN:可读事件EPOLLOUT:可写事件EPOLLERR:错误事件(自动监控,无需手动注册)EPOLLHUP:对端关闭(自动监控)EPOLLET:边缘触发,默认水平触发(LT)EPOLLONESHOT:一次性触发
返回值:成功返回0;失败返回‑1。
epoll_wait
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
功能:阻塞等待事件就绪,将就绪事件填充到events数组返回。
参数:
epfd:epoll实例fdevents:输出数组,存放就绪事件maxevents:数组最大长度timeout:毫秒;‑1无限阻塞;0非阻塞立即返回
返回:
-
0:就绪fd个数
- 0:超时无事件
- -1:调用失败
epoll优势:只返回就绪fd,不需要遍历全部连接;内核维护事件表,不用每次调用拷贝全部fd;百万并发场景性能远高于select。