Linux IO模式及 select、poll、epoll详解(转载)

这是目前入门级别感觉比较清晰又详尽的IO模式讲解,值得一看。

注:本文是对众多博客的学习和总结,可能存在理解错误。请带着怀疑的眼光,同时如果有错误希望能指出。

同步IO和异步IO,阻塞IO和非阻塞IO分别是什么,到底有什么区别?不同的人在不同的上下文下给出的答案是不同的。所以先限定一下本文的上下文。

本文讨论的背景是Linux环境下的network IO。

一 概念说明

在进行解释之前,首先要说明几个概念:
– 用户空间和内核空间
– 进程切换
– 进程的阻塞
– 文件描述符
– 缓存 I/O

用户空间与内核空间

现在操作系统都是采用虚拟存储器,那么对32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方)。操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核(kernel),保证内核的安全,操心系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。针对linux操作系统而言,将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为内核空间,而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,称为用户空间。

进程切换

为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换。因此可以说,任何进程都是在操作系统内核的支持下运行的,是与内核紧密相关的。

从一个进程的运行转到另一个进程上运行,这个过程中经过下面这些变化:
1. 保存处理机上下文,包括程序计数器和其他寄存器。
2. 更新PCB信息。
3. 把进程的PCB移入相应的队列,如就绪、在某事件阻塞等队列。
4. 选择另一个进程执行,并更新其PCB。
5. 更新内存管理的数据结构。
6. 恢复处理机上下文。

注:总而言之就是很耗资源,具体的可以参考这篇文章:进程切换

进程的阻塞

正在执行的进程,由于期待的某些事件未发生,如请求系统资源失败、等待某种操作的完成、新数据尚未到达或无新工作做等,则由系统自动执行阻塞原语(Block),使自己由运行状态变为阻塞状态。可见,进程的阻塞是进程自身的一种主动行为,也因此只有处于运行态的进程(获得CPU),才可能将其转为阻塞状态。当进程进入阻塞状态,是不占用CPU资源的

文件描述符fd

文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。

文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。

缓存 I/O

缓存 I/O 又被称作标准 I/O,大多数文件系统的默认 I/O 操作都是缓存 I/O。在 Linux 的缓存 I/O 机制中,操作系统会将 I/O 的数据缓存在文件系统的页缓存( page cache )中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。

缓存 I/O 的缺点:
数据在传输过程中需要在应用程序地址空间和内核进行多次数据拷贝操作,这些数据拷贝操作所带来的 CPU 以及内存开销是非常大的。

二 IO模式

刚才说了,对于一次IO访问(以read举例),数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。所以说,当一个read操作发生时,它会经历两个阶段:
1. 等待数据准备 (Waiting for the data to be ready)
2. 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)

正式因为这两个阶段,linux系统产生了下面五种网络模式的方案。

同步:
– 阻塞 I/O(blocking IO)
– 非阻塞 I/O(nonblocking IO)
– I/O 多路复用( IO multiplexing)
– 信号驱动 I/O( signal driven IO)

异步:
– 异步 I/O(asynchronous IO)

注:由于signal driven IO在实际中并不常用,所以我这只提及剩下的四种IO Model。

阻塞 I/O(blocking IO)

在linux中,默认情况下所有的socket都是blocking,一个典型的读操作流程大概是这样:

当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据(对于网络IO来说,很多时候数据在一开始还没有到达。比如,还没有收到一个完整的UDP包。这个时候kernel就要等待足够的数据到来)。这个过程需要等待,也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程的。而在用户进程这边,整个进程会被阻塞(当然,是进程自己选择的阻塞)。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。

所以,blocking IO的特点就是在IO执行的两个阶段都被block了。

非阻塞 I/O(nonblocking IO)

linux下,可以通过设置socket使其变为non-blocking。当对一个non-blocking socket执行读操作时,流程是这个样子:

当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回。

所以,nonblocking IO的特点是用户进程需要不断的主动询问kernel数据好了没有。

I/O 多路复用( IO multiplexing)

IO multiplexing就是我们说的select,poll,epoll,有些地方也称这种IO方式为event driven IO。select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select,poll,epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。

当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。

所以,I/O 多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()函数就可以返回。

这个图和blocking IO的图其实并没有太大的不同,事实上,还更差一些。因为这里需要使用两个system call (select 和 recvfrom),而blocking IO只调用了一个system call (recvfrom)。但是,用select的优势在于它可以同时处理多个connection。

所以,如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。)

在IO multiplexing Model中,实际中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。

信号驱动式U/O模型:

         可以用信号,让内核在描述符就绪时发送SIGIO信号通知我们。称为信号驱动式I/O

        我们首先开启套接字的信号驱动式I/O功能,并通过sigaction系统调用安装一个信号处理函数。该系统调用将立即返回,我们的进程继续工作,也就是说它没有被阻塞。当数据报准备好读取时,内核就为该进程产生一个SIGIO信号。我们随后既可以在信号处理函数中调用recvfrom读取数据报,并通知主循环数据已准备好待处理。也可以立即通知循环,让它读取数据报。

         无论如何处理SIGIO信号,这种模型的优势在于等待数据报到达期间进程不被阻塞。主循环可以继续执行,只要等待来自信号处理函数的通知:既可以是数据已准备好被处理,也可以是数据报已准备好被读取。

异步 I/O(asynchronous IO)

inux下的asynchronous IO其实用得很少。先看一下它的流程:

用户进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。

总结

blocking和non-blocking的区别

调用blocking IO会一直block住对应的进程直到操作完成,而non-blocking IO在kernel还准备数据的情况下会立刻返回。

synchronous IO和asynchronous IO的区别

在说明synchronous IO和asynchronous IO的区别之前,需要先给出两者的定义。POSIX的定义是这样子的:
– A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;
– An asynchronous I/O operation does not cause the requesting process to be blocked;

两者的区别就在于synchronous IO做”IO operation”的时候会将process阻塞。按照这个定义,之前所述的blocking IO,non-blocking IO,IO multiplexing都属于synchronous IO。

有人会说,non-blocking IO并没有被block啊。这里有个非常“狡猾”的地方,定义中所指的”IO operation”是指真实的IO操作,就是例子中的recvfrom这个system call。non-blocking IO在执行recvfrom这个system call的时候,如果kernel的数据没有准备好,这时候不会block进程。但是,当kernel中数据准备好的时候,recvfrom会将数据从kernel拷贝到用户内存中,这个时候进程是被block了,在这段时间内,进程是被block的。

而asynchronous IO则不一样,当进程发起IO 操作之后,就直接返回再也不理睬了,直到kernel发送一个信号,告诉进程说IO完成。在这整个过程中,进程完全没有被block。

各个IO Model的比较如图所示:

通过上面的图片,可以发现non-blocking IO和asynchronous IO的区别还是很明显的。在non-blocking IO中,虽然进程大部分时间都不会被block,但是它仍然要求进程去主动的check,并且当数据准备完成以后,也需要进程主动的再次调用recvfrom来将数据拷贝到用户内存。而asynchronous IO则完全不同。它就像是用户进程将整个IO操作交给了他人(kernel)完成,然后他人做完后发信号通知。在此期间,用户进程不需要去检查IO操作的状态,也不需要主动的去拷贝数据。

三 I/O 多路复用之select、poll、epoll详解

select,poll,epoll都是IO多路复用的机制。I/O多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。(这里啰嗦下)

select

int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

