Shakespeare

  • 首页

  • 关于

  • 标签

  • 分类

  • 归档

  • 公益 404

  • 搜索

网络IO模型

发表于 2020-06-09 更新于 2020-09-27 评论数: 阅读次数:

同步,阻塞

  • 同步
    • 同步通常是指一个任务的完成依赖于另外一个任务,只有等待被依赖的任务完成后,依赖的任务才能算完成,这是一种可靠的任务序列。要么都成功,要么都失败,两个任务的状态可保持一致。
  • 异步
    • 异步不需要等待被依赖的任务完成,只是通知被依赖的任务要完成什么工作,该任务就立即执行,只要整个任务完成就算完成了。至于被依赖的任务是否完成,另一任务无法确定,所以是不可靠的任务序列。
  • 阻塞
    • 阻塞调用是指咋调用结果返回之前,该线程会被挂起,一直处于等待消息通知,不能执行其他任务,在得到结果后返回。
  • 非阻塞
    • 与阻塞等待结果的方式相反,非阻塞调用若没有在调用后立刻获得调用结果,将会立刻返回。通常非阻塞都会多次调用,所以在提高了CPU利用率的同时,增加了线程切换的次数。

阻塞IO模型

应用程序调用一个IO函数,导致应用程序阻塞,等待数据准备好。从内核拷贝到用户空间,IO函数返回成功指示。

非阻塞IO模型

我们把一个SOCKET接口设置为非阻塞就是告诉内核,当所请求的IO操作无法完成时,不要将进程睡眠,而是返回一个错误。这样我们的IO操作函数将不断的测试数据是否已经准备好,如果没有准备好,继续测试,直到数据准备好为止。

IO复用模型

I/O复用模型会用到select、poll、epoll函数,这几个函数也会使进程阻塞,但是和阻塞I/O所不同的的,这两个函数可以同时阻塞多个I/O操作。而且可以同时对多个读操作,多个写操作的I/O函数进行检测,直到有数据可读或可写时,才真正调用I/O操作函数。

select

select模型维护一个fd_set(File descriptor,long 数组)(维护文件句柄),查询就绪事件并返回。

select系统调用的目的是:在一段指定时间内,监听用户感兴趣的文件描述符上的可读、可写和异常事件。poll和select应该被归类为这样的系统 调用,它们可以阻塞地同时探测一组支持非阻塞的IO设备,直至某一个设备触发了事件或者超过了指定的等待时间——也就是说它们的职责不是做IO,而是帮助 调用者寻找当前就绪的设备。
  • 优点
    在单一线程注册多个socket,并发处理多个IO请求。
  • 存在问题
    1. fd_set在每次调用select都需要拷贝到内核态,拷贝开销大,同时查询操作需要遍历fd_set,速度慢。
    2. fd_set大小被内核宏定义为1024(32位,64位2048),不可变。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      int select(int nfds, fd_set *readfds, fd_set *writefds,
      fd_set *exceptfds, struct timeval *timeout);

      void FD_ZERO(fd_set *fdset);
      //清空集合
      void FD_SET(int fd, fd_set *fdset);
      //将一个给定的文件描述符加入集合之中
      void FD_CLR(int fd, fd_set *fdset);
      //将一个给定的文件描述符从集合中删除
      int FD_ISSET(int fd, fd_set *fdset);
      // 检查集合中指定的文件描述符是否可以读写

      //返回值:失败-1 超时0 成功>0(就绪FD数量)

poll

1
2
3
4
5
6
7
8
9
10
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
//nfds_t nfds 记录数组fds中描述符的总数量
//timeout 等待的超时时间
//fd
typedef struct pollfd {
int fd; // 需要被检测或选择的文件描述符
short events; // 对文件描述符fd上感兴趣的事件
short revents; // 文件描述符fd上当前实际发生的事件
} pollfd_t;
//返回值为就绪文件数量,空为0,错误为1.

poll与select差距不大,用pollfd替代fd_set,解决了文件描述符大小上限问题。struct pollfd *fds fds是一个struct pollfd类型的数组,用于存放需要检测其状态的socket描述符,并且调用poll函数之后fds数组不会被清空;一个pollfd结构体表示一个被监视的文件描述符,通过传递fds指示 poll() 监视多个文件描述符。其中,结构体的events域是监视该文件描述符的事件掩码,由用户来设置这个域,结构体的revents域是文件描述符的操作结果事件掩码,内核在调用返回时设置这个域.

epoll

epoll同样只告知那些就绪的文件描述符,而且当我们调用epoll_wait()获得就绪文件描述符时,返回的不是实际的描述符,而是一个代表就绪描述符数量的值,你只需要去epoll指定的一个数组中依次取得相应数量的文件描述符即可,这里也使用了内存映射(mmap)技术,这样便彻底省掉了这些文件描述符在系统调用时复制的开销。

epoll是基于事件驱动的I/O方式,相对于select来说,epoll没有描述符个数限制,使用一个文件描述符管理多个描述符,将用户关心的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
int epoll_create(int size);
//创建一个epoll句柄,参数size表明内核要监听的描述符数量。调用成功时返回一个epoll句柄描述符,失败时返回-1。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
/*函数注册要监听的事件类型
epfd 表示epoll句柄
op 表示fd操作类型,有如下3种
EPOLL_CTL_ADD 注册新的fd到epfd中
EPOLL_CTL_MOD 修改已注册的fd的监听事件
EPOLL_CTL_DEL 从epfd中删除一个fd
fd 是要监听的描述符
event 表示要监听的事件
*/
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
//函数等待事件的就绪,成功时返回就绪的事件数目,调用失败时返回 -1,等待超时返回 0。
/*
epfd 是epoll句柄
events 表示从内核得到的就绪事件集合
maxevents 告诉内核events的大小
timeout 表示等待的超时事件
*/

//epoll_event 结构体
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};

typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;

EPOLL无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。

Epoll的2种工作方式-水平触发(LT)和边缘触发(ET)

  • LT:默认工作模式,即当epoll_wait检测到某描述符事件就绪并通知应用程序时,应用程序可以不立即处理该事件;下次调用epoll_wait时,会再次通知此事件.
  • ET:当epoll_wait检测到某描述符事件就绪并通知应用程序时,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次通知此事件。(直到你做了某些操作导致该描述符变成未就绪状态了,也就是说边缘触发只在状态由未就绪变为就绪时只通知一次).
– select poll epoll
操作方式 遍历 遍历 callback
底层实现 数组 链表 红黑树
IO效率 线性遍历o(n) 线性遍历 o(n) 事件通知方式,每当fd就绪,系统注册的回调函数就会被调用,将就绪fd放到readyList里面,时间复杂度O(1)
最大链接数 1024(x86)或2048(x64) 无上限 无上限
fd拷贝 每次调用select,都需要把fd集合从用户态拷贝到内核态 每次调用poll,都需要把fd集合从用户态拷贝到内核态 调用epoll_ctl时拷贝进内核并保存,之后每次epoll_wait不拷贝

信号驱动IO

异步IO

异步通知和异步IO(AIO),用户程序可以通过向内核发出I/O请求命令,不用等待I/O事件真正发生,可以继续做另外的事情,等I/O操作完成,内核会通过函数回调或者信号机制通知用户进程。这样很大程度提高了系统吞吐量。

计算机网络

发表于 2020-03-20 更新于 2020-09-12 评论数: 阅读次数:

物理层

通信方式

模拟信号是连续的信号,数字信号是离散的信号。带通调制把数字信号转换为模拟信号。根据信息在传输线上的传送方向,分为以下三种通信方式:
  • 单工通信:单向传输
  • 半双工通信:双向交替传输
  • 全双工通信:双向同时传输

    链路层

    基本问题

  1. 封装成帧
    将网络层传下来的分组添加首部和尾部,用于标记帧的开始和结束。
  2. 透明传输
    透明表示一个实际存在的事物看起来好像不存在一样。帧使用首部和尾部进行定界,如果帧的数据部分含有和首部尾部相同的内容,那么帧的开始和结束位置就会被错误的判定。需要在数据部分出现首部尾部相同的内容前面插入转义字符。如果数据部分出现转义字符,那么就在转义字符前面再加个转义字符。在接收端进行处理之后可以还原出原始数据。这个过程透明传输的内容是转义字符,用户察觉不到转义字符的存在。
  3. 差错检测
    目前数据链路层广泛使用了循环冗余检验(CRC)来检查比特差错。

    信道划分

  4. 广播信道
    一对多通信,一个节点发送的数据能够被广播信道上所有的节点接收到。所有的节点都在同一个广播信道上发送数据,因此需要有专门的控制方法进行协调,避免发生冲突(冲突也叫碰撞)。主要有两种控制方法进行协调,一个是使用信道复用技术,一是使用 CSMA/CD 协议。
  5. 点对点信道
    一对一通信。因为不会发生碰撞,因此也比较简单,使用 PPP 协议进行控制。

    信道复用技术

  6. 频分复用
    频分复用的所有主机在相同的时间占用不同的频率带宽资源。
  7. 时分复用
    时分复用的所有主机在不同的时间占用相同的频率带宽资源。使用频分复用和时分复用进行通信,在通信的过程中主机会一直占用一部分信道资源。但是由于计算机数据的突发性质,通信过程没必要一直占用信道资源而不让出给其它用户使用,因此这两种方式对信道的利用率都不高。
  8. 统计时分复用
    是对时分复用的一种改进,不固定每个用户在时分复用帧中的位置,只要有数据就集中起来组成统计时分复用帧然后发送。
  9. 波分复用
    光的频分复用。由于光的频率很高,因此习惯上用波长而不是频率来表示所使用的光载波。
  10. 码分复用
    为每个用户分配 m bit 的码片,并且所有的码片正交,对于任意两个码片(向量)S和T有 S.T/m = 0;为了讨论方便,取 m=8,设码片S为00011011。在拥有该码片的用户发送比特 1 时就发送该码片,发送比特 0 时就发送该码片的反码 11100100。
    在计算时将 00011011 记作 (-1 -1 -1 +1 +1 -1 +1 +1),可以得到

    当接收端使用码片S对接收到的数据进行内积运算时,结果为 0 的是其它用户发送的数据,结果为 1 的是用户发送的比特 1,结果为 -1 的是用户发送的比特 0。

    CSMA/CD 载波监听多点接入 / 碰撞检测。

  • 多点接入 :说明这是总线型网络,许多主机以多点的方式连接到总线上。
  • 载波监听 :每个主机都必须不停地监听信道。在发送前,如果监听到信道正在使用,就必须等待。
  • 碰撞检测 :在发送中,如果监听到信道已有其它主机正在发送数据,就表示发生了碰撞。虽然每个主机在发送数据之前都已经监听到信道为空闲,但是由于电磁波的传播时延的存在,还是有可能会发生碰撞。

记端到端的传播时延为 τ,最先发送的站点最多经过 2τ 就可以知道是否发生了碰撞,称 2τ 为 争用期 。只有经过争用期之后还没有检测到碰撞,才能肯定这次发送不会发生碰撞。当发生碰撞时,站点要停止发送,等待一段时间再发送。这个时间采用 截断二进制指数退避算法 来确定。从离散的整数集合 {0, 1, .., (2k-1)} 中随机取出一个数,记作 r,然后取 r 倍的争用期作为重传等待时间。当发生碰撞时,站点要停止发送,等待一段时间再发送。这个时间采用 截断二进制指数退避算法 来确定。从离散的整数集合 {0, 1, .., (2^k-1)} 中随机取出一个数,记作 r,然后取 r 倍的争用期作为重传等待时间。

PPP 协议

互联网用户通常需要连接到某个 ISP 之后才能接入到互联网,PPP 协议是用户计算机和 ISP 进行通信时所使用的数据链路层协议。

PPP 的帧格式:

  • F 字段为帧的定界符
  • A 和 C 字段暂时没有意义
  • FCS 字段是使用 CRC 的检验序列信息部分的长度不超过 1500

    MAC 地址

    MAC 地址是链路层地址,长度为 6 字节(48 位),用于唯一标识网络适配器(网卡)。一台主机拥有多少个网络适配器就有多少个 MAC 地址。例如笔记本电脑普遍存在无线网络适配器和有线网络适配器,因此就有两个 MAC 地址。

    局域网

    局域网是一种典型的广播信道,主要特点是网络为一个单位所拥有,且地理范围和站点数目均有限。主要有以太网、令牌环网、FDDI 和 ATM 等局域网技术,目前以太网占领着有线局域网市场。可以按照网络拓扑结构对局域网进行分类:

    以太网

    以太网是一种星型拓扑结构局域网。早期使用集线器进行连接,集线器是一种物理层设备, 作用于比特而不是帧,当一个比特到达接口时,集线器重新生成这个比特,并将其能量强度放大,从而扩大网络的传输距离,之后再将这个比特发送到其它所有接口。如果集线器同时收到两个不同接口的帧,那么就发生了碰撞。目前以太网使用交换机替代了集线器,交换机是一种链路层设备,它不会发生碰撞,能根据 MAC 地址进行存储转发。

