阿里二面面经
1.windows/linux,多线程/多进程
IBM测试,切换线程context的时候,windows比linux快一倍多。进出最快的锁(windows2k的 critical section和linux的pthread_mutex),windows比linux的要快五倍左右。可见多线程这个具体的领域内,linux还是稍逊windows一点。这应该是情有可原的,毕竟unix家族都是从多进程过来的,而 windows从头就是多线程的。
2.多线程的优缺点
(1). 一个进程中所有的线程共享全局变脸和内存
(2). 程序逻辑和控制方便
(3). 线程方式消耗的总资源比进程要好
(1). 每个线程和主程序共用地址空间,受限于2G地址空间
(2). 一个线程的崩溃可能影响到整个程序的稳定性;
(3). 到达一定的线程数程度后,即使再增加CPU也无法提高性能,例如Windows Server 2003,大约是1500个左右的线程数就快到极限了(线程堆栈设定为1M),如果设定线程堆栈为2M,还达不到1500个线程总数; 线程能够提高的总性能有限,而且线程多了之后,线程本身的调度也是一个麻烦事儿,需要消耗较多的CPU
3.多进程的优缺点
(1). 多个进程相互独立,不影响程序的稳定性,子进程崩溃没关系
(2). 通过增加cpu可以扩充性能
(3). 每个子进程都有2GB地址空间和相关资源,总体能够到达的性能会比较大
(4). 可以尽量减少线程加锁/解锁的影响,极大提高性能,就算是线程运行的模块算法效率低也没关系;
(1). 逻辑控制复杂,需要和主程序交互;
(2). 需要跨进程边界,如果有大数据量传送,就不太好,适合小数据量传送、密集运算
(3). 多进程调度开销比较大
最好是多进程和多线程结合,即根据实际的需要,每个CPU开启一个子进程,这个子进程开启多线程可以为若干同类型的数据进行处理。当然你也可以利用多线程+多CPU+轮询方式来解决问题……方法和手段是多样的,关键是自己看起来实现方便有能够满足要求,代价也合适
3.并发与程序类型
服务器并发:大量客户端同时访问,响应多个客户端。
对于人数少的,数据交互量小的,单进程,单工作线程即可处理。
对于单核cpu,开多工作线程,在线程切换上是很影响效率的,但是对于不同的程序,单核多线程也可提核服务器的工作效率。
io密集型程序:工作线程处理的回调函数,处于io阻塞状态,比如阻塞状态的send recv,数据库读写操做等,此时的io操做会阻塞线程,此时切换线程处理任务,可以提升服务器效率。io密集型程序由于大部分时间处于阻塞操做,cpu利用率很低,加多线程,可提升cpu利用率,利用多线程屏蔽掉线程阻塞时间。
cpu密集型程序:cpu利用率较高,程序任务并没有太多io操做,多线程对其并没有多大提升,相反,过多线程的切换,反而会浪费cpu时间,降低服务器效率
4.进程和线程上下文切换
进程切换分两步
1.切换页目录以使用新的地址空间。
2.切换内核栈和硬件上下文。
对于linux来说,线程和进程的最大区别就在于地址空间。对于线程切换,第1步是不需要做的,第2是进程和线程切换都要做的。所以明显是进程切换代价大。
线程上下文切换和进程上下问切换一个最主要的区别是线程的切换虚拟内存空间依然是相同的,但是进程切换是不同的。这两种上下文切换的处理都是通过操作系统内核来完成的。内核的这种切换过程伴随的最显著的性能损耗是将寄存器中的内容切换出。另外一个隐藏的损耗是上下文的切换会扰乱处理器的缓存机制。简单的说,一旦去切换上下文,处理器中所有已经缓存的内存地址一瞬间都作废了。还有一个显著的区别是当你改变虚拟内存空间的时候,处理的页表缓冲(processor’s Translation Lookaside Buffer (TLB))或者相当的神马东西会被全部刷新,这将导致内存的访问在一段时间内相当的低效。但是在线程的切换中,不会出现这个问题。
5.线程安全和可重入
线程安全:要确保函数线程安全,主要需要考虑的是线程之间的共享变量。属于同一进程的不同线程会共享进程内存空间中的全局区和堆,而私有的线程空间则主要包括栈和寄存器。因此,对于同一进程的不同线程来说,每个线程的局部变量都是私有的,而全局变量、局部静态变量、分配于堆的变量都是共享的。在对这些共享变量进行访问时,如果要保证线程安全,则必须通过加锁的方式。
可重入:概念基本没有比较正式的完整解释,但是它比线程安全要求更严格。根据经验,所谓“重入”,常见的情况是,程序执行到某个函数foo()时,收到信号,于是暂停目前正在执行的函数,转到信号处理函数,而这个信号处理函数的执行过程中,又恰恰也会进入到刚刚执行的函数foo(),这样便发生了所谓的重入。此时如果foo()能够正确的运行,而且处理完成后,之前暂停的foo()也能够正确运行,则说明它是可重入的
可重入与线程安全并不等同,一般说来,可重入的函数一定是线程安全的,但反过来不一定成立。
6. C10K问题的解决方案探讨
要解决这一问题,从纯网络编程技术角度看,主要思路有两个:
- 一个是对于每个连接处理分配一个独立的进程/线程;
- 另一个思路是用同一进程/线程来同时处理若干连接。
1思路一:每个进程/线程处理一个连接
这一思路最为直接。但是由于申请进程/线程会占用相当可观的系统资源,同时对于多进程/线程的管理会对系统造成压力,因此这种方案不具备良好的可扩展性。
因此,这一思路在服务器资源还没有富裕到足够程度的时候,是不可行的。即便资源足够富裕,效率也不够高。总之,此思路技术实现会使得资源占用过多,可扩展性差。
2思路二:每个进程/线程同时处理多个连接(IO多路复用)
IO多路复用从技术实现上又分很多种,我们逐一来看看下述各种实现方式的优劣。
● 实现方式1:传统思路最简单的方法是循环挨个处理各个连接,每个连接对应一个 socket,当所有 socket 都有数据的时候,这种方法是可行的。但是当应用读取某个 socket 的文件数据不 ready 的时候,整个应用会阻塞在这里等待该文件句柄,即使别的文件句柄 ready,也无法往下处理。
实现小结:直接循环处理多个连接。
问题归纳:任一文件句柄的不成功会阻塞住整个应用。
● 实现方式2:select要解决上面阻塞的问题,思路很简单,如果我在读取文件句柄之前,先查下它的状态,ready 了就进行处理,不 ready 就不进行处理,这不就解决了这个问题了嘛?于是有了 select 方案。用一个 fd_set 结构体来告诉内核同时监控多个文件句柄,当其中有文件句柄的状态发生指定变化(例如某句柄由不可用变为可用)或超时,则调用返回。之后应用可以使用 FD_ISSET 来逐个查看是哪个文件句柄的状态发生了变化。这样做,小规模的连接问题不大,但当连接数很多(文件句柄个数很多)的时候,逐个检查状态就很慢了。因此,select 往往存在管理的句柄上限(FD_SETSIZE)。同时,在使用上,因为只有一个字段记录关注和发生事件,每次调用之前要重新初始化 fd_set 结构体。
1 |
|
实现小结:有连接请求抵达了再检查处理。
问题归纳:句柄上限+重复初始化+逐个排查所有文件句柄状态效率不高。
● 实现方式3:poll 主要解决 select 的前两个问题:通过一个 pollfd 数组向内核传递需要关注的事件消除文件句柄上限,同时使用不同字段分别标注关注事件和发生事件,来避免重复初始化。
实现小结:设计新的数据结构提供使用效率。
问题归纳:逐个排查所有文件句柄状态效率不高。
● 实现方式4:epoll既然逐个排查所有文件句柄状态效率不高,很自然的,如果调用返回的时候只给应用提供发生了状态变化(很可能是数据 ready)的文件句柄,进行排查的效率不就高多了么。epoll 采用了这种设计,适用于大规模的应用场景。实验表明,当文件句柄数目超过 10 之后,epoll 性能将优于
select 和 poll;当文件句柄数目达到 10K 的时候,epoll 已经超过 select 和
poll 两个数量级。
实现小结:只返回状态变化的文件句柄。
问题归纳:依赖特定平台(Linux)。
因为Linux是互联网企业中使用率最高的操作系统,Epoll就成为C10K killer、高并发、高性能、异步非阻塞这些技术的代名词了。FreeBSD推出了kqueue,Linux推出了epoll,Windows推出了IOCP,Solaris推出了/dev/poll。这些操作系统提供的功能就是为了解决C10K问题。epoll技术的编程模型就是异步非阻塞回调,也可以叫做Reactor,事件驱动,事件轮循(EventLoop)。Nginx,libevent,node.js这些就是Epoll时代的产物。
● 实现方式5:由于epoll,
kqueue, IOCP每个接口都有自己的特点,程序移植非常困难,于是需要对这些接口进行封装,以让它们易于使用和移植,其中libevent库就是其中之一。跨平台,封装底层平台的调用,提供统一的 API,但底层在不同平台上自动选择合适的调用。按照libevent的官方网站,libevent库提供了以下功能:当一个文件描述符的特定事件(如可读,可写或出错)发生了,或一个定时事件发生了,libevent就会自动执行用户指定的回调函数,来处理事件。目前,libevent已支持以下接口/dev/poll, kqueue, event ports, select, poll 和 epoll。Libevent的内部事件机制完全是基于所使用的接口的。因此libevent非常容易移植,也使它的扩展性非常容易。目前,libevent已在以下操作系统中编译通过:Linux,BSD,Mac OS X,Solaris和Windows。使用libevent库进行开发非常简单,也很容易在各种unix平台上移植。