Linux 简要基础知识
- 用户空间/内核空间
操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核(kernel),保证内核的安全,操作系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间,内核功能模块运行在内核空间,而应用程序运行在用户空间. - 缓存I/O
缓存I/O又称为标准I/O,大多数文件系统的默认I/O操作都是缓存I/O.在Linux的缓存I/O机制中,操作系统会将I/O的数据缓存在文件系统的页缓存中,即数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间.
1.I/O复用
I/O复用使得程序能同时监听多个文件描述符,可以有效提高程序的性能,但本身是阻塞的,所以当多个文件描述符同时就绪时,如果不采取额外措施,将只能按顺序依次处理其中的每一个文件描述符,如果要实现高并发,则只能通过多线程或者多进程来实现.
- 先构造一张有关文件描述符的列表,将要监听的文件描述符添加到该表中.
- 然后调用一个函数,监听该表中的文件描述符,直到这些描述符表中的一个进行I/O操作时,该函数才返回;其中该函数是阻塞函数,而且对文件描述符的检测操作是由内核完成的.
1.select(轮询):
相关函数:
#include <sys/select.h>
int select (int nfds, fd_set* readfds, fd_set* writedfds, fd_set* exceptfds, struct timeval* timeout);
//如:
fd_set reads;
FD_ZERO(&reads);
FD_SET(lfd, &reads);
while(1)
{
ret = select(maxfd+1, &reads, NULL, NULL, NULL);
}
---参数:
nfds:要检测的文件描述中最大的fd+1;
readfds,writedfds,exceptfds:分别指向读集合,写集合和异常事件的描述符集合;
timeout:用来设置select的超时时间,当设置为NULL时表示永久阻塞,或者进行设置如:
struct timeval a;
a.tv_sec = 10;
a.tv_usec = 0;
文件描述符操作函数:
- 全部清空
○ void FD_ZERO(fd_set *set);
- 从集合中删除某一项
○ void FD_CLR(int fd, fd_set *set);
- 将某个文件描述符添加到集合
○ void FD_SET(int fd, fd_set *set);
- 判断某个文件描述符是否在集合中
○ int FD_ISSET(int fd, fd_set *set);
select的优缺点:
- 优点:跨平台
- 缺点:
1.每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大;
2.同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大.
3.底层通过数组的数据结构实现,且无法突破1024个文件描述符的限制.
2.poll(轮询):
相关函数:
#include <poll.h>
int poll(struct pollfd* fds, nfds_t nfds, int timeout);//成功时返回就绪文件描述符的总数
//如:
for(int i=0; i<1024; ++i)
{
allfd[i].fd = -1;
}
allfd[0].fd = lfd;
allfd[0].events = POLLIN;
while(1)
{
int ret = poll(allfd, max_index+1, -1);
}
---参数:
fds:是一种poll类型结构体数组, 指定文件描述符上发生的可读可写与异常事件;
struct pollfd {
int fd;/* 文件描述符 */
short events; /* 等待的事件 */
short revents; /* 实际发生的事件 */ - 内核给的反馈
};
nfds:数组的最大长度, 数组中最后一个使用的元素下标+1,每次系统内核都会轮询检测更新;
timeout:用来设置select的超时时间,当设置为-1时表示永久阻塞,0时会立即返回,>0时为等待的时长.
poll相比于select的优势在于poll可以突破文件描述符最大值1024的限制,且poll底层的实现采用的是内部链表数据结构.
3.epoll:
epoll是linux内核特有的复用函数,他会把用户关心的文件描述符的事件放在内核里的一个事件表上,无需像select和poll那样每次调用都要重复传入文件描述符集和事件集.但epoll需要一个额外的文件描述符,来唯一标示内核中的这个事件表,底层使用红黑树来完成.
相关函数:
#include <sys/epoll.h>
int epoll_create(int size);//创建epoll句柄,相当于创建一个根
//或者int epoll_create1(EPOLL_CLOEXEC);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);//将accept后的文件描述符栓到树上
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);//在超时时间内等待一组文件描述符上的事件,成功时返回就绪的文件描述符个数,
//而poll与select会返回整个文件描述符数组,用户代码需要遍历数组以找到哪些文件描述符上有活动的IO事件
---参数:
epoll_ctl函数:
epfd:是要操作的epoll_create产生的句柄;
op:为要指定的操作:
- EPOLL_CTL_ADD 注册新的fd到epfd中
- EPOLL_CTL_MOD 修改已注册的fd的监听事件
- EPOLL_CTL_DEL 从epfd中删除一个fd
fd:为要监听的文件描述符;
events:表示要监听的事件,如EPOLLIN(可读事件),EPOLLET(使用ET工作模式).
epoll_wait函数(作用是当检测到事件时,将所有就绪的事件从内核事件表中复制到它的第二个参数events所指的数组中,只用于就绪事件,通知内核fd文件I/O检测):
epfd:是要操作的epoll_create产生的句柄;
events: 表示从内核得到的就绪事件集合;
maxevents: 告诉内核events的大小,最多监听多少个事件;
timeout:设置超时事件.
4.epoll的工作模式
1.水平触发模式(默认的模式)
- 只要fd对应的缓冲区有数据,epoll_wait就会返回;
- 返回次数与发送数据的次数没有关系.
- 相当于一个效率较高的poll.
2.边沿触发模式
- fd默认的阻塞属性;
- 客户端给server发数据,发一次数据server的epoll_wait就返回一次,不在乎数据是否读完,在ET模式下,读事件触发,一次性把数据要是读不完,之后,就再不会触发ET时间了,这样会使套接字一直阻塞下去,读不到新数据;要是设置为非阻塞了,那我们在处理读时间处就应该设置循环,意思就是事件尽管触发一次,但读的次数由我来决定,我要读到套接字返回EAGAIN为止。这就是使用epoll的情况时,套接字设置为非阻塞的原因!
epoll的使用ET的实例"
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>
int main(int argc, const char* argv[])
{
if(argc < 2)
{
printf("eg: ./a.out port\n");
exit(1);
}
struct sockaddr_in serv_addr;
socklen_t serv_len = sizeof(serv_addr);
int port = atoi(argv[1]);
// 创建套接字
int lfd = socket(AF_INET, SOCK_STREAM, 0);
// 初始化服务器 sockaddr_in
memset(&serv_addr, 0, serv_len);
serv_addr.sin_family = AF_INET; // 地址族
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听本机所有的IP
serv_addr.sin_port = htons(port); // 设置端口
// 绑定IP和端口
bind(lfd, (struct sockaddr*)&serv_addr, serv_len);
// 设置同时监听的最大个数
listen(lfd, 36);
printf("Start accept ......\n");
struct sockaddr_in client_addr;
socklen_t cli_len = sizeof(client_addr);
// 创建epoll树根节点
int epfd = epoll_create(2000);
// 初始化epoll树
struct epoll_event ev;
// 设置边沿触发
ev.events = EPOLLIN;
ev.data.fd = lfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
struct epoll_event all[2000];
while(1)
{
// 使用epoll通知内核fd 文件IO检测
int ret = epoll_wait(epfd, all, sizeof(all)/sizeof(all[0]), -1);
printf("================== epoll_wait =============\n");
// 遍历all数组中的前ret个元素
for(int i=0; i<ret; ++i)
{
int fd = all[i].data.fd;
// 判断是否有新连接
if(fd == lfd)
{
// 接受连接请求
int cfd = accept(lfd, (struct sockaddr*)&client_addr, &cli_len);
if(cfd == -1)
{
perror("accept error");
exit(1);
}
// 设置文件cfd为非阻塞模式
int flag = fcntl(cfd, F_GETFL);
flag |= O_NONBLOCK;
fcntl(cfd, F_SETFL, flag);
// 将新得到的cfd挂到树上
struct epoll_event temp;
// 设置边沿触发
temp.events = EPOLLIN | EPOLLET;
temp.data.fd = cfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &temp);
// 打印客户端信息
char ip[64] = {0};
printf("New Client IP: %s, Port: %d\n",
inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ip, sizeof(ip)),
ntohs(client_addr.sin_port));
}
else
{
// 处理已经连接的客户端发送过来的数据
if(!all[i].events & EPOLLIN)
{
continue;
}
// 读数据
char buf[5] = {0};
int len;
// 循环读数据
while( (len = recv(fd, buf, sizeof(buf), 0)) > 0 )
{
// 数据打印到终端
write(STDOUT_FILENO, buf, len);
// 发送给客户端
send(fd, buf, len, 0);
}
if(len == 0)
{
printf("客户端断开了连接\n");
ret = epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
if(ret == -1)
{
perror("epoll_ctl - del error");
exit(1);
}
close(fd);
}
else if(len == -1)
{
if(errno == EAGAIN)
{
printf("缓冲区数据已经读完\n");
}
else
{
printf("recv error----\n");
exit(1);
}
}
}
}
}
close(lfd);
return 0;
}
epoll使用LT模式仿muduo的实现的epoll(更规范,考虑的东西更多):
#include <unistd.h>
#include <sys/types.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <sys/epoll.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <vector>
#include <algorithm>
#include <iostream>
using namespace std;
typedef std::vector<struct epoll_event> EventList;
int main(void)
{
signal(SIGPIPE, SIG_IGN); //忽略SIGPIPE信号
signal(SIGCHLD, SIG_IGN);//忽略SIGCHLD信号,僵尸进程直接交给init进程处理
//空闲fd
// int idlefd = open("/dev/null",O_RDONLY | O_CLOEXEC);
//创建套接字
int lfd = socket(AF_INET,SOCK_STREAM | SOCK_NONBLOCK ,0);
assert( lfd >= 0);
cout << lfd << endl;
struct sockaddr_in serv_addr;
memset(&serv_addr,0,sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(8888);
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
//bind绑定
int ret2 = bind(lfd,(struct sockaddr*)&serv_addr,sizeof(serv_addr));
assert(ret2 != -1);
listen(lfd,36);
cout << "start accept......" << endl;
vector<int> clients;
int epfd = epoll_create(5);
struct epoll_event ev;
ev.data.fd = lfd;
ev.events = EPOLLIN;
epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&ev);
eventlist events(16);
struct sockaddr_in client_addr;
socklen_t cli_len;
int connfd;
int ret;
while(1)
{
ret = epoll_wait(epfd,&*events.begin(),static_cast<int>(events.size()),-1);
assert(ret != -1);
// cout << ret << endl;
if(ret == 0) //什么事没有
continue;
if((size_t)ret == events.size()) //扩展
events.resize(events.size()*2);
for(int i = 0;i<ret;++i)
{
if(events[i].data.fd == lfd)
{
cli_len = sizeof(client_addr);
connfd = ::accept4(lfd,(struct sockaddr*)&client_addr,&cli_len,SOCK_NONBLOCK | SOCK_CLOEXEC);
//空闲fd,防止描述符已满
// int connfd = accept(lfd,(struct sockaddr*)&client_addr,&cli_len);
// if(connfd == -1)
// {
// if(errno == EMFILE)
// {
//过程:获得一个文件描述符名额;再accept(2)拿到socket连接的文件描述符;
//随后立刻close(2),这样就优雅地断开了与客户端的连接;最后重新打开空
//闲文件,把“坑”填上,以备再次出现这种情况时使用
// close(idlefd);
// idlefd = accept(lfd,NULL,NULL);
// close(idlefd);
// idlefd = open("/dev/null",O_RDONLY | O_CLOEXEC);
// continue;
// }
// else
// {
// perror("accept error");
// exit(1);
//// cout << "jjj" << endl;
// }
// }
char ip[64] = {0};
cout << "New client IP=" << inet_ntop(AF_INET,&client_addr.sin_addr.s_addr,ip,sizeof(ip)) << ",port = "
<< ntohs(client_addr.sin_port) << endl;
clients.push_back(connfd);
//将新得到的connfd挂载到树上
struct epoll_event temp;
temp.data.fd = connfd;
temp.events = EPOLLIN;
epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&temp);
}
else if (events[i].events & EPOLLIN)
{
connfd = events[i].data.fd;
if(connfd < 0)
continue;
// char buf[1024]={0};
char buf[1024] = {0};
int len = recv(connfd, buf, sizeof(buf),0);
if(len == -1)
{
if(errno == EAGAIN)
{
cout << "读完" << endl;
}
else
{
perror("read error...\n");
exit(1);
}
}
if(len == 0)
{
cout << "客户端断开了连接" << endl;
epoll_ctl(epfd,EPOLL_CTL_DEL,connfd,NULL);
close(connfd);
//remove将connfd指定的fd移动到vector的末尾
clients.erase(std::remove(clients.begin(),clients.end(),connfd),
clients.end());
continue;
}
cout << buf;
//回射回去
write(connfd,buf,strlen(buf));
}
}
}
close(lfd);
return 0;
}
三种I/O复用方式的比较:
select:本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。这样所带来的缺点是:
(1) 单个进程可监视的fd数量被限制.
(2) 需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大.
(3) 对socket进行扫描时是线性扫描.
poll:本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd.这个过程经历了多次无谓的遍历.
它没有最大连接数的限制,原因是它是基于链表来存储的,但是同样有一个缺点:
大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义.
poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd.
epoll:在前面说到的复制问题上,epoll使用mmap,共享内存减少复制开销。
还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知.
select:内核需要将消息传递到用户空间,都需要内核拷贝动作.
poll:同select.
epoll:epoll通过内核和用户空间共享一块内存来实现的.