以太网帧格式:

  • 类型 :标记上层使用的协议;
  • 数据 :长度在 46-1500 之间,如果太小则需要填充;
  • FCS :帧检验序列,使用的是 CRC 检验方法;

    交换机

    交换机具有自学习能力,学习的是交换表的内容,交换表中存储着 MAC 地址到接口的映射。正是由于这种自学习能力,因此交换机是一种即插即用设备,不需要网络管理员手动配置交换表内容。

    虚拟局域网

    虚拟局域网可以建立与物理位置无关的逻辑组,只有在同一个虚拟局域网中的成员才会收到链路层广播信息。使用 VLAN 干线连接来建立虚拟局域网,每台交换机上的一个特殊接口被设置为干线接口,以互连 VLAN 交换机。IEEE 定义了一种扩展的以太网帧格式 802.1Q,它在标准以太网帧上加进了4字节首部VLAN标签,用于表示该帧属于哪一个虚拟局域网。

网络层

网络层是整个互联网的核心,因此应当让网络层尽可能简单。网络层向上只提供简单灵活的、无连接的、尽最大努力交互的数据报服务。使用 IP 协议,可以把异构的物理网络连接起来,使得在网络层看起来好像是一个统一的网络。

IP数据报

  • 版本 : 有 4(IPv4)和 6(IPv6)两个值;
  • 首部长度 : 占 4 位,因此最大值为 15。值为 1 表示的是 1 个 32 位字的长度,也就是 4 字节。因为固定部分长度为 20 字节,因此该值最小为 5。如果可选字段的长度不是 4 字节的整数倍,就用尾部的填充部分来填充。
  • 区分服务 : 用来获得更好的服务,一般情况下不使用。
  • 总长度 : 包括首部长度和数据部分长度。
  • 生存时间 :TTL,它的存在是为了防止无法交付的数据报在互联网中不断兜圈子。以路由器跳数为单位,当 TTL 为 0 时就丢弃数据报。
  • 协议 :指出携带的数据应该上交给哪个协议进行处理,例如 ICMP、TCP、UDP 等。
  • 首部检验和 :因为数据报每经过一个路由器,都要重新计算检验和,因此检验和不包含数据部分可以减少计算的工作量。
  • 标识 : 在数据报长度过长从而发生分片的情况下,相同数据报的不同分片具有相同的标识符。
  • 片偏移 : 和标识符一起,用于发生分片的情况。片偏移的单位为 8 字节。

    IP地址


    通过在主机号字段中拿一部分作为子网号,把两级 IP 地址划分为三级 IP 地址。IP地址= {< 网络号 >, < 子网号 >, < 主机号 >}
    要使用子网,必须配置子网掩码。一个 B 类地址的默认子网掩码为 255.255.0.0,如果 B 类地址的子网占两个比特,那么子网掩码为 11111111 11111111 11000000 00000000,也就是 255.255.192.0。
    注意,外部网络看不到子网的存在。

    无分类编址 CIDR

    消除了传统 A 类、B 类和 C 类地址以及划分子网的概念,使用网络前缀和主机号来对 IP 地址进行编码,网络前缀的长度可以根据需要变化。
    IP 地址= {< 网络前缀号 >, < 主机号 >}
    CIDR 的记法上采用在 IP 地址后面加上网络前缀长度的方法,例如 128.14.35.7/20 表示前 20 位为网络前缀。
    CIDR 的地址掩码可以继续称为子网掩码,子网掩码首 1 长度为网络前缀的长度。一个 CIDR 地址块中有很多地址,一个 CIDR 表示的网络就可以表示原来的很多个网络,并且在路由表中只需要一个路由就可以代替原来的多个路由,减少了路由表项的数量。把这种通过使用网络前缀来减少路由表项的方式称为路由聚合,也称为 构成超网 。

在路由表中的项目由“网络前缀”和“下一跳地址”组成,在查找时可能会得到不止一个匹配结果,应当采用最长前缀匹配来确定应该匹配哪一个。

地址解析协议 ARP(Address Resolution Protocol)

每个主机都有一个 ARP 高速缓存,里面有本局域网上的各主机和路由器的 IP 地址到 MAC 地址的映射表。如果主机 A 知道主机 B 的 IP 地址,但是 ARP 高速缓存中没有该 IP 地址到 MAC 地址的映射,此时主机 A 通过广播的方式发送 ARP 请求分组,主机 B 收到该请求后会发送 ARP 响应分组给主机 A 告知其 MAC 地址,随后主机 A 向其高速缓存中写入主机 B 的 IP 地址到 MAC 地址的映射。

网际控制报文协议 ICMP(Internet Control Message Protocol)

ICMP 是为了更有效地转发 IP 数据报和提高交付成功的机会。它封装在 IP 数据报中,但是不属于高层协议。
ICMP 报文分为差错报告报文和询问报文。

  1. Ping 的原理是通过向目的主机发送 ICMP Echo 请求报文,目的主机收到之后会发送 Echo 回答报文。Ping 会根据时间和成功响应的次数估算出数据包往返时间以及丢包率。

  2. Traceroute 是 ICMP 的另一个应用,用来跟踪一个分组从源点到终点的路径。

    Traceroute 发送的 IP 数据报封装的是无法交付的 UDP 用户数据报,并由目的主机发送终点不可达差错报告报文。

    源主机向目的主机发送一连串的 IP 数据报。第一个数据报 P1 的生存时间 TTL 设置为 1,当 P1 到达路径上的第一个路由器 R1 时,R1 收下它并把 TTL 减 1,此时 TTL 等于 0,R1 就把 P1 丢弃,并向源主机发送一个 ICMP 时间超过差错报告报文;

    源主机接着发送第二个数据报 P2,并把 TTL 设置为 2。P2 先到达 R1,R1 收下后把 TTL 减 1 再转发给 R2,R2 收下后也把 TTL 减 1,由于此时 TTL 等于 0,R2 就丢弃 P2,并向源主机发送一个 ICMP 时间超过差错报文。

    不断执行这样的步骤,直到最后一个数据报刚刚到达目的主机,主机不转发数据报,也不把 TTL 值减 1。但是因为数据报封装的是无法交付的 UDP,因此目的主机要向源主机发送 ICMP 终点不可达差错报告报文。

    之后源主机知道了到达目的主机所经过的路由器 IP 地址以及到达每个路由器的往返时间。

网络地址转换 NAT

专用网内部的主机使用本地 IP 地址又想和互联网上的主机通信时,可以使用 NAT 来将本地 IP 转换为全球 IP。在以前,NAT 将本地 IP 和全球 IP 一一对应,这种方式下拥有 n 个全球 IP 地址的专用网内最多只可以同时有 n 台主机接入互联网。为了更有效地利用全球 IP 地址,现在常用的 NAT 转换表把传输层的端口号也用上了,使得多个专用网内部的主机共用一个全球 IP 地址。使用端口号的 NAT 也叫做网络地址与端口转换 NAPT。

路由器

路由器从功能上可以划分为:路由选择和分组转发。
分组转发结构由三个部分组成:交换结构、一组输入端口和一组输出端口。

路由分组转发流程

  1. 从数据报的首部提取目的主机的 IP 地址 D,得到目的网络地址 N。
  2. 若 N 就是与此路由器直接相连的某个网络地址,则进行直接交付;
  3. 若路由表中有目的地址为 D 的特定主机路由,则把数据报传送给表中所指明的下一跳路由器;
  4. 若路由表中有到达网络 N 的路由,则把数据报传送给路由表中所指明的下一跳路由器;
  5. 若路由表中有一个默认路由,则把数据报传送给路由表中所指明的默认路由器;
    报告转发分组出错。

    路由选择协议

    自治系统内部的路由选择:RIP 和 OSPF

自治系统间的路由选择:BGP

RIP

RIP 是一种基于距离向量的路由选择协议。距离是指跳数,直接相连的路由器跳数为 1。跳数最多为 15,超过 15 表示不可达。RIP 按固定的时间间隔仅和相邻路由器交换自己的路由表,经过若干次交换之后,所有路由器最终会知道到达本自治系统中任何一个网络的最短距离和下一跳路由器地址。

  • 对地址为 X 的相邻路由器发来的 RIP 报文,先修改报文中的所有项目,把下一跳字段中的地址改为 X,并把所有的距离字段加 1;
  • 若本路由表中不存在目的N的网络,则添加。
  • 若存在跳数更小的,则更新。
  • 若3分钟还没有收到相邻路由器的更新路由表,则把该相邻路由器标为不可达,即把距离置为 16。
    RIP 协议实现简单,开销小。但是RIP能使用的最大距离为 15,限制了网络的规模。并且当网络出现故障时,要经过比较长的时间才能将此消息传送到所有路由器。 坏消息传播的慢。

    OSPF

    开放表示OSPF不受某一家厂商控制,而是公开发表的;最短路径优先表示使用了 Dijkstra 提出的最短路径算法OSPF。OSPF具有以下特点:

向本自治系统中的所有路由器发送信息,这种方法是洪泛法。
发送的信息就是与相邻路由器的链路状态,链路状态包括与哪些路由器相连以及链路的度量,度量用费用、距离、时延、带宽等来表示。
只有当链路状态发生变化时,路由器才会发送信息。
所有路由器都具有全网的拓扑结构图,并且是一致的。相比于 RIP,OSPF 的更新过程收敛的很快.

BGP

AS 之间的路由选择很困难,主要是由于:

  1. 互联网规模很大;
  2. 各个 AS 内部使用不同的路由选择协议,无法准确定义路径的度量;
  3. AS 之间的路由选择必须考虑有关的策略,比如有些 AS 不愿意让其它 AS 经过。
    BGP 只能寻找一条比较好的路由,而不是最佳路由。每个 AS 都必须配置 BGP 发言人,通过在两个相邻 BGP 发言人之间建立 TCP 连接来交换路由信息。

    传输层

    UDP

    用户数据报协议 UDP(User Datagram Protocol)是无连接的,尽最大可能交付,没有拥塞控制,面向报文(对于应用程序传下来的报文不合并也不拆分,只是添加 UDP 首部),支持一对一、一对多、多对一和多对多的交互通信。

首部字段只有 8 个字节,包括源端口、目的端口、长度、检验和。12 字节的伪首部是为了计算检验和临时添加的。

TCP

传输控制协议 TCP(Transmission Control Protocol)是面向连接的,提供可靠交付,有流量控制,拥塞控制,提供全双工通信,面向字节流(把应用层传下来的报文看成字节流,把字节流组织成大小不等的数据块),每一条 TCP 连接只能是点对点的(一对一)。

  • 序号 :用于对字节流进行编号,例如序号为 301,表示第一个字节的编号为 301,如果携带的数据长度为 100 字节,那么下一个报文段的序号应为 401。
  • 确认号 :期望收到的下一个报文段的序号。例如 B 正确收到 A 发送来的一个报文段,序号为 501,携带的数据长度为 200 字节,因此 B 期望下一个报文段的序号为 701,B 发送给 A 的确认报文段中确认号就为 701。
  • 数据偏移 :指的是数据部分距离报文段起始处的偏移量,实际上指的是首部的长度。
  • 确认 ACK :当 ACK=1 时确认号字段有效,否则无效。TCP 规定,在连接建立后所有传送的报文段都必须把 ACK 置 1。
  • 同步 SYN :在连接建立时用来同步序号。当 SYN=1,ACK=0 时表示这是一个连接请求报文段。若对方同意建立连接,则响应报文中 SYN=1,ACK=1。
  • 终止 FIN :用来释放一个连接,当 FIN=1 时,表示此报文段的发送方的数据已发送完毕,并要求释放连接。
  • 窗口 :窗口值作为接收方让发送方设置其发送窗口的依据。之所以要有这个限制,是因为接收方的数据缓存空间是有限的。

    三次握手


    假设 A 为客户端,B 为服务器端。
  1. 首先 B 处于 LISTEN(监听)状态,等待客户的连接请求。
  2. A 向 B 发送连接请求报文,SYN=1,ACK=0,选择一个初始的序号 x。
  3. B 收到连接请求报文,如果同意建立连接,则向 A 发送连接确认报文,SYN=1,ACK=1,确认号为 x+1,同时也选择一个初始的序号 y。
  4. A 收到 B 的连接确认报文后,还要向 B 发出确认,确认号为 y+1,序号为 x+1。
  5. B 收到 A 的确认后,连接建立。

第三次握手是为了防止失效的连接请求到达服务器,让服务器错误打开连接。
TCP 的可靠连接是靠 seq( sequence numbers 序列号)来达成的。

客户端发送的连接请求如果在网络中滞留,那么就会隔很长一段时间才能收到服务器端发回的连接确认。客户端等待一个超时重传时间之后,就会重新请求连接。但是这个滞留的连接请求最后还是会到达服务器,如果不进行三次握手,那么服务器就会打开两个连接。如果有第三次握手,客户端会忽略服务器之后发送的对滞留连接请求的连接确认,不进行第三次握手,因此就不会再次打开连接。

四次挥手

  1. A 发送连接释放报文,FIN=1。
  2. B 收到之后发出确认,此时 TCP 属于半关闭状态,B 能向 A 发送数据但是 A 不能向 B 发送数据。
  3. 当 B 不再需要连接时,发送连接释放报文,FIN=1。
  4. A 收到后发出确认,进入 TIME-WAIT 状态,等待 2 MSL(最大报文存活时间)后释放连接。
  5. B 收到 A 的确认后释放连接。

客户端发送了 FIN 连接释放报文之后,服务器收到了这个报文,就进入了 CLOSE-WAIT 状态。这个状态是为了让服务器端发送还未传送完毕的数据,传送完毕之后,服务器会发送 FIN 连接释放报文。

