多进程/多线程相关

Linux对待多进程/多线程

Linux下不管是多线程还是多进程,最终均由do_fork()实现,只是进程创建时参数不同,从而导致不同的共享环境 多进程是立体交通系统,虽然造价高,上坡下坡多耗点油,但是不堵车()。 多线程是平面交通系统,造价低,但红绿灯(锁、访问控制)太多,老堵车。

  • 立体:进程间地址空间相互独立(copy-on-write)
  • 造价高:copy父进程地址空间
  • 通信:管道/fifo/共享内存/消息队列/信号量
  • 堵车少:通信顺畅
  • 造价低:不用copy资源
  • 堵车:多个线程争夺临界资源,难获得锁
  • linux的线程实现是在核外进行的,核内提供的是创建进程的接口do_fork()
  • 内核提供了两个系统调用__clone()fork()
  • 创建线程(pthread_create()实际调用fork)时,会依据不同的属性调用__clone(),并将参数传给do_fork.

从而创建的”进程”拥有共享的运行环境,仅栈是独立的,由__clone()传入。

为什么要使用多线程?

Linux线程在核内是以轻量级进程的形式存在的,拥有独立的进程表项,而所有的创建、同步、删除等操作都在核外pthread库中进行

在一个程序中,有很多操作非常耗时,如数据库读写,IO操作等,若使用单线程,那么程序就必须等待这些操作执行完成后才能执行其他操作。多线程可以避免这个问题。

多线程的优势和缺点

优势

  • 创建和终止
  • 上下文切换
  • 数据共享
  • 多核优势
  • 自然的编程模型:将工作分为多个模块,每个模块分配一个或多个执行单元

    缺点

  • 某个线程崩溃,整个进程崩溃

  • 其作为一种并发编程模型,效率并没有那么高,复杂度高,易出错,难以测试和定位错误
  • 复杂同步机制
  • 多线程四大陷阱
    • 死锁
    • 饿死
    • 活锁
    • 竞态条件

      Epoll相关面试题

      I/O多路复用是什么意思?

      文件描述符:当程序打开一个现有文件或创建一个新文件,内核向进程返回一个文件描述符

IO多路复用是一种同步IO模型,实现一个进程可以监视多个文件句柄(socket/文件/管道等),一旦某个文件句柄就绪,就能够通知程序进行相应的读写操作。
IO多路复用免去了多线程/进程之间的切换开销

端口和地址复用setsockopt

端口复用允许一个应用程序把多个套接字(IP:端口)绑定在一个端口上而不出错。

为什么要有端口复用?

为什么等待2MSL: 避免前后两个使用相同四元组的连接中的前一个连接的报文干扰后一个连接,即让此次TCP连接中的所有报文在网络中消失。

服务端主动结束后,在第三次握手后会有个等待释放时间(TIME_WAIT),这个时间大概为1-4分钟(2MSL),在这个时间段内,端口不会被释放。
SO_REUSEADDR允许单个进程绑定相同的端口到多个socket上,但每个socket绑定的ip地址不同

为什么一个端口可以建立多个链接

一个TPC连接由四元组组成

作为一个服务器监控一个端口,比如80端口,它为什么可以建立上百万个连接?首先要明白一点,当accept出来后的新socket,它所占用的本地端口依然是80端口

Select相关:

底层为fd_set数据结构,本质是一个long类型数组,每个元素都对应一个文件描述符,通过轮询所有文件描述符来判断是否有事件发生

优点:

  • 可移植性好
  • 连接数少且都很活跃的情况下,效率不错

    缺点

  • 可监听最大文件描述1024

  • 通过轮询检查事件是否发生

    POLL

    和select差不多,不过没有最大文件描述符限制,但依然采用轮询遍历

epoll读到一半又有新事件来了怎么办?

避免主线程epoll再次监听到同一可读事件,造成两个线程工作冲突。
解决:
将对应描述符设置为EPOLL_ONESHOT,效果:监听到一次事件后就将对应的描述符从监听集合中移除,也就不会再被监听到。根据判定是否再次将次描述符加入set。

Epoll两种触发模式的区别

  • 边沿触发