select 函数监视的文件描述符分3类,分别是writefds、readfds、和exceptfds。调用后select函数会阻塞,直到有描述副就绪(有数据 可读、可写、或者有except),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。当select函数返回后,可以 通过遍历fdset,来找到就绪的描述符。

select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点。select的一 个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但 是这样也会造成效率的降低。

poll

int poll (struct pollfd *fds, unsigned int nfds, int timeout);

不同与select使用三个位图来表示三个fdset的方式,poll使用一个 pollfd的指针实现。

struct pollfd { int fd; /* file descriptor */ short events; /* requested events to watch */ short revents; /* returned events witnessed */ };

pollfd结构包含了要监视的event和发生的event,不再使用select“参数-值”传递的方式。同时,pollfd并没有最大数量限制(但是数量过大后性能也是会下降)。 和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符。

从上面看,select和poll都需要在返回后,通过遍历文件描述符来获取已经就绪的socket。事实上,同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。

epoll

epoll是在2.6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。

一 epoll操作过程

epoll操作过程需要三个接口,分别如下:

int epoll_create(int size);//创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大 int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

1. int epoll_create(int size);
创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大,这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值,参数size并不是限制了epoll所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议
当创建好epoll句柄后,它就会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。

2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
函数是对指定描述符fd执行op操作。
– epfd:是epoll_create()的返回值。
– op:表示op操作,用三个宏来表示:添加EPOLL_CTL_ADD,删除EPOLL_CTL_DEL,修改EPOLL_CTL_MOD。分别添加、删除和修改对fd的监听事件。
– fd:是需要监听的fd(文件描述符)
– epoll_event:是告诉内核需要监听什么事,struct epoll_event结构如下:

struct epoll_event { __uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ }; //events可以是以下几个宏的集合: EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭); EPOLLOUT:表示对应的文件描述符可以写; EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来); EPOLLERR:表示对应的文件描述符发生错误; EPOLLHUP:表示对应的文件描述符被挂断; EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。 EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里

3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待epfd上的io事件,最多返回maxevents个事件。
参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。

二 工作模式

epoll对文件描述符的操作有两种模式:LT(level trigger)ET(edge trigger)。LT模式是默认模式,LT模式与ET模式的区别如下:
LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。
ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。

1. LT模式

LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的。

2. ET模式

ET(edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once)

ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。

3. 总结

假如有这样一个例子:
1. 我们已经把一个用来从管道中读取数据的文件句柄(RFD)添加到epoll描述符
2. 这个时候从管道的另一端被写入了2KB的数据
3. 调用epoll_wait(2),并且它会返回RFD,说明它已经准备好读取操作
4. 然后我们读取了1KB的数据
5. 调用epoll_wait(2)……

LT模式:
如果是LT模式,那么在第5步调用epoll_wait(2)之后,仍然能受到通知。

ET模式:
如果我们在第1步将RFD添加到epoll描述符的时候使用了EPOLLET标志,那么在第5步调用epoll_wait(2)之后将有可能会挂起,因为剩余的数据还存在于文件的输入缓冲区内,而且数据发出端还在等待一个针对已经发出数据的反馈信息。只有在监视的文件句柄上发生了某个事件的时候 ET 工作模式才会汇报事件。因此在第5步的时候,调用者可能会放弃等待仍在存在于文件输入缓冲区内的剩余数据。

当使用epoll的ET模型来工作时,当产生了一个EPOLLIN事件后,
读数据的时候需要考虑的是当recv()返回的大小如果等于请求的大小,那么很有可能是缓冲区还有数据未读完,也意味着该次事件还没有处理完,所以还需要再次读取:

while(rs){ buflen = recv(activeevents[i].data.fd, buf, sizeof(buf), 0); if(buflen < 0){ // 由于是非阻塞的模式,所以当errno为EAGAIN时,表示当前缓冲区已无数据可读 // 在这里就当作是该次事件已处理处. if(errno == EAGAIN){ break; } else{ return; } } else if(buflen == 0){ // 这里表示对端的socket已正常关闭. } if(buflen == sizeof(buf){ rs = 1; // 需要再次读取 } else{ rs = 0; } }

Linux中的EAGAIN含义

Linux环境下开发经常会碰到很多错误(设置errno),其中EAGAIN是其中比较常见的一个错误(比如用在非阻塞操作中)。
从字面上来看,是提示再试一次。这个错误经常出现在当应用程序进行一些非阻塞(non-blocking)操作(对文件或socket)的时候。

例如,以 O_NONBLOCK的标志打开文件/socket/FIFO,如果你连续做read操作而没有数据可读。此时程序不会阻塞起来等待数据准备就绪返回,read函数会返回一个错误EAGAIN,提示你的应用程序现在没有数据可读请稍后再试。
又例如,当一个系统调用(比如fork)因为没有足够的资源(比如虚拟内存)而执行失败,返回EAGAIN提示其再调用一次(也许下次就能成功)。

三 代码演示

下面是一段不完整的代码且格式不对,意在表述上面的过程,去掉了一些模板代码。

#define IPADDRESS "127.0.0.1" #define PORT 8787 #define MAXSIZE 1024 #define LISTENQ 5 #define FDSIZE 1000 #define EPOLLEVENTS 100 listenfd = socket_bind(IPADDRESS,PORT); struct epoll_event events[EPOLLEVENTS]; //创建一个描述符 epollfd = epoll_create(FDSIZE); //添加监听描述符事件 add_event(epollfd,listenfd,EPOLLIN); //循环等待 for ( ; ; ){ //该函数返回已经准备好的描述符事件数目 ret = epoll_wait(epollfd,events,EPOLLEVENTS,-1); //处理接收到的连接 handle_events(epollfd,events,ret,listenfd,buf); } //事件处理函数 static void handle_events(int epollfd,struct epoll_event *events,int num,int listenfd,char *buf) { int i; int fd; //进行遍历;这里只要遍历已经准备好的io事件。num并不是当初epoll_create时的FDSIZE。 for (i = 0;i < num;i++) { fd = events[i].data.fd; //根据描述符的类型和事件类型进行处理 if ((fd == listenfd) &&(events[i].events & EPOLLIN)) handle_accpet(epollfd,listenfd); else if (events[i].events & EPOLLIN) do_read(epollfd,fd,buf); else if (events[i].events & EPOLLOUT) do_write(epollfd,fd,buf); } } //添加事件 static void add_event(int epollfd,int fd,int state){ struct epoll_event ev; ev.events = state; ev.data.fd = fd; epoll_ctl(epollfd,EPOLL_CTL_ADD,fd,&ev); } //处理接收到的连接 static void handle_accpet(int epollfd,int listenfd){ int clifd; struct sockaddr_in cliaddr; socklen_t cliaddrlen; clifd = accept(listenfd,(struct sockaddr*)&cliaddr,&cliaddrlen); if (clifd == -1) perror("accpet error:"); else { printf("accept a new client: %s:%d\n",inet_ntoa(cliaddr.sin_addr),cliaddr.sin_port); //添加一个客户描述符和事件 add_event(epollfd,clifd,EPOLLIN); } } //读处理 static void do_read(int epollfd,int fd,char *buf){ int nread; nread = read(fd,buf,MAXSIZE); if (nread == -1) { perror("read error:"); close(fd); //记住close fd delete_event(epollfd,fd,EPOLLIN); //删除监听 } else if (nread == 0) { fprintf(stderr,"client close.\n"); close(fd); //记住close fd delete_event(epollfd,fd,EPOLLIN); //删除监听 } else { printf("read message is : %s",buf); //修改描述符对应的事件,由读改为写 modify_event(epollfd,fd,EPOLLOUT); } } //写处理 static void do_write(int epollfd,int fd,char *buf) { int nwrite; nwrite = write(fd,buf,strlen(buf)); if (nwrite == -1){ perror("write error:"); close(fd); //记住close fd delete_event(epollfd,fd,EPOLLOUT); //删除监听 }else{ modify_event(epollfd,fd,EPOLLIN); } memset(buf,0,MAXSIZE); } //删除事件 static void delete_event(int epollfd,int fd,int state) { struct epoll_event ev; ev.events = state; ev.data.fd = fd; epoll_ctl(epollfd,EPOLL_CTL_DEL,fd,&ev); } //修改事件 static void modify_event(int epollfd,int fd,int state){ struct epoll_event ev; ev.events = state; ev.data.fd = fd; epoll_ctl(epollfd,EPOLL_CTL_MOD,fd,&ev); } //注:另外一端我就省了

四 epoll总结

在 select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一 个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait() 时便得到通知。(此处去掉了遍历文件描述符,而是通过监听回调的的机制。这正是epoll的魅力所在。)

epoll的优点主要是一下几个方面:
1. 监视的描述符数量不受限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左 右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。select的最大缺点就是进程打开的fd是有数量限制的。这对 于连接数量比较大的服务器来说根本不能满足。虽然也可以选择多进程的解决方案( Apache就是这样实现的),不过虽然linux上面创建进程的代价比较小,但仍旧是不可忽视的,加上进程间数据同步远比不上线程间同步的高效,所以也不是一种完美的方案。

  1. IO的效率不会随着监视fd的数量的增长而下降。epoll不同于select和poll轮询的方式,而是通过每个fd定义的回调函数来实现的。只有就绪的fd才会执行回调函数。

如果没有大量的idle -connection或者dead-connection,epoll的效率并不会比select/poll高很多,但是当遇到大量的idle- connection,就会发现epoll的效率大大高于select/poll。

Java nio和多路复用

java 1.4 nio提供的select,这是一种多路复用I/O(multiplexed non-blocking I/O)模型,底层是使用select或者poll。I/O复用就是,阻塞在select或者poll系统调用的某一个之上,而不是阻塞在真正的I/O系统调用之上。JDK 5.0 update 9和JDK 6.0在linux下支持使用epoll,可以提高并发idle connection的性能(http://blogs.sun.com/alanb/entry/epoll)。
“BIO是指阻塞IO方式,即读和写必须为同步方式,NIO是指异步(用户进程未被IO阻塞)读,同步(用户进程被IO阻塞)写的方式,AIO是指异步读,异步写的方式。
在网络协议上java对于TCP/IP和UDP/IP均支持,在网络IO的操作上,目前java仅支持BIO和NIO两种方式。”

(读:读取内核状态是否准备好写:数据内核态->用户态的拷贝

Reactor 和 Proactor

这是目前网络编程中经常用到两种事件驱动的设计模式。

其实结合上面来看,reactor 模式思想就是基于非阻塞同步IO的方式,也即I/O 多路复用。

其流程如下:

  • 步骤 1) 等待事件 (Reactor 的工作)
  • 步骤 2) 发”已经可读”事件发给事先注册的事件处理者或者回调 ( Reactor 要做的)
  • 步骤 3) 读数据 (用户代码要做的)
  • 步骤 4) 处理数据 (用户代码要做的)