客户端接收到服务器端的 FIN 报文后进入此状态,此时并不是直接进入 CLOSED 状态,还需要等待一个时间计时器设置的时间 2MSL。这么做有两个理由:

  1. 确保最后一个确认报文能够到达。如果 B 没收到 A 发送来的确认报文,那么就会重新发送连接释放请求报文,A 等待一段时间就是为了处理这种情况的发生。
  2. 等待一段时间是为了让本连接持续时间内所产生的所有报文都从网络中消失,使得下一个新的连接不会出现旧的连接请求报文。

    TCP 可靠传输

    超时重传机制
    报文段从发送再到接收到确认所经过的时间称为往返时间 RTT,加权平均往返时间 RTTs 计算如下:
    其中,0 ≤ a < 1,RTTs 随着 a 的增加更容易受到 RTT 的影响。
    超时时间 RTO 应该略大于 RTTs,TCP 使用的超时时间计算如下:
    其中 RTT_d 为偏差的加权平均值。

    TCP 流量控制

    滑动窗口控制。

    拥塞控制

    如果网络出现拥塞,分组将会丢失,此时发送方会继续重传,从而导致网络拥塞程度更高。因此当出现拥塞时,应当控制发送方的速率。这一点和流量控制很像,但是出发点不同。流量控制是为了让接收方能来得及接收,而拥塞控制是为了降低整个网络的拥塞程度。

TCP 主要通过四个算法来进行拥塞控制:慢开始、拥塞避免、快重传、快恢复。

  1. 慢开始
    发送的最初执行慢开始,令 cwnd = 1,发送方只能发送 1 个报文段;当收到确认后,将 cwnd 加倍,因此之后发送方能够发送的报文段数量为:2、4、8 …。cwnd 指数增长。设置一个慢开始门限 ssthresh,当 cwnd >= ssthresh 时,进入拥塞避免,每个轮次只将 cwnd 加 1,cwnd线性增长。
    如果出现了超时,则令 ssthresh = cwnd / 2,然后重新执行慢开始。
  2. 快重传和快恢复
    在接收方,要求每次接收到报文段都应该对最后一个已收到的有序报文段进行确认。例如已经接收到 M1 和 M2,此时收到 M4,应当发送对 M2 的确认。在发送方,如果收到三个重复确认,那么可以知道下一个报文段丢失,此时执行快重传,立即重传下一个报文段。例如收到三个 M2,则 M3 丢失,立即重传 M3。在这种情况下,只是丢失个别报文段,而不是网络拥塞。因此执行快恢复,令 ssthresh = cwnd / 2 ,cwnd = ssthresh,注意到此时直接进入拥塞避免。慢开始和快恢复的快慢指的是 cwnd 的设定值,而不是 cwnd 的增长速率。慢开始 cwnd 设定为 1,而快恢复 cwnd 设定为 ssthresh。

    应用层

    域名系统

    DNS 是一个分布式数据库,提供了主机名和 IP 地址之间相互转换的服务。这里的分布式数据库是指,每个站点只保留它自己的那部分数据。域名具有层次结构,从上到下依次为:根域名、顶级域名、二级域名。
    DNS 可以使用 UDP 或者 TCP 进行传输,使用的端口号都为 53。大多数情况下 DNS 使用 UDP 进行传输,这就要求域名解析器和域名服务器都必须自己处理超时和重传从而保证可靠性。在两种情况下会使用 TCP 进行传输:
  3. 如果返回的响应超过的 512 字节(UDP 最大只支持 512 字节的数据)。
  4. 区域传送(区域传送是主域名服务器向辅助域名服务器传送变化的那部分数据)。

    文件传送协议

    使用 TCP 进行连接,它需要两个连接来传送一个文件:
  5. 控制连接:服务器打开端口号 21 等待客户端的连接,客户端主动建立连接后,使用这个连接将客户端的命令传送给服务器,并传回服务器的应答。
  6. 数据连接:用来传送一个文件数据。
    根据数据连接是否是服务器端主动建立,FTP 有主动和被动两种模式:
  • 主动模式:服务器端主动建立数据连接,其中服务器端的端口号为 20,客户端的端口号随机,但是必须大于 1024,因为 0~1023 是熟知端口号。
  • 被动模式:客户端主动建立数据连接,其中客户端的端口号由客户端自己指定,服务器端的端口号随机。
  • 主动模式要求客户端开放端口号给服务器端,需要去配置客户端的防火墙。被动模式只需要服务器端开放端口号即可,无需客户端配置防火墙。但是被动模式会导致服务器端的安全性减弱,因为开放了过多的端口号。

    动态主机配置协议

    DHCP (Dynamic Host Configuration Protocol) 提供了即插即用的连网方式,用户不再需要手动配置 IP 地址等信息。DHCP 配置的内容不仅是 IP 地址,还包括子网掩码、网关 IP 地址。DHCP 工作过程如下:
  1. 客户端发送 Discover 报文,该报文的目的地址为 255.255.255.255:67,源地址为 0.0.0.0:68,被放入 UDP 中,该报文被广播到同一个子网的所有主机上。如果客户端和 DHCP 服务器不在同一个子网,就需要使用中继代理。
  2. DHCP 服务器收到 Discover 报文之后,发送 Offer 报文给客户端,该报文包含了客户端所需要的信息。因为客户端可能收到多个 DHCP 服务器提供的信息,因此客户端需要进行选择。
  3. 如果客户端选择了某个 DHCP 服务器提供的信息,那么就发送 Request 报文给该 DHCP 服务器。
  4. DHCP 服务器发送 Ack 报文,表示客户端此时可以使用提供给它的信息。

    远程登录协议

    TELNET 用于登录到远程主机上,并且远程主机上的输出也会返回。TELNET 可以适应许多计算机和操作系统的差异,例如不同操作系统系统的换行符定义。

    电子邮件协议

    一个电子邮件系统由三部分组成:用户代理、邮件服务器以及邮件协议。邮件协议包含发送协议和读取协议,发送协议常用 SMTP,读取协议常用 POP3 和 IMAP。

    SMTP

    SMTP 只能发送 ASCII 码,而互联网邮件扩充 MIME 可以发送二进制文件。MIME 并没有改动或者取代 SMTP,而是增加邮件主体的结构,定义了非 ASCII 码的编码规则。

    POP3

    POP3 的特点是只要用户从服务器上读取了邮件,就把该邮件删除。但最新版本的 POP3 可以不删除邮件。

    IMAP

    IMAP 协议中客户端和服务器上的邮件保持同步,如果不手动删除邮件,那么服务器上的邮件也不会被删除。IMAP 这种做法可以让用户随时随地去访问服务器上的邮件。
    应用 应用层协议 端口号 传输层协议 备注
    域名解析 DNS 53 UDP/TCP 长度超过 512 字节时使用 TCP
    动态主机配置协议 DHCP 67/68 UDP
    简单网络管理协议 SNMP 161/162 UDP
    文件传送协议 FTP 20/21 TCP 控制连接 21,数据连接 20
    远程终端协议 TELNET 23 TCP
    超文本传送协议 HTTP 80 TCP
    简单邮件传送协议 SMTP 25 TCP
    邮件读取协议 POP3 110 TCP
    网际报文存取协议 IMAP 143 TCP
    # Web 页面请求过程
    ## DHCP 配置主机信息
    假设主机最开始没有 IP 地址以及其它信息,那么就需要先使用 DHCP 来获取。
  5. 主机生成一个 DHCP 请求报文,并将这个报文放入具有目的端口 67 和源端口 68 的 UDP 报文段中。
  6. 将该报文段则被放入在一个具有广播 IP 目的地址(255.255.255.255) 和源 IP 地址(0.0.0.0)的 IP 数据报中。
  7. 该数据报则被放置在 MAC 帧中,该帧具有目的地址 FF:FF:FF:FF:FF:FF,将广播到与交换机连接的所有设备。
  8. 连接在交换机的 DHCP 服务器收到广播帧之后,不断地向上分解得到 IP 数据报、UDP 报文段、DHCP 请求报文,之后生成 DHCP ACK 报文,该报文包含以下信息:IP 地址、DNS 服务器的 IP 地址、默认网关路由器的 IP 地址和子网掩码。
  9. 该报文被放入 UDP 报文段中,UDP 报文段有被放入 IP 数据报中,最后放入 MAC 帧中。该帧的目的地址是请求主机的 MAC 地址,因为交换机具有自学习能力,之前主机发送了广播帧之后就记录了 MAC 地址到其转发接口的交换表项,因此现在交换机就可以直接知道应该向哪个接口发送该帧。主机收到该帧后,不断分解得到 DHCP 报文。之后就配置它的 IP 地址、子网掩码和 DNS 服务器的 IP 地址,并在其 IP 转发表中安装默认网关。

    ARP 解析 MAC 地址

  10. 主机通过浏览器生成一个 TCP 套接字,套接字向 HTTP 服务器发送 HTTP 请求。为了生成该套接字,主机需要知道网站的域名对应的 IP 地址。
  11. 主机生成一个 DNS 查询报文,该报文具有 53 号端口,因为 DNS 服务器的端口号是 53。
  12. 该 DNS 查询报文被放入目的地址为 DNS 服务器 IP 地址的 IP 数据报中。
  13. 该 IP 数据报被放入一个以太网帧中,该帧将发送到网关路由器。
  14. DHCP 过程只知道网关路由器的 IP 地址,为了获取网关路由器的 MAC 地址,需要使用 ARP 协议。
  15. 主机生成一个包含目的地址为网关路由器 IP 地址的 ARP 查询报文,将该 ARP 查询报文放入一个具有广播目的(FF:FF:FF:FF:FF:FF)的以太网帧中,并向交换机发送该以太网帧,交换机将该帧转发给所有的连接设备,包括网关路由器。
  16. 网关路由器接收到该帧后,不断向上分解得到 ARP 报文,发现其中的 IP 地址与其接口的 IP 地址匹配,因此就发送一个 ARP 回答报文,包含了它的 MAC 地址,发回给主机。

    DNS 解析域名

  17. 知道了网关路由器的 MAC 地址之后,就可以继续 DNS 的解析过程了。
  18. 网关路由器接收到包含 DNS 查询报文的以太网帧后,抽取出 IP 数据报,并根据转发表决定该 IP 数据报应该转发的路由器。
  19. 因为路由器具有内部网关协议(RIP、OSPF)和外部网关协议(BGP)这两种路由选择协议,因此路由表中已经配置了网关路由器到达 DNS 服务器的路由表项。
  20. 到达 DNS 服务器之后,DNS 服务器抽取出 DNS 查询报文,并在 DNS 数据库中查找待解析的域名。
  21. 找到 DNS 记录之后,发送 DNS 回答报文,将该回答报文放入 UDP 报文段中,然后放入 IP 数据报中,通过路由器反向转发回网关路由器,并经过以太网交换机到达主机。

    HTTP 请求页面

  22. 有了 HTTP 服务器的 IP 地址之后,主机就能够生成 TCP 套接字,该套接字将用于向 Web 服务器发送 HTTP GET 报文。
  23. 在生成 TCP 套接字之前,必须先与 HTTP 服务器进行三次握手来建立连接。生成一个具有目的端口 80 的 TCP SYN 报文段,并向 HTTP 服务器发送该报文段。
  24. HTTP 服务器收到该报文段之后,生成 TCP SYN ACK 报文段,发回给主机。
  25. 连接建立之后,浏览器生成 HTTP GET 报文,并交付给 HTTP 服务器。
  26. HTTP 服务器从 TCP 套接字读取 HTTP GET 报文,生成一个HTTP 响应报文,将 Web 页面内容放入报文主体中,发回给主机。
  27. 浏览器收到 HTTP 响应报文后,抽取出 Web 页面内容,之后进行渲染,显示 Web 页面。

操作系统

发表于 2020-01-05 更新于 2020-08-24 分类于 foundation 评论数: 阅读次数:

基本特征

  • 共享-互斥共享和同时共享
  • 并发与并行
  • 虚拟-时(时间)分复用技术和空(空间)分复用技术
  • 异步

    基本功能

    进程管理

    进程与线程

  • 进程是资源分配的基本单位。进程控制块 (Process Control Block, PCB) 描述进程的基本信息和运行状态,所谓的创建进程和撤销进程,都是指对 PCB 的操作。
  • 线程是独立调度的基本单位。
  • 区别:
    1. 线程不占有资源
    2. 同一进程中的线程切换不会导致进程切换
    3. 线程间可以通过直接读写同一进程中的数据进行通信,但是进程通信需要借助 IPC。
    4. 由于创建或撤销进程时,系统都要为之分配或回收资源,如内存空间、I/O 设备等,所付出的开销远大于创建或撤销线程时的开销。类似地,在进行进程切换时,涉及当前执行进程 CPU 环境的保存及新调度进程 CPU 环境的设置,而线程切换时只需保存和设置少量寄存器内容,开销很小。

      切换


      就绪态,运行态,阻塞态

      进程调度算法

      批处理系统
      批处理系统没有太多的用户操作,在该系统中,调度算法目标是保证吞吐量和周转时间(从提交到终止的时间)。
  1. 先来先服务 first-come first-serverd(FCFS)