当被监控的文件描述符有【可读写事件】发生,epoll_wait()会通知处理程序,若没把数据读写完,也不会通知第二次。系统不会充斥大量你不关心的就绪文件描述符

  • 水平触发

只要读写缓冲区中有数据,则就会触发epoll_wait!

为什么用红黑树而不用AVL

红黑树和AVL:

  • 查找操作:二分查找,O(logn)
  • 插入操作:O(logn),但红黑树的任何不平衡会在2次旋转内解决(插入2次)
  • 删除操作:O(logn),但红黑树的任何不平衡会在3次旋转内解决(删除3次)

相同点:

  • 若插入引起了不平衡,AVL与红黑树均最多2次旋转使其平衡;

不同点:

  • 删除引起的不平衡,红黑树最多3次旋转内解决,而AVL树需从最深的不平衡节点来调整这个路径上所有节点的平衡性,因此需要若干次旋转
  • AVL树是高度平衡二叉树,故AVL查询速度快些。RB树会比AVL多一次查找
  • AVL树在大量数据插入和删除时,AVL树调整的次数多得多。

    答:

    由于AVL树在大量插入和删除时,AVL树调整的此处多得多,所以在大量结点增删时,效率比红黑树低。

    高并发易产生内存碎片过多的问题,原因和解决办法

    内存碎片

  • 内部碎片:由于采用固定大小的内存分区,当一个进程不能完全使用分给它固定的内存区域时便产生内部碎片,通常难以避免

  • 外部碎片:某些未分配的连续内存区域太小,以至于不能满足任意进程的内存分配请求,从而不能被利用的内存区域

    解决办法

    分页管理机制:将物理内存平均分割,并维护一个内存信息表

创建一个内存池,将哈希映射的内存池分为四个部分,对齐数分别为8、16、128、512,将这几部分内碎片进行一定程度的控制,外碎片通过合并解决

STL中的内存碎片问题

第一级配置器

以malloc,free,realloc等c函数执行实际的内存配置,释放,重新配置等操作;当内存需求不被满足时,会调用一个指定函数去处理

第二级配置器(可避免外部碎片,以128B为分界)

若分配区块大于128B,则交予第一级配置器处理。
若分配区块<128B,则以**内存池管理;**每次配置一大块内存,并维护对应的16个空闲链表(分别管理大小为8、16、24、...120、128)。下次若有相同大小的内存需求,则直接从free-list中取,若有小额区块被释放,则由配置器回收到free-list
https://blog.csdn.net/huangyimo/article/details/78947875?utm_medium=distribute.pc_relevant_bbs_down.none-task-blog-baidujs-1.nonecase&depth_1-utm_source=distribute.pc_relevant_bbs_down.none-task-blog-baidujs-1.nonecase

ET模式下使用EPOLLONESHOT

EPOLLONESHOT

为相关的文件描述符设置一次性行为。这意味着在使用epoll_wait(2)提取事件后,关联的文件描述符在内部被禁用,并且epoll接口不会报告其他事件。用户必须使用EPOLL_CTL_MOD调用epoll_ctl(),以使用新的事件掩码(events)重新武装文件描述符。

问题

如果是多线程在处理,一个SOCKET事件到来,数据开始解析,这时候这个SOCKET又来了同样一个这样的事件,而你的数据解析尚未完成,那么程序会自动调度另外一个线程或者进程来处理新的事件,这造成一个很严重的问题,不同的线程或者进程在处理同一个SOCKET的事件,这会使程序的健壮性大降低而编程的复杂度大大增加!!即使在ET模式下也有可能出现这种情况!!

解决方案

第一种

单独的线程解析数据,接受数据线程处理完之后立刻将数据转移至另外的线程

第二种

EPOLLET + EPOLLONESHOT
处理写成当前的SOCKET后不再重新注册相关事件,那么这个事件就不再响应了或者说触发了。要想重新注册事件则需要调用epoll_ctl重置文件描述符上的事件,这样前面的socket就不会出现竞态这样就可以通过手动的方式来保证同一SOCKET只能被一个线程处理,不会跨越多个线程。

blocking(默认)和nonblock模式下read/write行为的区别

一般来说,服务器会采用线程池+Nonblock I/O+多路IO复用(EPOLL)

