1.fd只是一个整数,在open时产生。起到一个索引的作用,进程通过PCB中的文件描述符表找到该fd所指向的文件指针filp。
文件描述符的操作(如: open)返回的是一个文件描述符,内核会在每个进程空间中维护一个文件描述符表, 所有打开的文件都将通过此表中的文件描述符来引用;
而流(如: fopen)返回的是一个FILE结构指针, FILE结构是包含有文件描述符的,FILE结构函数可以看作是对fd直接操作的系统调用的封装, 它的优点是带有I/O缓存
每个进程在PCB(Process Control Block)即进程控制块中都保存着一份文件描述符表,文件描述符就是这个表的索引,文件描述表中每个表项都有一个指向已打开文件的指针,现在我们明确一下:已打开的文件在内核中用file结构体表示,文件描述符表中的指针指向file结构体。
2.netstat :shell命令,用于显示各种网络相关信息,如网络连接,路由表,接口状态等。
3.tcpdump:(dump the traffic on a network)主要是截获通过本机网络接口的数据用以分析。能够截获当前所有通过本机网卡的数据包。它拥有灵活的过滤机制,可以确保得到想要的数据。tcpdump可以将网络中传送的数据包的“头”完全截获下来提供分析。它支持针对网络层、协议、主机、网络或端口的过滤,并提供and、or、not等逻辑语句来帮助你去掉无用的信息。
4.ipcs:检查系统上共享内存的分配。ipcs 命令往标准输出写入一些关于活动进程间通信设施的信息。如果没有指定任何标志,ipcs 命令用简短格式写入一些关于当前活动消息队列、共享内存段、信号量、远程队列和本地队列标题。
5.ipcrm:手动解除系统上共享内存的分配,删除消息队列、信号集、或者共享内存标识。
6.# cat /proc/meminfo 查看cpu信息
7.#df -lh 查看硬盘信息
8.#cat /proc/scsi/scsi 查看更加详细的信息
9.#dmesg | grep eth 查看网卡信息
10.#uname -a 查看系统版本号
11.共享内存原理:
共享内存定义:共享内存是最快的可用IPC(进程间通信)形式。它允许多个不相关的进程去访问同一部分逻辑内存。共享内存是由IPC为一个进程创建的一个特殊的地址范围,它将出现在进程的地址空间中。其他进程可以把同一段共享内存段“连接到”它们自己的地址空间里去。所有进程都可以访问共享内存中的地址。如果一个进程向这段共享内存写了数据,所做的改动会立刻被有访问同一段共享内存的其他进程看到。因此共享内存对于数据的传输是非常高效的。
共享内存的原理:共享内存是最有用的进程间通信方式之一,也是最快的IPC形式。两个不同进程A、B共享内存的意思是,同一块物理内存被映射到进程A、B各自的进程地址空间。进程A可以即时看到进程B对共享内存中数据的更新,反之亦然。
1、查看共享内存,使用命令:ipcs -m
2、删除共享内存,使用命令:ipcrm -m [shmid]
12.如何定位内存泄漏?
内存泄漏是指堆内存的泄漏。堆内存是指程序从堆中分配的、大小任意的(内存块的大小可以在程序运行期决定)、使用完后必须显示释放的内存。应用程序一般使用malloc、realloc、new等函数从堆中分配到一块内存,使用完后,程序必须负责相应的调用free或delete释放该内存块。否则,这块内存就不能被再次使用,我们就说这块内存泄漏了。
C++程序缺乏相应的手段来检测内存信息,只能使用top指令观察进程的动态内存总额。而且程序退出时,我们无法获知任何内存泄漏信息。
使用Linux命令回收内存,可以使用ps、kill两个命令检测内存使用情况和进行回收。在使用超级用户权限时使用命令“ps”,它会列出所有正在运行的程序名称和对应的进程号(PID)。kill命令的工作原理是向Linux操作系统的内核送出一个系统操作信号和程序的进程号(PID)
13.动态链接库和静态链接库的区别?
动态链接是指在生成可执行文件时不将所有程序用到的函数链接到一个文件,因为有许多函数在操作系统带的dll文件中,当程序运行时直接从操作系统中找。 而静态链接就是把所有用到的函数全部链接到exe文件中。
动态链接是只建立一个引用的接口,而真正的代码和数据存放在另外的可执行模块中,在运行时再装入;而静态链接是把所有的代码和数据都复制到本模块中,运行时就不再需要库了。
14.多线程和多进程的区别(重点面试官最最关心的一个问题,必须从cpu调度,上下文切换,数据共享,多核cup利用率,资源占用,等等各方面回答,然后有一个问题必须会被问到:哪些东西是一个线程私有的?答案中必须包含寄存器)
维度 | 多进程 | 多线程 | 总结 |
数据共享、同步 | 数据是分开的;共享数据复杂,需要IPC;操作简单 | 多线程共享进程数据;共享简单;操作复杂 | 各有优势 |
内存、CPU | 占用内存多,切换复杂,CPU占用率低 | 占用内存少,切换简单,CPU占用率高 | 线程占优 |
创建销毁、切换 | 创建销毁、切换复杂,速度慢 | 创建销毁、切换简单,速度快 | 线程占优 |
编程调试 | 编程简单,调试简单 | 编程复杂,调试复杂 | 进程占优 |
可靠性 | 进程之间不会影响 | 一个线程挂掉,导致整个进程挂 | 进程占优 |
分布式 | 可用于多核多机分布,拓展简单 | 多核分布 | 进程占优 |
线程和进程之间的比较:
子进程继承父进程属性 | 子线程继承父线程属性 |
实际用户ID,实际组ID,有效用户ID,有效组ID; 附加组ID; 进程组ID; 会话ID; 控制终端; 设置用户ID标志和设置组ID标志; 当前工作目录; 根目录; 文件模式创建屏蔽字(umask); 信号屏蔽和安排; 针对任一打开文件描述符的在执行时关闭(close-on-exec)标志; 环境; 连接的共享存储段; 存储映射; 资源限制; | 进程中的所有信息对该进程的所有线程都是共享的; 可执行的程序文本; 程序的全局内存; 堆内存; 栈; 文件描述符; 信号的处理是进程中所有线程共享的(注意:如果信号的默认处理是终止该进程那么即是把信号传给某个线程也一样会将进程杀掉);
|
父进程和子进程之间的区别 | 子线程特有: |
fork的返回值(=0子进程); 进程ID不同; 两个进程具有不同的父进程ID; 子进程的tms_utime,tms_stime,tms_cutime以及tms_ustime均被设置为0; 不继承父进程设置的文件锁; 子进程的未处理闹钟被清除; 子进程的未处理信号集设置为空集; | 线程ID; 一组寄存器值; 栈; 调度优先级和策略; 信号屏蔽字; errno变量; 线程私有数据; |
15.什么是内核级的线程?
内核级线程是操作系统内核实现、管理和调度的一种线程。由于有操作系统管理,所以操作系统是知道线程的存在,并为其安排时间片,管理与其有关的内核对象。因为内核级线程是由内核来管理,所以每次线程创建、切换都要执行一个模式切换例程,所以内核级线程效率比较低,而且内核级线程的调度是由操作系统的设计者来决定的,所以缺乏灵活性。但是内核级线程有一个优点就是当一个进程的某个线程因为一个系统调用或者缺页中断而阻塞时,不会导致该进程的所有线程阻塞。
内核级线程的优点: 较好的并行能力,一个进程内的线程阻塞不会影响该进程内的其他线程
内核级线程的缺点: 线程管理的开销过大,缺乏灵活性。
用户级线程是通过运行在用户态的运行时库来管理的,其优点是,线程的一切(包括调度、创建)都可以完全由用户自己决定,所以具有较高的灵活性。而且由于是在用户态上进行管理,所以就省去了内核管理的开销,所以具有高效率。 但是用户级线程有一个致命的缺点:一个进程内的某一个线程阻塞将导致整个进程内的所有线程全部阻塞。而且由于用户级线程没有时间片概念,所以每个线程必须运行一段时间后将CPU让个其他的线程使用,否则,该线程将独占CPU。
用户级线程的优点: 有较高的灵活性和高效率
用户级线程的缺点: 较差并发能力
16.进程地址空间分布:fork()会创建一个子进程,会复制父进程的所有资源,但是会拥有和父进程相独立的数据资源与代码区。写时拷贝思想:在fork创建子进程时,子进程复制整个父进程的PCB,包括页表在内,在对子进程中的数据进行操作的时候,页表才开辟新的物理内存,这一点可以类比C++中的写时拷贝。
Linux虚拟内存的大小为2^32(在32位的x86机器上),内核将这4G字节的空间分为两部分。最高的1G字节(从虚地址0xC0000000到0xFFFFFFFF)供内核使用,称为“内核空间”。而较低的3G字节(从虚地址0x00000000到0xBFFFFFFF),供各个进程使用,称为“用户空间”。因为每个进程可以通过系统调用进入内核,因此,Linux内核空间由系统内的所有进程共享。于是,从具体进程的角度来看,每个进程可以拥有4G字节的虚拟地址空间(也叫虚拟内存)。
17.什么是守护进程?
守护进程就是通常讲Daemon进程,是linux后台执行的一种服务进程,特点是独立于控制终端、周期性地执行某种任务或等待处理某些发生事件,不会随终端关闭而停止,直到接受停止信息才会结束,且一般采用以d结尾的名字。
举例有哪些是守护进程?
linux后台的一些系统服务进程,没有控制终端,不能直接和用户交互,不受用户登录、注销的影响,一直在运行着,他们都是守护进程,如:预读入缓输出机制的实现;ftp服务器(借助vsftpd实现),nfs服务器。
18.linux内存管理机制?
Linux虚拟内存的实现需要6种机制的支持:地址映射机制、内存分配回收机制、缓存和刷新机制、请求页机制、交换机制和内存共享机制
内存管理程序通过映射机制把用户程序的逻辑地址映射到物理地址。当用户程序运行时,如果发现程序中要用的虚地址没有对应的物理内存,就发出了请求页要求。如果有空闲的内存可供分配,就请求分配内存(于是用到了内存的分配和回收),并把正在使用的物理页记录在缓存中(使用了缓存机制)。如果没有足够的内存可供分配,那么就调用交换机制;腾出一部分内存。另外,在地址映射中要通过TLB(翻译后援存储器)来寻找物理页;交换机制中也要用到交换缓存,并且把物理页内容交换到交换文件中,也要修改页表来映射文件地址。
19.快速排序:不稳定,最快时间复杂度是nlog(n).快排的性能在所有排序算法里面是最好的,数据规模越大快速排序的性能越优。快排在极端情况下会退化成 On^2 的算法,因此假如在提前得知处理数据可能会出现极端情况的前提下,可以选择使用较为稳定的归并排序
//快速排序
void quick_sort(int s[], int l, int r)
{
if (l < r)
{
//Swap(s[l], s[(l + r) / 2]); //将中间的这个数和第一个数交换 参见注1
int i = l, j = r, x = s[l];
while (i < j)
{
while(i < j && s[j] >= x) // 从右向左找第一个小于x的数
j--;
if(i < j)
s[i++] = s[j];
while(i < j && s[i] < x) // 从左向右找第一个大于等于x的数
i++;
if(i < j)
s[j--] = s[i];
}
s[i] = x;
quick_sort(s, l, i - 1); // 递归调用
quick_sort(s, i + 1, r);
}
}
TCP报头:首部长度最常用是20字节,最大值是60字节(限制最大值是为了减少开销)
总长度指的是首部加上数据之和的长度,单位是字节。总长度是16位。数据报最大长度是65535字节。
6位标记位:
URG: 标识紧急指针是否有效
ACK: 标识确认序号是否有效
PSH: 用来提示接收端应用程序立刻将数据从tcp缓冲区读走
RST: 要求重新建立连接. 我们把含有RST标识的报文称为复位报文段
SYN: 请求建立连接. 我们把含有SYN标识的报文称为同步报文段
FIN: 通知对端, 本端即将关闭. 我们把含有FIN标识的报文称为结束报文段
TCP和IP协议的区别: IP协议设计为分组转发协议,每一跳都要经过一个中间节点,路由的设计是TCP/IP网络的另一大创举,这样,IP协议就无需方向性,路由信息和协议本身不再强关联,它们仅仅通过IP地址来关联,因此,IP协议更加简单。路由器作为中间节点也不能太复杂,这涉及到成本问题,因此路由器只负责选路以及转发数据包。
TCP是一种面向连接的、可靠的、字节流服务。1)面向连接:TCP面向链接,面向连接意味着两个使用TCP的应用(通常是一个客户和一个服务器)在彼此交换数据之前必须通过三次握手先建立一个TCP连接。在一个TCP中仅有两方彼此通信,多播和广播不能用于TCP。UDP是不可靠的传输,传输前不需要建立链接,可以应用多播和广播实现一对多的通信。2)可靠性:TCP提供端到端的流量控制,对收到的数据进行确认,采用超时重发,对失序的数据进行重新排序等机制保证数据通信的可靠性。而UDP是一种不可靠的服务,接收方可能不能收到发送方的数据报。3)TCP是一种流模式的协议,UDP是一种数据报模式的协议。进程的每个输出操作都正好产生一个UDP数据报,并组装成一份待发送的IP数据报。TCP应用程序产生的全体数据与真正发送的单个IP数据报可能没有什么联系。TCP会有粘包和半包的现象。4)效率上:速度上,一般TCP速度慢,传输过程中需要对数据进行确认,超时重发,还要对数据进行排序。UDP没有这些机制所以速度快。数据比例,TCP头至少20个字节,UDP头8个字节,相对效率高。组装效率上:TCP头至少20个字节,UDP头8个字节,系统组装上TCP相对慢。5)用途上:用于TCP可靠性,http,ftp使用。而由于UDP速度快,视频,在线游戏多用UDP,保证实时性。
TCP可能发送100个“包”,而接收到50个“包”,不是丢“包”了,而是每次接受的“包”都比发送的多,其实TCP并没有包的概念。例如,每次发10个字节,可能读得时候一次读了20个字节。TCP是一种流模式的协议,在接收到的缓存中按照发送的包得顺序自动按照顺序拼接好,因为数据基本来自同一个主机,而且是按照顺序发送过来的,TCP的缓存中存放的就是,连续的数据。感觉好像是多封装了一步比UDP。而UDP因为可能两个不同的主机,给同一个主机发送,(一个端口可能收到多个应用程序的数据),或者按照TCP那样合并数据,必然会造成数据错误。我觉得关键的原因还是,TCP是面向连接,而UDP是无连接的,这就导致,TCP接收的数据为一个主机发来且有序无误的,而UDP可能是多个主机发来的无序,可能错误的。
19.三次握手:
3次握手的目的很简单,就是分配资源,初始化序列号,这时还不涉及数据传输,3次就足够做到这个了,而4次挥手的目的是终止数据传输,并回收资源,此时两个端点两个方向的序列号已经没有了任何关系,必须等待两方向都没有数据传输时才能拆除虚链路,不像初始化时那么简单,发现SYN标志就初始化一个序列号并确认SYN的序列号。因此必须单独分别在一个方向上终止该方向的数据传输。
20.洪水攻击原理:
在Server端返回一个确认的SYN-ACK包的时候有个潜在的弊端,他可能不会接到Client端回应的ACK包。这个也就是所谓的半开放连接,Server端需要耗费一定的数量的系统内存来等待这个未决的连接,虽然这个数量是受限的,但是恶意者可以通过创建很多的半开放式连接来发动SYN洪水攻击 。
目前有两种SYN Flood攻击方式,但它与所有服务器都没有收到ACK的事实有关。恶意用户无法接收ACK,因为服务器向假IP地址发送SYN-ACK,跳过最后一条ACK消息或模拟SYN的源IP地址。在这两种情况下,服务器都需要时间来复制通知,这可能会导致简单的网络拥塞而无需ACK。
建议的措施包括SYN cookie和限制在特定时间段内从同一源请求的新连接数,但最新的TCP / IP堆栈没有上面提到的瓶颈因为它位于SYN Flood和其他基于通道的容量之间。攻击类型应该很少或没有区别。
什么是SYN cookie?SYN cookie就是用一个cookie来响应TCP SYN请求的TCP实现,根据上面的描述,在正常的TCP实现中,当S接收到一个SYN数据包,他返回一个SYN-ACK包来应答,然后进入TCP-SYN-RECV(半开放连接)状态来等待最后返回的ACK包。S用一个数据空间来描述所有未决的连接,然而这个数据空间的大小是有限的,所以攻击者将塞满这个空间。(上面是造成洪水攻击原理的原因)==>(解决方式)在TCP SYN COOKIE的执行过程中,当S接收到一个SYN包的时候,他返回一个SYN-ACK包,这个数据包的ACK序列号是经过加密的,也就是说,它由源地址,端口源次序,目标地址,目标端口和一个加密种子计算得出。然后S释放所有的状态。如果一个ACK包从C返回,S将重新计算它来判断它是不是上个SYN-ACK的返回包。如果这样,S就可以直接进入TCP连接状态并打开连接。这样,S就可以避免守侯半开放连接了。
21.传统的fork是在创建一个子进程的时候,子进程和父进程共享正文段,复制数据段,堆,栈到子进程。linux中的fork机制:创建一个子进程,内核只为子进程创建虚拟空间,不分配物理内存,和父进程共享物理空间,当父进程中有更改相应段的行为发生时,才为子进程分配物理空间。(这就是写时复制)。vfork()子进程共享父进程的所有资源。区别:
1)传统的fork函数直接把所有资源复制给新的进程,效率很低下。
2)写时拷贝在需要写入时,数据才会被复制,没有数据写入时,fork()的开销实际只是复制父进程的页表以及给子进程创建唯一的进程描述符。有数据要写入前,会将将要改变的数据页复制给子进程。
22.exit()与_exit()的区别?
从图中可以看出,_exit 函数的作用是:直接使进程停止运行,清除其使用的内存空间,并清除其在内核的各种数据结构;exit 函数则在这些基础上做了一些小动作,在执行退出之前还加了若干道工序。exit() 函数与 _exit() 函数的最大区别在于exit()函数在调用exit系统调用前要检查文件的打开情况,把文件缓冲区中的内容写回文件(也就是图中的“清理I/O缓冲”)。
两个函数调用的库不同:
#include <unistd.h> void _exit(int status);
#include <stdlib.h> void exit(int status);
23.exec()和fork()的区别:fork用来创建子进程,处理的对象是进程;而exec()是用来处理程序,重新加载一个进程里的程序。 一个进程一旦调用exec类函数,它本身就“死亡”了,系统把代码段替换成新的程序的代码,废弃原有的数据段和堆栈段,并为新程序分配新的数据段与堆栈段,唯一留下的,就是进程号,也就是说,对系统而言,还是同一个进程,不过已经是另一个程序了。不过exec类函数中有的还允许继承环境变量之类的信息,这个通过exec系列函数中的一部分函数的参数可以得到。但是为什么不把他们合成一个syscall?
1)接口的通用性:要知道二者不一定必须要合在一起使用的,存在这种情况,即当前进程的程序已经执行完成,我可以直接通过exec来加载新程序,并不需要fork出一个新的进程,fork因为是完全复制进程,是很浪费时间的。还有进程监听TCP端口的时候,我们需要生成子进程来处理一些特定需求,然后主进程继续监听,但是这个时候是不需要exec的。
2)效率:fork出一整个进程会占用很多空间(需要存贮进程的指令,数据,栈),将两个syscall 分开可以让我们在子进程执行制定程序前对其进行修改,会高效很多。还有一点,如果create process的话,就要涉及从一个地址空间把所有数据拿出来然后修改,然后迁移,跨地址空间的修改会很费时;两个分别的是先复制,然后根据exec来做reload。
24.BSS段:可执行程序包括BSS段、数据段、代码段(也称文本段)。BSS(Block Started by Symbol)通常是指用来存放程序中未初始化的全局变量和静态变量的一块内存区域。特点是:可读写的,在程序执行之前BSS段会自动清0。所以,未初始的全局变量在程序执行之前已经成0了。注意和数据段的区别,BSS存放的是未初始化的全局变量和静态变量,数据段存放的是初始化后的全局变量和静态变量。UNIX下可使用size命令查看可执行文件的段大小信息。如size a.out。
25.L1 cache, L2 cache, L3 cache:通常L1和L2缓存都是每个CPU一个的, L1缓存有分为L1i和L1d,分别用来存储指令和数据。L2缓存是不区分指令和数据的。L3缓存多个核心共用一个,通常也不区分指令和数据。CPU从Cache数据的最小单位是字节,Cache从Memory拿数据的最小单位(以word为单位)是64Bytes,Memory从硬盘拿数据(bolck 为单位)通常最小是4092Bytes。
1)L1 cache: 3 cycles
2)L2 cache: 11 cycles
3)L3 cache: 25 cycles
4)Main Memory: 100 cycles
L1 cache比L2 cache快的原因:
容量:L1的容量通常比L2小,容量大的SRAM访问时间就越长,同样制程和设计情况下,访问延时与容量的开方大致成正比的。
距离:通常L1 Cache离CPU核心需要数据的地方更近,而L2 Cache则处于边缓位置,访问数据时,L2 Cache需要通过更远的铜线,甚至更多的电路,从而增加了延时。L1 Cache分为ICache(指令缓存)和DCache(数据缓存),指令缓存ICache通常是放在CPU核心的指令预取单远附近的,数据缓存DCache通常是放在CPU核心的load/store单元附近。而L2 Cache是放在CPU pipeline之外的。
最简单的替换策略称为LRU(least recently used),即Cache管理单元记录每个Cache line最近被访问的时间,每次需要evict时,选最近一次访问时间最久远的那一条做为victim。
26.内存错误以及对策:
1)内存分配未成功,却被使用?
对策:在使用内存之前检查分配是否分配成功,用p!=null判断。
2)内存分配成功,未初始化就使用:
对策:内存的缺省值没有统一的标准。大部分编译器以0为初始值,但并不完全是,因此内存初始化时需要赋初值。
27.什么是滑动窗口(必问)?
28.connect会阻塞,怎么解决?(必考必问,提示:设置非阻塞,返回之后用select检测状态)
29.海量数据处理面试题(外排):https://www.cnblogs.com/simonote/articles/3087185.html
30.信号机制:
信号机制是进程之间相互传递消息的一种方法,信号全称为软中断信号,也有人称作软中断。从它的命名可以看出,它的实质和使用很象中断。所以,信号可以说是进程控制的一部分。软中断信号(signal,又简称为信号)用来通知进程发生了异步事件。进程之间可以互相通过系统调用kill发送软中断信号。内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。注意,信号只是用来通知某进程发生了什么事件,并不给该进程传递任何数据。
收到信号的进程对各种信号有不同的处理方法。处理方法可以分为三类:第一种是类似中断的处理程序,对于需要处理的信号,进程可以指定处理函数,由该函数来处理。第二种方法是忽略某个信号,对该信号不做任何处理,就象未发生过一样。第三种方法是,对该信号的处理保留系统的默认值,这种缺省操作,对大部分的信号的缺省操作是使得进程终止。进程通过系统调用signal来指定进程对某个信号的处理行为。在进程表的表项中有一个软中断信号域,该域中每一位对应一个信号,当有信号发送给进程时,对应位置位。由此可以看出,进程对不同的信号可以同时保留,但对于同一个信号,进程并不知道在处理之前来过多少个。
发出信号的原因很多,这里按发出信号的原因简单分类,以了解各种信号类型:
(1) 与进程终止相关的信号。当进程退出或者子进程终止时,发出这类信号。
(2) 与进程例外事件相关的信号。如进程越界,或企图写一个只读的内存区域(如程序正文区),或执行一个特权指令及其他各种硬件错误。
(3) 与在系统调用期间遇到不可恢复条件相关的信号。如执行系统调用exec时原有资源已经释放,而目前系统资源又已耗尽。
(4) 与执行系统调用时遇到非预测错误条件相关的信号。如执行一个并不存在的系统调用。
(5) 在用户态下的进程发出的信号。如进程调用系统调用kill向其他进程发送信号。
(6) 与终端交互相关的信号。如用户关闭一个终端,或按下break键等情况。
(7) 跟踪进程执行的信号。
很多信号是与机器的体系结构相关的,首先列出的是POSIX.1中列出的信号:
SIGHUP 1 A 终端挂起或者控制进程终止
SIGINT 2 A 键盘中断(如break键被按下)
SIGQUIT 3 C 键盘的退出键被按下
SIGILL 4 C 非法指令
SIGABRT 6 C 由abort(3)发出的退出指令
SIGFPE 8 C 浮点异常
SIGKILL 9 AEF Kill信号
SIGSEGV 11 C 无效的内存引用
SIGPIPE 13 A 管道破裂: 写一个没有读端口的管道
SIGALRM 14 A 由alarm(2)发出的信号
SIGTERM 15 A 终止信号
SIGUSR1 30,10,16 A 用户自定义信号1
SIGUSR2 31,12,17 A 用户自定义信号2
SIGCHLD 20,17,18 B 子进程结束信号
SIGCONT 19,18,25 进程继续(曾被停止的进程)
SIGSTOP 17,19,23 DEF 终止进程
SIGTSTP 18,20,24 D 控制终端(tty)上按下停止键
SIGTTIN 21,21,26 D 后台进程企图从控制终端读
SIGTTOU 22,22,27 D 后台进程企图从控制终端写
下面的信号没在POSIX.1中列出,而在SUSv2列出
信号 值 处理动作 发出信号的原因
--------------------------------------------------------------------
SIGBUS 10,7,10 C 总线错误(错误的内存访问)
SIGPOLL A Sys V定义的Pollable事件,与SIGIO同义
SIGPROF 27,27,29 A Profiling定时器到
SIGSYS 12,-,12 C 无效的系统调用 (SVID)
SIGTRAP 5 C 跟踪/断点捕获
SIGURG 16,23,21 B Socket出现紧急条件(4.2 BSD)
SIGVTALRM 26,26,28 A 实际时间报警时钟信号(4.2 BSD)
SIGXCPU 24,24,30 C 超出设定的CPU时间限制(4.2 BSD)
SIGXFSZ 25,25,31 C 超出设定的文件大小限制(4.2 BSD)
注意信号SIGKILL和SIGSTOP既不能被捕捉,也不能被忽略。信号SIGIOT与SIGABRT是一个信号。可以看出,同一个信号在不同的系统中值可能不一样,所以建议最好使用为信号定义的名字,而不要直接使用信号的值。
1)内核对信号的基本处理方法
内核给一个进程发送软中断信号的方法,是在进程所在的进程表项的信号域设置对应于该信号的位。这里要补充的是,如果信号发送给一个正在睡眠的进程,那么要看该进程进入睡眠的优先级,如果进程睡眠在可被中断的优先级上,则唤醒进程;否则仅设置进程表中信号域相应的位,而不唤醒进程。这一点比较重要,因为进程检 查是否收到信号的时机是:一个进程在即将从内核态返回到用户态时;或者在一个进程要进入或离开一个适当的低调度优先级睡眠状态时。
内核处理一个进程收到的信号的时机是在一个进程从内核态返回用户态时。所以当一个进程在内核态下运行时,软中断信号并不立即起作用,要等到将返回用户态时才处理。进程只有处理完信号才会返回用户态,进程在用户态下不会有未处理完的信号。
内核处理一个进程收到的软中断信号是在该进程的上下文中,因此,进程必须处于运行状态。前面介绍概念的时候讲过,处理信号有三种类型:进程接收到信号后退出;进程忽略该信号;进程收到信号后执行用户设定用系统调用signal的函数。当进程接收到一个它忽略的信号时,进程丢弃该信号,就象没有收到该信号似 的继续运行。如果进程收到一个要捕捉的信号,那么进程从内核态返回用户态时执行用户定义的函数。而且执行用户定义的函数的方法很巧妙,内核是在用户栈上创建一个新的层,该层中将返回地址的值设置成用户定义的处理函数的地址,这样进程从内核返回弹出栈顶时就返回到用户定义的函数处,从函数返回再弹出栈顶时, 才返回原先进入内核的地方。这样做的原因是用户定义的处理函数不能且不允许在内核态下执行(如果用户定义的函数在内核态下运行的话,用户就可以获得任何权限)。
在信号的处理方法中有几点特别要引起注意。第一在一些系统中,当一个进程处理完中断信号返回用户态之前,内核清除用户区中设定的对该信号的处理例程的地址,即下一次进程对该信号的处理方法又改为默认值,除非在下一次信号到来之前再次使用signal系统调用。这可能会使得进程 在调用signal之前又得到该信号而导致退出。在BSD中,内核不再清除该地址。但不清除该地址可能使得进程因为过多过快的得到某个信号而导致堆栈溢 出。为了避免出现上述情况。在BSD系统中,内核模拟了对硬件中断的处理方法,即在处理某个中断时,阻止接收新的该类中断。
第二个要引起注意的是,如果要捕捉的信号发生于进程正在一个系统调用中时,并且该进程睡眠在可中断的优先级上,这时该信号引起进程作一次longjmp,跳出睡眠 状态,返回用户态并执行信号处理例程。当从信号处理例程返回时,进程就象从系统调用返回一样,但返回了一个错误代码,指出该次系统调用曾经被中断。这要注 意的是,BSD系统中内核可以自动地重新开始系统调用。
第三个要注意的地方:若进程睡眠在可中断的优先级上,则当它收到一个要忽略的信号时,该进程被唤醒,但不做longjmp,一般是继续睡眠。但用户感觉不到进程曾经被唤醒,而是象没有发生过该信号一样。
第四个要注意的地方:内核对子进程终止(SIGCLD)信号的处理方法与其他信号有所区别。当进程检查出收到了一个子进程终止的信号时,缺省情况下,该进程 就象没有收到该信号似的,如果父进程执行了系统调用wait,进程将从系统调用wait中醒来并返回wait调用,执行一系列wait调用的后续操作(找 出僵死的子进程,释放子进程的进程表项),然后从wait中返回。SIGCLD信号的作用是唤醒一个睡眠在可被中断优先级上的进程。如果该进程捕捉了这个 信号,就象普通信号处理一样转到处理例程。如果进程忽略该信号,那么系统调用wait的动作就有所不同,因为SIGCLD的作用仅仅是唤醒一个睡眠在可被 中断优先级上的进程,那么执行wait调用的父进程被唤醒继续执行wait调用的后续操作,然后等待其他的子进程。
如果一个进程调用signal系统调用,并设置了SIGCLD的处理方法,并且该进程有子进程处于僵死状态,则内核将向该进程发一个SIGCLD信号。
24.堆排序 https://www.cnblogs.com/chengxiao/p/6129630.html
堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。(平均的时间复杂度是O(nlogn))
大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]
小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]
堆排序的基本思想是:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了。
25.Hashmap原理:HashMap基于hashing原理,我们通过put()和get()方法储存和获取对象。当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashcode,让后找到bucket位置来储存值对象。当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。HashMap使用链表来解决碰撞问题,当发生碰撞了,对象将会储存在链表的下一个节点中。 HashMap在每个链表节点中储存键值对对象。
当两个不同的键对象的hashcode相同时会发生什么? 它们会储存在同一个bucket位置的链表中。键对象的equals()方法用来找到键值对。
hashmap和hashtable的区别:HashMap和Hashtable都实现了Map接口,但决定用哪一个之前先要弄清楚它们之间的分别。主要的区别有:线程安全性,同步(synchronization),以及速度。
1)HashMap几乎可以等价于Hashtable,除了HashMap是非synchronized的,并可以接受null。
2)HashMap是非synchronized,而Hashtable是synchronized,这意味着Hashtable是线程安全的,多个线程可以共享一个Hashtable;而如果没有正确的同步的话,多个线程是不能共享HashMap的。
3)由于Hashtable是线程安全的也是synchronized,所以在单线程环境下它比HashMap要慢。如果你不需要同步,只需要单一线程,那么使用HashMap性能要好过Hashtable。ConcurrentHashMap也是线程安全的,但性能比HashTable好很多,HashTable是锁整个Map对象,而ConcurrentHashMap是锁Map的部分结构。
26. 数据库方面:
数据库怎么保证数据一致性?
数据库事务执行的四个基本要素:原子性、一致性、隔离性、持久性
原子性:事务包含的所有数据库操作要么全部成功,要不全部失败回滚。
一致性:一个事务执行之前和执行之后都必须处于一致性状态。
隔离性:一个事务未提交的业务结果是否对于其它事务可见。级别一般有:read_uncommit,read_commit,read_repeatable,串行化访问。
持久性:一个事务一旦被提交了,那么对数据库中数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。
脏读、不可重复读、幻读:
脏数据所指的就是未提交的数据。也就是说,一个事务正在对一条记录做修改,在这个事务完成并提交之前,这条数据是处于待定状态的(可能提交也可能回滚),这时,第二个事务来读取这条没有提交的数据,并据此做进一步的处理,就会产生未提交的数据依赖关系。这种现象被称为脏读。
不可重复读(Non-Repeatable Reads):一个事务先后读取同一条记录,而事务在两次读取之间该数据被其它事务所修改,则两次读取的数据不同,我们称之为不可重复读。
幻读(Phantom Reads):一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象就称为幻读。
分布式数据库:一致性、可用性、分区容错性
第一范式:强调的是列的原子性,即列不能够再分成其他几列。
第二范式:首先是 1NF,另外包含两部分内容,一是表必须有一个主键;二是没有包含在主键中的列必须完全依赖于主键,而不能只依赖于主键的一部分。
第三范式:首先是 2NF,另外非主键列必须直接依赖于主键,不能存在传递依赖。即不能存在:非主键列 A 依赖于非主键列 B,非主键列 B 依赖于主键的情况。
BCNF范式:BCNF是3NF的改进形式,一个满足BCNF的关系模式的条件:
1.所有非主属性对每一个码都是完全函数依赖。
2.所有的主属性对每一个不包含它的码,也是完全函数依赖。
3.没有任何属性完全函数依赖于非码的任何一组属性。
数据库:B树和B+树
索引
27.java中的复制的了解?是复制引用还是复制内存?
28.线程和进程的区别?
29.final:任何变量前被 final 修饰就是 final 变量,定义的类前被 final 修饰就是 final 类,任何方法前被 final 修饰就是final方法。
使用 final 方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。在早期的Java实现版本中,会将final方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升。在最近的 Java 版本中,不需要使用 final 方法进行这些优化了。
final修饰变量和修饰引用变量的区别?
final修饰变量,变量不可以再更改。但是final修饰引用变量,引用不可以修改但是被引用的对象是可以更改的。
final 修饰过的变量会在常量池里, 普通变量会在堆了。
final的好处:
-
final方法比非final快一些
-
final关键字提高了性能。JVM和Java应用都会缓存final变量。
-
final变量可以安全的在多线程环境下进行共享,而不需要额外的同步开销。
-
使用final关键字,JVM会对方法、变量及类进行优化
30.容失性:
31.os如何实现内存管理:https://blog.csdn.net/marvin_huoshan/article/details/83479351
- 单一连续分配
- 固定分区分配
- 动态分区分配
- 动态重定位分区分配
- 其他
32.上下文切换:
上下文切换就是从当前执行任务切换到另一个任务执行的过程。但是,为了确保下次能从正确的位置继续执行,在切换之前,会保存上一个任务的状态。
进程上下文切换与线程上下文切换最主要的区别就是线程的切换虚拟空间内存是相同的(因为都是属于自己的进程),但是,进程切换的虚拟空间内存则是不同的。
33.进程间调度:一般来说,进程调度分为三种类型:
-
中断处理过程(包括时钟中断、
I/O
中断、系统调用和异常)中,直接调用schedule
,或者返回用户态时根据need_resched
标记调用schedule
; -
内核线程可以直接调用
schedule
进行进程切换,也可以在中断处理过程中进行调度,也就是说内核线程作为一类的特殊的进程可以主动调度,也可以被动调度; -
用户态进程无法实现主动调度,仅能通过陷入内核态后的某个时机点进行调度,即在中断处理过程中进行调度。
为了控制进程的执行,内核必须有能力挂起正在 CPU
上执行的进程,并恢复以前挂起的某个进程的执行的过程,叫做进程切换、任务切换、上下文切换。挂起正在 CPU
上执行的进程,与中断时保存现场是有区别的,中断前后是在同一个进程上下文中,只是由用户态转向内核态执行。
多线程是如何实现的?
主要是CPU通过给每个线程分配CPU时间片来实现多线程的。即使是单核处理器(CPU)也可以执行多线程处理。分配时间片使用的是时间片轮转算法,在时间片轮转调度算法中,系统根据先来先服务的原则,将所有的就绪进程排成一个就绪队列,并且每隔一段时间产生一次中断,激活系统中的进程调度程序,完成一次处理机调度,把处理机分配给就绪队列队首进程,让其执行指令。当时间片结束或进程执行结束,系统再次将CPU分配给队首进程。
JAVA反射机制:是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。比如getXXX()、equals()都是反射。
线程生命周期分为:新建、就绪(可运行状态), 运行、阻塞和死亡https://blog.csdn.net/weixin_42806169/article/details/104987810
synchronized:该关键字解决多个线程访问共享资源时的同步性问题,被该关键字修饰的方法或代码块,可以被保证任意时刻都只有一个线程在执行。
主要用三种用法:
修饰实例方法,作用于当前类的对象,进入同步代码块前需要获得当前对象实例的锁
修饰静态方法,作用于当前类对象,进入同步代码块前需要获得当前类对象的锁,该锁作用于类的所有对象实例
修饰代码块,包括静态代码块和非静态代码块,作用域与上述两类对应
底层原理:
修饰代码块时,通过monitorenter和monitorexit指令来标识代码块的开始和结束位置;当执行monitorenter指令时,线程会试图获取锁,也就是monitor对象,该对象存在于每个java对象的对象头中,这也解释了为何java中任何对象都可以作为锁;在获取锁后,计数器从原本的0变为1,而在执行monitorexit指令后,计数器又恢复为0,表示锁被释放。修饰方法时,jvm通过ACC_SYNCHRONIZED标识来表明方法是同步的,从而执行相应的同步调用。
volatile:易失性。该关键字只修饰变量,通过保证代码的有序性和可见性从而尽量保证线程安全;但是该关键字不能保证原子性。
可见性:某个共享变量被一个线程修改后,其它使用该共享变量的线程会立刻知晓该变量被修改,在使用这个共享变量时,不是从自己的工作内存中读取,而是去主内存中读取
有序性:由于虚拟机在运行期会对代码进行优化,代码执行的顺序有可能被修改;有序性保证在优化代码时,指令的位置不变。
不保证原子性例子:
public class VolatileDemo {
private static volatile int count = 0;
public static void main(String[] args) throws InterruptedException {
// 创建10个线程,每个线程执行的任务是1000次对静态变量t加1操作
// 期望最终得到count值为10*1000,但会出现很多小于期望值的结果
for (int i = 0; i < 10; i++)
new Thread(() -> {
for (int j = 0; j < 1000; j++)
count++;
}).start();
Thread.sleep(5000);
System.out.println(count);
}
}
volatile实现原理:每个线程都有独立的工作内存,而包括共享变量在内的所有变量都存在于主内存中。在对该关键字修饰的变量进行写操作时,会使用到cpu提供的Lock前缀指令,该指令具有如下作用:
1)将当前处理器缓存行的数据刷新到主内存中。
2)1中的操作会使其它缓存了该内存地址的缓存行的相应数据失效。
锁池和等待池:
锁池,当某个对象锁正在被线程占用,其它想要获得该对象锁的线程就会进入该对象的锁池内等待;持有该对象锁的线程释放该锁后,锁池内的线程便可以竞争该对象锁;锁池内的线程都处于Runnable状态
等待池,一个线程调用了某个对象的wait()方法后,那么该线程则进入该对象的等待池等待;等待池内的线程处于Blocked状态。
notify()方法唤醒等待池中的一个线程进入锁池,notifyAll()方法唤醒等待池中的所有线程都进入到锁池。
自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。获取锁的线程一直处于活跃状态,但是并没有执行任何有效的任务,使用这种锁会造成busy-waiting。
public class SpinLock {
private AtomicReference cas = new AtomicReference();
public void lock() {
Thread current = Thread.currentThread();
// 利用CAS
while (!cas.compareAndSet(null, current)) {
// DO nothing
}
}
public void unlock() {
Thread current = Thread.currentThread();
cas.compareAndSet(current, null);
}
}
自旋锁存在问题:
1) 若某线程持有锁的时间过长,就会导致其它等待获取锁的线程进入循环等待,消耗CPU。使用不当会造成CPU使用率极高。
2.)上面Java实现的自旋锁不是公平的,即无法满足等待时间最长的线程优先获取锁。不公平的锁就会存在“线程饥饿”问题。
自旋锁优点:
自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是active的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快。
非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁的时候需要从内核态恢复,需要线程上下文切换。 (线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能)。
死锁:
所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。两个或者是两个以上的进程都在等待对方执行完毕才能继续往下执行的时候就会产生死锁,这些线程都陷入例如无限的等待。
死锁产生的原因:
1)竞争资源。
产生死锁中的竞争资源之一指的是竞争不可剥夺资源(例如:系统中只有一台打印机,可供进程P1使用,假定P1已占用了打印机,若P2继续要求打印机打印将阻塞)
产生死锁中的竞争资源另外一种资源指的是竞争临时资源(临时资源包括硬件中断、信号、消息、缓冲区内的消息等),通常消息通信顺序进行不当,则会产生死锁
2)进程间推进顺序非法。
产生死锁的必要条件:
互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
循环等待条件:在发生死锁时,必然存在一个进程--资源的环形链。若干个进程之间形成一种首尾相接的循环等待资源关系。
预防死锁:
资源一次性分配:一次性分配所有资源,这样就不会再有请求了:(破坏请求条件)
只要有一个资源得不到分配,也不给这个进程分配其他的资源:(破坏请保持条件)
可剥夺资源:即当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源(破坏不可剥夺条件)
资源有序分配法:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反(破坏环路等待条件)
检测死锁:
1)首先为每个进程和每个资源指定一个唯一的号码;
2)然后建立资源分配表和进程等待表。
解除死锁:
1)剥夺资源:从其它进程剥夺足够数量的资源给死锁进程,以解除死锁状态;
2)撤消进程:可以直接撤消死锁进程或撤消代价最小的进程,直至有足够的资源可用,死锁状态.消除为止;所谓代价是指优先级、运行代价、进程的重要性和价值等。
B树和B+树:
B+树和B树相比,主要的不同点在以下2项:
1)内部节点中,关键字的个数与其子树的个数相同,不像B树种,子树的个数总比关键字个数多1个
2)所有指向文件的关键字及其指针都在叶子节点中,不像B树,有的指向文件的关键字是在内部节点中。换句话说,B+树中,内部节点仅仅起到索引的作用,在搜索过程中,如果查询和内部节点的关键字一致,那么搜索过程不停止而是继续向下搜索这个分支。
根据B+树的结构,我们可以发现B+树相比于B树,在文件系统,数据库系统当中,更有优势,原因如下:
1)B+树的磁盘读写代价更低
B+树的内部结点并没有指向关键字具体信息的指针。因此其内部结点相对B树更小。如果把所有同一内部结点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多。一次性读入内存中的需要查找的关键字也就越多。相对I/O读写次数也就降低了。
2)B+树的查询效率更加稳定
由于内部结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。
3)B+树更有利于对数据库的扫描
B树在提高了磁盘IO性能的同时并没有解决元素遍历的效率低下的问题,而B+树只需要遍历叶子节点就可以解决对全部关键字信息的扫描,所以对于数据库中频繁使用的 range query,B+树有着更高的性能。
TCP 粘包:
原因:
1)发送端需要等缓冲区满才发送出去,造成粘包
2)接收方不及时接收缓冲区的包,造成多个包接收
通过上层的应用来解决,常见的方式有:(Netty常见解决方案)
- 在报文末尾增加换行符表明一条完整的消息,这样在接收端可以根据这个换行符来判断消息是否完整。
- 将消息分为消息头、消息体。可以在消息头中声明消息的长度,根据这个长度来获取报文(比如 808 协议)。
- 规定好报文长度,不足的空位补齐,取的时候按照长度截取即可。
介绍 HTTP :https://hit-alibaba.github.io/interview/basic/network/HTTP.html
HTTP 方法 :
- HTTP1.0定义了三种请求方法: GET, POST 和 HEAD方法
- HTTP1.1新增了五种请求方法:OPTIONS, PUT, DELETE, TRACE 和 CONNECT
GET和POST区别:
1)数据传输方式不同:GET请求通过URL传输数据,而POST的数据通过请求体传输。
2)安全性不同:POST的数据因为在请求主体内,所以有一定的安全性保证,而GET的数据在URL中,通过历史记录,缓存很容易查到数据信息。
3)数据类型不同:GET只允许 ASCII 字符,而POST无限制
4)GET无害: 刷新、后退等浏览器操作GET请求是无害的,POST可能重复提交表单
5)特性不同:GET是安全(这里的安全是指只读特性,就是使用这个方法不会引起服务器状态变化)且幂等(幂等的概念是指同一个请求方法执行多次和仅执行一次的效果完全相同),而POST是非安全非幂等
PUT 和 POST 的区别:
PUT 和POST方法的区别是,PUT方法是幂等的:连续调用一次或者多次的效果相同(无副作用),而POST方法是非幂等的。除此之外还有一个区别,通常情况下,PUT的URI指向是具体单一资源,而POST可以指向资源集合。
『POST表示创建资源,PUT表示更新资源』这种说法是错误的,两个都能创建资源,根本区别就在于幂等性
HTTP 状态码:
200 OK
客户端请求成功301 Moved Permanently
请求永久重定向302 Moved Temporarily
请求临时重定向304 Not Modified
文件未修改,可以直接使用缓存的文件。400 Bad Request
由于客户端请求有语法错误,不能被服务器所理解。401 Unauthorized
请求未经授权。这个状态代码必须和WWW-Authenticate报头域一起使用403 Forbidden
服务器收到请求,但是拒绝提供服务。服务器通常会在响应正文中给出不提供服务的原因404 Not Found
请求的资源不存在,例如,输入了错误的URL500 Internal Server Error
服务器发生不可预期的错误,导致无法完成客户端的请求。503 Service Unavailable
服务器当前不能够处理客户端的请求,在一段时间之后,服务器可能会恢复正常。
数据库锁:
同样是重定向,302,303,307区别?
302是http1.0的协议状态码,在http1.1版本的时候为了细化302状态码又出来了两个303和307。303明确表示客户端应当采用get方法获取资源,他会把POST请求变为GET请求进行重定向。 307会遵照浏览器标准,不会从post变为get。
keep-alive:
在早期的HTTP/1.0中,每次http请求都要创建一个连接,而创建连接的过程需要消耗资源和时间,为了减少资源消耗,缩短响应时间,就需要重用连接。在后来的HTTP/1.0中以及HTTP/1.1中,引入了重用连接的机制,就是在http请求头中加入Connection: keep-alive来告诉对方这个请求响应完成后不要关闭,下一次咱们还用这个请求继续交流。协议规定HTTP/1.0如果想要保持长连接,需要在请求头中加上Connection: keep-alive。
keep-alive的优点:
- 较少的CPU和内存的使用(由于同时打开的连接的减少了)
- 允许请求和应答的HTTP管线化
- 降低拥塞控制 (TCP连接减少了)
- 减少了后续请求的延迟(无需再进行握手)
- 报告错误无需关闭TCP连接
JVM类加载器:
方法区和堆是所有线程共享的内存区域;而java栈、本地方法栈和程序计数器是运行是线程私有的内存区域。
JVM内存结构主要有三大块:堆内存、方法区和栈。堆内存是JVM中最大的一块由年轻代和老年代组成,而年轻代内存又被分成三部分,Eden空间、From Survivor空间、To Survivor空间,默认情况下年轻代按照8:1:1的比例来分配;
方法区存储类信息、常量、静态变量等数据,是线程共享的区域,为与Java堆区分,方法区还有一个别名Non-Heap(非堆);栈又分为java虚拟机栈和本地方法栈主要用于方法的执行。
程序计数器(Program Counter Register)
程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie方法,这个计数器值则为空(Undefined)。
此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
JVM栈(JVM Stacks)
与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不等同于对象本身,根据不同的虚拟机实现,它可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。
其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。
本地方法栈(Native Method Stacks)
本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。
线程池:
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
final int index = i;
try {
Thread.sleep(index * 1000);
}
catch (InterruptedException e) {
e.printStackTrace();
}
cachedThreadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println(index);
}
});
}
固定长度的线程池:
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
final int index = i;
fixedThreadPool.execute(new Runnable() {
@Override
public void run() {
try {
System.out.println(index);
Thread.sleep(2000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
}