非抢占式的调度算法,按照请求的顺序进行调度。有利于长作业,但不利于短作业,因为短作业必须一直等待前面的长作业执行完毕才能执行,而长作业又需要执行很长时间,造成了短作业等待时间过长。

  1. 先来先服务 first-come first-serverd(FCFS)

非抢占式的调度算法,按估计运行时间最短的顺序进行调度。长作业有可能会饿死,处于一直等待短作业执行完毕的状态。因为如果一直有短作业到来,那么长作业永远得不到调度。

  1. 先来先服务 first-come first-serverd(FCFS)

最短作业优先的抢占式版本,按剩余运行时间的顺序进行调度。 当一个新的作业到达时,其整个运行时间与当前进程的剩余时间作比较。如果新的进程需要的时间更少,则挂起当前进程,运行新的进程。否则新的进程等待。

交互式系统

批处理系统没有太多的用户操作,在该系统中,调度算法目标是保证吞吐量和周转时间(从提交到终止的时间)。

  1. 时间片轮转

将所有就绪进程按 FCFS 的原则排成一个队列,每次调度时,把 CPU 时间分配给队首进程,该进程可以执行一个时间片。当时间片用完时,由计时器发出时钟中断,调度程序便停止该进程的执行,并将它送往就绪队列的末尾,同时继续把 CPU 时间分配给队首的进程。时间片轮转算法的效率和时间片的大小有很大关系。

  1. 优先度调度

为每个进程分配一个优先级,按优先级进行调度。为了防止低优先级的进程永远等不到调度,可以随着时间的推移增加等待进程的优先级。

  1. 多级反馈队列

多级队列是为这种需要连续执行多个时间片的进程考虑,它设置了多个队列,每个队列时间片大小都不同。进程在第一个队列没执行完,就会被移到下一个队列。

每个队列优先权也不同,最上面的优先权最高。因此只有上一个队列没有进程在排队,才能调度当前队列上的进程。

可以看成是时间片轮转调度算法和优先级调度算法的结合。

实时系统

实时系统要求一个请求在一个确定时间内得到响应。分为硬实时和软实时,前者必须满足绝对的截止时间,后者可以容忍一定的超时。

同步互斥

  • 对临界资源进行访问的那段代码称为临界区。

  • 管程

  • 同步互斥

    进程通信

    管道

    管道是通过调用 pipe 函数创建的,fd[0] 用于读,fd[1] 用于写。

    1
    2
    unistd.h 
    int pipe(int fd[2]);
  • 半双工通信。

  • 只能在父子进程和兄弟进程中使用。

    FIFO

    命名管道,去除了管道只能在父子进程中使用的限制

    1
    2
    3
    #include <sys/stat.h>
    int mkfifo(const char *path, mode_t mode);
    int mkfifoat(int fd, const char *path, mode_t mode);

FIFO 常用于客户-服务器应用程序中,FIFO 用作汇聚点,在客户进程和服务器进程之间传递数据。

消息队列
  • 消息队列可以独立于读写进程存在,从而避免了 FIFO 中同步管道的打开和关闭时可能产生的困难。
  • 避免了 FIFO 的同步阻塞问题,不需要进程自己提供同步方法。
  • 读进程可以根据消息类型有选择地接收消息,而不像 FIFO 那样只能默认地接收。
    信号量
    共享存储
    套接字
    共享存储

    死锁

    必要条件
    资源互斥 占有和等待 不可抢占 死循环
    处理方法
    鸵鸟策略
    死锁检测
    不试图阻止死锁,而是当检测到死锁发生时,采取措施进行恢复。
  1. 每种类型一个资源的死锁检测

从一个节点开始深度优先遍历,查找是否存在有向环。

  1. 每种类型多个资源的死锁检测

    上图中,有三个进程四个资源,每个数据代表的含义如下:
  • E 向量:资源总量
  • A 向量:资源剩余量
  • C 矩阵:每个进程所拥有的资源数量,每一行都代表一个进程拥有资源的数量
  • R 矩阵:每个进程请求的资源数量

递归,直到所有进程均被访问,若存在未被访问进程,则产生死锁。

死锁恢复
  • 利用抢占恢复
  • 利用回滚恢复
  • 通过杀死进程恢复
    死锁预防
    破坏4个必要条件
  1. 要求进程一次性的请求所有需要的资源,并且阻塞这个进程直到所有请求都同时满足。
  2. 要求进程一次性的请求所有需要的资源,并且阻塞这个进程直到所有请求都同时满足。
  3. 资源编号,顺序请求。
    死锁避免
  • 进程启动拒绝:如果一个进程的请求会导致死锁,则不启动该进程
  • 资源分配拒绝:如果一个进程增加的资源请求会导致死锁,则不允许此分配 银行家算法

安全状态

  • 即使所有进程突然请求对资源的最大需求,也仍然存在某种调度次序能够使得每一个进程运行完毕,则称该状态是安全的。
    银行家算法
  • 单个资源的银行家算法
  • 多资源的银行家算法


上图中有五个进程,四个资源。左边的图表示已经分配的资源,右边的图表示还需要分配的资源。最右边的 E、P 以及 A 分别表示:总资源、已分配资源以及可用资源,注意这三个为向量,而不是具体数值,例如 A=(1020),表示 4 个资源分别还剩下 1/0/2/0。
算法:
按照分配矩阵进行分配,寻找小于等于可用资源的行,如果不存在则状态不安全。存在则释放该行继续迭代。
状态不安全。

内存管理

虚拟内存

虚拟内存允许程序不用将地址空间中的每一页都映射到物理内存.

  • 内存碎片 内外碎片

内存分配策略

  1. 首次适配
  • 第一个合适的块。
  1. 最佳适配
  • 遍历全部空间,差值最小。
  1. 最差适配
  • 差值最大。

    碎片整理

  1. 压缩式
  2. 交换式

    虚拟内存

分页系统映射

内存管理单元(MMU)管理着地址空间和物理内存的转换,其中的页表(Page table)存储着页(程序地址空间)和页框(物理内存空间)的映射表。

  • 虚拟地址=页号+偏移量

    页表项最后一位表示是否存在于内存中,1 表示存在.0不存在,则需要进行页面替换.

    页面置换算法

  1. 全局置换算法
  • 工作集 一个进程当前正在使用的逻辑页面集合。
  • 常驻集 指在当前时刻,进程实际驻留在内存当中的页面集合;工作集是进程在运行过程中固有的性质,而常驻集取决于系统分配给进程的物理页面数目,以及所采用的页面置换算法;
  • 当进程常驻集的大小达到某个数目后,再给它分配更多的物理页面,缺页率也不会明显下降
缺页率页面置换算法
  • 常驻集大小可变。例如,每个进程在刚开始运行的时候,先根据程序的大小给它分配一定数目的物理页面,然后在运行过程中,再动态的调整常驻集的大小。
  • 可采用全局页面置换的方式,当发生一个缺页中断时,被置换的页面可以是在其他进程当中,各个并发进程竞争地使用物理页面。
  • 缺页率表示“缺页次数/内存访问次数”(比率)或“缺页的平均时间间隔的倒数”

抖动问题:

如果分配给一个进程的物理页面很少,不能包含整个的工作集,那常驻集含于工作集,那么进程将会造成很多的缺页中断,需要频繁地在内存到外存之间替换页面,从而使进程的运行速度变得很慢,我们把这种状态称为“抖动”。

原因: 如果分配给一个进程的物理页面很少,不能包含整个的工作集,那常驻集含于工作集,那么进程将会造成很多的缺页中断,需要频繁地在内存到外存之间替换页面,从而使进程的运行速度变得很慢,我们把这种状态称为“抖动”。

  1. 局部置换算法
    OPT Optimal replacement algorithm
    选择被在此访问时间最长的那一页。是一种理想算法,实际无法实现。
    FIFO First in First out
    FIFO 会产生Belady异常(当所分配的物理块数增大而页故障数不减反增的异常现象)
  2. 改进的FIFO算法 第二次机会算法

根据FIFO维护一只链表,尾插法,设置标志位R,页面被使用时,R=0。从头开始查找R=1的页面,如果存在说明该页最久未使用,进行置换。如果R=1,则将R=0,将该页面放在尾部。

LRU, Least Recently Used

实现 LRU,需要在内存中维护一个所有页面的链表。当一个页面被访问时,将这个页面移到链表表头。这样就能保证链表表尾的页面是最近最久未访问的。该方法代价较高。

LRU需要寄存器和栈的硬件支持。LRU是堆栈类的算法。理论上可以证明,堆栈类算法不可能出现Belady异常。FIFO算法基于队列实现,不是堆栈类算法。

NRU, Not Recently Used(clock算法)
  1. 简单clock算法

维护一个标志位R(read),将内存中所有页面都通过链接指针链接成一个循环队列。换页时,按照FIFO的算法检查将R=O的页面置换。

  1. 改进的clock算法

每个页面设置两个标志位,R(read)和M(modify)。被读时,R=1.被修改时,M=1,R=0。

  • 最近未被访问,也未被修改(R=0, M=0)。
  • 最近被访问,但未被修改(R=1, M=0)。
  • 最近未被访问,但被修改(R=0, M=1)。
  • 最近被访问,被修改(R=1, M=1)。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    deque<map<int,int>> *que;
    auto deque_point = que.begin();
    while(true){
    while(deque_point){
    if(R=0,M=0)
    return;
    ++deque_point;
    }
    while(deque_point){
    if(R=0,M=1)
    return;
    *deque_point->M = 0;
    ++deque_point;
    }
    deque_point = que.begin();
    }

优先换出已经被修改的脏页面(R=0,M=1),而不是被频繁使用的干净页面(R=1,M=0).

分段

  1. 虚拟内存采用的是分页技术,也就是将地址空间划分成固定大小的页,每一页再与内存进行映射。
  • 段表 操作系统进行初始化,操作者可以进行后续设置。
  • 段页式—程序的地址空间划分成多个拥有独立地址空间的段,每个段上的地址空间划分成大小相同的页。这样既拥有分段系统的共享和保护,又拥有分页系统的虚拟内存功能。

  1. 分页与分段
  • 对程序员的透明性:分页透明,但是分段需要程序员显式划分每个段。

  • 地址空间的维度:分页是一维地址空间,分段是二维的。

  • 大 小是否可以改变:页的大小不可变,段的大小可以动态改变。

  • 出现的原因:分页主要用于实现虚拟内存,从而获得更大的地址空间;分段主要是为了使程序和数据可以被划分为逻辑上独立的地址空间并且有助于共享和保护。

    文件管理

    设备管理

    磁盘结构

  • 盘面(Platter):一个磁盘有多个盘面;

  • 磁道(Track):盘面上的圆形带状区域,一个盘面可以有多个磁道;

  • 扇区(Track Sector):磁道上的一个弧段,一个磁道可以有多个扇区,它是最小的物理储存单位,目前主要有 512 bytes 与 4 K 两种大小;

  • 磁头(Head):与盘面非常接近,能够将盘面上的磁场转换为电信号(读),或者将电信号转换为盘面的磁场(写);

  • 制动手臂(Actuator arm):用于在磁道之间移动磁头;

  • 主轴(Spindle):使整个盘面转动。

    磁盘调度算法

    读写一个磁盘块的时间的影响因素有:

  • 旋转时间(主轴转动盘面,使得磁头移动到适当的扇区上)

  • 寻道时间(制动手臂移动,使得磁头移动到适当的磁道上)

  • 实际的数据传输时间
    其中,寻道时间最长,因此磁盘调度的主要目标是使磁盘的平均寻道时间最短。

  1. FCFS frist come frist served
  2. SSTF shortest seek time frist

优先调度与当前磁头所在磁道距离最近的磁道。容易产生饥饿现象。远端请求长时间无法响应。

  1. SCAN

电梯总是保持一个方向运行,直到该方向没有请求为止,然后改变运行方向。单向访问距离最近的磁道。直到该方向上没有未完成的磁盘请求,然后改变方向。

  1. CSCAN

保持最大磁道挨着最小磁道和SCAN相似,但是到达内部之后立即调整到最外层,继续往内。或者相反。

系统调用

Linux 系统调用

|task|command|
| —- | —-|
|进程控制|fork();exit();wait()|
|进程通信| pipe(); shmget(); mmap()|
|文件操作| open(); read();write()|
|设备操作| ioctl();read();write()|
|信息维护| getpid(); alarm(); sleep()|
|安全| chmod();umask();chown()|

内核

大内核

大内核是将操作系统功能作为一个紧密结合的整体放到内核。由于各模块共享信息,因此有很高的性能。

微内核

由于操作系统不断复杂,因此将一部分操作系统功能移出内核,从而降低内核的复杂性。移出的部分根据分层的原则划分成若干服务,相互独立。
在微内核结构下,操作系统被划分成小的、定义良好的模块,只有微内核这一个模块运行在内核态,其余模块运行在用户态。因为需要频繁地在用户态和核心态之间进行切换,所以会有一定的性能损失。

中断

外中断

由 CPU 执行指令以外的事件引起,如 I/O 完成中断,表示设备输入/输出处理已经完成,处理器能够发送下一个输入/输出请求。此外还有时钟中断、控制台中断等。

异常

由 CPU 执行指令的内部事件引起,如非法操作码、地址越界、算术溢出等。

陷入

用户系统调用