几个重要结论

NONBLOCK BLOCK 共性
read 【接收缓冲区】空时,等待 【接收缓冲区】空时,立即返回-1(errno=EAGAIN,EWOULDBLOCK) 接收缓冲区有数据时立即返回,而不是等待给定的readbuff
write 【发送缓冲区】放下整个buffer才返回 【发送缓冲区】返回能够放下的字节数,之后调用则返回-1(errno = EAGAIN或EWOULDBLOCK)

情况分析

假设A机器上的一个进程a正在和B机器上的进程b通信:某一时刻a正阻塞在socket的read调用上(或者在nonblock下轮询socket)

  • 当b进程终止时,无论应用程序是否显式关闭了socket(OS会负责在进程结束时关闭所有文件描述符,对于socket,则会发送一个FIN包到对面)
    • ”同步通知“:进程a对已经收到FIN的socket调用read,如果已经读完了receive buffer的剩余字节,则会返回EOF:0
    • ”异步通知“:如果进程a正阻塞在read调用上(前面已经提到,此时receive buffer一定为空,因为read在receive buffer有内容时就会返回),则read调用立即返回EOF,进程a被唤醒。
  • socket在收到FIN后,虽然调用read会返回EOF,但进程a依然可以其调用write,因为根据TCP协议,收到对方的FIN包只意味着对方不会再发送任何消息
  • 假如b进程是异常终止的,发送FIN包是OS代劳的,b进程已经不复存在,当机器再次收到该socket的消息时,会回应RST(因为拥有该socket的进程已经终止)。a进程对收到RST的socket调用write时,操作系统会给a进程发送SIGPIPE,默认处理动作是终止进程

    强枚举类型避免【不同错误代码之间进行比较】

    在标准C++中,枚举类型不是类型安全的。枚举类型被视为整数,这使得两种不同的枚举类型之间可以进行比较C++11 引进了一种特别的 “枚举类”,可以避免上述的问题。使用 enum class 的语法来声明:enum class Enumeration{ Val1, Val2, Val3 = 100, Val4 / = 101 /,};此种枚举为类型安全的。枚举类型不能隐式地转换为整数;也无法与整数数值做比较

    网络相关

    TCP可靠机制

  • 三次握手四次挥手

  • 超时重传
  • 拥塞控制(拥塞窗口cwnd)
    • 慢恢复(2倍)
    • 拥塞避免(设置拥塞门槛为当前cwnd的一半,cwnd=1)
    • 快重传(3次ACK)
    • 快恢复(设置拥塞门槛和cwnd为当前cwnd的一半)
  • 流量控制(滑动窗口swnd)

    MTU,MSS说下,如何确定MSS?

  • Maximum Transit Unit,一般是物理接口(数据链路层)限制ip层传输的最大报文段长度,缺省=1500B;

    • 当IP层传输的报文段>1500时,需要分片,这些片的IP Header ID相同
  • Maximum Segment Size,指的是IP层限制TCP传输的最大分段大小,仅包含TCP Payload;

    • 用来限制应用层最大发送字节数
    • 若MTU=1500B,MSS=1500-20-20=1460;
    • tcp三次握手确定MSS。
      • 取双方协商的最小者!

        TIME_WAIT说下?挥手的状态变化

        TIME_WAIT主要为了确保该TCP连接中发送的所有报文消失在网络中,以免旧TCP连接的报文影响新连接的报文。
        TIME_WAIT一般在FIN发起方。
        挥手状态变化:
  • 客户端

    • 终止等待1
    • 终止等待2
    • TIME-WAIT
    • CLOSED
  • 服务器

    • 关闭等待
    • 最后确认(LAST-ACK)
    • 关闭

      OSI 7层模型每层报文报头长度

  • 传输层

    • TCP: 20~40
    • UDP:8
  • 网络层
    • IP:20~60
    • ICMP
    • ARP地址解析:8
  • 链路层
    • 以太帧:18
  • 物理层

    网络编程相关

    说说非阻塞socket 的connect

    对于linux 服务器编程有如下系统调用:

  • 服务器:socket bind listen accept read write

  • 客户端:socket connect read write