而proactor模式思想基于完全异步IO方式。

  • 步骤 1) 等待事件 (Proactor 的工作)
  • 步骤 2) 读数据(看,这里变成成了让 Proactor 做这个事情)
  • 步骤 3) 把数据已经准备好的消息给用户处理函数,即事件处理者(Proactor 要做的)
  • 步骤 4) 处理数据 (用户代码要做的)

区别就在第三步骤,所以如果我们的网络库使用了Reactor设计模式,使用的是IO复用,那么可以通过封装第三个步骤,变成proactor 模式,这样可以让用户进程对读数据这一步无感知,那么使用了 Proactor设计模式封装过的IO多路复用模型在使用上与异步IO模型无差异,为什么这样做,虽然不会提升我们系统的性能,我们的服务在跨平台的时候可以让用户代码脱离 IO模型可能不一样的限制,降低耦合,提高生产率。

参考

用户空间与内核空间,进程上下文与中断上下文[总结]
进程切换
维基百科-文件描述符
Linux 中直接 I/O 机制的介绍
IO – 同步,异步,阻塞,非阻塞 (亡羊补牢篇)
Linux中select poll和epoll的区别
IO多路复用之select总结
IO多路复用之poll总结
IO多路复用之epoll总结

(转载自:https://segmentfault.com/a/1190000003063859,作者:人云思云

mysql编译安装

以前的项目都用Oracle,新项目准备使用Mysql数据库,所以就先装个玩一下,把安装过程记录一下,当 是学习mysql 的开篇了!mysql是开源的,作为一只有那么点GEEK情结的超猿类,除了安装外,还是要编译一把才过瘾的!

编译

  • 基础工具准备:

    要编译,首先需要准备好编译的环境工具,此次我的linux环境还是CentOS6.5版本,通过以下脚本安装好最新的编译工具与库文件:

[root@iZ28gxqlqfsZ ~]# yum install gcc gcc-c++ ncurses-devel perl&lt;

同时我们需要安装cmake,我们使用当前最新的稳定版本V3.2,命令如下:

[root@iZ28gxqlqfsZ ~]# wget http://www.cmake.org/files/v3.2/cmake-3.2.0.tar.gz 
[root@iZ28gxqlqfsZ ~]# tar -xzvf cmake-3.2.0.tar.gz 
[root@iZ28gxqlqfsZ ~]# cd cmake-3.2.0
  • 源码准备

正常进入mysql官网下载页面,找不到源码的下载按钮(听说是中国被墙),进入如下页面后选择一个国家的FTP资源去下载,我选择了鸟国的!

http://dev.mysql.com/downloads/mirrors.html
[root@iZ28gxqlqfsZ ~]# wgetftp:ftp://ftp.jaist.ac.jp/pub/mysql/Downloads/MySQL-5.5/mysql-5.5.44.tar.gz
[root@iZ28gxqlqfsZ ~]# tar -zxv -f mysql-5.5.44.tar.gz
  • 用户环境准备:

数据库用户组与用户名

[root@iZ28gxqlqfsZ ~]# groupadd mysql
[root@iZ28gxqlqfsZ ~]# useradd -r -g mysql mysql

mysql安装目录

[root@iZ28gxqlqfsZ local]# mkdir <span style="color: #ff0000;">/usr/local/mysql</span>
[root@iZ28gxqlqfsZ local]# cd /usr/local/mysql
[root@iZ28gxqlqfsZ mysql]# chown -R mysql:mysql .

mysql数据库文件目录

[root@iZ28gxqlqfsZ ~]# mkdir <span style="color: #ff0000;">/data/mysql</span>
[root@iZ28gxqlqfsZ ~]# cd /data/mysql
[root@iZ28gxqlqfsZ ~]# chown -R mysql:mysql .

环境变形执行路径更改

vim /etc/profile 
#mysql path
PATH=/usr/local/mysql/bin:/usr/local/mysql/lib:$PATH
export PATH

source /etc/profile

设置源码编译配置脚本:

[root@iZ28gxqlqfsZ mysql-5.5.44]# cmake \
> -DCMAKE_INSTALL_PREFIX=/usr/local/mysql \
> -DDEFAULT_CHARSET=utf8 \
> -DDEFAULT_COLLATION=utf8_general_ci \
> -DWITH_INNOBASE_STORAGE_ENGINE=1 \
> -DWITH_ARCHIVE_STORAGE_ENGINE=1 \
> -DWITH_BLACKHOLE_STORAGE_ENGINE=1 \
> -DMYSQL_DATADIR=/data/mysql \
> -DMYSQL_TCP_PORT=3306 \
> -DEXTRA_CHARSETS=all
  • 编译mysql

删除CMakeCache.txt,后重新编译:

[root@iZ28gxqlqfsZ mysql-5.5.44]# rm CMakeCache.txt
[root@iZ28gxqlqfsZ mysql-5.5.44]# make
  • 安装MYSQL:
[root@iZ28gxqlqfsZ mysql-5.5.44]# make install

到这一步mysql 数据库就已经编译安装好了!

初始化

  • 初始化数据库
[root@iZ28gxqlqfsZ ~]# cd /usr/local/mysql
[root@iZ28gxqlqfsZ ~]# scripts/mysql_install_db --user=mysql --datadir=/data/mysql

配置文件修改

[root@iZ28gxqlqfsZ mysql]# cp /usr/local/mysql/support-files/my-medium.cnf /etc/my.cnf
[root@iZ28gxqlqfsZ mysql]# cp support-files/mysql.server /etc/init.d/mysqld
[root@iZ28gxqlqfsZ mysql]# cp /usr/local/mysql/support-files/my-medium.cnf  /usr/local/mysql/my.cnf

修改启动参数:
my.cnf 配置的[mysqld]项新增两个目录配置

[root@iZ28gxqlqfsZ mysql]# vi /usr/local/mysql/my.cnf

[mysqld]
...
basedir = /usr/local/mysql
datadir = /data/mysql
  • 启动数据库

启动数据库并让数据库开机自动启动

[root@iZ28gxqlqfsZ ~]# /usr/local/mysql/scripts/mysql_install_db --user=mysql --datadir=/data/mysql

[root@iZ28gxqlqfsZ mysql]# chkconfig –level 35 mysqld on

检查mysql服务是否启动:查看3306端口是否由mysqld 进程正在监听,因为没有设置密码,直接打mysql 命令进入看是否能进入root用户

[root@iZ28gxqlqfsZ mysql]# netstat -tulnp | grep 3306
[root@iZ28gxqlqfsZ mysql]# mysql

到此,mysql 数据库已经编译安装完成!小伙伴可以愉快进去玩耍了!当然mysql 数据库跟Oracle 数据库的上层的使用基本差不多,只是有些细微的结构上的差别,下面就列出最基本的操作!

 

Better understanding Linux secondary dependencies solving with examples

除了man 资料外觉得是挺详细的资料,收藏!

http://www.kaizou.org/2015/01/linux-libraries/

08 Jan 2015 by David Corvoysier

A few months ago I stumbled upon a linking problem with secondary dependencies I couldn’t solved without overlinking the corresponding libraries.

I only realized today in a discussion with my friend Yann E. Morin that not only did I use the wrong solution for that particular problem, but that my understanding of the gcc linking process was not as good as I had imagined.

This blog post is to summarize what I have now understood.

There is also a small repository on github with the mentioned samples.

A few words about Linux libraries

This paragraph is only a brief summary of what is very well described in The Linux Documentation Project library howto.

Man pages for the linux linker and loader are also a good source of information.

There are three kind of libraries in Linux: static, shared and dynamically loaded (DL).

Dynamically loaded libraries are very specific to some use cases like plugins, and would deserve an article on their own. I will only focus here on static and shared libraries.

Static libraries

A static library is simply an archive of object files conventionally starting with thelib prefix and ending with the .a suffix.

Example:

libfoobar.a

Static libraries are created using the ar program:

$ ar rcs libfoobar.a foo.o bar.o

Linking a program with a static library is as simple as adding it to the link command either directly with its full path:

$ gcc -o app main.c /path/to/foobar/libfoobar.a

or indirectly using the -l/L options:

$ gcc -o app main.c -lfoobar -L/path/to/foobar

Shared libraries

A shared library is an ELF object loaded by programs when they start.

Shared libraries follow the same naming conventions as static libraries, but with the .sosuffix instead of .a.

Example:

libfoobar.so

Shared library objects need to be compiled with the -fPIC option that produces position-independent code, ie code that can be relocated in memory.

$ gcc -fPIC -c foo.c
$ gcc -fPIC -c bar.c

The gcc command to create a shared library is similar to the one used to create a program, with the addition of the -shared option.

$ gcc -shared -o libfoobar.so foo.o bar.o

Linking against a shared library is achieved using the exact same commands as linking against a static library:

$ gcc -o app main.c libfoobar.so

or

$ gcc -o app main.c -lfoobar -L/path/to/foobar

Shared libraries and undefined symbols

An ELF object maintains a table of all the symbols it uses, including symbols belonging to another ELF object that are marked as undefined.

At compilation time, the linker will try to resolve an undefined symbol by linking it either statically to code included in the overall output ELF object or dynamically to code provided by a shared library.

If an undefined symbol is found in a shared library, a DT_NEEDED entry is created for that library in the output ELF target.

The content of the DT_NEEDED field depends on the link command: – the full path to the library if the library was linked with an absolute path, – the library name otherwise (or the library soname if it was defined).

You can check the dependencies of an ELF object using the readelf command:

$ readelf -d main

or

$ readelf -d libbar.so

When producing an executable a symbol that remains undefined after the link will raise an error: all dependencies must therefore be available to the linker in order to produce the output binary.

For historic reason, this behavior is disabled when building a shared library: you need to specify the --no-undefined (or -z defs) flag explicitly if you want errors to be raised when an undefined symbol is not resolved.

$ gcc -Wl,--no-undefined -shared -o libbar.so -fPIC bar.c

or

$ gcc -Wl,--no-undefined -shared -o libbar.so -fPIC bar.c

Note that when producing a static library, which is just an archive of object files, no actual ‘linking’ operation is performed, and undefined symbols are kept unchanged.

Library versioning and compatibility

Several versions of the same library can coexist in the system.

By conventions, two versions of the same library will use the same library name with a different version suffix that is composed of three numbers:

  • major revision,
  • minor revision,
  • build revision.

Example:

libfoobar.so.1.2.3

This is often referred as the library real name.

Also by convention, the library major version should be modified every time the library binary interface (ABI) is modified.

Following that convention, an executable compiled with a shared library version is theoretically able to link with another version of the same major revision.

This concept if so fundamental for expressing compatibility between programs and shared libraries that each shared library can be associated a soname, which is the library name followed by a period and the major revision:

Example:

libfoobar.so.1

The library soname is stored in the DT_SONAME field of the ELF shared object.

The soname has to be passed as a linker option to gcc.

$ gcc -shared -Wl,-soname,libfoobar.so.1 -o libfoobar.so foo.o bar.o

As mentioned before, whenever a library defines a soname, it is that soname that is stored in the DT_NEEDED field of ELF objects linked against that library.

Solving versioned libraries dependencies at build time

As mentioned before, libraries to be linked against can be specified using a shortened name and a path:

$ gcc -o app main.c -lfoobar -L/path/to/foobar

When installing a library, the installer program will typically create a symbolic link from the library real name to its linker name to allow the linker to find the actual library file.

Example:

/usr/lib/libfoobar.so -> libfoobar.so.1.5.3

The linker uses the following search paths to locate required shared libraries:

  • directories specified by -rpath-link options (more on that later)
  • directories specified by -rpath options (more on that later)
  • directories specified by the environment variable LD_RUN_PATH
  • directories specified by the environment variable LD_LIBRARY_PATH
  • directories specified in DT_RUNPATH or DT_RPATH of a shared library are searched for shared libraries needed by it
  • default directories, normally /lib and /usr/lib
  • directories listed inthe /etc/ld.so.conf file

Solving versioned shared libraries dependencies at runtime

On GNU glibc-based systems, including all Linux systems, starting up an ELF binary executable automatically causes the program loader to be loaded and run.

On Linux systems, this loader is named /lib/ld-linux.so.X (where X is a version number). This loader, in turn, finds and loads recursively all other shared libraries listed in theDT_NEEDED fields of the ELF binary.

Please note that if a soname was specified for a library when the executable was compiled, the loader will look for the soname instead of the library real name. For that reason, installation tools automatically create symbolic names from the library soname to its real name.

Example:

/usr/lib/libfoobar.so.1 -> libfoobar.so.1.5.3

When looking fo a specific library, if the value described in the DT_NEEDED doesn’t contain a /, the loader will consecutively look in:

  • directories specified at compilation time in the ELF object DT_RPATH (deprecated),
  • directories specified using the environment variable LD_LIBRARY_PATH,
  • directories specified at compile time in the ELF object DT_RUNPATH,
  • from the cache file /etc/ld.so.cache, which contains a compiled list of candidate libraries previously found in the augmented library path (can be disabled at compilation time),
  • in the default path /lib, and then /usr/lib (can be disabled at compilation time).

Proper handling of secondary dependencies

As mentioned in the introduction, my issue was related to secondary dependencies, ie shared libraries dependencies that are exported from one library to a target.

Let’s imagine for instance a program main that depends on a library libbar that itself depends on a shared library libfoo.

We will use either a static libbar.a or a shared libbar.so.

foo.c

int foo()
{
    return 42;
}

bar.c

int foo();

int bar()
{
    return foo();
}

main.c

int bar();

int main(int argc, char** argv)
{
    return bar();
}

Creating the libfoo.so shared library

libfoo has no dependencies but the libc, so we can create it with the simplest command:

$ gcc -shared -o libfoo.so -fPIC foo.c

Creating the libbar.a static library

As said before, static libraries are just archives of object files, without any means to declare external dependencies.

In our case, there is therefore no explicit connection whatsoever between libbar.a and libfoo.so.

$ gcc -c bar.c
$ ar rcs libbar.a bar.o

Creating the libbar.so dynamic library

The proper way to create the libbar.so shared library it by explicitly specifying it depends on libfoo:

$ gcc -shared -o libbar2.so -fPIC bar.c -lfoo -L$(pwd)

This will create the library with a proper DT_NEEDED entry for libfoo.

$ readelf -d libbar.so
Dynamic section at offset 0xe08 contains 25 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [libfoo.so]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
...

However, since undefined symbols are not by default resolved when building a shared library, we can also create a “dumb” version without any DT_NEEDED entry:

$ gcc -shared -o libbar_dumb.so -fPIC bar.c

Note that it is very unlikely that someone actually chooses to create such an incomplete library on purpose, but it may happen that by misfortune you encounter one of these beasts in binary form and still need to link against it (yeah, sh… happens !).

Creating the main executable

Linking against the libbar.a static library

As mentioned before, when linking an executable, the linker must resolve all undefined symbols before producing the output binary.

Trying to link only with libbar.a produces an error, since it has an undefined symbol and the linker has no clue where to find it:

$ gcc -o app_s main.c libbar.a
libbar.a(bar.o): In function `bar':
bar.c:(.text+0xa): undefined reference to `foo'
collect2: error: ld returned 1 exit status

Adding libfoo.so to the link command solves the problem:

$ gcc -o app main.c libbar.a -L$(pwd) -lfoo

You can verify that the app binary now explicitly depends on libfoo:

$ readelf -d app
Dynamic section at offset 0xe18 contains 25 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [libfoo.so]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
...

At run-time, the dynamic linker will look for libfoo.so, so unless you have installed it in standard directories (/lib or /usr/lib) you need to tell it where it is:

LD_LIBRARY_PATH=$(pwd) ./app

To summarize, when linking an executable against a static library, you need to specify explicitly all dependencies towards shared libraries introduced by the static library on the link command.

Note however that expressing, discovering and adding implicit static libraries dependencies is typically a feature of your build system (autotools, cmake).

Linking against the libbar.so shared library

As specified in the linker documentation, when the linker encounters an input shared library it processes all its DT_NEEDED entries as secondary dependencies:

  • if the linker output is a shared relocatable ELF object (ie a shared library), it will add all DT_NEEDED entries from the input library as new DT_NEEDED entries in the output,
  • if the linker ouput is a non-shared, non-relocatable link (our case), it will automatically add the libraries listed in the DT_NEEDED of the input library on the link command line, producing an error if it can’t locate them.

So, let’s see what happens when dealing with our two shared libraries.

Linking against the “dumb” library

When trying to link an executable against the “dumb” version of libbar.so, the linker encounters undefined symbols in the library itself it cannot resolve since it lacks theDT_NEEDED entry related to libfoo:

$ gcc -o app main.c -L$(pwd) -lbar_dumb
libbar_dumb.so: undefined reference to `foo'
collect2: error: ld returned 1 exit status

Let’s see how we can solve this.

Adding explicitly the libfoo.so dependency

Just like we did when we linked against the static version, we can just add libfoo to the link command to solve the problem:

$ gcc -o app main.c -L$(pwd) -lbar_dumb -lfoo

It creates an explicit dependency in the app binary:

$ readelf -d app
Dynamic section at offset 0xe18 contains 25 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [libbar_dumb.so]
 0x0000000000000001 (NEEDED)             Shared library: [libfoo.so]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
...

Again, at runtime you may need to tell the dynamic linker where libfoo.so is:

$ LD_LIBRARY_PATH=$(pwd) ./app

Note that having an explicit dependency to libfoo is not quite right, since our application doesn’t use directly any symbols from libfoo. What we’ve just done here is called overlinking, and it is BAD.

Let’s imagine for instance that in the future we decide to provide a newer version oflibbar that uses the same ABI, but based on a new version of libfoo with a different ABI: we should theoretically be able to use that new version of libbar without recompiling our application, but what would really happen here is that the dynamic linker would actually try to load the two versions of libfoo at the same time, leading to unpredictable results. We would therefore need to recompile our application even if it is still compatible with the newest libbar.

As a matter of fact, this actually happened in the past: a libfreetype update in the debian distro caused 583 packages to be recompiled, with only 178 of them actually using it.

Ignoring libfoo dependency

There is another option you can use when dealing with the “dumb” library: tell the linker to ignore its undefined symbols altogether:

$ gcc -o app main.c -L$(pwd) -lbar_dumb -Wl,--allow-shlib-undefined

This will produce a binary that doesn’t declare its hidden dependencies towards libfoo:

$ readelf -d app
Dynamic section at offset 0xe18 contains 25 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [libbar_dumb.so]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
...

This isn’t without consequences at runtime though, since the dynamic linker is now unable to resolve the executable dependencies:

$ ./app: symbol lookup error: ./libbar_dumb.so: undefined symbol: foo

Your only option is then to load libfoo explicitly (yes, this is getting uglier and uglier):

$ LD_PRELOAD=$(pwd)/libfoo.so LD_LIBRARY_PATH=$(pwd) ./app
Linking against the “correct” library
Doing it the right way

As mentioned before, when linking against the correct shared library, the linker encounters the libfoo.so DT_NEEDED entry, adds it to the link command and finds it at the path specified by -L, thus solving the undefined symbols … or at least that is what I expected:

$ gcc -o app main.c -L$(pwd) -lbar
/usr/bin/ld: warning: libfoo.so, needed by libbar.so, not found (try using -rpath or -rpath-link)
/home/diec7483/dev/linker-example/libbar.so: undefined reference to `foo'
collect2: error: ld returned 1 exit status

Why the error ? I thought I had done everything by the book !

Okay, let’s take a look at the ld man page again, looking at the -rpath-link option. This says:

When using ELF or SunOS, one shared library may require another. This happens when an “ld -shared” link includes a shared library as one of the input files. When the linker encounters such a dependency when doing a non-shared, non-relocatable link, it will automatically try to locate the required shared library and include it in the link, if it is not included explicitly. In such a case, the -rpath-link option specifies the first set of directories to search. The -rpath-link option may specify a sequence of directory names either by specifying a list of names separated by colons, or by appearing multiple times.

Ok, this is not crystal-clear, but what it actually means is that when specifying the path for a secondary dependency, you should not use -L but -rpath-link:

$ gcc -o app main.c -L$(pwd) -lbar -Wl,-rpath-link=$(pwd)

You can now verify that app depends only on libbar:

$ readelf -d app
Dynamic section at offset 0xe18 contains 25 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [libbar.so]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
...

And this is finally how things should be done.

You may also use -rpath instead of -rpath-link but in that case the specified path will be stored in the resulting executable, which is not suitable if you plan to relocate your binaries. Tools like cmake use the -rpath during the build phase (make), but remove the specified path from the executable during the installation phase(make install).

Conclusion

To summarize, when linking an executable against:

  • a static library, you need to specify all dependencies towards other shared libraries this static library depends on explicitly on the link command.

  • a shared library, you don’t need to specify dependencies towards other shared libraries this shared library depends on, but you may need to specify the path to these libraries on the link command using the -rpath/-rpath-link options.

Note however that expressing, discovering and adding implicit libraries dependencies is typically a feature of your build system (autotools, cmake), as demonstrated in my samples.

Linux下高并发最大连接数限制及解决

转自 http://blog.sae.sina.com.cn/archives/1988

1、修改用户进程可打开文件数限制
在Linux平台上,无论编写客户端程序还是服务端程序,在进行高并发TCP连接处理时,最高的并发数量都要受到系统对用户单一进程同时可打开文件数量的限制(这是因为系统为每个TCP连接都要创建一个socket句柄,每个socket句柄同时也是一个文件句柄)。可使用ulimit命令查看系统允许当前用户进程打开的文件数限制:

[speng@as4 ~]$ ulimit -n
1024

这表示当前用户的每个进程最多允许同时打开1024个文件,这1024个文件中还得除去每个进程必然打开的标准输入,标准输出,标准错误,服务器监听 socket,进程间通讯的unix域socket等文件,那么剩下的可用于客户端socket连接的文件数就只有大概1024-10=1014个左右。也就是说缺省情况下,基于Linux的通讯程序最多允许同时1014个TCP并发连接。

对于想支持更高数量的TCP并发连接的通讯处理程序,就必须修改Linux对当前用户的进程同时打开的文件数量的软限制(soft limit)和硬限制(hardlimit)。其中软限制是指Linux在当前系统能够承受的范围内进一步限制用户同时打开的文件数;硬限制则是根据系统硬件资源状况(主要是系统内存)计算出来的系统最多可同时打开的文件数量。通常软限制小于或等于硬限制。
修改上述限制的最简单的办法就是使用ulimit命令:

[speng@as4 ~]$ ulimit -n

上述命令中,在中指定要设置的单一进程允许打开的最大文件数。如果系统回显类似于“Operation notpermitted”之类的话,说明上述限制修改失败,实际上是因为在中指定的数值超过了Linux系统对该用户打开文件数的软限制或硬限制。因此,就需要修改Linux系统对用户的关于打开文件数的软限制和硬限制。
第一步,修改/etc/security/limits.conf文件,在文件中添加如下行:

speng soft nofile 10240
speng hard nofile 10240

其中speng指定了要修改哪个用户的打开文件数限制,可用’*’号表示修改所有用户的限制;soft或hard指定要修改软限制还是硬限制;10240则指定了想要修改的新的限制值,即最大打开文件数(请注意软限制值要小于或等于硬限制)。修改完后保存文件。
第二步,修改/etc/pam.d/login文件,在文件中添加如下行:

session required /lib/security/pam_limits.so

这是告诉Linux在用户完成系统登录后,应该调用pam_limits.so模块来设置系统对该用户可使用的各种资源数量的最大限制(包括用户可打开的最大文件数限制),而pam_limits.so模块就会从/etc/security/limits.conf文件中读取配置来设置这些限制值。修改完后保存此文件。
第三步,查看Linux系统级的最大打开文件数限制,使用如下命令:

[speng@as4 ~]$ cat /proc/sys/fs/file-max
12158

这表明这台Linux系统最多允许同时打开(即包含所有用户打开文件数总和)12158个文件,是Linux系统级硬限制,所有用户级的打开文件数限制都不应超过这个数值。通常这个系统级硬限制是Linux系统在启动时根据系统硬件资源状况计算出来的最佳的最大同时打开文件数限制,如果没有特殊需要,不应该修改此限制,除非想为用户级打开文件数限制设置超过此限制的值。修改此硬限制的方法是修改/etc/rc.local脚本,在脚本中添加如下行:

echo 22158 > /proc/sys/fs/file-max

这是让Linux在启动完成后强行将系统级打开文件数硬限制设置为22158。修改完后保存此文件。
完成上述步骤后重启系统,一般情况下就可以将Linux系统对指定用户的单一进程允许同时打开的最大文件数限制设为指定的数值。如果重启后用 ulimit-n命令查看用户可打开文件数限制仍然低于上述步骤中设置的最大值,这可能是因为在用户登录脚本/etc/profile中使用ulimit -n命令已经将用户可同时打开的文件数做了限制。由于通过ulimit-n修改系统对用户可同时打开文件的最大数限制时,新修改的值只能小于或等于上次 ulimit-n设置的值,因此想用此命令增大这个限制值是不可能的。所以,如果有上述问题存在,就只能去打开/etc/profile脚本文件,在文件中查找是否使用了ulimit-n限制了用户可同时打开的最大文件数量,如果找到,则删除这行命令,或者将其设置的值改为合适的值,然后保存文件,用户退出并重新登录系统即可。
通过上述步骤,就为支持高并发TCP连接处理的通讯处理程序解除关于打开文件数量方面的系统限制。
2、修改网络内核对TCP连接的有关限制(参考对比下篇文章“优化内核参数”)
在Linux上编写支持高并发TCP连接的客户端通讯处理程序时,有时会发现尽管已经解除了系统对用户同时打开文件数的限制,但仍会出现并发TCP连接数增加到一定数量时,再也无法成功建立新的TCP连接的现象。出现这种现在的原因有多种。
第一种原因可能是因为Linux网络内核对本地端口号范围有限制。此时,进一步分析为什么无法建立TCP连接,会发现问题出在connect()调用返回失败,查看系统错误提示消息是“Can’t assign requestedaddress”。同时,如果在此时用tcpdump工具监视网络,会发现根本没有TCP连接时客户端发SYN包的网络流量。这些情况说明问题在于本地Linux系统内核中有限制。其实,问题的根本原因在于Linux内核的TCP/IP协议实现模块对系统中所有的客户端TCP连接对应的本地端口号的范围进行了限制(例如,内核限制本地端口号的范围为1024~32768之间)。

当系统中某一时刻同时存在太多的TCP客户端连接时,由于每个TCP客户端连接都要占用一个唯一的本地端口号(此端口号在系统的本地端口号范围限制中),如果现有的TCP客户端连接已将所有的本地端口号占满,则此时就无法为新的TCP客户端连接分配一个本地端口号了,因此系统会在这种情况下在connect()调用中返回失败,并将错误提示消息设为“Can’t assignrequested address”。有关这些控制逻辑可以查看Linux内核源代码,以linux2.6内核为例,可以查看tcp_ipv4.c文件中如下函数:

static int tcp_v4_hash_connect(struct sock *sk)

请注意上述函数中对变量sysctl_local_port_range的访问控制。变量sysctl_local_port_range的初始化则是在tcp.c文件中的如下函数中设置:

void __init tcp_init(void)

内核编译时默认设置的本地端口号范围可能太小,因此需要修改此本地端口范围限制。
第一步,修改/etc/sysctl.conf文件,在文件中添加如下行:

net.ipv4.ip_local_port_range = 1024 65000

这表明将系统对本地端口范围限制设置为1024~65000之间。请注意,本地端口范围的最小值必须大于或等于1024;而端口范围的最大值则应小于或等于65535。修改完后保存此文件。
第二步,执行sysctl命令:

[speng@as4 ~]$ sysctl -p

如果系统没有错误提示,就表明新的本地端口范围设置成功。如果按上述端口范围进行设置,则理论上单独一个进程最多可以同时建立60000多个TCP客户端连接。

第二种无法建立TCP连接的原因可能是因为Linux网络内核的IP_TABLE防火墙对最大跟踪的TCP连接数有限制。此时程序会表现为在 connect()调用中阻塞,如同死机,如果用tcpdump工具监视网络,也会发现根本没有TCP连接时客户端发SYN包的网络流量。由于 IP_TABLE防火墙在内核中会对每个TCP连接的状态进行跟踪,跟踪信息将会放在位于内核内存中的conntrackdatabase中,这个数据库的大小有限,当系统中存在过多的TCP连接时,数据库容量不足,IP_TABLE无法为新的TCP连接建立跟踪信息,于是表现为在connect()调用中阻塞。此时就必须修改内核对最大跟踪的TCP连接数的限制,方法同修改内核对本地端口号范围的限制是类似的:
第一步,修改/etc/sysctl.conf文件,在文件中添加如下行:

net.ipv4.ip_conntrack_max = 10240

这表明将系统对最大跟踪的TCP连接数限制设置为10240。请注意,此限制值要尽量小,以节省对内核内存的占用。

第二步,执行sysctl命令:

[speng@as4 ~]$ sysctl -p

如果系统没有错误提示,就表明系统对新的最大跟踪的TCP连接数限制修改成功。如果按上述参数进行设置,则理论上单独一个进程最多可以同时建立10000多个TCP客户端连接。

3、使用支持高并发网络I/O的编程技术
在Linux上编写高并发TCP连接应用程序时,必须使用合适的网络I/O技术和I/O事件分派机制。
可用的I/O技术有同步I/O,非阻塞式同步I/O(也称反应式I/O),以及异步I/O。在高TCP并发的情形下,如果使用同步I/O,这会严重阻塞程序的运转,除非为每个TCP连接的I/O创建一个线程。但是,过多的线程又会因系统对线程的调度造成巨大开销。因此,在高TCP并发的情形下使用同步 I/O是不可取的,这时可以考虑使用非阻塞式同步I/O或异步I/O。非阻塞式同步I/O的技术包括使用select(),poll(),epoll等机制。异步I/O的技术就是使用AIO。
从I/O事件分派机制来看,使用select()是不合适的,因为它所支持的并发连接数有限(通常在1024个以内)。如果考虑性能,poll()也是不合适的,尽管它可以支持的较高的TCP并发数,但是由于其采用“轮询”机制,当并发数较高时,其运行效率相当低,并可能存在I/O事件分派不均,导致部分TCP连接上的I/O出现“饥饿”现象。而如果使用epoll或AIO,则没有上述问题(早期Linux内核的AIO技术实现是通过在内核中为每个 I/O请求创建一个线程来实现的,这种实现机制在高并发TCP连接的情形下使用其实也有严重的性能问题。但在最新的Linux内核中,AIO的实现已经得到改进)。
综上所述,在开发支持高并发TCP连接的Linux应用程序时,应尽量使用epoll或AIO技术来实现并发的TCP连接上的I/O控制,这将为提升程序对高并发TCP连接的支持提供有效的I/O保证。

内核参数sysctl.conf的优化

/etc/sysctl.conf 是用来控制linux网络的配置文件,对于依赖网络的程序(如web服务器和cache服务器)非常重要,RHEL默认提供的最好调整。

推荐配置(把原/etc/sysctl.conf内容清掉,把下面内容复制进去):

net.ipv4.ip_local_port_range = 1024 65536
net.core.rmem_max=16777216
net.core.wmem_max=16777216
net.ipv4.tcp_rmem=4096 87380 16777216
net.ipv4.tcp_wmem=4096 65536 16777216
net.ipv4.tcp_fin_timeout = 10
net.ipv4.tcp_tw_recycle = 1
net.ipv4.tcp_timestamps = 0
net.ipv4.tcp_window_scaling = 0
net.ipv4.tcp_sack = 0
net.core.netdev_max_backlog = 30000
net.ipv4.tcp_no_metrics_save=1
net.core.somaxconn = 262144
net.ipv4.tcp_syncookies = 0
net.ipv4.tcp_max_orphans = 262144
net.ipv4.tcp_max_syn_backlog = 262144
net.ipv4.tcp_synack_retries = 2
net.ipv4.tcp_syn_retries = 2

这个配置参考于cache服务器varnish的推荐配置和SunOne 服务器系统优化的推荐配置。

varnish调优推荐配置的地址为:http://varnish.projects.linpro.no/wiki/Performance

不过varnish推荐的配置是有问题的,实际运行表明“net.ipv4.tcp_fin_timeout = 3”的配置会导致页面经常打不开;并且当网友使用的是IE6浏览器时,访问网站一段时间后,所有网页都会打不开,重启浏览器后正常。可能是国外的网速快吧,我们国情决定需要调整“net.ipv4.tcp_fin_timeout = 10”,在10s的情况下,一切正常(实际运行结论)。

修改完毕后,执行:

/sbin/sysctl -p /etc/sysctl.conf
/sbin/sysctl -w net.ipv4.route.flush=1

命令生效。为了保险起见,也可以reboot系统。

调整文件数:
linux系统优化完网络必须调高系统允许打开的文件数才能支持大的并发,默认1024是远远不够的。

执行命令:

Shell代码
echo ulimit -HSn 65536 >> /etc/rc.local
echo ulimit -HSn 65536 >>/root/.bash_profile
ulimit -HSn 65536

Nginx 安装与配置

nginx 安装配置

1.配置yum 源:

cd /etc/yum.repos.d/ vim nginx.repo

 

[nginx]

name=nginx repo

baseurl=http://nginx.org/packages/centos/$releasever/$basearch/ gpgcheck=0

enabled=1

/etc/yum.repos.d 目录下产生了yum源nginx.repo文件。

安装运行:

yum install nginx –y

 

/etc/init.d/nginx start

测试访问访问,默认http 端口为80.

如果还无法访问,一般就需要修改下防火墙啦。

iptables -I INPUT 5 -i eth0 -p tcp --dport 80 -m state --state NEW,ESTABLISHED -j ACCEPT

service iptables save

service iptables restart

Nginx的命令以及配置文件位置:

/etc/init.d/nginx start # 启动Nginx服务

/etc/init.d/nginx stop # 停止Nginx服务

/etc/nginx/nginx.conf # Nginx配置文件位置

至此,Nginx已经全部配置安装完成。

下面来说说其配置:nginx 启动的时候需要/etc/nginx/nginx.conf  这个配置文件,但是里面内容怎么配置大伙还是自己去看吧,因为配置项实在太多,就在使用过程中踩坑过吧,哈,给个本人初学的比较详尽的配置讲解地址,有时间自个去看,就着源码吃下去味道更好。

 http://developer.51cto.com/art/201004/194472.htm

网络编程大并发实战-C100K

上篇进行了一个C10K的实战,并得到了一个初步的测试数据来支撑C10K的结果,那么本篇我们进行一个升级版C100K的实战。

对此我们还是使用之前的xlink 库,测试下epoll 及aio 的性能能否达到我们续期,C100K。

epoll

对于C100K问题,首先我们要解决的不是程序问题,而是系统设置问题,连接数的限制,文件句柄的限制,同时,对于客户端来说,由于单个IP端口数的限制,端口是16位的其最大端口数为 2的16次方全部利用起来也就只能支持60K多的连接(如果是土豪有很多测试机器可以当客户端端口限制基本可以忽略,不够测试就堆机器吧)针对如上问题解决如下:

句柄问题:

1.修改/etc/security/limits.conf (改为1024000 <NR_OPEN是为之后 1M问题做准备),

* soft nofile 1024000
* hard nofile 1024000

 

2.修改/etc/sysctl.conf:

fs.file-max = 1048576

[root@localhost ~]# sysctl -p
端口问题(客户端):

将可用端口全部开放,设置/etc/sysctl.conf:

net.ipv4.ip_local_port_range = 1024 65535

的只这样还是不够C100K,所以还需要多加几块网卡,或者添加虚拟地址。好吧咱就用最实惠的方式加虚拟IP吧。

还有一些TCP参数调优的东西,C100K就不列了,毕竟也就是个初步测试,主要是检验下实战代码的,咱就略了,等到C1M再说。

环境还是同前述,就直接来看看结果吧:

连接数 102399 注册数 102399
连接耗时
(ms)
2893 注册耗时
(ms)
6563

结论:C100K在3秒左右就轻松解决,而且该机器性能一般,同时该C100K全部活跃的QPS测试出来大致达到了20K/S。所以C100K中有20K的活跃度都是轻松搞定。

本来还想拿去年写的IOCP 压一下出个结果,看来还是放到接下来的C1M问题去试试吧,C100K应该完成没压力。(主要原因是懒,还没想好把IOCP跟xlink 合并在一起)。就先这样吧,C100K这种对于当前金融公司的产品应该完全够用了,基本都不需要分布式了,当然对于像淘宝这种有大客户体量的公司来说,只是C100K是不够的。接下来就试试C1M吧。

网络编程大并发实战-C10K

网络通信是由用户需求推动,早期,大家都玩单机游戏,上个互联网有个上百人就算是比较大型应用,当时并没有那么大的需求。但是,随着PC普及,互联网的爆发期,游戏经历单机,到局域网,到互联网多人大型游戏;互联网的发展也从1.0的浏览下网页,到2.0的各种社群交互互动,C10K问题首先涌现。

问题

早期的IO模型基本都是一个进程或者一个线程一个连接,C10K,物理资源是他的天花板。不管是否分布式,那都是及其耗费物理资源,成本巨大。

分析

既然问题是由于进程或者线程引起的物理资源问题,那么突破口也资源这里,IO线程模型使用就是让一个进程或者线程处理多个连接,网络IO就使用多路复用。在早期由一个比较成熟的就是select 模型,而后又发展出了poll,epoll,同时还有aio,这些在前面章节都有涉及,在这里我们就直接上代码来验证他们能否解决C10K问题,并进行分析。在此我们通过写一个简单的网络库,来验证各种实现方式模型是否可行。简单就叫其link吧。使用reactor 模式,该库包含select,poll,epoll等。线程模型为单线程接受处理,单线程接受多线程处理通信两种方式。统一level_tringer触发,统一处理成事件回调模式。通过测试对比每种实现方式来看看是如何解决C10K,及其中的一些问题及奥秘的。

select

在 linux 下使用select 有诸多连接限制,并且修改麻烦,同时 select 模型 在windows 也是参照linux下的select 模型移植过来的,并且windows 下只要重新定义 FD_SETSIZE 大小就可以改变原来 每个 接收所以就在本机windows 下测试。

逻辑:建立完成所有连接后,所有连接同时发送一个业务注册包,服务收到后立马应答。

客户端发送10239个链接,服务端稳定的接收了该10239个连接并注册成功,但是其响应时间有点延迟:

结果:内存占用连接数10239 大概100M,CPU 0- 5%的波动。

连接数 127 1023 10239 注册数 128/128 1023/1023 10239/
10239
连接耗时(MS) 23 179 4230 注册耗时
(MS)
16 36 7590

测试环境:以上测试数据在两台局域网的PC机器中进行,i5双核4线程,8G内存,其中还开了其他应用不是非常纯净,服务器测试程序开了1个连接接收线程,4个连接处理线程。

分析:同时测试服务器应该还有可改进的空间,为了能把select ,epoll等所有模型都放到LINK 库中,回调嵌套太多,内存拷贝等还有很多可优化,同时服务器调优下。所以如果再认真点做好,windows 下的这个 非阻塞 select 模型C10K 都应该不是问题,至少连接上,可以处理,但是发包应答的延迟还是挺致命的,有机会后续可以优化看看,那么如果直接使用windows提供的eventselect 模型应该更加容易达成这个结果了。当然这个结果已经出乎意料,linux 下的select 没有试过,但是windows下的这个结果,

结论:select模型下,初略估算C10K的连接,每秒2000的活跃连接并发还是能轻松支持。

epoll

架构模型同上面的,在做这个测试之前我们其实已经知道epoll 是高效能的,所以100,1000级别的咱就没必要测试了,直接看下 C10K的测试结果

结果:内存占用连接数10239 大概79M,CPU 0- 5%的波动。

连接数 10239 注册数 10239
连接耗时(ms) 469 注册耗时
(ms)
454
测试环境:Intel(R) Xeon(R) CPU E5-2430 0 @ 2.20GHz,4核,8G内存,测试服务器非常不纯净,将就用,服务器测试程序开了1个连接接收线程,4个连接处理线程。
结论:还是非常轻松的就能支持C10K连接,并且并发轻易上20K。

总结:

改测试库写的非常仓促,还是有很多改进空间。如果只是需要C10K的连接数的话,select 跟epoll 都能满足需求,就目前测试情况来看,select 也没有传说中的不稳定,还是挺稳定,但是select 并活跃并发数虽然也能上到 10K,但是其相应时间应该已经超出了正常业务场景的需求,所以如果并发数量不多,使用select 也能一战,这也是为什么以前的游戏服务器,虽然 epoll ,IOCP已经成熟的情况下,select 还是能撑起半壁江山,也是不无道理。所以如果要上生产大并发的话,epoll 还是不二选择。