链接装载

  • 预处理阶段:处理以 # 开头的预处理命令;
  • 编译阶段:翻译成汇编文件;
  • 汇编阶段:将汇编文件翻译成可重定位目标文件;
  • 链接阶段:将可重定位目标文件和 printf.o 等单独预编译好的目标文件进行合并,得到最终的可执行目标文件。

    静态链接

  1. 符号解析:每个符号对应于一个函数、一个全局变量或一个静态变量,符号解析的目的是将每个符号引用与一个符号定义关联起来。
  2. 重定位:链接器通过把每个符号定义与一个内存位置关联起来,然后修改所有对这些符号的引用,使得它们指向这个内存位置。
  3. 在可执行文件中产生一份拷贝。

    目标文件

  • 可执行目标文件:可以直接在内存中执行;

  • 可重定位目标文件:可与其它可重定位目标文件在链接阶段合并,创建一个可执行目标文件;

  • 共享目标文件:这是一种特殊的可重定位目标文件,可以在运行时被动态加载进内存并链接;

    动态链接

    在内存中一般只保留该编译单元的一份拷贝。

  • 装载时动态链接(Load-time Dynamic Linking):这种用法的前提是在编译之前已经明确知道要调用DLL中的哪几个函数,编译时在目标文件中只保留必要的链接信息,而不含DLL函数的代码;当程序执行时,调用函数的时候利用链接信息加载DLL函数代码并在内存中将其链接入调用程序的执行空间中(全部函数加载进内存),其主要目的是便于代码共享。(动态加载程序,处在加载阶段,主要为了共享代码,共享代码内存)

  • 运行时动态链接(Run-time Dynamic Linking):这种方式是指在编译之前并不知道将会调用哪些DLL函数,完全是在运行过程中根据需要决定应调用哪个函数,将其加载到内存中(只加载调用的函数进内存),并标识内存地址,其他程序也可以使用该程序,并用LoadLibrary和GetProcAddress动态获得DLL函数的入口地址。(dll在内存中只存在一份,处在运行阶段)

sql

发表于 2019-10-04 更新于 2020-09-15 评论数: 阅读次数:

SELECT col,col,col 找什么?
FROM table 从哪找?
WHERE col 条件 条件是啥?

条件:数字(where)

当查找条件col是数字
select * from table where col = 1;
Operator Condition SQL Example 解释
, !=, < <=, >, >= Standard numerical operators col != 4 等于 大于 小于
BETWEEN … AND … Number is within range of two values (inclusive) col BETWEEN 1.5 AND 10.5 在 X 和 X之间
NOT BETWEEN … AND … Number is not within range of two values (inclusive) co NOT BETWEEN 1 AND10 不在 X 和 X之间
IN (…) Number exists in a list col IN (2, 4, 6) 在 X 集合
NOT IN (…) Number does not exist in a list col NOT IN (1, 3, 5) 不在 X 集合
## 条件:文本(where)
当查找条件col是文本
select * from table where col like ‘%jin’;
Operator Condition SQL Example 解释
= Case sensitive exact string comparison (notice the single equals) col = “abc” 等于
!= or <> Case sensitive exact string inequality comparison col != “abcd” 不等于
LIKE Case insensitive exact string comparison col LIKE “ABC” 等于
NOT LIKE Case insensitive exact string inequality comparison col NOT LIKE “ABCD” 不等于
% Used anywhere in a string to match a sequence of zero or more characters (only with LIKE or NOT LIKE) col LIKE “%AT%” (matches “AT”, “ATTIC”, “CAT” or even “BATS”) 模糊匹配
_ Used anywhere in a string to match a single character (only with LIKE or NOT LIKE) col LIKE “AN_” (matches “AND”, but not “AN”) 模糊匹配单字符
IN (…) String exists in a list col IN (“A”, “B”, “C”) 在集合
NOT IN (…) String does not exist in a list co NOT IN (“D”, “E”, “F”) 不在集合
## 排序(rows)
需要对结果rows排序和筛选部分rows
select * from table where col > 1 order by col asc limit 2 offset 2
Operator Condition SQL Example 解释
ORDER BY . ORDER BY col ASC/DESC 按col排序
ASC . ORDER BY col ASC/DESC 升序
DESC . ORDER BY col ASC/DESC 降序
LIMIT OFFSET . LIMIT num_limit OFFSET
ORDER BY . ORDER BY col1 ASC,col2 DESC 多列排序
## join:连表(table)
当查找的数据在多张关联table里
select * from table1 left join table2 on table1.id = table2.id where col > 1
Operator Condition SQL Example 解释
JOIN .. ON .. . t1 JOIN t2 ON t1.id = t2.id 按ID连成1个表
INNER JOIN . t1 INNER JOIN t2 ON t1.id = t2.id 只保留id相等的row
LEFT JOIN . t1 LEFT JOIN t2 ON t1.id = t2.id 保留t1的所有row
RIGHT JOIN . t1 RIGHT JOIN t2 ON t1.id = t2.id 保留t2的所有row
IS/IS NOT NULL . col IS/IS NOT NULL col是不是为null
## 算式(select / where)
当需要对select的col 或 where条件的col 经过一定计算后才能使用
select ,col2 from table where col/2 > 1
Operator Condition SQL Example 解释
+ - * / % . col1 + col2 col加减乘除
substr . substr(col,0,4) 字符串截取
AS . col * 2 AS col_new col取别名
## 统计(select)
对查找的rows需要按col分组统计的情况
select count(*),avg(col),col from table where col > 1 group by col
Operator Condition SQL Example 解释
COUNT(*), COUNT(column) A common function used to counts the number of rows in the group if no column name is specified. Otherwise, count the number of rows in the group with non-NULL values in the specified column. count(col) 计数
MIN(column) Finds the smallest numerical value in the specified column for all rows in the group. min(col) 最小
MAX(column) Finds the largest numerical value in the specified column for all rows in the group. max(col) 最大
AVG(column) Finds the average numerical value in the specified column for all rows in the group. avg(col) 平均
SUM(column) Finds the sum of all numerical values in the specified column for the rows in the group. sum(col) 求和
GROUP BY . group by col,col2 分组
HAVING . HAVING col>100 分组后条件

子表 (table)

一次select的结果rows作为下一次select的临时table才能得到最终结果
select * from (select * from table where col > 1) as tmp where col < 1
Operator Condition SQL Example 解释
(select -)as tmp (select -)as tmp select结果做子表
in(select -) in(select -) select结果做条件
avg(select -) avg(select -) select结果做条件

c++2.0

发表于 2019-08-04 更新于 2020-07-06 评论数: 阅读次数:

auto

  • auto不能用来声明函数的返回值。但是如果函数有一个尾随的返回类型,auto是可以声明返回值。
    1
    2
    3
    4
    template<class T1, class T2>
    auto test(T1 t1,T2 t2) -> **decltype**(t1 + t2){
    return t1+t2;
    }

std::array

  • array对象的长度是固定的,使用了静态存储区,即存储在栈上,效率跟数组相同,但是更加的安全。

nullptr

  • nullptr->void*
  • nullptr是std::nullptr_t。存在和任何指针类型以及类成员指针类型的空值,bool类型的隐式转换。
  • 不存在整型的隐式转换。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    f(int *p){}
    f1(bool o){}
    f2(std::share_ptr<int> p){}
    int p1 = NULL;
    int p2 = nullptr;
    if(p1==p2);
    f(p2);
    f1(p2);
    f2(p2);
    int i = nullptr; //false

基于范围的for循环

1
2
3
4
5
6
7
8
9
10
11
12
13
for ( val declare : collection )
statement
for(int e : arr)
{
e = e*e;
std::cout<<e<<std::endl;
}
template<class InputIterator, class Function>
Function for_each(
InputIterator _First,
InputIterator _Last,
Function _Func
);

override final

  • override:表示函数应当重写基类中的虚函数。(纯虚函数,抽象类)
  • final:表示派生类不应当重写这个虚函数。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    class A{
    public:
    virtual void func(int i){
    cout<<"A::f"<<endl;
    }
    };
    class B : public A{
    public:
    virtual void func(long l) final {
    cout<<"B::f"<<endl;
    }
    };
    class C : public A{
    public:
    virtual void func(int i) override {
    cout<<"B::f"<<endl;
    }
    };

强类型枚举

  • 传统enum会将常量暴露在外层作用域。存在隐式整型转型。
  • 强类型枚举由关键字enum class标识,不存在隐式转整型。
    1
    2
    enum class a{one,two,all};
    a ob = a::one;

variadic templates 可变参数列表, 类型和个数任意

1
2
3
4
5
6
7
8
9
10
11
template <class T, class... Args>
void print(const T &first, const Args &... args){
cout<<first<<endl;
print(args...);
}
void print(){}

template< class...T>
void print(const Args &... args){
/**/
}

智能指针 pointer-like classess

#include<memoray>

  • 智能指针构造函数是explicit,拒绝内置指针隐式转换为智能指针。
  • shared_ptr<T> 可以指向同一地址空间,通过count计数,防止成为空悬指针。
  • unique_ptr<T> 独占内存空间。
  • 不要使用get()初始化指针,会产生未定义行为。

常用共有函数列表

  • p.get() 返回内置指针。
  • swap(a,b) a.swap() 交换指针

shared_ptr<T> p 独有函数

  • make_shared<T>(arg) 返回一个指针 指向用arg初始化的对象。
  • shared_ptr<T> p(q) 拷贝构造函数赋值。
  • p.unique()->bool
  • p.use_count()->int
1
2
3
4
5
6
shared_ptr<double>pd;
double *p_reg = new double;
pd = p_reg;//错误,不允许隐式转换
pd = shared_ptr<double>(p_reg);//正确
shared_ptr<double> pshared = p_reg;//错误,不允许隐式转换
shared_ptr<double> pshared(p_reg);//正确
  • 不能将smart point 应用非堆内存.
  • 同一个shared_ptr多线程写的情况下,线程不安全。

auto_ptr<T> p(new T);

  • 旧版本的auto_ptr,所有权转移,尽量用新智能指针替代。
  • auto_ptr缺点:
    1. auto_ptr对象不能保存指向静态分配对象的指针。否则,当auto_ptr对象本身被撤销时,它将试图删除指向非动态分配对象的指针,导致未定义的行为。
    2. 不要使用auto_ptr对象保存指向动态分配数组的指针,标准库析构实现采用的是delete。
    3. 所有权无法共享,不支持拷贝赋值。

      weak_ptr<T> p(new T);

  • weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象. 进行该对象的内存管理的是那个强引用的 shared_ptr. weak_ptr只是提供了对管理对象的一个访问手段. 弱引用并不修改该对象的引用计数, 这意味这弱引用它并不对对象的内存进行管理.
  • weak_ptr指向shared_ptr指针指向的对象的内存,却并不拥有该内存。
  • 解决shared_ptr环形引用的问题.(两个对象各自包含指向彼此的shared_ptr成员,形成环状引用,引用计数永远为1,不能销毁,造成内存泄漏).

    unique_ptr<T> p(new T);

  • unique_ptr<T> 拒绝拷贝和赋值。但可以通过u.reset() .u.release()间接拷贝和赋值。
  • release()->pointer u.reset(m) 提供可选指针参数列表,u = m
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    template < class T>
    class shared_ptr{
    private:
    T *p;
    public:
    shared_ptr(T *pm):p(pm){}
    T& operator*() const{return *p;}
    T* operator->() const{return p;}
    };
    //->运算符作用之后得到的输出继续用->作用。

迭代器 pointer-like classes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
template< class T>
struct __list_node{
void *prev;
void *next;
T data;
};
template< class T,class Ref, class Ptr>
class __list_iterator{
typedef __list_iterator<T,Ref,Ptr> self;
typedef Ptr pointer;
typedef Ref reference;
typedef __list_node<T>* link_type;
link_type node;
bool operator==(const self&x)const{
return node == x.node;
}
reference operator*() const {return (*node).data;}
pointer operator->() const {return &(operator*());}
}

std::bind

  • 将函数、成员函数和闭包转成function函数对象

  • 将多元(n>1)函数转成一元函数或者(n-1)元函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    template< class F, class... Args >
    /*unspecified*/ bind( F&& f, Args&&... args );
    template< class R, class F, class... Args >
    /*unspecified*/ bind( F&& f, Args&&... args );
    auto x1 = bind(f,10,20,10);
    auto x2 = bind(f,std::placeholders::_1,2,3);
    auto x3 = bind(f,std::placeholders::_1,23,std::placeholders::_2);
    auto x4 = bind<double>(f,std::placeholders::_1,std::placeholders::_2,5);
    cout<<x1()<<endl;
    cout<<x2(1)<<endl;
    cout<<x3(20,32)<<endl;
    cout<<x4(23,12)<<endl;
  • 某些无法拷贝的参数,只能使用引用传递,需要使用ref

    1
    for_each(words.begin(),words.end,bind(print,ref(os),_1,' '));
  • bind 无法绑定重载函数,因为重载函数参数个数不同。

  • bind 绑定成员函数,需要类的实例作为第二个参数。

  • bind 绑定成员函数,不需要类实例。

    1
    2
    3
    A a();
    auto m = bind(&A::f(),a);
    m();

