实习面试整理
网络部分
https与http的介绍
超文本传输协议(英语:HyperText Transfer Protocol,缩写:HTTP)是一个客户端(用户)和服务端(网站)之间请求和应答的标准,通常使用 TCP协议 。通过使用 网页浏览器 、 网络爬虫 或者其它的工具,客户端发起一个HTTP请求到服务器上指定端口(默认 端口 为80)。我们称这个客户端为用户代理程序(user agent)。应答的服务器上存储着一些资源,比如HTML文件和图像。我们称这个应答服务器为源服务器(origin server)。在用户代理和源服务器中间可能存在多个“中间层”,比如 代理服务器 、 网关 或者 隧道 (tunnel)。
超文本传输安全协议(英语:HyperText Transfer Protocol Secure,缩写:HTTPS;常称为HTTP over TLS、HTTP over SSL或HTTP Secure)是一种通过 计算机网络 进行安全通信的 传输协议 。HTTPS经由 HTTP 进行通信,但利用 SSL/TLS 来 加密 数据包。HTTPS开发的主要目的,是提供对 网站 服务器的 身份认证 ,保护交换数据的隐私与 完整性 。这个协议由 网景 公司(Netscape)在1994年首次提出,随后扩展到 互联网 上。
从输入URL到页面加载发生了什么
DNS解析
DNS解析的过程就是寻找哪台机器上有你需要资源的过程。
互联网上每一台计算机的唯一标识是它的IP地址,但是IP地址并不方便记忆。用户更喜欢用方便记忆的网址去寻找互联网上的其它计算机,也就是上面提到的百度的网址。所以互联网设计者需要在用户的方便性与可用性方面做一个权衡,这个权衡就是一个网址到IP地址的转换,这个过程就是DNS解析。TCP连接
HTTP协议是使用TCP作为其传输层协议的,当TCP出现瓶颈时,HTTP也会受到影响。
HTTP报文是包裹在TCP报文中发送的,服务器端收到TCP报文时会解包提取出HTTP报文。但是这个过程中存在一定的风险,HTTP报文是明文,如果中间被截取的话会存在一些信息泄露的风险。那么在进入TCP报文之前对HTTP做一次加密就可以解决这个问题了。HTTPS协议的本质就是HTTP + SSL(or TLS)。在HTTP报文进入TCP报文之前,先使用SSL对HTTP报文进行加密。从网络的层级结构看它位于HTTP协议与TCP协议之间。
HTTPS在传输数据之前需要客户端与服务器进行一个握手(TLS/SSL握手),在握手过程中将确立双方加密传输数据的密码信息。TLS/SSL使用了非对称加密,对称加密以及hash等。
HTTPS相比于HTTP,虽然提供了安全保证,但是势必会带来一些时间上的损耗,如握手和加密等过程,是否使用HTTPS需要根据具体情况在安全和性能方面做出权衡。
发送HTTP请求
其实这部分又可以称为前端工程师眼中的HTTP,它主要发生在客户端。发送HTTP请求的过程就是构建HTTP请求报文并通过TCP协议中发送到服务器指定端口(HTTP协议80/8080, HTTPS协议443)。HTTP请求报文是由三部分组成: 请求行, 请求报头和请求正文。服务器处理请求并返回HTTP报文
自然而然这部分对应的就是后端工程师眼中的HTTP。后端从在固定的端口接收到TCP报文开始,这一部分对应于编程语言中的socket。它会对TCP连接进行处理,对HTTP协议进行解析,并按照报文格式进一步封装成HTTP Request对象,供上层使用。这一部分工作一般是由Web服务器去进行,我使用过的Web服务器有Tomcat, Jetty和Netty等等。
HTTP响应报文也是由三部分组成: 状态码, 响应报头和响应报文。浏览器解析渲染页面
浏览器是一个边解析边渲染的过程。首先浏览器解析HTML文件构建DOM树,然后解析CSS文件构建渲染树,等到渲染树构建完成后,浏览器开始布局渲染树并将其绘制到屏幕上。 这个过程比较复杂,涉及到两个概念: reflow(回流)和repain(重绘)。DOM节点中的各个元素都是以盒模型的形式存在,这些都需要浏览器去计算其位置和大小等,这个过程称为relow;当盒模型的位置,大小以及其他属性,如颜色,字体,等确定下来之后,浏览器便开始绘制内容,这个过程称为repain。页面在首次加载时必然会经历reflow和repain。reflow和repain过程是非常消耗性能的,尤其是在移动设备上,它会破坏用户体验,有时会造成页面卡顿。所以我们应该尽可能少的减少reflow和repain。
SSL/TLS协议
传输层安全性协议(英语:Transport Layer Security,缩写:TLS)及其前身安全套接层(英语:Secure Sockets Layer,缩写:SSL)是一种 安全协议 ,目的是为 互联网 通信提供安全及数据 完整性 保障。
握手过程
开始加密通信之前,客户端和服务器首先必须建立连接和交换参数,这个过程叫做握手(handshake)。
握手阶段分成五步:
1. 客户端给出协议版本号、一个客户端生成的随机数(Client random),以及客户端支持的加密方法。
2. 服务器确认双方使用的加密方法,并给出数字证书、以及一个服务器生成的随机数(Server random)。
3. 客户端确认数字证书有效,然后生成一个新的随机数(Premaster secret),并使用数字证书中的公钥,加密这个随机数,发给服务器。
4. 服务器使用自己的私钥,获取客户端发来的随机数(即Premaster secret)。
5. 客户端和服务器根据约定的加密方法,使用前面的三个随机数,生成”对话密钥”(session key),用来加密接下来的整个对话过程。
同时注意一些细节:
- Client random和Server random是可知,只有Premaster secret是外界无法获取的。
- 握手后仍然是HTTP通话过程,但是数据是通过对话密钥加密的,只有主机和服务器知道对话密钥,外界无法获取。
- 握手之后的对话使用”对话密钥”加密(对称加密,即使用一个密钥来进行加密解密),服务器的公钥和私钥只用于加密和解密”对话密钥”(非对称加密),无其他作用。
- 整个对话过程中(握手阶段和其后的对话),服务器的公钥和私钥只需要用到一次。
- 整个握手阶段都不加密(也没法加密),都是明文的。因此,如果有人窃听通信,他可以知道双方选择的加密方法,以及三个随机数中的两个。整个通话的安全,只取决于第三个随机数(Premaster secret)能不能被破解。理论上,只要服务器的公钥足够长(比如2048位),那么Premaster secret可以保证不被破解。
- 我们可以考虑把握手阶段的算法从默认的 RSA算法 ,改为 Diffie-Hellman算法 (简称DH算法)。采用DH算法后,Premaster secret不需要传递,双方只要交换各自的参数,就可以算出这个随机数。
session的恢复
握手阶段用来建立SSL连接。如果出于某种原因,对话中断,就需要重新握手。
这时有两种方法可以恢复原来的session:一种叫做session ID,另一种叫做session ticket。
session ID的思想很简单,就是每一次对话都有一个编号(session ID)。如果对话中断,下次重连的时候,只要客户端给出这个编号,且服务器有这个编号的记录,双方就可以重新使用已有的”对话密钥”,而不必重新生成一把。
session ID是目前所有浏览器都支持的方法,但是它的缺点在于session ID往往只保留在一台服务器上。所以,如果客户端的请求发到另一台服务器,就无法恢复对话。session ticket就是为了解决这个问题而诞生的,目前只有Firefox和Chrome浏览器支持。
客户端不再发送session ID,而是发送一个服务器在上一次对话中发送过来的session ticket。 这个session ticket是加密的,只有服务器才能解密,其中包括本次对话的主要信息,比如对话密钥和加密方法。当服务器收到session ticket以后,解密后就不必重新生成对话密钥了。
TCP协议
传输控制协议(英语:Transmission Control Protocol,缩写:TCP)是一种面向连接的、可靠的、基于 字节流 的 传输层 通信协议。
传送的数据单位协议是TCP报文段。
TCP不提供广播或多播服务。
由于TCP要提供可靠的、面向连接的运输服务,因此不可避免地增加许多的开销。
TCP报文段在传输层是抽象的端到端的可靠的全双工信道,在传输层以下(路由器)并不会知道是否建立了TCP连接。
套接字 socket = (IP:Port)
每一条TCP连接唯一地被通信两端的两个端点(即两个套接字)所确定
连接的三次握手
- 建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SENT状态,等待服务器确认;SYN:同步序列编号(Synchronize Sequence Numbers)。
- 服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;
- 客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握手。
关闭的四次挥手
- 主动关闭方发送一个FIN,用来关闭主动方到被动关闭方的数据传送,也就是主动关闭方告诉被动关闭方:我已经不 会再给你发数据了(当然,在fin包之前发送出去的数据,如果没有收到对应的ack确认报文,主动关闭方依然会重发这些数据),但是,此时主动关闭方还可 以接受数据。
- 被动关闭方收到FIN包后,发送一个ACK给对方,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号)。
- 被动关闭方发送一个FIN,用来关闭被动关闭方到主动关闭方的数据传送,也就是告诉主动关闭方,我的数据也发送完了,不会再给你发数据了。
- 主动关闭方收到FIN后,发送一个ACK给被动关闭方,确认序号为收到序号+1,至此,完成四次挥手。
【问题1】为什么连接的时候是三次握手,关闭的时候却是四次挥手?
因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,”你发的FIN报文我收到了”。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。
【问题2】为什么不能用两次握手进行连接?
答:3次握手完成两个重要的功能,既要双方做好发送数据的准备工作(双方都知道彼此已准备好),也要允许双方就初始序列号进行协商,这个序列号在握手过程中被发送和确认。
【问题3】如果已经建立了连接,但是客户端突然出现故障了怎么办?
TCP还设有一个保活计时器,显然,客户端如果出现故障,服务器不能一直等下去,白白浪费资源。服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为2小时,若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔75秒钟发送一次。若一连发送10个探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接。
TCP拥塞控制
出现拥塞的原因:对资源需求总和 > 可用资源。
防止过多的数据注入到网络中,使网络中的路由器或链路不致过载。
TCP采用基于窗口的方法进行拥塞控制
TCP慢启动
一般通信时,发送方一开始便向网络发送多个报文段,直至达到接收方通告的窗口大小为止。当发送方和接收方处于同一个局域网时,这种方式是可以的。但是如果在发送方和接收方之间存在多个路由器和速率较慢的链路时,就有可能出现一些问题。一些中间路由器必须缓存分组,并有可能耗尽存储器的空间。 慢启动算法通过观察到新分组进入网络的速率应该与另一端返回确认的速率相同而进行工作 。
用来确定网络的负载能力,有小到大逐渐增大拥塞窗口数值。
TCP拥塞避免
让拥塞窗口CWND缓慢地增大(线性),这样可以迅速减少主机发送到网络中的分组数,使得发生拥塞的路由器有足够的时间把队列中积压的分组处理完毕。
TCP快重传
快重传算法可以让发送方尽早知道发现了个别报文段的丢失。
发送方只要一连收到三个重复确认,就知道接收方确实没有收到报文段,因而应当立即进行重传。
TCP快恢复
当发送端收到连续三个重复的确认时,由于发送方现在认为网络很可能没有发生拥塞,因此现在不执行慢开始算法,而是执行快恢复算法。
TCP流量控制
利用滑动窗口机制可以很方便的在TCP连接上实现流量控制。让发送方的发送速率不要太快,既要让接收方来得及接收,也不要使网络发生拥塞。
UDP协议
用户数据报协议(英语:User Datagram Protocol,缩写:UDP;又称用户数据包协议)是一个简单的面向 数据报 的 通信协议 ,位于 OSI模型 的 传输层 。
UDP是无连接的,发送数据之前不需要建立连接
UDP使用尽最大努力交付,不保证可靠交付
UDP是面向报文的。UDP一次交付一个完整的报文,不做拆分。
UDP没有拥塞控制,因此网络上出现拥塞不会使主机发送速率降低。适合多媒体通信。
UDP支持1v1 1vN Nv1 NvN的交互通信
C与C++
C++的四大特性
抽象
对具体事物的定义过程。
封装
继承
多态
C++面向对象机制
其实从上面的四大特性就可以就可以说明面向对象的机制。
面向对象编程使我们可以把对象的状态以及处理这些状态的函数绑定在一起,而封装和继承则使我们可以管理相互依赖性,并使可以通过更清晰和更easy的方式来重用代码。
更多的解释参考回顾C、C++、Java
C/C++的编译过程
编译一个.c的过程只需要这样
1 | $ gcc hello.c # 编译 |
上述gcc命令其实依次执行了四步操作:
- 预处理(Preprocessing)
预处理相当于根据预处理指令组装新的C/C++程序。经过预处理,会产生一个没有宏定义,没有条件编译指令,没有特殊符号的输出文件,这个文件的含义同原本的文件无异,只是内容上有所不同。- 读取C/C++源程序,对其中的伪指令(以#开头的指令)进行处理
①将所有的“#define”删除,并且展开所有的宏定义
②处理所有的条件编译指令。这些伪指令的引入使得程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理。预编译程序将根据有关的文件,将那些不必要的代码过滤掉。1
如:”#if"、"#ifdef”、“#elif”、“#else”、“endif”等。
③处理“#include”预编译指令,将被包含的文件插入到该预编译指令的位置。
(注意:这个过程可能是递归进行的,也就是说被包含的文件可能还包含其他文件) - 删除所有的注释
- 添加行号和文件名标识。
以便于编译时编译器产生调试用的行号信息及用于编译时产生的编译错误或警告时能够显示行号 - 保留所有的#pragma编译器指令
- 读取C/C++源程序,对其中的伪指令(以#开头的指令)进行处理
- 编译(Compilation)
将预处理完的文件进行一系列词法分析、语法分析、语义分析及优化后,产生相应的汇编代码文件。 - 汇编(Assemble)
将编译完的汇编代码文件翻译成机器指令,并生成可重定位目标程序的.o文件,该文件为二进制文件,字节编码是机器指令。
汇编器是将汇编代码转变成机器可以执行的指令,每一个汇编语句几乎都对应一条机器指令。所以汇编器的汇编过程相对于编译器来讲比较简单,它没有复杂的语法,也没有语义,也不需要做指令优化,只是根据汇编指令和机器指令的对照表一一翻译即可。
3. 链接(Linking)
通过链接器将一个个目标文件(或许还会有库文件)链接在一起生成一个完整的可执行程序。
C++重载
C++ 允许在同一作用域中的某个函数和运算符指定多个定义,分别称为函数重载和运算符重载。
参考回顾C、C++、Java
指针、引用
参考回顾C、C++、Java
OOP(面向对象)与POP(面向过程)
参考回顾C、C++、Java
class、struct、union的区别
C语言中,struct只是一个聚合数据类型,没有权限设置,无法添加成员函数,无法实现面向对象编程,且如果没有typedef结构名,声明结构变量必须添加关键字struct。
C++中,struct功能大大扩展,可以有权限设置(默认权限为public),可以像class一样有成员函数,继承(默认public继承),可以实现面对对象编程,允许在声明结构变量时省略关键字struct。
C是一种过程化的语言,struct只是作为一种复杂数据类型定义,struct中只能定义成员变量,不能定义成员函数(在纯粹的C语言中,struct不能定义成员函数,只能定义变量)。
C++中的struct和class的区别:对于成员访问权限以及继承方式,class中默认的是private的,而struct中则是public的。class还可以用于表示模板类型,struct则不行。
C与C++中的union:一种数据格式,能够存储不同的数据类型,但只能同时存储其中的一种类型。
union:共用体,也叫联合体,在一个“联合”内可以定义多种不同的数据类型, 一个被说明为该“联合”类型的变量中,允许装入该“联合”所定义的任何一种数据,这些数据共享同一段内存,以达到节省空间的目的。union变量所占用的内存长度等于最长的成员的内存长度。
操作系统部分
堆和栈
大多数操作系统会将内存空间分为内核空间和用户空间,而每个进程的内存空间又有如下的“默认”区域。
栈:栈用于维护函数调用的上下文,离开栈函数调用就会无法实现。栈通常在用户空间的最高地址处分配,通常有数兆字节。
堆:堆用来容纳应用程序动态分配的内存区域,我们使用malloc 或者new分配内存时,得到的内存来自堆里。堆通常存于栈的下方(低地址方向),堆一般比栈大很多,可以有几十至数百兆字节的容量。
可执行文件镜像:可执行文件由装载器在装载时将可执行文件读取到内存或者映射到内存。
栈是由高地址向低地址增长。
堆是由低地址向高地址增长。
在iOS,所有进程(执行的程序)都必须占用一定数量的内存,它或是用来存放从磁盘载入的程序代码,或是存放取自用户输入的数据等等。不过进程对这些内存的管理方式因内存用途不一而不尽相同,有些内存是事先静态分配和统一回收的,而有些却是按需要动态分配和回收的。
我们主要来看堆区和栈区
- 堆(heap)区:堆是由程序员分配和释放,用于存放进程运行中被动态分配的内存段,它大小并不固定,可动态扩张或缩减。当进程调用alloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用realse释放内存时,被释放的内存从堆中被剔除(堆被缩减),因为我们现在iOS基本都使用ARC来管理对象,所以不用我们程序员来管理,但是我们要知道这个对象存储的位置。
- 栈(stack)区:栈是由编译器自动分配并释放,用户存放程序临时创建的局部变量,存放函数的参数值,局部变量等。也就是说我们函数括弧“{}”中定义的变量(但不包括static声明的变量,static意味这在数据段中存放变量)。除此以外在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也回被存放回栈中。由于栈的先进后出特点,所以栈特别方便用来保存/恢复调用现场。从这个意义上将我们可以把栈看成一个临时数据寄存、交换的内存区。
其他的部分
- 代码区:代码段是用来存放可执行文件的操作指令(存放函数的二进制代码),也就是说是它是可执行程序在内存种的镜像。代码段需要防止在运行时被非法修改,所以只准许读取操作,而不允许写入(修改)操作——它是不可写的。
- 全局(静态)区包含下面两个分区:
- 数据区:数据段用来存放可执行文件中已初始化全局变量,换句话说就是存放程序静态分配的变量和全局变量。
- BSS区:BSS段包含了程序中未初始化全局变量。
- 常量区:常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量。
上述几种内存区域中数据段、BSS和堆通常是被连续存储的——内存位置上是连续的,而代码段和栈往往会被独立存放。
栈是向低地址扩展的数据结构,是一块连续的内存的区域。堆是向高地址扩展的数据结构,是不连续的内存区域。
1 |
|
他们的区别
申请方式和回收方式
栈区(stack) :由编译器自动分配并释放
堆区(heap):由程序员分配和释放(现由ARC管理,调用release会报错,但是deinit(对象销毁前调用的函数)可以使用)
申请后系统的响应
- 栈区(stack):存储每一个函数在执行的时候都会向操作系统索要资源,栈区就是函数运行时的内存,栈区中的变量由编译器负责分配和释放,内存随着函数的运行分配,随着函数的结束而释放,由系统自动完成。只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
- 堆区(heap):操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。
申请大小的限制
- 栈区(stack):栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,栈的大小是2M(也可能是1M,我看网上说得,我也不清楚),如果申请的空间超过栈的剩余空间时,将提示栈溢出。因此,能从栈获得的空间较小。
- 堆区(heap):堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。
申请效率的比较
- 栈区(stack):由系统自动分配,速度较快。但程序员是无法控制的。
- 堆区(heap):是由alloc分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便.
分配方式的比较
栈区(stack):有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloc函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。
堆区(heap):堆都是动态分配的,没有静态分配的堆。
分配效率的比较
栈区(stack):栈是操作系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。
堆区(heap):堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。
为什么要把堆和栈区分出来呢?
第一,从软件设计的角度看,栈代表了处理逻辑,而堆代表了数据。这样分开,使得处理逻辑更为清晰。分而治之的思想。这种隔离、模块化的思想在软件设计的方方面面都有体现。
第二,堆与栈的分离,使得堆中的内容可以被多个栈共享(也可以理解为多个线程访问同一个对象)。这种共享的收益是很多的。一方面这种共享提供了一种有效的数据交互方式(如:共享内存),另一方面,堆中的共享常量和缓存可以被所有栈访问,节省了空间。
第三,栈因为运行时的需要,比如保存系统运行的上下文,需要进行地址段的划分。由于栈只能向上增长,因此就会限制住栈存储内容的能力。而堆不同,堆中的对象是可以根据需要动态增长的,因此栈和堆的拆分,使得动态增长成为可能,相应栈中只需记录堆中的一个地址即可。
第四,面向对象就是堆和栈的完美结合。其实,面向对象方式的程序与以前结构化的程序在执行上没有任何区别。但是,面向对象的引入,使得对待问题的思考方式发生了改变,而更接近于自然方式的思考。当我们把对象拆开,你会发现,对象的属性其实就是数据,存放在堆中;而对象的行为(方法),就是运行逻辑,放在栈中。我们在编写对象的时候,其实即编写了数据结构,也编写的处理数据的逻辑。不得不承认,面向对象的设计,确实很美。
虚拟内存
虚拟内存允许操作系统摆脱物理RAM的限制。虚拟内存管理器创建一个逻辑地址空间(或“虚拟”地址空间),然后将其分为大小统一的内存块,称为 页数。处理器及其内存管理单元(MMU)维护一个 页面表,将程序逻辑地址空间中的页面映射到计算机RAM中的硬件地址。当程序的代码访问内存中的地址时,MMU使用页表将指定的逻辑地址转换为实际的硬件内存地址。该转换自动发生,并且对于正在运行的应用程序是透明的。
就程序而言,其逻辑地址空间中的地址始终可用。但是,如果应用程序访问当前不在物理RAM中的内存页面上的地址,则发生页面错误。发生这种情况时,虚拟内存系统将调用特殊的页面错误处理程序以立即响应该错误。页面错误处理程序停止当前执行的代码,找到物理内存的空闲页面,从磁盘加载包含所需数据的页面,更新页面表,然后将控制权返回给程序的代码,然后该代码可以访问内存地址一般。这个过程称为分页。
如果物理内存中没有可用的空闲页面,则处理程序必须首先释放现有页面以为新页面腾出空间。系统发行页面的方式取决于平台。在OS X中,虚拟内存系统通常将页面写入后备存储。的后备存储是基于磁盘的存储库,其中包含给定进程使用的内存页的副本。将数据从物理内存移动到后备存储称为分页(或“交换”);将数据从后备存储移回物理内存称为分页(或“交换”)。在iOS中,没有后备存储,因此永远不会将页面调出到磁盘,但是仍会根据需要从磁盘调入只读页面。
进程与线程
进程
- 进程是指在系统中正在运行的一个应用程序,比如同时打开微信和Xcode,系统会分别启动2个进程;
- 每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间内;
线程
- 一个进程要想执行任务,必须得有线程(每一个进程至少要有一条线程),是进程中执行运算的最小单位,是进程中的一个实体,是被系统独立调度和分派的基本单位;
- 一个进程(程序)的所有任务都在线程中执行;
- 一个程序有且只有一个主线程,程序启动时创建(调用main来启动),主线程的生命周期是和应用程序绑定,程序退出时,主线程也停止;
- 同一时间内,一个线程只能执行一个任务,若要在1个进程中执行多个任务,那么只能一个个的按顺序执行这些任务(线程的串行);
- 线程自己不拥有系统资源,只拥有在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源;
线程的几种状态
- 新建状态:新创建一个线程对象;
- 就绪状态:线程对象创建之后,其他线程调用了该对象的start方法,该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权;
- 运行状态:就绪状态的线程获取了CPU,执行程序代码;
- 阻塞状态:因某种原因放弃CPU使用权,暂停运行,知道线程进入就绪状态,才有机会转到运行状态;
- 死亡状态:线程执行完了或者因异常退出了run方法,线程生命周期结束;
进程和线程比较:
- 线程是CPU调度(执行任务)的最小单位,是程序执行的最小单元;
- 进程是CPU分配资源和调度的单位;
- 一个程序可以对应多个进程,一个进程可以有多个线程,但至少要有一个线程,而一个线程只能属于一个进程;
- 同一个进程内的线程共享进程的所有资源;
多线程:
- 概念:一个进程中可以开启多条线程,每一条线程可以并行(同时)执行不同的任务;
- 原理:同一时间,CPU只能处理一条线程,只有一条线程在工作,多线程并发(同时)执行,其实是CPU快速的在多条线程之间调度(切换),如果CPU调度线程的时间足够快,就造成了多线程并发执行的假象;
- 注意:如果线程很多,CPU会在N多线程之间调度,会消耗大量CPU资源,每条线程被调度执行的频次会降低(线程的执行效率会降低);
多线程的优缺点:
- 优点: 能适当的提高程序的执行效率以及资源利用率(CPU、内存利用率)
- 缺点: 创建线程是有开销的,iOS下主要成本包括:内核数据结构(大约1kb)、栈空间(子线程512kb,主线程1MB)、创建线程大约需要90毫秒的创建时间,如果开启大量的线程,会降低程序的性能(一般最多3到5个);线程越多,CPU在调度线程上的开销就越大; 程序设计更加复杂(比如线程之间的通信、多线程的数据共享)
主线程:
- 一个iOS程序运行后,默认会开启1条线程,称为“主线程”或“UI线程”
- 作用: 显示/刷新UI界面, 处理UI事件(点击事件,滚动事件,拖拽事件)
- 使用注意:不要将耗时的操作放到主线程中,耗时操作应放在子线程(后台线程,非主线程); 凡是和UI相关的操作应放在主线程中操作
iOS中多线程的实现方案:
- pthread :一套通用的多线程API,适用于Unix、Linux、Windows等系统,跨平台、可移植,使用难度大,c语言,线程生命周期由程序员管理
- NSTread:oc语言,面向对象,简单易用,可直接操作线程对象 ,线程生命周期由程序员管理
- GCD:(常用)替代NSTread等线程技术,充分利用设备的多核,线程生命周期自动管理,c语言
- NSOperation:(常用)底层是GCD,比GCD多了一些更简单实用的功能,使用更加面向对象,线程生命周期自动管理
死锁
所谓死锁,通常指有两个线程T1和T2都卡住了,并等待对方完成某些操作。T1不能完成是因为它在等待T2完成。但T2也不能完成,因为它在等待T1完成。于是大家都完不成,就导致了死锁(DeadLock)。
产生死锁的四个必要条件:
- 互斥条件:一个资源每次只能被一个进程使用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。
避免死锁
让系统处于安全状态
安全序列问题
预防死锁
破坏请求和保持条件 :一个进程请求资源时,他不能持有不可抢占资源。
破坏不可抢占条件:进程已占有的资源会被暂时释放。
破坏循环等待条件:对系统所有资源类型进行线性排序,并赋予不同序号然后按序请求资源。
总结
预防 ——> 避免 ——> 检测(资源分配图) ——> 解除(终止进程)
同步&异步 串行&并发
同步执行:比如这里的dispatch_sync,这个函数会把一个block加入到指定的队列中,而且会一直等到执行完blcok,这个函数才返回。因此在block执行完之前,调用dispatch_sync方法的线程是阻塞的。
异步执行:一般使用dispatch_async,这个函数也会把一个block加入到指定的队列中,但是和同步执行不同的是,这个函数把block加入队列后不等block的执行就立刻返回了。
dispatch_async 和 dispatch_sync 他们的作用是将 任务(block)添加进指定的队列中。并根据是否为sync决定调用该函数的线程是否需要阻塞。
注意:这里调用该函数的线程并不执行 参数中指定的任务(block块),任务的执行者是GCD分配给任务所在队列的线程。
结论:调用dispatch_sync和dispatch_async的线程,并不一定是任务(block块)的执行者。
串行队列:比如这里的dispatch_get_main_queue。这个队列中所有任务,一定按照FIFO(先来后到的顺序)执行。不仅如此,还可以保证在执行某个任务时,在它前面进入队列的所有任务肯定执行完了。对于每一个不同的串行队列,系统会为这个队列建立唯一的线程来执行代码。
并发队列:比如使用dispatch_get_global_queue。这个队列中的任务也是按照FIFO(先来后到的顺序)开始执行,注意是开始,但是它们的执行结束时间是不确定的,取决于每个任务的耗时。并发队列中的任务:GCD会动态分配多条线程来执行。具体几条线程取决于当前内存使用状况,线程池中线程数等因素。