对于客户端的connect来说,有如下几点:

若此时为TCP连接

  • connect函数会触发【三次握手】
  • connect超时延迟为75s
  • connect默认为阻塞状态,若服务器无响应,则客户端需等待75s才能返回

故客户端需要额外处理任务时,可将socket设置为非阻塞

  1. int flag=fcntl(cfd,F_GETFL,0);
  2. if(flag<0) perror("fcntl");
  3. fcntl(cfd,F_SETFL,flag | O_NONBLOCK);
  4. /* 此时调用connect,若连接操作无法完成,则立即返回-1,并设置errno*/
  5. int ret=connect(cfd,(struct sockaddr*)&s_addr,addr_len);
  6. while(ret<0){
  7. if(errno == EINPROGRESS ){
  8. /*正在连接中。。。*/
  9. }else{
  10. perror("connect");
  11. exit(0);
  12. }
  13. }

非阻塞connect的问题:

  • 连接建立成功时,socket描述符变为可写(连接建立时,写缓冲区空闲,所以可写)
  • 连接建立失败时,socket描述符变为可读可写(未决的错误导致)

    int getsockopt(int socket, int level, int option_name void *restrict option_value, socklen_t *restrict option_len);

  • 功能:获取一个套接字的选项
  • level:协议层次
    • SOL_SOCKET: 套接字层次
    • IPPROTO_IP: ip层次
    • IPPROTO_TCP : TCP层次
  • option_name : 选项名称
    • image.png

image.png

  • option_value :获取选项的值
  • option_len: value的长度

select epoll测试(非法端口测试connect失败时的情况)

HTTP管线化

image.png

  • 图中第一种请求方式,就是单次发送request请求,收到response后再进行下一次请求,显示是很低效的。
  • 于是http1.1提出了管线化(pipelining)技术,就是如图中第二中请求方式,一次性发送多个request请求。
  • 然而pipelining在接收response返回时,也必须依顺序接收,如果前一个请求遇到了阻塞,后面的请求即使已经处理完毕了,仍然需要等待阻塞的请求处理完毕。这种情况就如图中第三种,第一个请求阻塞后,后面的请求都需要等待,这也就是队头阻塞(Head of line blocking)。
  • 为了解决上述阻塞问题,http2中提出了多路复用(Multiplexing)技术,Multiplexing是通信和计算机网络领域的专业名词。http2中将多个请求复用同一个tcp链接中,将一个TCP连接分为若干个流(Stream),每个流中可以传输若干消息(Message),每个消息由若干最小的二进制帧(Frame)组成。也就是将每个request-response拆分为了细小的二进制帧Frame,这样即使一个请求被阻塞了,也不会影响其他请求,如图中第四种情况所示。

    概念

  • HTTP管线化是将多个HTTP要求(request)整批提交的技术,而在传送过程中不需先等待服务端的回应。管线化机制须通过永久连接(persistent connection)完成,仅HTTP/1.1支持此技术(HTTP/1.0不支持),并且只有GET和HEAD要求可以进行管线化,而POST则有所限制。此外,初次创建连接时也不应启动管线机制,因为对方(服务器)不一定支持HTTP/1.1版本的协议。

  • 浏览器将HTTP要求大批提交可大幅缩短页面的加载时间,特别是在传输延迟(lag/latency)较高的情况下(如卫星连接)。此技术之关键在于多个HTTP的要求消息可以同时塞入一个TCP分组中,所以只提交一个分组即可同时发出多个要求,借此可减少网络上多余的分组并降低线路负载。

    与keepalive区别

    持久连接使得多数请求以管线化(pipelining)方式发送成为可能。从前发送请求后需等待并收到响应,才能发送下一个请求。【管线化技术出现后,不用等待响应亦可直接发送下一个请求。】这样就能够做到同时并行发送多个请求,而不需要一个接一个地等待响应了。

    与http2.0多路复用区别

    http2.0通过stream支持连接的多路复用,即二进制协议。

  • stream用唯一id来标识

  • stream会确定好frame的顺序,另一端按照顺序来处理
  • stream可被任意一端关闭
  • stream可单方面使用或共享使用
  • 一条连接可以包含多个stream