cast

  1. const_cast
  • ‘const_cast’ 这个操作符可以去掉变量const属性或者volatile属性的转换符,消除对象的常量性.
  1. static_cast
  • ‘static_cast’允许执行任意的隐式转换和相反转换动作,但没有运行时类型检查来保证转换的安全性.
  1. dynamic_cast
  • ‘dynamic_cast’只用于对象的指针和引用。当用于多态类型时,它允许任意的隐式类型转换以及相反过程,如果基类没有虚函数,也就无法判断一个基类指针变量所指对象的真实类型, 这时候dynamic_cast只能用来做安全的转换(upercasting),如从派生类指针转换成基类指针,而这种转换其实并不需要dynamic_cast参与。
    . 如果一个引用类型执行了类型转换并且这个转换是不可能的
  1. reinterpret_cast
  • 是一个指针、引用、算术类型、函数指针或者成员指针。它可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针。这个操作符能够在非相关的类型之间转换。操作结果只是简单的从一个指针到别的指针的值的二进制拷贝。
  1. 在上行转换中,static_cast和dynamic_cast效果是一样的,而且都比较安全,因为向上转换的对象一般是指向子类对象的子类类型指针;而在下行转换中,由于可以定义就不同了指向子类对象的父类类型指针,同时static_cast只在编译时进行类型检查,而dynamic_cast是运行时类型检查,则需要视情况而定.

未完待续

–…-….-…- -.-..—–.—- -…….——.- -.-.-…..-…- -.——……. -..—……… -..—…-.-.-. -..—.-.—.-. ——–….–.. —-.—……. -.-..–.-.-.-.- ——–….–.. ——.-.-.—- —–..-.—..- –……….-. —-..-.-.—.- -……—-.-.-. -.—.—–…- —.-.-…—– –..-.—-..-.- -.——–.-.– -..—..-.-…. –……….-.

内存管理(ptmalloc,tcmalloc,jemalloc)

发表于 2019-08-01 更新于 2020-09-14 评论数: 阅读次数:

内存分配方式

  1. 栈
    栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。

  2. 堆
    手动delete,程序结束后,OS自动回收。堆是一块连续的内存区域。这块内存区域的起始位置和结束位置,并且这块连续的内存区域可以被程序员随意使用,并且这块区域是很大。

  3. 全局/静态存储区
    未初始化全局区和已初始化全局区

存储内容

  1. 栈:在栈中,第一个进栈的是主函数下一条指令的地址,然后是函数的各个参数,在大多数编译器中,参数是由右往左入栈,然后是函数中的局部变量。注意,静态变量不入栈。出栈则刚好顺序相反。

  2. 堆:一般在堆的头部用一个字节存放堆的大小,具体内容由程序员安排。

LINUX 系统调用函数

大内存各种malloc基本上都是直接mmap的。而对于小数据,则通过向操作系统申请扩大堆顶,这时候操作系统会把需要的内存分页映射过来,然后再由这些malloc管理这些堆内存块,减少系统调用。而在free内存的时候,不同的malloc有不同的策略,不一定会把内存真正地还给系统,所以很多时候,如果访问了free掉的内存,并不会立即Run Time Error,只有访问的地址没有对应的内存分页,才会崩掉。

brk()和sbrk()

1
2
3
#include <unistd.h>
int brk(void *addr);
void *sbrk(intptr_t increment);
  1. brk()和sbrk()改变程序间断点的位置。程序间断点就是程序数据段的结尾。(程序间断点是为初始化数据段的起始位置).通过增加程序间断点进程可以更有效的申请内存 。当addr参数合理、系统有足够的内存并且不超过最大值时brk()函数将数据段结尾设置为addr,即间断点设置为addr。sbrk()将程序数据空间增加increment字节。当increment为0时则返回程序间断点的当前位置。
  2. brk()成功返回0,失败返回-1并且设置errno值为ENOMEM(注:在mmap中会提到)。sbrk()成功返回之前的程序间断点地址。如果间断点值增加,那么这个指针(指的是返回的之前的间断点地址)是指向分配的新的内存的首地址。如果出错失败,就返回一个指针并设置errno全局变量的值为ENOMEM。
  3. 这两个函数都用来改变 “program break” (程序间断点)的位置,改变数据段长度(Change data segment size),实现虚拟内存到物理内存的映射。
  4. brk()函数直接修改有效访问范围的末尾地址实现分配与回收。
  5. sbrk()参数函数中:当increment为正值时,间断点位置向后移动increment字节。同时返回移动之前的位置,相当于分配内存。当increment为负值时,位置向前移动increment字节,相当与于释放内存,其返回值没有实际意义。当increment为0时,不移动位置只返回当前位置。参数increment的符号决定了是分配还是回收内存。
  6. brk和sbrk分配的内存需要等到高地址内存释放以后才能释放,无法直接释放,需要按照顺序。

mmap()

当开辟的空间大于128k时,mmap()系统调用函数来在虚拟地址空间中(堆和栈中间,称为文件映射区域的地方)找一块空间来开辟。

mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享。

1
void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);

参数说明:
成功执行时,mmap()返回被映射区的指针。失败时,mmap()返回MAP_FAILED[其值为(void *)-1], error被设为以下的某个值:

1
2
3
4
5
6
7
8
9
10
11
12
 1 EACCES:访问出错
2 EAGAIN:文件已被锁定,或者太多的内存已被锁定
3 EBADF:fd不是有效的文件描述词
4 EINVAL:一个或者多个参数无效
5 ENFILE:已达到系统对打开文件的限制
6 ENODEV:指定文件所在的文件系统不支持内存映射
7 ENOMEM:内存不足,或者进程已超出最大内存映射数量
8 EPERM:权能不足,操作不允许
9 ETXTBSY:已写的方式打开文件,同时指定MAP_DENYWRITE标志
10 SIGSEGV:试着向只读区写入
11 SIGBUS:试着访问不属于进程的内存区
返回错误类型
  • start:映射区的开始地址

  • length:映射区的长度

  • prot:期望的内存保护标志,不能与文件的打开模式冲突。是以下的某个值,可以通过or运算合理地组合在一起

    1
    2
    3
    4
    1 PROT_EXEC :页内容可以被执行
    2 PROT_READ :页内容可以被读取
    3 PROT_WRITE :页可以被写入
    4 PROT_NONE :页不可访问
  • fd:有效的文件描述词。如果MAP_ANONYMOUS被设定,为了兼容问题,其值应为-1

  • flags:指定映射对象的类型,映射选项和映射页是否可以共享。它的值可以是一个或者多个以下位的组合体

  • offset:被映射对象内容的起点

SGI STL 的 allocator

  • STL 包含一级配置爱和二级配置器,对于大于128bytes的空间,直接使用第一级配置器第一级配置器以malloc(),free(),realloc()等C函数执行实际的内存配置、释放、重新配置等操作,并且能在内存需求不被满足的时候,调用一个指定的函数。
  • 小于128bytes的内存申请,使用内存池进行管理。基本思路是设计一个free_list[16]数组,负责管理从8bytes到128 bytes不同大小的内存块(chunk),每一个内存块都由连续的固定大小(fixed size block)的很多chunk组成,并用指针链表串接起来。当用户要获取此大小的内存时,就在free_list的链表找一个最近的free chunk回传给用户,同时将此chunk从free_list里删除,即把此chunk前后chunk指针链结起来。用户使用完释放的时候,则把此chunk放回到free_list中,放到最前面的start_free的位置。这样经过若干次allocator和deallocator后,free_list中的链表可能并不像初始的时候那么是chunk按内存分布位置依次链接的。假如free_list中不够时,allocator会自动再分配一块新的较大的内存区块来加入到free_list链表中。

ptmalloc(glibc)

  • ptmalloc 包含一个主分配区和多个非主分配区(非主分配区智能通过mmap()申请内存)。 当某个线程调用malloc的时候,会先查看线程私有变量中是否已经存在一个分配区,如果存在则尝试加锁,如果加锁失败则遍历arena链表试图获取一个没加锁的arena, 如果依然获取不到则创建一个新的非主分配区。
  • 分配的内存在ptmalloc中使用chunk表示,每个chunk至少需要8个字节额外的开销。用户free掉的内存不会马上归还操作系统,ptmalloc会统一管理heap和mmap区域的空闲chunk,避免了频繁的系统调用。
  • ptmalloc将相似大小的chunk用双向链表链接起来,这样的一个链表被称为一个bin。Ptmalloc 一共维护了128个bin,并使用一个数组来存储这些bin。
  • 数组中的第一个为 unsorted bin, 数组中从 2 开始编号的前 64 个 bin 称为 small bins, 同一个small bin中的chunk具有相同的大小。small bins后面的bin被称作large bins。

    分配步骤

  1. 在free的时候,ptmalloc会检查附近的chunk,并尝试把连续空闲的chunk合并成一个大的chunk,放到unstored bin里。
  2. 但是当很小的chunk释放的时候,ptmalloc会把它并入fast bin中。同样,某些时候,fast bin里的连续内存块会被合并并加入到一个unsorted bin里,然后再才进入普通bin里。
  3. 所以malloc小内存的时候,是先查找fast bin,再查找unsorted bin,最后查找普通的bin,如果unsorted bin里的chunk不合适,则会把它扔到bin里。
  4. Ptmalloc的分配的内存顶部还有一个top chunk,如果前面的bin里的空闲chunk都不足以满足需要,就是尝试从top chunk里分配内存。如果top chunk里也不够,就要从操作系统里拿了。
  5. 特别大的内存,会直接从系统mmap出来,不受chunk管理,这样的内存在回收的时候也会munmap还给操作系统。

    回收free

    和分配过程反过来,加上一些chunk合并和从一个bin转移到另一个bin的操作。如果顶部有足够大的空闲chunk,则收缩堆顶并还给操作系统。

    缺点

  6. Ptmalloc默认后分配内存先释放,因为内存回收是从top chunk开始的,top chunk无法释放时,其他的也无法释放。
  7. 多线程频繁分配和释放内存,会造成频繁加解锁。
  8. 不定期分配长生命周期的内存,容易造成内碎片,影响内存回收。

tcmalloc(google thread cache malloc(线程缓存分配器))


其内存管理分为线程内存和中央堆两部分。

小对象分配

对于小块内存分配,其内部维护了几十个不同大小的分配器,和ptmalloc不同的是,它的每个分配器的大小差是不同的,依此按8字节、16字节、32字节等间隔开。在内存分配的时候,会找到最小复合条件的,比如833字节到1024字节的内存分配请求都会分配一个1024大小的内存块。如果这些分配器的剩余内存不够了,会向中央堆申请一些内存,打碎以后填入对应分配器中。同样,如果中央堆也没内存了,就向中央内存分配器申请内存。

在线程缓存内的60个分配器(_文档上说60个,但是我在2.0的代码里看到得是86个_)分别维护了一个大小固定的自由空间链表,直接由这些链表分配内存的时候是不加锁的。但是中央堆是所有线程共享的,在由其分配内存的时候会加自旋锁(spin lock)。

在线程内存池每次从中央堆申请内存的时候,分配多少内存也直接影响分配性能。申请地太少会导致频繁访问中央堆,也就会频繁加锁,而申请地太多会导致内存浪费。在tcmalloc里,这个每次申请的内存量是动态调整的,调整方式使用了类似把tcp窗口反过来用的慢启动(slow start)算法调整max_length,每次申请内存是申请max_length和每个分配器对应的num_objects_to_move中取小的值的个数的内存块。

num_objects_to_move取值比较简单,是以64K为基准,并且最小不低于2,最大不高于32的值。也就是说,对于大于等于32K的分配器这个值为2(好像没有这样的分配器 class),对于小于2K的分配器,统一为32。

大内存分配

对于大内存分配(大于8个分页, 即32K),tcmalloc直接在中央堆里分配。中央堆的内存管理是以分页为单位的,同样按大小维护了256个空闲空间链表,前255个分别是1个分页、2个分页到255个分页的空闲空间,最后一个是更多分页的小的空间。这里的空间如果不够用,就会直接从系统申请了。

分页管理

Tcmalloc使用一种叫span的东西来管理内存分页,一个span可以包含几个连续分页。一个span的状态只有未分配(这时候在空闲链表中),作为大对象分配,或作为小对象分配(这时候span内记录了小对象的class size)。

在资源释放的时候,首先计算其分页编号,然后再查找出它对应的span,如果它是一个小对象,则直接归入小对象分配器的空闲链表。等到空闲空间足够大以后划入中央堆。如果是大对象,则会把物理地址连续的前后的span也找出来,如果空闲则合并,并归入中央堆中。

jemalloc(facebook)

  • Jemalloc 把内存分配分为了三个部分,第一部分类似tcmalloc,是分别以8字节、16字节、64字节等分隔开的small class;第二部分以分页为单位,等差间隔开的large class;然后就是huge class。内存块的管理也通过一种chunk进行,一个chunk的大小是2^k (默认4 MB)。通过这种分配实现常数时间地分配small和large对象,对数时间地查询huge对象的meta(使用红黑树)。

  • Jemalloc在64bits系统上使用下面的size-class分类:

    1. Small: [8], [16, 32, 48, …, 128], [192, 256, 320, …, 512], [768, 1024, 1280, …, 3840]
    2. Large: [4 KiB, 8 KiB, 12 KiB, …, 4072 KiB]
    3. Huge: [4 MiB, 8 MiB, 12 MiB, …]
  • small/large对象查找metadata需要常量时间, huge对象通过全局红黑树在对数时间内查找。

  • 虚拟内存被逻辑上分割成chunks(默认是4MB,1024个4k页),应用线程通过round-robin算法在第一次malloc的时候分配arena, 每个arena都是相互独立的,维护自己的chunks, chunk切割pages到small/large对象。free()的内存总是返回到所属的arena中,而不管是哪个线程调用free()。

  • Jemalloc也使用了分配区(arena)来维护内存。线程按第一次分配small或者large内存请求的顺序Round-Robin地选择一个分配区。每个分配区都维护了一系列分页,来提供small和large的内存分配请求。并且从一个分配区分配出去的内存块,在释放的时候一定会回到该分配区。

  • 每个分配区内都会包含meta信息,记录了其对应的chunk列表,每个chunk的头部都记录了chunk的分配信息。在使用某一个chunk的时候,会把它分割成很多个run,并记录到bin中。不同size的class对应着不同的bin,这点和前面两种分配器一样。在bin里,都会有一个红黑树来维护空闲的run,并且在run里,使用了bitmap来记录了分配状态。

  • 和前面两种分配器不同的是,分配区会同时维护两组run的红黑树,一组是未被分配过内存(clean区域),另一组是回收的内存(dirty区域)。而且不是前面那两种分配器所使用的链表。同样,每次分配,都是选取最小且符合条件的内存块。但是优先从dirty区域查找。

  • 由于分配区的设计和ptmalloc差不多。在访问分配区的时候需要对其加锁,或者对某一个size的bin加粒度更小的锁。为了减少锁征用,这里又参照tcmalloc引入了线程缓存。并且其线程缓存的垃圾回收机制和tcmalloc一样,也是基于分配请求的频率自动调整的。线程缓存的结构就像一个简化版的arena,加了一些垃圾回收的控制信息。

  1. 小内存(small class): 线程缓存bin -> 分配区bin(bin加锁) -> 系统调用
  2. 中型内存(large class):分配区bin(bin加锁) -> 系统调用
  3. 大内存(huge class): 直接mmap组织成N个chunk+全局huge红黑树维护(带缓存)

基本数据结构整理

发表于 2019-07-30 更新于 2020-06-05 分类于 foundation 评论数: 阅读次数:

顺序结构

顺序栈

1
2
3
4
5
6
7
8
9
10
11
12
13
template <class T>
class sq_stack{
private:
T val;
int top;
int size;
public:
sq_stack();
~sq_stack();
T pop();
void insert(T val);
int get_size(sq_stack s);
}

队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template<class T>
class sq_queue{
private:
T val;
int front;
int rear;
int maxsize;
public:
sq_queue();
~sq_queue();
int get_rear(sq_queue r);
int get_front(sq_queue f);
void insert(T val);
}

顺序表

1
2
3
4
5
6
template <class T>
class sq_list{
vector<T> a;
int length;
int size;
}

链式结构

##

1
2
3
4
5
6
7
8
9
10
11
12
13
template <class T>
class link_node{
private:
T data;
link_node *next;
public:
link_node(T d):data(d),next(nullptr){}
~link_node();
}
typedef struct Lnode{
elemtype data;
struct Lnode *next;
}Lnode,*LinkList;

哈希表

  • 哈希函数:H(key): K -> D , key ∈ K

  • 构造方法

    1. 直接定址法
    2. 除留余数法
    3. 数字分析法
    4. 折叠法
    5. 平方取中法
  • 冲突解决方法
    链地址法:key相同用单链表链接
    开放定址法:

  • 线性探测法:放到key的下一个位置,Hi = (H(key) + i) % m

  • 二次探测法:Hi = (H(key) + i) % m

  • 随机探测法:Hi= (H(key) + 伪随机数) % m

1
2
3
4
5
6
7
8
9
10
11
12
typedef char KeyType;

typedef struct{
KeyType key;
}RcdType;

typedef struct{
RcdType *rcd;
int size;
int count;
bool *tag;
}HashTable;

🌲

二叉🌲

存储结构

  1. 顺序存储
  2. 链式存储

    遍历方式

  3. 先序
  4. 后序
  5. 中序
  6. 层次

    实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    template <class T>
    class binary_node{
    private:
    binary_node *left;
    binary_node *right;
    T val;
    public:
    binary_node(int x):val(x),left(nullptr),right(nullptr){}
    ~binary_node();
    void in_order(binary_node *root);
    void pre_order(binary_node *root);
    void post_order(binary_node *root);
    void level_order(binary_node *root);
    }

类型

  • 满二叉🌲
  • 完全二叉🌲(堆)
    • 大根堆
    • 小根堆
  • 二叉查找🌲(排序🌲)
    1. 左子树上所有结点的值均小于或等于它的根结点的值。
    2. 右子树上所有结点的值均大于或等于它的根结点的值。
    3. 左、右子树也分别为二叉排序树。
    • 查找的最大次数等于数的高度。缺点(多次插入偏向一边的数据,会导致🌲严重不平衡)
  • 平衡二叉🌲(AVL)
  1. 平衡因子BF 左高-右高 ,根节点BF>1 右旋 顺时针 <-1 左旋
  2. 插入结点后,最小不平衡子树的BF与它的子树的BF符号相反时,就需要对结点先进行一次旋转以使得符号相同后,再反向旋转一次才能够完成平衡操作
  • 最小失衡🌲(平衡二叉树插入新的节点导致失衡的子🌲):调整策略

    • LL 左孩子右旋
    • RR 右孩子左旋
    • LR 左孩子左旋,再右旋
    • RL 右孩子的左子🌲先右旋,在左旋

      红黑🌲

      解决了二叉查找树的不平衡问题。最大查找次数不会超过最短路径的两倍。主要是用它来存储有序的数据,它的时间复杂度是O(lgn),效率高。
    1. 节点是红色或黑色。
    2. 根节点为黑色。
    3. 所有的叶子节点都是黑色的空节点。
    4. 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)。
    5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。

      自平衡

    • 变色

    • 旋转
      左旋转
      逆时针旋转红黑树的两个节点,使得父节点被自己的右孩子取代,而自己成为自己的左孩子。

      右旋转
      顺时针旋转红黑树的两个节点,使得父节点被自己的左孩子取代,而自己成为自己的右孩子。

      实现

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      enum tree_color(RED,BLACK);
      template <class T>
      class rb_node{
      public:
      tree_color color;
      T key;
      rb_node *left;
      rb_node *right;
      rb_node *parent;
      rb_node(T val,rb_color c,rb_node l,rb_node r,rb_node p):key(val),color(c),left(l),right(r),parent(p){}
      };
      template <class T>
      class rb_tree{
      rb_tree();
      ~rb_tree();
      void pre_order();
      void in_order();
      void post_order();
      rb_node<T>* re_search(T key); //递归查找
      rb_node<T>* iter_serach(T key); //非递归
      T minkey();
      T maxkey();
      rb_node<T>* successor(rb_node<T> *x);
      rb_node<T>* predecessor(rb_node<T> *x);
      void insert(T key);
      void remove(T key);
      void destory();
      void print();
      private:
      void left_rotate(rb_node<T>* &root,rb_node<T>* &x);
      void right_rotate(rb_node<T>* &root,rb_node<T>* &x);
      }

      ## B🌲
      * m阶B🌲
      1. 每个结点最多有m-1个关键字
      2. 根结点最少可以只有1个关键字
      3. 每个结点中的关键字都按照从小到大的顺序排列,每个关键字的左子树中的所有关键字都小于它,而右子树中的所有关键字都大于它
      4. 根结点到每个叶子结点高度相同
      5. 非根结点至少有Math.ceil(m/2)-1个关键字

      ### 实现
      ```c++
      template <class T>
      struct B_tree_node {
      int n;
      vector<T> *key;
      bool is_leaf;
      struct B_tree_node *parent;
      vector<B_tree_node *> *child;
      };

      template <class T>
      class B_tree{
      private:
      B_tree_node<T>* root;
      int m;
      public:
      B_tree(int tVal = 2);
      ~B_tree();
      B_tree_node<T>* search_tree(B_tree_node<T>* root, T k ,int &index);
      B_tree_node<T>* getRoot();
      void insert(B_tree_node<T>* root,B_tree_node<T>* node);
      void delete(B_tree_node<T>* root,B_tree_node<T>* node);
      T get_value(B_tree_node<T>* root,T value);
      };

B+

  • M阶B+🌲
    1. 内结点存有关键字和指向孩子结点指针。外结点存有关键字和数据。
    2. 叶子结点还有关键字,按照关键字大小排序,叶子结点中存在指向兄弟结点的指针。
    3. 一颗B+🌲存在两个指针,一个指向根结点,一个指向存有最小关键字的结点。
    4. 有n棵子树的结点中含有n个关键字,每个关键字不保存数据,只用来索引。
    5. 所有数据均存储在叶子结点中。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      template <class T>
      class bp_node{
      public:
      bp_node();
      virtual ~bp_node();
      private:
      T data;
      };
      template <class T>
      class inter_node: public bp_node{
      public:
      }

八叉树

图

排序

查找

递归

折半查找

归并查找

快速排序

迭代

折半查找

归并查找

c++杂乱笔记2

发表于 2019-07-30 更新于 2020-08-24 分类于 语言 评论数: 阅读次数:

OOP

多态

  • C++多态分类及其实现
    1. 重载多态(Ad-hoc Polymorphism,编译期) 函数重载,运算符重载
    2. 子类型多态(Subtype Polymorphism,运行期) 虚函数
    3. 参数多态性(Parametric PolyMorphism) 编译期 类模板,函数模板
    4. 强制多态(Coerion PolyMorphism 编译期,运行期) 类型转换

静态多态(编译期间,早绑定)

  • 函数重载,模板

    动态多态(运行期间,晚绑定)

  • 虚函数
  • 多态与非多态的实质区别就是函数地址是早绑定还是晚绑定
    tips:
  • 普通函数不可是virtual
  • static 不可为virtual
  • 构造函数不可为virtual,因为在调用构造函数时,虚表指针并没有在对象的内存空间中,必须要构造函数调用完成后才会形成虚表指针
    tips:C++ 隐藏
  • 如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。
  • 如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆).

虚函数指针相关

虚函数指针和虚函数表

  • A b;

    父类虚函数表
  • 包含虚函数的类才会有虚函数表,同属于一个类的对象共享虚函数表, 但是有各自的虚函数指针.虚函数表实质是一个指针数组,里面存的是虚函数的函数指针。
  • 虚表指针放在类成员中的第一个地址(C++的编译器应该是保证虚函数表的指针存在于对象实例中最前面的位置(),可以通过(long)(&b) 访问虚表地址,(long)(long)(&b) 访问第一个第一个虚函数地址。
    1
    2
    3
    pFun = (Fun)*((int*)*(int*)(&b));
    pFun();
    reinterpret_cast<Fun>(*((long *)*(long *)(&d)))();


单继承,不重写虚函数

单继承,重写虚函数

多继承,不重写虚函数

多继承,重写虚函数

virtual ~f()

作用
* 解决based point-> derived object,用based point 删除derived object.
* 如果父类的虚函数是private或是protected的,但这些非public的虚函数同样会存在于虚函数表中,所以,我们同样可以使用访问虚函数表的方式来访问这些non-public的虚函数.

虚继承

虚继承用于解决多继承条件下的菱形继承问题(费存储空间、存在二义性)

* 底层实现原理与编译器相关,一般通过虚基类指针和虚基类表实现,每个虚继承的子类都有一个虚基类指针(占用一个指针的存储空间,4字节)和虚基类表(不占用类对象的存储空间)(需要强调的是,虚基类依旧会在子类里面存在拷贝,只是仅仅最多存在一份而已,并不是不在子类里面了);当虚继承的子类被当做父类继承时,虚基类指针也会被继承。

* 实际上,vbptr 指的是虚基类表指针(virtual base table pointer),该指针指向了一个虚基类表(virtual table),虚表中记录了虚基类与本类的偏移地址;通过偏移地址,这样就找到了虚基类成员,而虚继承也不用像普通多继承那样维持着公共基类(虚基类)的两份同样的拷贝,节省了存储空间。

虚函数和虚继承

  • 都使用了虚指针(占用类的存储空间)和虚表(不占类存储空间)
  • 虚函数表存储的是虚函数地址,虚基类表存储相对直接继承类的位偏移。
  • 虚基类在子类中存在一份拷贝,而虚函数不占用存储空间。

tips:

  • 模板类可以使用虚函数。
  • 但任何类的成员模板函数不能是虚函数。

聚合类

用户可以直接访问其成员,并且具有特殊的初始化语法形式。满足如下特点:
  • 所有成员都是 public
  • 没有定义任何构造函数
  • 没有类内初始化
  • 没有基类,也没有 virtual 函数

内存分配管理

  1. malloc
    申请指定字节内存,初始值不确定。

char *str = (char*) malloc(100);

  1. calloc
    为指定长度的对象,分配能容纳其指定个数的内存。申请到的内存的每一位(bit)都初始化为 0。
  2. realloc
    更改以前分配的内存长度(增加或减少)。当增加长度时,可能需将以前分配区的内容移到另一个足够大的区域,而新增区域内的初始值则不确定。
  3. alloca
    在栈上申请内存。程序在出栈的时候,会自动释放内存。但是需要注意的是,alloca 不具可移植性, 而且在没有传统堆栈的机器上很难实现。alloca 不宜使用在必须广泛移植的程序中。C99 中支持变长数组 (VLA),可以用来替代 alloca。
  • new/new[]先底层调用malloc,然后调用构造函数。
  • delete/delete[]先调用析构函数,然后free,指针置为空。
  • new能自动计算所需字节数,无需像malloc一样手动操作。

pacement new

允许传递额外的地址参数,预先在指定的内存区域创建对象。

1
2
3
4
5
6
new (place_address) type
new (place_address) type (initializers)
new (place_address) type [size]
new (place_address) type [size] { braced initializer list}
//place_address 为 point
//initializers可能为空的初始化列表

tips:

  • delete this 合法前提
    • 之后不再调用this指针(任何调用)
    • this对象由new而不是其他任何方式分配的。
    • 该成员函数是this对象最后调用的的成员函数。
    • 剩下的成员函数(delete this之后的)不接触到this对象,包括调用任何其他成员函数或访问任何数据成员。

只在堆上生成对象的类

方法:将析构函数私有。
  • (C++ 是静态绑定语言,编译器管理栈上对象的生命周期,编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性。若析构函数不可访问,则不能在栈上创建对象。)
  • 在栈上建立一个类对象,是由编译器为对象在栈空间中分配内存,是通过直接移动栈顶指针,挪出适当的空间,然后在这片内存空间上调用构造函数形成一个栈对象。使用这种方法,直接调用类的构造函数。

只在栈上生成对象的类

方法:将new和delete操作符重载为私有。
  • :C++ 是静态绑定语言,编译器管理栈上对象的生命周期,编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性。若析构函数不可访问,则不能在栈上创建对象。
  • 动态建立类对象,是使用new运算符将对象建立在堆空间中。这个过程分为两步,第一步是执行operator new()函数,在堆空间中搜索合适的内存并进行分配;第二步是调用构造函数构造对象,初始化这片内存空间。这种方法,间接调用类的构造函数。

c++杂乱笔记1

发表于 2019-07-29 更新于 2020-09-12 分类于 语言 评论数: 阅读次数:

const(不可变)

作用

1. 修饰常量
2. 修饰指针  指针常量和常量指针
3. 常量引用  避免拷贝,避免对值的修改,不能使用非const引用指向const对象。
4. 修饰成员函数   函数内部不能修改成员函数

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const int a1;
int get() const;
const char* p1 = a2;//常量指针,指向常量的指针,保护指向的地址空间数据不可变。指针可变,可以修改const指针所指向的地址,但是不能通过const对象指针来进行,自以为指向const的指针。
char* p = a2;
*p = a3;
char* const p2 = a3;//指针常量, 保护指针,指针自身的地址空间不可变,数据可变。
//不能把常量的地址赋给指针变量
void get(const int val);
void get(const int *val);
void get(int *const val);

//file1.cpp
int cout;
const int cout2;
extern const int cout3;
//file2.cpp
extern int cout; //在全局作用域里定义非const变量时,它在整个程序中都可以访问
cout++;
cout2; //error除非特别说明,在全局作用域声明的const变量是定义该对象的文件的局部变量,只存在于那个文件,不能被其他文件访问。
extern const int cout3;
cout3++;

static()

作用

1. 修饰普通变量,修改变量的存储区域和生命周期,使变量存储在静态区,在 main 函数运行前分配空间,如果有初始值就用初始值初始化,如果没有初始值系统用默认值初始化。
2. 修饰普通函数,该函数仅在定义该函数的文件内才能使用。
3. 修饰成员变量,修饰成员变量使所有的对象只保存一个该变量,而且不需要生成对象就可以访问该成员。
4. 修饰成员函数,修饰成员函数使得不需要生成对象就可以访问该函数,使用类名作用域符号访问,但是在 static 函数内不能访问非静态成员。

this指针

1. 指向非静态成员的特殊指针。
2. 当对一个对象调用成员函数时,编译程序先将对象的地址赋给 this 指针,然后调用成员函数,每次成员函数存取数据成员时,都隐式使用 this 指针。
3. this 指针被隐含地声明为: ClassName *const this,这意味着不能给 this 指针赋值。在 ClassName 类的 const 成员函数中,this 指针的类型为:const ClassName* const,这说明不能对 this 指针所指向的这种对象是不可修改的(即不能对这种对象的数据成员进行赋值操作)。
4. this 只是一个右值,不能使用&this.
5. 在以下场景中,经常需要显式引用 this 指针:
    1. 为实现对象的链式引用。
    2. 为避免对同一对象进行赋值操作。
    3. 在实现一些数据结构时,如 list。

inline

特征

1. 相当于直接执行函数体。
2. 相当于宏操作,但是多了类型检查。
3. 编译器一般不内敛包含循环,递归,switch等复杂操作的inline函数。
4. 在类声明中定义的函数,除了虚函数的成员函数会自动隐式地当成inline函数。

使用

1
2
3
4
5
// 类外定义需要显示inline
class A{
void b();
}
inline void A::b(){};

编译器对inline函数的处理过程

1. 函数体复制到调用点。
2. 为局部变量分配内存空间。
3. 将inline函数中的输入参数和返回值映射到调用方法的局部变量空间。
4. 如果 inline 函数有多个返回点,将其转变为 inline 函数代码块末尾的分支(使用 goto)。

优劣

优点

1. 内联函数同宏函数一样将在被调用处进行代码展开,省去了参数压栈、栈帧开辟与回收,结果返回等,从而提高程序运行速度。
2. 内联函数相比宏函数来说,在代码展开时,会做安全检查或自动类型转换(同普通函数),而宏定义则不会。
3. 在类中声明同时定义的成员函数,自动转化为内联函数,因此内联函数可以访问类的成员变量,宏定义则不能。
4. 内联函数在运行时可调试,而宏定义不可以。

缺点

1. 代码膨胀,省去了调用函数的开销。关键在于函数体内执行时间和调用函数开销的平衡。
2. inline函数无法随着函数库升级而升级,inline函数改变需要重新编译,无法直接链接。
3. 是否内联,程序员不可控。内联函数只是对编译器的建议,是否对函数内联,决定权在于编译器。

tips:

  1. virtual可以inline,但是当virtual表示多态性时,不能内联。
  2. inline是编译器建议编译器内联,在程序运行之前,但是多态性的表现在程序运行中。
  3. 当编译器明确知道调用对象是哪一个类时,具有实际对象,而不是对象指针或引用,可以使用 inline virtual
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class A{
    public:
    virtual void foo(){};
    }
    class B:A{
    public:
    virtual void foo(){};
    }
    A *a = new B();
    a->B::foo();
    delete a;
    a = nullptr;

volatile

1
volatile int a = 1;
1. volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素(操作系统、硬件、其它线程等)更改。所以使用 volatile 告诉编译器不应对这样的对象进行优化。
2. volatile 关键字声明的变量,每次访问时都必须从内存中取出值(没有被 volatile 修饰的变量,可能由于编译器的优化,从 CPU 寄存器中取值)
3. const 可以是 volatile (如只读的状态寄存器)
4. 指针可以是 volatile
1
2
3
4
5
6
7
8
## assert()
断言,是宏,而非函数。assert 宏的原型定义在 <assert.h>(C)、<cassert>(C++)中,其作用是如果它的条件返回错误,则终止程序执行。可以通过定义 NDEBUG 来关闭 assert,但是需要在源代码的开头,include <assert.h> 之前。

### 使用
```c++
# define NDEBUG
# cinlude <cassert>
assert(p!=NULL);

pragma pack(n)

强制设定struct,union,class成员变量以n 字节对齐方式。

1
2
#pragma pack(push)
#pragma pack(8)

位域

1
Bit mode:2;

类可以将其(非静态)数据成员定义为位域(bit-field),在一个位域中含有一定数量的二进制位。

  • 位域内存布局和机器有关
  • 类型必须是整型或者枚举,signed int根据具体实现而定。
  • 取地址和指针不能作用于位域。

extern “C”

按照C语言方式编译和链接。
extern “C” 的作用是让 C++ 编译器将 extern “C” 声明的代码当作 C 语言代码处理,可以避免 C++ 因符号修饰导致代码不能和C语言库中的符号进行链接的问题。

1
2
3
4
5
6
7
#ifdef _cp
extern "C"{
#endif
void *memeset(void *,int ,size_t);
#ifdef _cp
}
#endif

struct

c

1
2
3
4
5
typedef struct a{}s;
// 等价于
struct a{};
typedef struct a s;
// 但两个标识符名称空间不相同。

cpp

编译器定位符号的规则改变,首先搜索全局标识符表,如果没有将会搜索类标识符表。

没有同名类 A,可省略struct

1
2
struct A{};
void m(A m);

如果有同名类 A,则A 代表类A,struct A 代表结构体A。

  • class和struct 本质区别是默认的访问权限,struct默认public,class默认private。

union

联合(union)是一种节省空间的特殊的类,一个 union 可以有多个数据成员,但是在任意时刻只有一个数据成员可以有值。当某个成员被赋值后其他成员变为未定义状态。联合有如下特点:
* 默认权限public
* 可以含有析构函数,构造函数
* 不能含有引用类型的成员
* 不能作为derived class,不能作为base class
* 不能含有virtual
* 匿名union在定义所在的scope可直接访问union member
* 匿名union不能包含protected和private member
* 全局匿名联合必须是static。

explicit

  • 修饰构造函数,防止隐式转换和复制初始化。
  • 修饰转换函数,防止隐式转换,按语境转换除外。
  • 按语境转换
    1. if、while、for 的控制表达式;
    2. 内建逻辑运算符 !、&& 和 || 的操作数;
    3. 条件运算符 ?: 的首个操作数;
    4. static_assert 声明中的谓词;
    5. noexcept 说明符中的表达式;
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      class A{
      A(int a){}
      operator bool() const {return false;}
      };
      class B{
      B(int b){}
      explicit operator bool() const{return false;}
      };
      void getA(A a){}
      void getB(B b){}
      int main(){
      A a1(1);//直接初始化
      A a2 = 1;//复制初始化
      A a3{1};//直接列表初始化
      A a4 = {1};//复制列表初始化
      A a5 = (A)1;//允许static_cast 显示转型
      getA(1);//允许int到A的隐式转换
      if (a1);//使用转换函数A::operator bool() 的从A到bool的隐式转换
      bool a6(a1);//使用转换函数 A::operator bool()的从A到bool的隐式转换
      bool a7 = a1;//使用转换函数A::operator bool()的从A到bool的隐式转换
      bool a8 = static_cast<bool>(a1);//static_cast 进行直接初始化
      B b1(1);//ok
      B b2 = 1;//false, explitct修饰的构造函数不可复制初始化,不可复制列表初始化
      getB(b1);// 被explicit修饰的构造函数的对象不允许你int到B隐式转换
      if(b1);
      bool b6(b1);
      //被explicit 修饰的转换函数的对象可以按语境转换。
      bool b7 = b1; false,被explicit修饰的构造函数的对象不允许B到bool的转型。
      bool b8 = static_cast<bool>(b1);
      return 0;
      }

引用

1) 左值引用声明符:声明 S& D; 将 D 声明为到 声明说明符序列 所确定的类型 S 的左值引用。
2) 右值引用声明符:声明 S&& D; 将 D 声明为到 声明说明符序列 所确定的类型 S 的右值引用。
* 不存在 void 的引用,也不存在引用的引用,引用的数组,指向引用的指针.
* 引用坍缩:容许通过模板或 typedef 中的类型操作构成引用的引用,这种情况下适用引用坍缩(reference coolapsing)规则:右值引用的右值引用 坍缩成右值引用,所有其他组合均 坍缩成左值引用;
* 当函数的返回值是左值引用时,函数调用表达式成为左值表达式:
1
2
3
4
5
6
7
typedef int&  lref;
typedef int&& rref;
int n;
lref& r1 = n; // r1 的类型是 int&
lref&& r2 = n; // r2 的类型是 int&
rref& r3 = n; // r3 的类型是 int&
rref&& r4 = 1; // r4 的类型是 int&&

左值引用

常规引用,一般表示对象的身份。

右值引用

右值引用就是必须绑定到右值(一个临时对象、将要销毁的对象)的引用,一般表示对象的值。
右值引用可实现转移语义(Move Sementics)和精确传递(Perfect Forwarding),它的主要目的有两个方面:
消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率。
能够更简洁明确地定义泛型函数。
右值引用可用于为临时对象延长生存期(左值引用亦能延长临时对象生存期,但不能通过左值引用修改)

成员初始化列表

有些场合必须要用初始化列表:
1. 常量成员,因为常量只能初始化不能赋值,所以必须放在初始化列表里面
2. 引用类型,引用必须在定义的时候初始化,并且不能重新赋值,所以也要写在初始化列表里面
3. 没有默认构造函数的类类型,因为使用初始化列表可以不必调用默认构造函数来初始化

jupyter notebook 导入conda环境

发表于 2019-07-10 更新于 2020-06-05 评论数: 阅读次数:
  1. jupyter notebook导入conda环境
    • 不使用ipykernel,采用nb_conda
      conda install nb_conda

注意安装的时候不要激活环境

可以直接在jupyter里面安装包

可以在ipynb里面更改环境

coder

10 日志
2 分类
17 标签
GitHub E-Mail
© 2020 LAC
|