当前位置 : 首页 » 文章分类 :  开发  »  面试准备12-计算机基础

面试准备12-计算机基础

Java面试准备之计算机基础


Shell和Linux命令

常用Linux命令

lsof列出当前系统打开文件

lsof (list open files)是一个列出当前系统打开文件的工具。在linux系统环境下,任何事物都可以以文件形式存在,通过文件不仅可以访问常规的数据,还可以访问网络连接和硬件。

-p 进程号:列出指定进程号所打开的文件

top结果中load过高可能原因?

top命令中load average显示的是最近1分钟、5分钟和15分钟的系统平均负载。

系统平均负载被定义为在特定时间间隔内运行队列中(在CPU上运行或者等待运行多少进程)的平均进程数。

cpu load过高问题排查
https://www.cnblogs.com/lddbupt/p/5779655.html

kill -2,-9,-15

默认(缺省)情况下,kill发送的是SIGTERM,即15(SIGTERM)信号,”kill PID”与”kill -15 PID”是一样的。
这个信号通常会要求程序自己正常退出,是一种比较安全的用法。但它是可以被阻塞,处理和忽略的,所以对于有的进程,会中止失败。

另一个常用的信号是9(SIGKILL),这个命令表示立即结束程序,是不能被阻塞,处理和忽略的。在TERM信号失效的情况下,可以尝试使用”kill -9 PID”
事实上,SIGKILL信号是直接发给init进程的,它收到该信号后,负责终止pid指定的进程。

Kill -2 :功能类似于Ctrl + C 是程序在结束之前,能够保存相关数据,然后再退出。

Shell脚本

  • !!,连续两个英文叹号,表示执行上一条指令

  • $n,n为数字,$n为执行脚本的第一个参数,相当于main函数的argv[n]。例如,$0就是argv[0],即脚本程序自身的名称

  • $#,传递到脚本的参数个数,不包括脚本本身

  • $*,以一个单字符串显示所有向脚本传递的参数,不包括$0

  • $@,与$*类似,从$1开始的所有参数,但每个都作为独立的字符串

$*$@的区别
相同点:都是引用所有参数。
不同点:只有在双引号中体现出来。假设在脚本运行时写了三个参数 1、2、3,,则"$*"等价于 “1 2 3”(单个字符串),而"$@"等价于 “1” “2” “3”(三个字符串)。

  • $?:当前shell进程中,上一个命令的返回值,如果上一个命令成功执行则$?的值为0,否则为其他非零值,常用做if语句条件
  • $$:当前shell进程的pid
  • $!:后台运行的最后一个进程的pid
  • $-:显示Shell使用的当前选项,与set命令功能相同。
  • $_:之前命令的最后一个参数

打印nginx的进程号

ps -ef|grep nginx|grep master|awk '{print $2}'

统计单词出现的次数

1、统计文本中每个单词(空格、制表、换行分隔)出现的次数?

masikkk masikkk xx xx x x     kkk kk  kkk
english    count  ? com         masikkk
madaimeng    devgou  com

解答:
cat word_count.txt| tr -s ' \t' '\n' | sort |uniq -c
结果:

1 ?
2 com
1 count
1 devgou
1 english
1 kk
2 kkk
1 madaimeng
3 masikkk
2 x
2 xx

解释:tr -s ' \t' '\n' 连续的 空格 和 制表符 压缩为1个,并替换为换行符

2、找出出现次数最多的3个单词?
cat word_count.txt| tr -s ' \t' '\n' | sort |uniq -c|sort -r|head -3
结果

3 masikkk
2 xx
2 x

统计nginx日志中出现次数最多的10个ip

1、Nginx access 日志如下,里面有ip字段,写shell脚本找出出现次数最多的10个ip,按出现次数从高到低倒序排列输出?
access-10000.log

3.112.230.252 - - [11/Mar/2020:13:52:40 +0800] "GET /eureka/apps/delta HTTP/1.1" 200 89 "-" "Java-EurekaClient/v1.9.13" "-"
3.112.230.252 - - [11/Mar/2020:13:52:40 +0800] "GET /eureka/apps/delta HTTP/1.1" 200 89 "-" "Java-EurekaClient/v1.9.13" "-"
3.112.230.252 - - [11/Mar/2020:13:52:48 +0800] "PUT /eureka/apps/STATISTIC/ip-172-26-4-123.ap-northeast-1.compute.internal:statistic:8002?status=UP&lastDirtyTimestamp=1583842800449 HTTP/1.1" 200 0 "-" "Java-EurekaClient/v1.9.13" "-"
3.112.230.252 - - [11/Mar/2020:13:52:50 +0800] "PUT /eureka/apps/DISQUS/ip-172-26-4-123.ap-northeast-1.compute.internal:disqus:8001?status=UP&lastDirtyTimestamp=1583842811792 HTTP/1.1" 200 0 "-" "Java-EurekaClient/v1.9.13" "-"
220.181.108.108 - - [11/Mar/2020:13:54:01 +0800] "GET /blog/IMG_3600.JPG?imageMogr2/auto-orient HTTP/1.1" 200 1370025 "-" "Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html)" "-"
66.249.79.155 - - [11/Mar/2020:13:54:39 +0800] "GET /article/Java-Interview-3-Concurrent/ HTTP/1.1" 404 555 "-" "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.96 Mobile Safari/537.36 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" "-"
123.57.52.15 - - [11/Mar/2020:13:56:02 +0800] "GET /article/TortoiseSVN/ HTTP/1.1" 200 39095 "-" "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36" "-"
220.181.108.160 - - [11/Mar/2020:13:56:36 +0800] "GET /tags/CV/ HTTP/1.1" 200 614440 "-" "Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html)" "-"
3.112.230.252 - - [11/Mar/2020:13:56:40 +0800] "GET /eureka/apps/delta HTTP/1.1" 200 89 "-" "Java-EurekaClient/v1.9.13" "-"
3.112.230.252 - - [11/Mar/2020:13:56:40 +0800] "GET /eureka/apps/delta HTTP/1.1" 200 89 "-" "Java-EurekaClient/v1.9.13" "-"
124.166.232.105 - - [11/Mar/2020:13:56:42 +0800] "GET /tags/CV/ HTTP/1.1" 200 614440 "-" "Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html)" "-"
207.46.13.88 - - [11/Mar/2020:13:56:49 +0800] "GET /comments?pathname=%2Fcategories%2F HTTP/1.1" 200 294 "-" "msnbot/2.0b (+http://search.msn.com/msnbot.htm)" "-"
111.206.221.51 - - [11/Mar/2020:13:56:52 +0800] "POST /statistic HTTP/1.1" 200 400 "http://masikkk.com/tags/CV/" "Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1 (compatible; Baiduspider-render/2.0; +http://www.baidu.com/search/spider.html)" "-"
220.194.45.154 - - [11/Mar/2020:13:57:03 +0800] "GET /tags/Raft/ HTTP/1.1" 304 0 "http://masikkk.com/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36" "-"
220.194.45.154 - - [11/Mar/2020:13:57:04 +0800] "POST /statistic HTTP/1.1" 200 401 "http://masikkk.com/tags/Raft/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36" "-"
220.194.45.154 - - [11/Mar/2020:13:57:05 +0800] "GET /favicon.ico HTTP/1.1" 200 4286 "http://masikkk.com/tags/Raft/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36" "-"
3.112.230.252 - - [11/Mar/2020:13:57:10 +0800] "GET /eureka/apps/delta HTTP/1.1" 200 89 "-" "Java-EurekaClient/v1.9.13" "-"
220.194.45.154 - - [11/Mar/2020:13:57:10 +0800] "GET /article/Raft/ HTTP/1.1" 200 40857 "http://masikkk.com/tags/Raft/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36" "-"
3.112.230.252 - - [11/Mar/2020:13:57:10 +0800] "GET /eureka/apps/delta HTTP/1.1" 200 89 "-" "Java-EurekaClient/v1.9.13" "-"
220.194.45.154 - - [11/Mar/2020:13:57:10 +0800] "GET /comments?pathname=%2Farticle%2FRaft%2F HTTP/1.1" 200 37 "http://masikkk.com/article/Raft/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36" "-"
220.194.45.154 - - [11/Mar/2020:13:57:11 +0800] "POST /statistic HTTP/1.1" 200 401 "http://masikkk.com/article/Raft/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36" "-"
220.194.45.154 - - [11/Mar/2020:13:57:13 +0800] "GET /update/ HTTP/1.1" 200 220557 "http://masikkk.com/article/Raft/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36" "-"
220.194.45.154 - - [11/Mar/2020:13:57:14 +0800] "GET /comments?pathname=%2Fupdate%2F HTTP/1.1" 200 294 "http://masikkk.com/update/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36" "-"
220.194.45.154 - - [11/Mar/2020:13:57:14 +0800] "GET /statistic/ranks?limit=15 HTTP/1.1" 200 1280 "http://masikkk.com/update/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36" "-"
220.194.45.154 - - [11/Mar/2020:13:57:14 +0800] "POST /statistic HTTP/1.1" 200 442 "http://masikkk.com/update/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36" "-"
3.112.230.252 - - [11/Mar/2020:13:57:19 +0800] "PUT /eureka/apps/STATISTIC/ip-172-26-4-123.ap-northeast-1.compute.internal:statistic:8002?status=UP&lastDirtyTimestamp=1583842800449 HTTP/1.1" 200 0 "-" "Java-EurekaClient/v1.9.13" "-"
3.112.230.252 - - [11/Mar/2020:13:57:20 +0800] "PUT /eureka/apps/DISQUS/ip-172-26-4-123.ap-northeast-1.compute.internal:disqus:8001?status=UP&lastDirtyTimestamp=1583842811792 HTTP/1.1" 200 0 "-" "Java-EurekaClient/v1.9.13" "-"

第一列就是 ip 先用 cut 把第一列单独提出来 cut -d' ' -f1 access-10000.log
或者用 awk 提取 awk '{print $1}' access-10000.log
然后排序,统计个数,再排序取top n即可
cut -d' ' -f1 access-10000.log | sort |uniq -c| sort -r |head -10
或者
awk '{print $1}' access-10000.log | sort |uniq -c|sort -r|head -10
结果为:

10 3.112.230.252
10 220.194.45.154
 1 66.249.79.155
 1 220.181.108.160
 1 220.181.108.108
 1 207.46.13.88
 1 124.166.232.105
 1 123.57.52.15
 1 111.206.221.51

2、找出 2020.3.11 日 出现次数超过 5 的所有 ip,统计个数后倒序输出
grep 11/Mar/2020 access-10000.log 根据日期筛选出行,然后
awk '{print $1}' access-10000.log | sort |uniq -c|awk '{if ($1 >= 50) print}'| sort -r
解释:提取第一列 ip, 因为uniq统计的重复行必须是相邻的,所以必须先把所有ip排序,然后 uniq统计重复行,awk 根据第一列个数过滤出大于50的,再倒序排序后输出

3、找出所有 get 请求中访问量最高的10个页面(或api,或 uri 或 pathname),例如 GET /article/hexo-17-FontAwesome/ 不包含 *.js,*.css,*.png, favicon.ico 这些静态文件
grep GET access-10000.log |egrep -v 'favicon.ico|.css|.js|.png' |awk '{print $7}' | sort | uniq -c | sort -r | head -10
解释:先过滤出有 GET 的行,再过滤 不包含 favicon.ico|.css|.js|.png 的行,awk打印第7列,也就是 pathname 列,找出top 10
结果如下

6 /eureka/apps/delta
2 /tags/CV/
1 /update/
1 /tags/Raft/
1 /statistic/ranks?limit=15
1 /comments?pathname=%2Fupdate%2F
1 /comments?pathname=%2Fcategories%2F
1 /comments?pathname=%2Farticle%2FRaft%2F
1 /blog/IMG_3600.JPG?imageMogr2/auto-orient
1 /article/TortoiseSVN/

组成原理

唐朔飞-计算机组成原理(第二版).pdf 带目录
计算机网络第五版.pdf
数据结构 C语言 严蔚敏 pdf
https://github.com/CroMarmot/kaoyanziliao/tree/master/912computer

原码反码补码

机器数和真值

一个数在计算机中的二进制表示形式, 叫做这个数的机器数。
机器数是带符号的,在计算机用一个数的最高位存放符号, 正数为0, 负数为1

因为第一位是符号位,所以机器数的形式值就不等于真正的数值。
所以,为区别起见,将带符号位的机器数对应的真正数值称为机器数的真值。

对于一个数, 计算机要使用一定的编码方式进行存储. 原码, 反码, 补码是机器存储一个具体数字的编码方式。

原码

原码就是符号位加上真值的绝对值, 即用第一位表示符号, 其余位表示值。
原码就是符号位加上数字的二进制表示,int为例,第一位表示符号 (0正数 1负数)
n+1位原码的表示范围为 -(2^n - 1) ~ 2^n - 1

简单起见一个字节表示
+7的原码为: 00000111
-7的原码为: 10000111
对于原码来说,绝对值相等的正数和负数只有符号位不同。

反码

正数的反码是其本身。
负数的反码是在其原码的基础上, 符号位不变,其余各个位取反。
换言之 该数的绝对值取反(绝对值取反各位都取反)。
n+1位反码的表示范围为 -(2^n - 1) ~ 2^n - 1

为了简单起见,我们用1个字节来表示一个整数:
+7的反码为: 00000111
-7的反码为: 11111000

补码

正数的补码就是其本身。
负数的补码是在其原码的基础上, 符号位不变, 其余各位取反, 最后加1,即在反码的基础上加1
n+1位补码的表示范围为 -2^n ~ 2^n - 1

为了简单起见,我们用1个字节来表示一个整数:
+7的补码为: 00000111
-7的补码为: 11111001

补码可以在负数上多表示一位。
比如8位二进制的话,补码1000 0000 表示 -128=-2^7,即n+1位二进制补码可表示的最小负数为 -2^n。
注意补码表示的最小负值(符号位为1,其余全是0)不能使用正常的规则转换为反码和原码,因为反码和补码无法表示此数。

总结
正数的原码、反码、补码相同
负数反码是原码符号位不变化其余各位数取反,负数的补码是原码符号位不变其余各位取反加1,即反码加1

原码, 反码, 补码 详解
https://blog.csdn.net/zq602316498/article/details/39404043


操作系统


内存

虚拟内存

虚拟内存是操作系统内核为了对进程地址空间进行管理(process address space management)而精心设计的一个逻辑意义上的内存空间概念。
我们程序中的指针其实都是这个虚拟内存空间中的地址。凡是程序运行过程中可能需要用到的指令或者数据都必须在虚拟内存空间中。

为了能够让程序在物理机器上运行,必须有一套机制可以让这些虚拟内存空间映射到物理内存空间(实实在在的RAM内存条上的空间),这就是操作系统中 页映射表(page table)

内核会为系统中每一个进程维护一份相互独立的页映射表。页映射表的基本原理是将程序运行过程中需要访问的一段虚拟内存空间通过页映射表映射到一段物理内存空间上,这样CPU访问对应虚拟内存地址的时候就可以通过这种查找页映射表的机制访问物理内存上的某个对应的地址。
“页(page)”是虚拟内存空间向物理内存空间映射的基本单元。

下图演示了虚拟内存空间和物理内存空间的相互关系,它们通过 Page Table 关联起来。其中虚拟内存空间中着色的部分分别被映射到物理内存空间对应相同着色的部分。而虚拟内存空间中灰色的部分表示在物理内存空间中没有与之对应的部分,也就是说灰色部分没有被映射到物理内存空间中。这么做也是本着“按需映射”的指导思想,因为虚拟内存空间很大,可能其中很多部分在一次程序运行过程中根本不需要访问,所以也就没有必要将虚拟内存空间中的这些部分映射到物理内存空间上。


虚拟地址通过页表映射到物理地址

虚拟内存空间大只能表示程序运行过程中可访问的空间比较大,不代表物理内存空间占用也大。

驻留内存

驻留内存,顾名思义是指那些被映射到进程虚拟内存空间的物理内存。

上图中,在系统物理内存空间中被着色的部分都是驻留内存。比如,A1、A2、A3和A4是进程A的驻留内存;B1、B2和B3是进程B的驻留内存。进程的驻留内存就是进程实实在在占用的物理内存。一般我们所讲的进程占用了多少内存,其实就是说的占用了多少驻留内存而不是多少虚拟内存。因为虚拟内存大并不意味着占用的物理内存大。

共享内存

上图中,进程 A 虚拟内存空间中的 A4 和进程 B 虚拟内存空间中的 B3 都映射到了物理内存空间的 A4/B3 部分,这部分就是进程 A 和进程 B 的共享内存。

为什么会出现这样的情况呢?
其实我们写的程序会依赖于很多外部的动态库(.so),比如 libc.so, libld.so 等等。这些动态库在内存中仅仅会保存/映射一份,如果某个进程运行时需要这个动态库,那么动态加载器会将这块内存映射到对应进程的虚拟内存空间中。
此外,多个进展之间通过共享内存的方式相互通信也会出现这样的情况。

这么一来,就会出现不同进程的虚拟内存空间会映射到相同的物理内存空间。这部分物理内存空间其实是被多个进程所共享的,所以我们将他们称为共享内存,用 SHR 来表示。某个进程占用的内存除了和别的进程共享的内存之外就是自己的独占内存了。所以要计算进程独占内存的大小只要用RES的值减去SHR值即可

top命令中的VIRT/RES/SHR

VIRT virtual memory usage 虚拟内存。
进程需要的虚拟内存总量,单位kb。包括进程使用的库、代码、数据等。
VIRT=SWAP+RES
VIRT 包含了在已经映射到物理内存空间的部分和尚未映射到物理内存空间的部分总和。

RES resident memory usage 常驻内存。
进程当前使用的、未被换出的物理内存大小,单位kb。不包括swap out。
RES=CODE+DATA
RES 指进程虚拟内存空间中已经映射到物理内存空间的那部分的大小。
看进程在运行过程中占用了多少内存应该看RES的值而不是VIRT的值。

SHR shared memory 共享内存。共享内存大小,单位kb

理解virt res shr之间的关系 - linux
https://www.orchome.com/298


用户空间与内核空间

现在操作系统都是采用虚拟存储器,那么对32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方)。
操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。
为了保证用户进程不能直接操作内核(kernel),保证内核的安全,操心系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。
针对linux操作系统而言,将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为内核空间,而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,称为用户空间。


内存分页

在基本的分页概念中,我们把程序分成等长的小块。这些小块叫做“页(Page)”,同样内存也被我们分成了和页面同样大小的”页框(Frame)“,一个页可以装到一个页框里。在执行程序的时候我们根据一个页表去查找某个页面在内存的某个页框中,由此完成了逻辑到物理的映射。

为什么要分页?(内存碎片/虚拟内存)

内存的分段和分页管理方式和由此衍生的一堆段页式等都属于内存的不连续分配。什么叫不连续分配?就是把程序分割成一块一块的装入内存,在物理上不用彼此相连,在逻辑上使用段表或者页表将离散分布的这些小块串起来形成逻辑上连续的程序。

(1)解决空间浪费碎片化问题
由于将虚拟内存空间和物理内存空间按照某种规定的大小进行分配,这里我们称之为页(Page),然后按照页进行内存分配,也就克服了外部碎片的问题。

(2)解决程序大小受限问题
程序增长有限是因为一个程序需要全部加载到内存才能运行,因此解决的办法就是使得一个程序无须全部加载就可以运行。使用分页也可以解决这个问题,只需将当前需要的页面放在内存里,其他暂时不用的页面放在磁盘上,这样一个程序同时占用内存和磁盘,其增长空间就大大增加了。而且,分页之后,如果一个程序需要更多的空间,给其分配一个新页即可(而无需将程序倒出倒进从而提高空间增长效率)。

一般一个内存页大小为4KB

在32位操作系统中,一个地址对应32位2进制数,则能寻址到4GB(2^32)的地址.而在linux内存管理中,内存以页为单位进行管理,一般情况下每页4KB大小,4GB内存就有(4GB/4KB=2^20)个页.所以可将一个32位的地址分为20位+12位.前20位可以确定出地址在2^20个页中的哪一页,剩下的12位就可以确定出在这一页中的哪个位置.
反过来,若是知道了页内为12位,那每页大小就肯定为了


c/c++中delete/free如何知道释放多少内存?

在使用c或者c++的时候我们经常用到malloc/free和new/delete,在使用malloc申请内存的时候我们给定了需要申请的内存大小,但是在free或者delete的时候并不需要提供这个大小,那么程序是怎么实现准确无误的释放内存的呢?

实际上,在申请内存的时候,申请到的地址会比你实际的地址大一点点,他包含了一个存有申请空间大小的结构体。
比如你申请了20byte的空间,实际上系统申请了48bytes的block
这样在free的时候就不需要提供任何其他的信息,可以正确的释放内存

How does free know how much to free?
https://stackoverflow.com/questions/1518711/how-does-free-know-how-much-to-free


IO

缓冲IO(传统IO)

Linux 中传统的 I/O 操作是一种缓冲 I/O,I/O 过程中产生的数据传输通常需要在缓冲区中进行多次的拷贝操作。

缓存 I/O 又被称作标准 I/O,大多数文件系统的默认 I/O 操作都是缓存 I/O。在 Linux 的缓存 I/O 机制中,操作系统会将 I/O 的数据缓存在文件系统的 页缓存(page cache)中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间

一般来说,在传输数据的时候,用户应用程序需要分配一块大小合适的缓冲区用来存放需要传输的数据。以应用程序从文件中读取一块数据然后把这块数据通过网络发送到接收端去为例,用户应用程序只是需要调用两个系统调用 read() 和 write() 就可以完成这个数据传输操作,应用程序并不知晓在这个数据传输的过程中操作系统所做的数据拷贝操作。对于 Linux 操作系统来说,基于数据排序或者校验等各方面因素的考虑,操作系统内核会在处理数据传输的过程中进行多次拷贝操作。在某些情况下,这些数据拷贝操作会极大地降低数据传输的性能。

当应用程序访问某块数据时,操作系统首先会检查,是不是最近访问过此文件,文件内容是否缓存在内核缓冲区,如果是,操作系统则直接根据 read 系统调用提供的 buf 地址,将内核缓冲区的内容拷贝到 buf 所指定的用户空间缓冲区中去。如果不是,操作系统则首先将磁盘上的数据拷贝的内核缓冲区,这一步目前主要依靠 DMA 来传输,然后再把内核缓冲区上的内容拷贝到用户缓冲区中。接下来, write 系统调用再把用户缓冲区的内容拷贝到网络堆栈相关的内核缓冲区中,最后 socket 再把内核缓冲区的内容发送到网卡上。


缓冲IO示意图

从上图中可以看出,共产生了四次数据拷贝,即使使用了 DMA 来处理了与硬件的通讯,CPU仍然需要处理两次数据拷贝,与此同时,在用户态与内核态也发生了多次上下文切换,无疑也加重了CPU负担。
在此过程中,我们没有对文件内容做任何修改,那么在内核空间和用户空间来回拷贝数据无疑就是一种浪费,而零拷贝主要就是为了解决这种低效性。

缓存 I/O 的缺点:
数据在传输过程中需要在应用程序地址空间和内核进行多次数据拷贝操作,这些数据拷贝操作所带来的 CPU 以及内存开销是非常大的。


零拷贝技术

零拷贝就是一种避免 CPU 将数据从一块存储拷贝到另外一块存储的技术。

mmap() 跳过用户空间

mmap() 让数据传输不需要经过user space

减少拷贝次数的一种方法是调用mmap()来代替read调用:

buf = mmap(diskfd, len);
write(sockfd, buf, len);

应用程序调用 mmap(),磁盘上的数据会通过DMA被拷贝到内核缓冲区,接着操作系统会把这段内核缓冲区与应用程序共享,这样就不需要把内核缓冲区的内容往用户空间拷贝。
应用程序再调用 write() , 操作系统直接将内核缓冲区的内容拷贝到socket缓冲区中,这一切都发生在内核态,最后,socket缓冲区再把数据发到网卡去。

sendfile() 文件到socket零拷贝

从2.1版内核开始,Linux引入了 sendfile() 来简化操作:

#include<sys/sendfile.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

系统调用 sendfile() 在代表输入文件的描述符 in_fd 和代表输出文件的描述符 out_fd 之间传送文件内容(字节)。
描述符 out_fd 必须指向一个套接字,而 in_fd 指向的文件必须是可以 mmap 的。
这些局限限制了 sendfile 的使用,使 sendfile 只能将数据从文件传递到套接字上,反之则不行
使用 sendfile 不仅减少了数据拷贝的次数,还减少了上下文切换,数据传送始终只发生在kernel space。


sendfile()示意图

splice() 任意文件描述符间零拷贝

sendfile() 只适用于将数据从文件拷贝到套接字上,限定了它的使用范围。
Linux在2.6.17版本引入 splice() 系统调用,用于在两个文件描述符中移动数据:

#define _GNU_SOURCE         /* See feature_test_macros(7) */
#include <fcntl.h>
ssize_t splice(int fd_in, loff_t *off_in, int fd_out, loff_t *off_out, size_t len, unsigned int flags);

splice调用在两个文件描述符之间移动数据,而不需要数据在内核空间和用户空间来回拷贝。
他从fd_in拷贝len长度的数据到fd_out,但是有一方必须是管道设备,这也是目前splice的一些局限性。

写时复制(Copy On Write,COW)

写时复制是计算机编程中的一种优化策略,它的基本思想是这样的:如果有多个应用程序需要同时访问同一块数据,那么可以为这些应用程序分配指向这块数据的指针,在每一个应用程序看来,它们都拥有这块数据的一份数据拷贝,当其中一个应用程序需要对自己的这份数据拷贝进行修改的时候,就需要将数据真正地拷贝到该应用程序的地址空间中去,也就是说,该应用程序拥有了一份真正的私有数据拷贝,这样做是为了避免该应用程序对这块数据做的更改被其他应用程序看到。这个过程对于应用程序来说是透明的,如果应用程序永远不会对所访问的这块数据进行任何更改,那么就永远不需要将数据拷贝到应用程序自己的地址空间中去。这也是写时复制的最主要的优点。

浅析Linux中的零拷贝技术(讲的非常透彻,比IMB的文章还清晰易懂)
https://www.jianshu.com/p/fad3339e3448

Linux 中的零拷贝技术,第 1 部分
https://www.ibm.com/developerworks/cn/linux/l-cn-zerocopy1/index.html

Linux 中的零拷贝技术,第 2 部分
https://www.ibm.com/developerworks/cn/linux/l-cn-zerocopy2/index.html

面试被问到“零拷贝”!你真的理解吗?
https://mp.weixin.qq.com/s/UejA6zQFwIatWRp2uTEatg


5种IO模型

对于一次IO访问(以read举例),数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。
所以说,当一个read操作发生时,它会经历两个阶段:

  1. 等待数据准备 (Waiting for the data to be ready)
  2. 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)

正式因为这两个阶段,linux系统产生了5种IO模型
在《Unix网络编程》一书中提到了五种IO模型,分别是:

  1. 阻塞IO
  2. 非阻塞IO
  3. 多路复用IO
  4. 信号驱动IO
  5. 异步IO。

阻塞IO(blocking IO)

阻塞IO(blocking IO) 是最传统的一种IO模型,即在读写数据过程中用户线程会发生阻塞现象并交出CPU
在linux中,默认情况下所有的socket都是blocking
典型的阻塞IO系统调用有 read(), write(), send(), recv(), sendto(), recvfrom()

当用户进程调用了 recvfrom 这个系统调用,kernel就开始了IO的第一个阶段:准备数据(对于网络IO来说,很多时候数据在一开始还没有到达。比如,还没有收到一个完整的UDP包。这个时候kernel就要等待足够的数据到来)。这个过程需要等待,也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程的。而在用户进程这边,整个进程会被阻塞(当然,是进程自己选择的阻塞)。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。
所以,blocking IO的特点就是在IO执行的两个阶段都被block了。

非阻塞IO(nonblocking IO)

当用户线程发起一个read操作后,并不需要等待,而是马上就得到了一个结果。如果结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦内核中的数据准备好了,并且又再次收到了用户线程的请求,那么它马上就将数据拷贝到了用户线程,然后返回。
在非阻塞IO模型中,用户线程需要不断地询问内核数据是否就绪,也就说非阻塞IO不会交出CPU,而会一直占用CPU

当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回。
所以,nonblocking IO的特点是用户进程需要不断的主动询问kernel数据好了没有。

多路复用IO(IO multiplexing)

多路复用IO模型是目前使用得比较多的模型。Java NIO实际上就是多路复用IO
在多路复用IO模型中,会有一个线程不断去轮询多个socket的状态,只有当socket真正有读写事件时,才真正调用实际的IO读写操作。因为在多路复用IO模型中,只需要使用一个线程就可以管理多个socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有在真正有socket读写事件进行时,才会使用IO资源,所以它大大减少了资源占用。

在Java NIO中,是通过 selector.select() 去查询每个通道是否有到达事件,如果没有事件,则一直阻塞在那里,因此这种方式会导致用户线程的阻塞。

IO multiplexing 就是我们说的 select, poll, epoll,有些地方也称这种IO方式为event driven IO。select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select,poll,epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。

当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。
所以,I/O 多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()函数就可以返回。

所以,如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。
select/epoll 的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接

信号驱动IO(signal driven IO)

在信号驱动IO模型中,当用户线程发起一个IO请求操作,会给对应的socket注册一个信号函数,然后用户线程会继续执行,当内核数据就绪时会发送一个信号给用户线程,用户线程接收到信号之后,便在信号函数中调用IO读写操作来进行实际的IO请求操作。

异步IO(asynchronous IO)

异步IO模型才是最理想的IO模型。在异步IO模型中,当用户线程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从内核的角度,当它收到一个asynchronous read之后,它会立刻返回,说明read请求已经成功发起了,因此不会对用户线程产生任何block。然后,内核会等待数据准备完成,然后将数据拷贝到用户线程,当这一切都完成之后,内核会给用户线程发送一个信号,告诉它read操作完成了。
也就说用户线程完全不需要知道实际的整个IO操作是如何进行的,只需要先发起一个请求,当接收内核返回的成功信号时表示IO操作已经完成,可以直接去使用数据了。
也就说在异步IO模型中,IO操作的两个阶段(IO请求、数据读写)都不会阻塞用户线程,这两个阶段都是由内核自动完成,然后发送一个信号告知用户线程操作已完成。

前面四种IO模型实际上都属于同步IO,只有最后一种是真正的异步IO,因为无论是多路复用IO还是信号驱动模型,IO操作的第2个阶段都会引起用户线程阻塞,也就是内核进行数据拷贝的过程都会让用户线程阻塞。

Linux IO模式及 select、poll、epoll详解
https://segmentfault.com/a/1190000003063859

Nginx为什么比Apache Httpd高效:原理篇
http://www.mamicode.com/info-detail-1156329.html


3种多路复用IO(NIO)模型select/poll/epoll

select, poll, epoll 都是IO多路复用(IO Multiplexing)的机制
所以,也可以说, select, poll, epoll 是3种 NIO 的实现方式
I/O 多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。
但 select, pselect, poll, epoll 本质上都是同步 I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的。

select()

int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
select 函数监视的文件描述符分 3 类,分别是 writefds, readfds, exceptfds, 调用后 select 函数会阻塞,传入的 fd 集合由用户态拷贝到内核态,然后查询每个 fd 对应的设备状态,直到有描述符就绪(有数据 可读、可写、或者有 exception),或者超时( timeout 指定等待时间,如果立即返回设为 null 即可),函数返回。当 select 函数返回后,可以 通过遍历 fdset,来找到就绪的描述符。

select 目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点。
select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在 Linux 上一般为 1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但是这样也会造成效率的降低。

poll()

int poll (struct pollfd *fds, unsigned int nfds, int timeout);
不同与 select 使用三个位图来表示三个 fdset 的方式,poll 使用一个 pollfd 的指针实现

struct pollfd {
    int fd; /* file descriptor */
    short events; /* requested events to watch */
    short revents; /* returned events witnessed */
};

pollfd 结构包含了要监视的 event 和发生的 event,不再使用 select “参数-值”传递的方式。 同时,pollfd并没有最大数量限制(但是数量过大后性能也是会下降)
poll 本质上和 select 没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个 fd 对应的设备状态。
和 select 函数一样,poll 返回后,需要轮询 pollfd 来获取就绪的描述符。

select 和 poll 都需要在返回后,通过遍历文件描述符来获取已经就绪的 socket。
事实上,同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。


epoll()

epoll 是在 2.6 内核中提出的,是之前的 select 和 poll 的增强版本。
相对于 select 和 poll 来说,epoll 更加灵活,没有描述符限制。epoll 使用一个文件描述符管理多个描述符,将用户关心的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的 copy 只需一次。

epoll操作过程需要三个接口,分别如下:

int epoll_create(int size); //创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
epoll_create()

int epoll_create(int size);
创建一个 epoll 的句柄,size 用来告诉内核这个监听的数目一共有多大,这个参数不同于 select() 中的第一个参数,给出最大监听的 fd+1 的值,参数 size 并不是限制了 epoll 所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议。
当创建好 epoll 句柄后,它就会占用一个 fd 值,在 linux 下如果查看 /proc/进程id/fd/,是能够看到这个 fd 的,所以在使用完 epoll 后,必须调用 close() 关闭,否则可能导致 fd 被耗尽。

epoll_ctl()

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
对指定描述符 fd 执行 op 操作。

  • epfd: 是 epoll_create() 的返回值。
  • op: 表示操作,用三个宏来表示:添加 EPOLL_CTL_ADD,删除 EPOLL_CTL_DEL,修改 EPOLL_CTL_MOD。分别添加、删除和修改对 fd 的监听事件。
  • fd: 是需要监听的 fd(文件描述符)
  • epoll_event: 是告诉内核需要监听什么事

struct epoll_event 结构如下:

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

//events可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
epoll_wait()

int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待 epfd 上的 io 事件,最多返回 maxevents 个事件。
参数 events 用来从内核得到事件的集合,maxevents 告之内核这个 events 有多大,这个 maxevents 的值不能大于创建 epoll_create() 时的 size,参数 timeout 是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。

水平触发(Level Trigger)和边缘触发(Edge Trigger)

epoll 对文件描述符的操作有两种模式:LT(level trigger)和ET(edge trigger)。LT 模式是默认模式,LT模式与ET模式的区别如下:

  • LT模式:当 epoll_wait 检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。
  • ET模式:当 epoll_wait 检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。

LT模式
LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的。

ET模式
ET(edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once)

ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。

Linux IO模式及 select、poll、epoll详解
https://segmentfault.com/a/1190000003063859


epoll相对于select/poll有哪些优化

select, poll, epoll 都是IO多路复用(IO Multiplexing)的机制
I/O 多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但 select, poll, epoll本质上都是同步 I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的。

select 缺点:
1、每次调用 select,都需要把 fd 集合从用户态拷贝到内核态,这个开销在 fd 很多时会很大。
2、每次调用 select 都需要在内核遍历传递进来的所有 fd,即采用轮询的方法,效率较低,性能随着监视文件描述符数量的增长而下降。
3、select 最大的缺陷就是单个进程所打开的 FD 是有一定限制的,它由 FD_SETSIZE 设置,默认值是 1024。

epoll 是对 select 的改进,可以避免上述的三个缺点,我们先看一下 epoll 和 select 的调用接口上的不同。
select 只提供了一个函数 select
epoll提供了三个函数:
epoll_create:创建一个epoll句柄
epoll_ctl:注册要监听的事件类型
epoll_wait:等待事件的产生

对于1:epoll 的解决方案在 epoll_ctl 函数中。每次注册新的事件到 epoll 句柄中时(在 epoll_ctl 中指定 EPOLL_CTL_ADD ),会把所有的 fd 拷贝进内核,而不是在 epoll_wait 的时候重复拷贝,epoll 保证了每个fd在整个过程中只会拷贝一次。

对于2:epoll 的解决方案不像 select 那样每次都把 current 轮流加入 fd 对应的设备等待队列中,而只在 epoll_ctl 时把 current 挂一遍(这一遍必不可少)并为每个 fd 指定一个回调函数,当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调函数,而这个回调函数会把就绪的 fd 加入一个就绪链表)。epoll_wait 的工作实际上就是在这个就绪链表中查看有没有就绪的fd(利用 schedule_timeout() 实现睡一会,判断一会的效果,和select实现是类似的)。

对于3:epoll 没有这个限制,它所支持的 FD 上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。

epoll 的优点
1、select 只能监听 1024 个文件描述符(poll做了改进,监听的文件描述符个数没有限制)。epoll监视的描述符数量不受限制,它所支持的FD上限是linux最大可以打开文件的数目。
2、select/poll 的性能都会随着监视文件描述符数量的增长而下降,因为他们都是从所有 fd 集合中遍历找出就绪的 fd,也就是时间复杂度是 O(n)
epoll 的 IO 效率不会随着监视 fd 的数量的增长而下降,因为 epoll 不同于 select/poll 轮询的方式,而是通过每个 fd 定义的回调函数来实现的,只有就绪的 fd 才会执行回调函数,也就是只管活跃的连接,和连接总数无关。所以时间复杂度是 O(1)

为什么nginx性能比apache性能好(对比select和epoll)
https://segmentfault.com/q/1010000003885108

nginx性能为啥比Apache性能好(主要讲epoll)
https://blog.csdn.net/resilient/article/details/52584007


进程/线程/并发

进程和线程的区别

进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。每个进程都有自己的独立内存空间,不同进程通过进程间通信来通信。由于进程比较重量,占据独立的内存,所以上下文进程间的切换开销(栈、寄存器、虚拟内存、文件句柄等)比较大,但相对比较稳定安全。

线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易丢失数据。

进程让操作系统的并发性成为可能,而线程让进程的内部并发成为可能。

但是要注意,一个进程虽然包括多个线程,但是这些线程是共同享有进程占有的资源和地址空间的。

进程是操作系统进行资源分配的基本单位,而线程是操作系统进行调度的基本单位。

由于多个线程是共同占有所属进程的资源和地址空间的,那么就会存在一个问题:
如果多个线程要同时访问某个资源,怎么处理?
这就是线程的同步和并发控制问题。

并行(Parallel)与并发(Concurrent)的区别

并发(concurrency)和并行(parallellism)是:
解释一:并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生。
解释二:并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。
解释三:并发是在一台处理器上“同时”处理多个任务,并行是在多台处理器上同时处理多个任务。如hadoop分布式集群
所以并发编程的目标是充分的利用处理器的每一个核,以达到最高的处理性能。

并发的关键是你有处理多个任务的能力,不一定要同时。
并行的关键是你有同时处理多个任务的能力。

并发是不是一个线程,并行是多个线程?
答:并发和并行都可以是很多个线程,就看这些线程能不能同时被(多个)cpu执行,如果可以就说明是并行,而并发是多个线程被(一个)cpu 轮流切换着执行。

多线程是否一定比单线程快?

那么可能有朋友会问,现在很多时候都采用多线程编程,那么是不是多线程的性能一定就由于单线程呢?

不一定,要看具体的任务以及计算机的配置。比如说:
对于单核CPU,如果是CPU密集型任务,如解压文件,多线程的性能反而不如单线程性能,因为解压文件需要一直占用CPU资源,如果采用多线程,线程切换导致的开销反而会让性能下降。

但是对于比如交互类型的任务,肯定是需要使用多线程的、

而对于多核CPU,对于解压文件来说,多线程肯定优于单线程,因为多个线程能够更加充分利用每个核的资源。

虽然多线程能够提升程序性能,但是相对于单线程来说,它的编程要复杂地多,要考虑线程安全问题。因此,在实际编程过程中,要根据实际情况具体选择。

Java并发编程:进程和线程之由来 - 海子
http://www.cnblogs.com/dolphin0520/p/3910667.html


进程间通信(IPC)方式

进程间通信(IPC,Inter-Process Communication)
http://blog.csdn.net/laviolette/article/details/39319955

1 管道(PIPE),一种半双工通信方式,且只能在父子进程间使用
管道:连接一个读进程和一个写进程的共享文件
2 有名管道(FIFO),可用于任意进程间通信
3 信号(Signal),用于通知某个进程某事件已发生
4 信号量(Semaphore),处理进程间同步互斥,低级通信
5 消息队列(Message Queue),
6 共享内存(Shared Memory),最快的IPC方式,可以举我项目中捕获进程和处理进程的例子
7 套接字(Socket),可用于不同主机间的进程通信

本地的进程间通信(IPC)有很多种方式,但可以总结为下面4类:

  • 消息传递(管道、FIFO、消息队列)
  • 同步(互斥量、条件变量、读写锁、文件和写记录锁、信号量)
  • 共享内存(匿名的和具名的)
  • 远程过程调用(RPC 等)

UDS 进程间Socket

Unix Domain Socket 又叫 IPC(inter-process communication 进程间通信) socket,用于实现同一主机上的进程间通信。
socket 原本是为网络通讯设计的,通信双方在同一台机器上时,虽然也可以通过 127.0.0.1 通信,但这样还需要经过网络协议栈绕一圈。
这时,就可以用到 Unix Domain Socket,UDS 不需要经过网络协议栈,不需要打包拆包、计算校验和、维护序号和应答等,只是将应用层数据从一个进程拷贝到另一个进程。

线程间通信方式

1 互斥锁和条件变量
2 共享变量
3 信号量


协程Coroutine

协程就是用户空间下的线程。

协程就是一个不由OS内核抢占调度,而由程序管理在用户态自管理的协作式“线程”,不用线程,就减少了OS的线程数,省去了cpu线程切换的开销。
就是说协程的调度由应用来决定,不需要cpu调度参与,不需要切换上下文,完全由用户程序去控制的调度单位。

go协程

func Add(x, y int) {
    z := x + y
    fmt.Println(z)
}

func main() {
    for i:=0; i<10; i++ {
        go Add(i, i)
    }
}

如上代码,在一个函数调用前加上 go 关键字,这次调用就会在一个新的协程中并发执行。当被调用的函数返回时,这个协程也自动结束。需要注意的是,如果这个函数有返回值,那么这个返回值会被丢弃。

python协程

Python 语言也可以通过 yield/send 的方式实现协程。


死锁的四个必要条件

产生死锁的四个必要条件:
(1) 互斥条件:一个资源每次只能被一个进程使用。
(2) 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
(3) 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
(4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之
一不满足,就不会发生死锁。


守护进程

进程组与会话期

进程组(process group):一个或多个进程的集合,每个进程都有一个进程组ID,这个ID就是进程组长的进程ID。且该进程组ID不会因组长进程的退出而受到影响。

会话期(session):会话期是一个或多个进程组的集合。通常,一个会话开始于用户登录,终止于用户退出,在此期间该用户运行的所有进程都属于这个会话期。每个会话有唯一一个会话首进程(session leader),会话ID为会话首进程ID

控制终端(controlling terminal) :每一个会话可以有一个单独的控制终端,与控制终端连接的会话首进程就是控制进程(controlling process)。 这时候,与当前终端交互的就是前台进程组,其他的都是后台进程组。

setsid()

进程调用 setsid()函数会:

首先请注意:只有当该进程不是一个进程组长时,才会成功创建一个新的会话期。
(1)摆脱原会话的控制。该进程变成新会话期的首进程
(2)摆脱原进程组。成为一个新进程组的组长
(3)摆脱终端控制。如果在调用 setsid() 前,该进程有控制终端,那么与该终端的联系被解除。
如果该进程是一个进程组的组长,此函数返回错误。

Linux如何创建守护进程?

创建守护进程的的一般步骤:

1、fork()创建子进程,父进程exit()退出
这是创建守护进程的第一步。由于守护进程是脱离控制终端的,因此,完成第一步后就会在Shell终端里造成程序已经运行完毕的假象。之后的所有工作都在子进程中完成,而用户在Shell终端里则可以执行其他命令,从而在形式上做到了与控制终端的脱离,在后台工作。

2、在子进程中调用 setsid() 函数创建新的会话
在调用了 fork() 函数后,子进程全盘拷贝了父进程的会话期、进程组、控制终端等,虽然父进程退出了,但会话期、进程组、控制终端等并没有改变,因此,这还不是真正意义上的独立开来,而 setsid() 函数能够使进程完全独立出来。

3、再次 fork() 一个子进程并让父进程退出。
现在,进程已经成为无终端的会话组长,但它可以重新申请打开一个控制终端,可以通过 fork() 一个子进程,该子进程不是会话首进程,该进程将不能重新打开控制终端。退出父进程。

4、在子进程中调用 chdir() 函数,让根目录 ”/” 成为子进程的工作目录
这一步也是必要的步骤。使用fork创建的子进程继承了父进程的当前工作目录。由于在进程运行中,当前目录所在的文件系统(如“/mnt/usb”)是不能卸载的,这对以后的使用会造成诸多的麻烦(比如系统由于某种原因要进入单用户模式)。因此,通常的做法是让”/“作为守护进程的当前工作目录,这样就可以避免上述的问题,当然,如有特殊需要,也可以把当前工作目录换成其他的路径,如/tmp。改变工作目录的常见函数是chdir。

5、在子进程中调用 umask() 函数,设置进程的文件权限掩码为0
文件权限掩码是指屏蔽掉文件权限中的对应位。比如,有个文件权限掩码是050,它就屏蔽了文件组拥有者的可读与可执行权限。由于使用fork函数新建的子进程继承了父进程的文件权限掩码,这就给该子进程使用文件带来了诸多的麻烦。因此,把文件权限掩码设置为0,可以大大增强该守护进程的灵活性。设置文件权限掩码的函数是umask。在这里,通常的使用方法为umask(0)。

6、在子进程中关闭任何不需要的文件描述符
同文件权限码一样,用fork函数新建的子进程会从父进程那里继承一些已经打开了的文件。这些被打开的文件可能永远不会被守护进程读写,但它们一样消耗系统资源,而且可能导致所在的文件系统无法卸下。
在上面的第二步之后,守护进程已经与所属的控制终端失去了联系。因此从终端输入的字符不可能达到守护进程,守护进程中用常规方法(如printf)输出的字符也不可能在终端上显示出来。所以,文件描述符为0、1和2 的3个文件(常说的输入、输出和报错)已经失去了存在的价值,也应被关闭。

7、守护进程退出处理
当用户需要外部停止守护进程运行时,往往会使用 kill 命令停止该守护进程。所以,守护进程中需要编码来实现 kill 发出的signal信号处理,达到进程的正常退出。

【Linux编程】守护进程(daemon)详解与创建
http://blog.csdn.net/woxiaohahaa/article/details/53487602


计算机网络


IP地址

localhost 和 127.0.0.1 的区别

所有以 127 开头的IP地址都是回环地址(Loop back address),是主机用于向自身发送通信的一个特殊地址。所谓的回环地址,通俗的讲,就是我们在主机上发送给127开头的IP地址的数据包会被发送的主机自己接收,根本传不出去,外部设备也无法通过回环地址访问到本机。
正常的数据包会从IP层进入链路层,然后发送到网络上;而给回环地址发送数据包,数据包会直接被发送主机的IP层获取,后面就没有链路层他们啥事了。
而127.0.0.1作为{127}集合中的一员,当然也是个回环地址。只不过127.0.0.1经常被默认配置为localhost的IP地址。
当操作系统初始化本机的TCP/IP协议栈时,设置协议栈本身的IP地址为127.0.0.1(保留地址),并注入路由表。当IP层接收到目的地址为127.0.0.1(准确的说是:网络号为127的IP)的数据包时,不调用网卡驱动进行二次封装,而是立即转发到本机IP层进行处理,由于不涉及底层操作。因此,ping 127.0.0.1 一般作为测试本机TCP/IP协议栈正常与否的判断之一。

localhost 首先是一个域名,也是本机地址,它可以被配置为任意的IP地址(也就是说,可以通过hosts这个文件进行更改的),不过通常情况下都指向:
IPv4 中指向 127.0.0.1
IPv6 中指向 [::1]
之所以我们经常把 localhost 与 127.0.0.1 认为是同一个是因为我们使用的大多数电脑上都将 localhost 指向了 127.0.0.1 这个地址。

localhot 不经网卡传输,这点很重要,它不受网络防火墙和网卡相关的的限制。
127.0.0.1 需要通过网卡传输,依赖网卡,并受到网络防火墙和网卡相关的限制。

一般访问本地服务用 localhost 是最好的,localhost 不会解析成 ip,也不会占用网卡、网络资源。
有时候用 localhost 可以,但用 127.0.0.1 就不可以的情况就是在于此。猜想localhost访问时,系统带的本机当前用户的权限去访问,而用ip的时候,等于本机是通过网络再去访问本机,可能涉及到网络用户的权限。


mysql localhost 和 127.0.0.1 的区别

mysql -h 127.0.0.1 的时候,使用 TCP/IP 连接,连上后 status 查看状态能看到 Connection: 127.0.0.1 via TCP/IP
mysql -h localhost 的时候,不使用 TCP/IP 连接的,而使用Unix socket,连上后 status 查看状态能看到 Connection: Localhost via UNIX socket

实验如下,使用的是 mariadb ,和 mysql 相同,注意看 status 命令返回结果中的 Connection 信息

1、mysql -h localhost 连接

mysql -h localhost -udevelopment -p uds
Enter password:
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MariaDB connection id is 27
Server version: 10.3.7-MariaDB Homebrew

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MariaDB [uds]> status
--------------
mysql  Ver 15.1 Distrib 10.3.7-MariaDB, for osx10.13 (x86_64) using readline 5.1

Connection id:        27
Current database:    uds
Current user:        development@localhost
SSL:            Not in use
Current pager:        less
Using outfile:        ''
Using delimiter:    ;
Server:            MariaDB
Server version:        10.3.7-MariaDB Homebrew
Protocol version:    10
Connection:        Localhost via UNIX socket
Server characterset:    utf8
Db     characterset:    utf8
Client characterset:    utf8
Conn.  characterset:    utf8
UNIX socket:        /tmp/mysql.sock
Uptime:            22 days 27 min 32 sec

Threads: 9  Questions: 1357  Slow queries: 0  Opens: 488  Flush tables: 1  Open tables: 481  Queries per second avg: 0.000

2、mysql -h 127.0.0.1 连接

mysql -h 127.0.0.1 -udevelopment -p uds
Enter password:
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MariaDB connection id is 28
Server version: 10.3.7-MariaDB Homebrew

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MariaDB [uds]> status
--------------
mysql  Ver 15.1 Distrib 10.3.7-MariaDB, for osx10.13 (x86_64) using readline 5.1

Connection id:        28
Current database:    uds
Current user:        development@localhost
SSL:            Not in use
Current pager:        less
Using outfile:        ''
Using delimiter:    ;
Server:            MariaDB
Server version:        10.3.7-MariaDB Homebrew
Protocol version:    10
Connection:        127.0.0.1 via TCP/IP
Server characterset:    utf8
Db     characterset:    utf8
Client characterset:    utf8
Conn.  characterset:    utf8
TCP port:        3306
Uptime:            22 days 31 min 22 sec

Threads: 9  Questions: 1721  Slow queries: 0  Opens: 488  Flush tables: 1  Open tables: 481  Queries per second avg: 0.000
--------------

0.0.0.0 本机所有ip

IPV4中,0.0.0.0地址被用于表示一个无效的,未知的或者不可用的目标。

  • 在服务器中,0.0.0.0 指的是本机上的所有IPV4地址,如果一个主机有两个IP地址,192.168.1.1 和 10.1.2.1,并且该主机上的一个服务监听的地址是0.0.0.0, 那么通过两个ip地址都能够访问该服务。
  • 在路由中,0.0.0.0表示的是默认路由,即当路由表中没有找到完全匹配的路由的时候所对应的路由。

私有IP

私有IP地址范围:
10.0.0.010.255.255.255,即10.0.0.0/8
172.16.0.0
172.31.255.255,即172.16.0.0/12
192.168.0.0~192.168.255.255,即192.168.0.0/16


UDP协议

UDP协议全称是用户数据报协议,在网络中它与TCP协议一样用于处理数据包,是一种无连接的协议。
在OSI模型中,在第四层——传输层,处于IP协议的上一层。UDP有不提供数据包分组、组装和不能对数据包进行排序的缺点,也就是说,当报文发送之后,是无法得知其是否安全完整到达的。

它有以下几个特点:
1、 面向无连接
首先 UDP 是不需要和 TCP一样在发送数据前进行三次握手建立连接的,想发数据就可以开始发送了。并且也只是数据报文的搬运工,不会对数据报文进行任何拆分和拼接操作。

具体来说就是:
在发送端,应用层将数据传递给传输层的 UDP 协议,UDP 只会给数据增加一个 UDP 头标识下是 UDP 协议,然后就传递给网络层了
在接收端,网络层将数据传递给传输层,UDP 只去除 IP 报文头就传递给应用层,不会任何拼接操作

2、 有单播,多播,广播的功能
UDP 不止支持一对一的传输方式,同样支持一对多,多对多,多对一的方式,也就是说 UDP 提供了单播,多播,广播的功能。

3、 UDP是面向报文的
发送方的UDP对应用程序交下来的报文,在添加首部后就向下交付IP层。UDP对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界。因此,应用程序必须选择合适大小的报文

4、 不可靠性
首先不可靠性体现在无连接上,通信都不需要建立连接,想发就发,这样的情况肯定不可靠。

并且收到什么数据就传递什么数据,并且也不会备份数据,发送数据也不会关心对方是否已经正确接收到数据了。

再者网络环境时好时坏,但是 UDP 因为没有拥塞控制,一直会以恒定的速度发送数据。即使网络条件不好,也不会对发送速率进行调整。这样实现的弊端就是在网络条件不好的情况下可能会导致丢包,但是优点也很明显,在某些实时性要求高的场景(比如电话会议)就需要使用 UDP 而不是 TCP。

TCP和UDP对比

特性 UDP TCP
是否连接 无连接 面向连接
是否可靠 不可靠传输,不使用流量控制和拥塞控制 可靠传输,使用流量控制和拥塞控制
连接对象个数 支持一对一,一对多,多对一和多对多交互通信 只能是一对一通信
传输方式 面向报文 面向字节流
首部开销 首部开销小,仅8字节 首部最小20字节,最大60字节
适用场景 适用于实时应用(IP电话、视频会议、直播等) 适用于要求可靠传输的应用,例如文件传输

一文搞懂TCP与UDP的区别
https://blog.fundebug.com/2019/03/22/differences-of-tcp-and-udp/


TCP协议

TCP协议全称是传输控制协议是一种面向连接的、可靠的、基于字节流的传输层通信协议,由 IETF 的 RFC 793 定义。
TCP 是面向连接的、可靠的流协议。流就是指不间断的数据结构,你可以把它想象成排水管中的水流。

三次握手

所谓三次握手(Three-Way Handshake)即建立TCP连接,就是指建立一个TCP连接时,需要客户端和服务端总共发送3个包以确认连接的建立。


跳跃表

(1)第一次握手:
Client 将标志位 SYN 置为 1,随机产生一个值 seq=J,并将该数据包发送给 Server,Client进入 SYN_SENT 状态,等待 Server 确认。
(2)第二次握手:
Server 收到数据包后由标志位 SYN=1 知道 Client 请求建立连接,Server 将标志位 SYN 和 ACK 都置为1,ack=J+1,随机产生一个值seq=K,并将该数据包发送给 Client 以确认连接请求,Server进入 SYN_RCVD 状态。
(3)第三次握手:
Client 收到确认后,检查 ack是否为J+1,ACK 是否为1,如果正确则将标志位 ACK 置为1,ack=K+1,并将该数据包发送给Server,Server 检查ack是否为 K+1,ACK是否为1,如果正确则连接建立成功,Client 和 Server 进入 ESTABLISHED 状态,完成三次握手,随后Client与Server之间可以开始传输数据了。

一个TCP连接必须要经过三次“对话”才能建立起来,其中的过程非常复杂,只简单的 描述下这三次对话的简单过程:主机A向主机B发出连接请求数据包:“我想给你发数据,可以吗?”,这是第一次对话;主机B向主机A发送同意连接和要求同步 (同步就是两台主机一个在发送,一个在接收,协调工作)的数据包:“可以,你什么时候发?”,这是第二次对话;主机A再发出一个数据包确认主机B的要求同 步:“我现在就发,你接着吧!”,这是第三次对话。三次“对话”的目的是使数据包的发送和接收同步,经过三次“对话”之后,主机A才向主机B正式发送数 据。

为什么需要3次握手?

TCP握手是为了协商什么?
是为了协商通信双方数据起点的序列号seq!

TCP 设计中一个基本设定就是,通过TCP 连接发送的每一个包,都有一个sequence number。
而因为每个包都是有序列号的,所以都能被确认收到这些包。
确认机制是累计的,所以一个对sequence number X 的确认,意味着 X 序列号之前(不包括 X) 包都是被确认接收到的。

谢希仁《计算机网络》第四版 中关于为什么两次握手不行的解释:
建立三次握手主要是因为A发送了再一次的确认,那么A为什么会再确认一次呢,主要是为了防止已失效的连接请求报文段又突然传送给B,从而产生了错误。
异常情况下,A发送的请求报文连接段并没有丢失,而是在某个网络节点滞留较长时间,以致延误到请求释放后的某个时间到达B,本来是一个早已失效的报文段,但是B收到了此失效连接请求报文段后,就误以为A又重新发送的连接请求报文段,并发送确认报文段给A,同意建立连接,如果没有三次握手,那么B发送确认后,连接就建立了,而此时A没有发送建立连接的请求报文段,于是不理会B的确认,也不会给B发送数据,而B却一直等待A发送数据,因此B的许多资源就浪费了

其他解释:
A syn, seq_a -> B
B syn, seq_b -> A
如果只有2次握手,但是B无法知道A是否已经接收到自己的同步信号,如果这个同步信号丢失了,A和B就B的初始序列号将无法达成一致。

四次挥手

由于TCP连接时全双工的,因此,每个方向都必须要单独进行关闭,这一原则是当一方完成数据发送任务后,发送一个 FIN 来终止这一方向的连接,收到一个 FIN 只是意味着这一方向上没有数据流动了,即不会再收到数据了,但是在这个 TCP 连接上仍然能够发送数据,直到这一方向也发送了 FIN。首先进行关闭的一方将执行主动关闭,而另一方则执行被动关闭


第一次挥手:
Client(首先没有数据发送的一方)调用 close() 发送 FIN,用来关闭 Client 到 Server 的数据传送,Client 进入 FIN_WAIT_1 状态。
第二次挥手:
Server 收到 FIN 后,发送一个 ACK 给 Client,确认序号为收到序号+1(与 SYN 相同,一个 FIN 占用一个序号),Server 进入 CLOSE_WAIT 状态。
Client 接收到 ACK 后,进入 FIN_WAIT_2 状态
第三次挥手:
Server 也没有数据要发送后,调用 close() 发送一个 FIN,用来关闭 Server 到 Client 的数据传送,Server 进入 LAST_ACK 状态。
第四次挥手:
Client 收到 FIN 后,Client 进入 TIME_WAIT 状态,接着发送一个 ACK 给 Server,确认序号为收到序号+1
Server 收到 ACK 后,进入 CLOSED 状态,完成四次挥手。

为什么连接是三次握手而关闭时需要四次握手?

答:因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,”你发的FIN报文我收到了”。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。

理论经典:TCP协议的3次握手与4次挥手过程详解
http://blog.csdn.net/omnispace/article/details/52701752

tcp三次握手四次挥手(及原因)详解
http://blog.csdn.net/xulu_258/article/details/51146489


TCP状态转换图


TCP状态转换图

TIME_WAIT 状态

什么情况下会进入 TIME_WAIT 状态?

TCP 四次握手结束后,连接双方都不再交换消息,但主动关闭的一方保持这个连接在一段时间内不可用。
主动调用 close() 关闭连接的一方会进入 TIME_WAIT 状态

为什么要保持 TIME_WAIT 状态一段时间?

为了理解 TIME_WAIT 状态的必要性,我们先来假设没有这么一种状态会导致的问题。暂以 A、B 来代指 TCP 连接的两端,A 为主动关闭的一端。
1、四次挥手中,A 发 FIN, B 响应 ACK,B 再发 FIN,A 响应 ACK 实现连接的关闭。而如果 A 响应的 ACK 包丢失,B 会以为 A 没有收到自己的关闭请求,然后会重试向 A 再发 FIN 包。
如果没有 TIME_WAIT 状态,A 不再保存这个连接的信息,收到一个不存在的连接的包,A 会响应 RST 包,导致 B 端异常响应。
此时, TIME_WAIT 是为了保证全双工的 TCP 连接正常终止。

2、我们还知道,TCP 下的 IP 层协议是无法保证包传输的先后顺序的。如果双方挥手之后,一个网络四元组(src/dst ip/port)被回收,而此时网络中还有一个迟到的数据包没有被 B 接收,A 应用程序又立刻使用了同样的四元组再创建了一个新的连接后,这个迟到的数据包才到达 B,那么这个数据包就会让 B 以为是 A 刚发过来的。
此时, TIME_WAIT 的存在是为了保证网络中迷失的数据包正常过期。

由以上两个原因,TIME_WAIT 状态的存在是非常有意义的。

TIME_WAIT 时长是多少?

TIME_WAIT 的时长应该保证关闭连接后这个连接在网络中的所有数据包都过期。
数据包的寿命由 最大分段寿命(MSL, Maximum Segment Lifetime)决定,它表示一个 TCP 分段可以存在于互联网系统中的最大时间,由 TCP 的实现,超出这个寿命的分片都会被丢弃。

TIME_WAIT 状态由主动关闭的 A 来保持,那么我们来考虑对于 A 来说,可能接到上一个连接的数据包的最大时长:A 刚发出的数据包,能保持 MSL 时长的寿命,它到了 B 端后,B 端由于关闭连接了,会响应 RST 包,这个 RST 包最长也会在 MSL 时长后到达 A,那么 A 端只要保持 TIME_WAIT 到达 2MST 就能保证网络中这个连接的包都会消失。

MSL 的时长被 RFC 定义为 2分钟,但在不同的 unix 实现上,这个值不并确定,我们常用的 CentOS 上,它被定义为 30s,我们可以通过 /proc/sys/net/ipv4/tcp_fin_timeout 这个文件查看和修改这个值。
即一般 TIME_WAIT 是 60 秒。

为什么压测时server端会出现大量TIME_WAIT状态?

由于 TIME_WAIT 的存在,每个连接被主动关闭后,这个连接就要保留 2MSL(60s) 时长,一个网络五元组 (tcp/udp, src_ip:src_port, dst_ip:dst_port) 也要被冻结 60s。而我们机器默认可被分配的端口号约有 30000 个(可通过 /proc/sys/net/ipv4/ip_local_port_range 文件查看)。
由于服务器监听的端口会复用,这些 TIME_WAIT 状态的连接并不会对服务器造成太大影响,只是会占用一些系统资源。
但如果并发请求过多,还是会造成无法及时响应。造成
Non HTTP response code: java.net.NoRouteToHostException/Non HTTP response message: Can’t assign requested address (Address not available)
错误

如何解决服务器的大量TIME_OUT状态?

解决方法:
1、修改 ipv4.ip_local_port_range,增大可用端口范围
cat /proc/sys/net/ipv4/ip_local_port_range 查看可使用的端口范围,默认值是 net.ipv4.ip_local_port_range = 32768 61000
改为 net.ipv4.ip_local_port_range = 1024 65535
执行:sudo sysctl -p ,使设置立即生效。

2、修改 /etc/sysctl.conf 编辑文件,加入以下内容:
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_tw_recycle = 1
net.ipv4.tcp_fin_timeout = 30
然后执行 sudo sysctl -p 让参数生效。

net.ipv4.tcp_tw_reuse = 1 表示开启重用。允许将 TIME-WAIT sockets 重新用于新的 TCP 连接,默认为 0,表示关闭;
net.ipv4.tcp_tw_recycle = 1 表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭。
net.ipv4.tcp_fin_timeout 修改系統默认的 TIMEOUT 时间

系统调优你所不知道的TIME_WAIT和CLOSE_WAIT
https://zhuanlan.zhihu.com/p/40013724

Dec 15, 2018 - 谈谈 TCP 的 TIME_WAIT
https://zhenbianshu.github.io/2018/12/talk_about_tcp_timewait.html


RST 标志

正常情况下,tcp 通过四次挥手来关闭连接:
1、A 没有数据要发送后发 FIN 给 B
2、B 返回 ACK,之后还可以继续发送数据
3、B 也没有数据要发送后,发 FIN 给 A
4、A 返回 ACK,之后等待 TIME_WAIT 后关闭连接。

但还有一种紧急关闭连接的方法:
1、一方发送 RST 标志给另一方,表明自己既不发送也不接收了,也就是直接关闭了双向的通信。
2、另一方收到 RST 标志后,抛弃连接。


SO_LINGER 选项

SO_LINGER 选项用来设置延迟关闭的时间,等待套接字发送缓冲区中的数据发送完成。

没有设置该选项时,在调用 close() 后,在发送完 FIN 后会立即进行一些清理工作并返回。如果设置了 SO_LINGER 选项,并且等待时间为正值,则在清理之前会等待一段时间。

以调用close()主动关闭为例,在发送完FIN包后,会进入FIN_WAIT_1状态。如果没有延迟关闭(即设置SO_LINGER选项),在调用tcp_send_fin()发送FIN后会立即调用sock_orphan()将sock结构从进程上下文中分离。分离后,用户层进程不会再接收到套接字的读写事件,也不知道套接字发送缓冲区中的数据是否被对端接收。

如果设置了SO_LINGER选项,并且等待时间为大于0的值,会等待 SO_LINGER 超时时间后从FIN_WAIT_1迁移到FIN_WAIT_2状态。我们知道套接字进入FIN_WAIT_2状态是在发送的FIN包被确认后,而FIN包肯定是在发送缓冲区中的最后一个字节,所以FIN包的确认就表明发送缓冲区中的数据已经全部被接收。当然,如果等待超过SO_LINGER选项设置的时间后,还是没有收到FIN的确认,则继续进行正常的清理工作,Linux下也没有返回错误。

从这里看来,SO_LINGER选项的作用是等待发送缓冲区中的数据发送完成,但是并不保证发送缓冲区中的数据一定被对端接收(对端宕机或线路问题),只是说会等待一段时间让这个过程完成。如果在等待的这段时间里接收到了带数据的包,还是会给对端发送RST包,并且会reset掉套接字,因为此时已经关闭了接收通道。

设置SO_LINGER为0来减少TIME_WAIT状态

SO_LINGER 还有一个作用就是用来减少 TIME_WAIT 套接字的数量。在设置 SO_LINGER 选项时,指定等待时间为0,此时调用主动关闭时不会发送 FIN 来结束连接,而是直接将连接设置为 CLOSE 状态,清除套接字中的发送和接收缓冲区,直接对对端发送 RST 包。

具体的,在调用 close() 之前,设置 SO_LINGER 的超时时间为 0,此时调用 close() 会发送一个 RST 给对端,可以立即结束TCP连接,但这不是正常的 TCP 连接结束,典型的会导致 Connection reset by peer 错误。

慎重使用 SO_LINGER(timeout=0) 选项,使用 RST 代替 FIN 直接强制关闭连接,主动关闭的一方也不会进入 TIME_WAIT 阶段,会减少系统的连接数,提高并发连接能力,但是这种异常关闭连接的方式,TCP 连接关闭的 TIME_WAIT 的作用也就没有了,是个有利有弊的用法,尽量不要使用,而是通过设计应用层协议来避免 TIME_WAIT 连接过多的问题。

When is TCP option SO_LINGER (0) required?
https://stackoverflow.com/questions/3757289/when-is-tcp-option-so-linger-0-required

setsockopt 设置TCP的选项SO_LINGER
https://www.cnblogs.com/kex1n/p/7401042.html


TCP长连接/短连接

短连接最大的优点是方便,特别是脚本语言,由于执行完毕后脚本语言的进程就结束了,基本上都是用短连接。
但短连接最大的缺点是将占用大量的系统资源,例如:本地端口、socket句柄
导致这个问题的原因其实很简单:tcp协议层并没有长短连接的概念,因此不管长连接还是短连接,连接建立->数据传输->连接关闭的流程和处理都是一样的。

正常的TCP客户端连接在关闭后,会进入一个 TIME_WAIT 的状态,持续的时间一般在 14 分钟,对于连接数不高的场景,14 分钟其实并不长,对系统也不会有什么影响,
但如果短时间内(例如1s内)进行大量的短连接,则可能出现这样一种情况:客户端所在的操作系统的socket端口和句柄被用尽,系统无法再发起新的连接!

举例来说:假设每秒建立了 1000 个短连接,假设 TIME_WAIT 的时间是1分钟,则 1 分钟内需要建立 6W 个短连接,
由于 TIME_WAIT 时间是 1 分钟,这些短连接1分钟内都处于TIME_WAIT状态,都不会释放,而Linux默认的本地端口范围配置是:net.ipv4.ip_local_port_range = 32768 61000
不到3W,因此这种情况下新的请求由于没有本地端口就不能建立了。

tcp短连接TIME_WAIT问题解决方法大全(1)——高屋建瓴
https://blog.csdn.net/yunhua_lee/article/details/8146830


Socket通信

Socket简介

Socket 是对 TCP/IP 协议族的一种封装,是应用层与 TCP/IP 协议族通信的中间软件抽象层。从设计模式的角度看来,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。

Socket 还可以认为是一种网络间不同计算机上的进程通信的一种方法,利用三元组(ip地址,协议,端口)就可以唯一标识网络中的进程,网络中的进程通信可以利用这个标志与其它进程进行交互。

Socket 起源于 Unix ,Unix/Linux 基本哲学之一就是“一切皆文件”,都可以用“打开(open) –> 读写(write/read) –> 关闭(close)”模式来进行操作。因此 Socket 也被处理为一种特殊的文件。

Linux Socket编程(不限Linux)
https://www.cnblogs.com/skynet/archive/2010/12/12/1903949.html


网络字节序与主机字节序

主机字节序就是我们平常说的大端和小端模式:不同的CPU有不同的字节序类型,这些字节序是指整数在内存中保存的顺序,这个叫做主机序。

标准的 Big-Endian 和 Little-Endian 的定义如下:
a) Little-Endian 就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
b) Big-Endian 就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。

网络字节序:4个字节的32 bit值以下面的次序传输:首先是0~7bit,其次8~15bit,然后16~23bit,最后是24~31bit。这种传输次序称作大端字节序。由于TCP/IP首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序。字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序,一个字节的数据没有顺序的问题了。

所以:在将一个地址绑定到socket的时候,请先将主机字节序转换成为网络字节序,而不要假定主机字节序跟网络字节序一样使用的是Big-Endian。由于这个问题曾引发过血案!公司项目代码中由于存在这个问题,导致了很多莫名其妙的问题,所以请谨记对主机字节序不要做任何假定,务必将其转化为网络字节序再赋给socket。


socket通信流程

socket() 创建socket

#include <sys/socket.h>
int socket(int domain, int type, int protocol);

socket 函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而socket()用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。

domain 即协议域,又称为协议族(family)。常用的协议族有,AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如 AF_INET 表示32位IPv4地址加16位端口号,AF_UNIX表示绝对路径名作为地址。
type socket类型,SOCK_STREAM表示TCP,SOCK_DGRAM表示UDP
protocol 协议类型,一般设为0,自动选择type对应的协议

当我们调用socket创建一个socket时,返回的socket描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用bind()函数,否则就当调用connect()、listen()时系统会自动随机分配一个端口。

bind() 给socket绑定地址

#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

sockfd socket() 函数返回的 socket 描述符
addr 要绑定给 sockfd 的地址(IP+端口),指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同,比如 ipv4 地址是 sockaddr_in 结构,ipv6 地址是 sockaddr_in6 结构
addrlen 地址长度

其中

struct sockaddr_in {
    sa_family_t    sin_family; /* address family: AF_INET */
    in_port_t      sin_port;   /* 端口号,16位网络字节顺序,要用htons()转换 */
    struct in_addr sin_addr;   /* IP地址 */
};
struct in_addr {
    uint32_t       s_addr;     /* IP地址,32位网络字节顺序,要用htonl()转换 */
};

listen() 服务端监听socket

#include <sys/socket.h>
int listen(int sockfd, int backlog);

sockfd 要监听的socket描述符
backlog 此socket的最大连接个数(队列长度)

connect() 客户端连接

#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

sockfd 客户端的socket描述符
addr 服务器的socket地址(IP+端口)
addrlen 地址长度

accept() 服务器接受请求

#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

sockfd 服务器的socket描述符
addr addr在函数调用后被填入客户端的地址
addrlen 客户端地址长度
如果 accpet() 成功,那么其返回值是由内核自动生成的一个全新的描述字,代表与返回客户的TCP连接。
accept() 为阻塞函数

TCP 服务器端依次调用 socket(), bind(), listen() 之后,就会监听指定的 socket 地址了。TCP 客户端依次调用 socket(), connect() 之后就向 TCP 服务器发送了一个连接请求。TCP 服务器监听到这个请求之后,就会调用 accept() 函数取接收请求,这样连接就建立好了。之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作。

注意:
accept() 的第一个参数为服务器的 socket 描述字,是服务器开始调用 socket() 函数生成的,称为监听socket描述字;而 accept() 函数返回的是已连接的 socket 描述字
一个服务器通常通常仅仅只创建一个监听 socket 描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接 socket 描述字,当服务器完成了对某个客户的服务,相应的已连接 socket 描述字就被关闭。

read()/write() I/O操作

网络I/O操作有下面几组可用的方法:
read()/write()
recv()/send()
readv()/writev()
recvmsg()/sendmsg()
recvfrom()/sendto()

#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);

#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);

ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

用的比较多的是 read()/write()
read() 函数是负责从 fd 中读取内容。当读成功时,read返回实际所读的字节数,如果返回的值是0表示已经读到文件的结束了,小于0表示出现了错误。如果错误为EINTR说明读是由中断引起的,如果是ECONNREST表示网络连接出了问题。
write() 函数将 buf 中的 count 字节内容写入文件描述符fd。成功时返回写的字节数。失败时返回-1,并设置errno变量。

在网络程序中,当我们向套接字文件描述符写时有俩种可能。
1) write的返回值大于0,表示写了部分或者是全部的数据。
2) 返回的值小于0,此时出现了错误。
我们要根据错误类型来处理。如果错误为 EINTR 表示在写的时候出现了中断错误。如果为EPIPE表示网络连接出现了问题(对方已经关闭了连接)。

close() 关闭socket

#include <unistd.h>
int close(int fd);

关闭本进程的 socket 连接。
成功则返回0,错误返回-1,错误码errno:EBADF表示fd不是一个有效描述符;EINTR表示close函数被信号中断;EIO表示一个IO错误。

close 只是减少描述符 sockfd 的参考数,并不直接关闭连接,只有当描述符的参考数为 0 时才关闭连接。
也就是说

也可以选择单向关闭连接

#include<sys/socket.h>
int shutdown(int sockfd, int how);

how:
SHUT_RD 或 0 禁止接收 关闭sockfd上的读功能,此选项将不允许sockfd进行读操作。
SHUT_WR 或 1 禁止发送 关闭sockfd的写功能,此选项将不允许sockfd进行写操作。
SHUT_RDWR 或 2 禁止发送和接收 关闭sockfd的读写功能。
成功则返回0,错误返回-1,错误码errno:EBADF表示sockfd不是一个有效描述符;ENOTCONN表示sockfd未连接;ENOTSOCK表示sockfd是一个文件描述符而不是socket描述符。
该函数允许你只停止在某个方向上的数据传输,而一个方向上的数据传输继续进行,比如可以关闭某socket的写操作而允许继续在该socket上接受数据直至读入所有数据。

shutdown()和close()的区别

1、close 关闭本进程的socket id,但链接还是开着的,用这个socket id的其它进程还能用这个链接,能读或写这个socket id

2、shutdown 则破坏了socket 链接,读的时候可能侦探到EOF结束符,写的时候可能会收到一个SIGPIPE信号,这个信号可能直到socket buffer被填充了才收到。shutdown 可直接关闭描述符,不考虑描述符的参考数,可选择中止一个方向的连接。

3、如果有多个进程共享一个套接字,close每被调用一次,计数减1,直到计数为0时,也就是所用进程都调用了close,套接字将被释放。
在多进程中如果一个进程中shutdown(sfd, SHUT_RDWR)后其它的进程将无法进行通信,但一个进程close(sfd)将不会影响到其它进程。

更多关于close和shutdown的说明
1、只要 TCP 栈的读缓冲里还有未读取(read)数据,则调用close时会直接向对端发送RST。

2、shutdown与socket描述符没有关系,即使调用shutdown(fd, SHUT_RDWR)也不会关闭fd,最终还需close(fd)(理解不了,是不是说错了?)。

3、可以认为shutdown(fd, SHUT_RD)是空操作,因为shutdown后还可以继续从该socket读取数据,这点也许还需要进一步证实。在已发送FIN包后write该socket描述符会引发EPIPE/SIGPIPE。

4、当有多个socket描述符指向同一socket对象时,调用close时首先会递减该对象的引用计数,计数为0时才会发送FIN包结束TCP连接。shutdown不同,只要以SHUT_WR/SHUT_RDWR方式调用即发送FIN包。

5、SO_LINGER与close,当SO_LINGER选项开启但超时值为0时,调用close直接发送RST(这样可以避免进入TIME_WAIT状态,但破坏了TCP协议的正常工作方式),SO_LINGER对shutdown无影响。

6、TCP连接上出现RST与随后可能的TIME_WAIT状态没有直接关系,主动发FIN包方必然会进入TIME_WAIT状态,除非不发送FIN而直接以发送RST结束连接。


Socket通信示例代码

服务端:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>

#define MAXLINE 4096

int main(int argc, char** argv)
{
    int    listenfd, connfd;
    struct sockaddr_in     servaddr;
    char    buff[4096];
    int     n;

    if( (listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1 ){
        printf("create socket error: %s(errno: %d)\n",strerror(errno),errno);
        exit(0);
    }

    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(6666);

    if( bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1){
        printf("bind socket error: %s(errno: %d)\n",strerror(errno),errno);
        exit(0);
    }

    if( listen(listenfd, 10) == -1){
        printf("listen socket error: %s(errno: %d)\n",strerror(errno),errno);
        exit(0);
    }

    printf("======waiting for client's request======\n");
    while(1){
        if( (connfd = accept(listenfd, (struct sockaddr*)NULL, NULL)) == -1){
            printf("accept socket error: %s(errno: %d)",strerror(errno),errno);
            continue;
        }
        n = recv(connfd, buff, MAXLINE, 0);
        buff[n] = '\0';
        printf("recv msg from client: %s\n", buff);
        close(connfd);
    }

    close(listenfd);
}

客户端:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>

#define MAXLINE 4096

int main(int argc, char** argv)
{
    int    sockfd, n;
    char    recvline[4096], sendline[4096];
    struct sockaddr_in    servaddr;

    if( argc != 2){
        printf("usage: ./client <ipaddress>\n");
        exit(0);
    }

    if( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0){
        printf("create socket error: %s(errno: %d)\n", strerror(errno),errno);
        exit(0);
    }

    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(6666);
    if( inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0){
        printf("inet_pton error for %s\n",argv[1]);
        exit(0);
    }

    if( connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0){
        printf("connect error: %s(errno: %d)\n",strerror(errno),errno);
        exit(0);
    }

    printf("send msg to server: \n");
    fgets(sendline, 4096, stdin);
    if( send(sockfd, sendline, strlen(sendline), 0) < 0)
    {
        printf("send msg error: %s(errno: %d)\n", strerror(errno), errno);
        exit(0);
    }

    close(sockfd);
    exit(0);
}

ping命令原理

ping命令,ping在应用层,但是直接使用网络层的ICMP协议,跳过了传输层。
只能用ping命令测试ip,不能ping端口。


网络安全

加密算法

对称加密(DES/AES)

对称加密指的就是加密和解密使用同一个秘钥,所以叫做对称加密。对称加密只有一个秘钥,作为私钥。
常见的对称加密算法:DES(Data Encryption Standard),AES,3DES等等。

非对称加密(RSA)

非对称加密指的是:加密和解密使用不同的秘钥,一把作为公开的公钥,另一把作为私钥。公钥加密的信息,只有私钥才能解密。私钥加密的信息,只有公钥才能解密。

公钥加密,私钥解密,或私钥加密,公钥解密

加密密码和解密密码是相对的,如果用加密密码加密那么只有解密密码才能解密,如果用解密密码加密则只有加密密码能解密,所以它们被称为密码对,其中的一个可以在网络上发送、公布,叫做公钥,而另一个则只有密钥对的所有人才持有,叫做私钥,私钥不以任何形式传播。
非对称加密算法需要两个密钥:公开密钥(public key)和私有密钥(private key)。公开密钥与私有密钥是一对,如果用公开密钥对数据进行加密,只有用对应的私有密钥才能解密;如果用私有密钥对数据进行加密,那么只有用对应的公开密钥才能解密。因为加密和解密使用的是两个不同的密钥,所以这种算法叫作非对称加密算法。

非对称加密算法实现机密信息交换的基本过程是:
甲方生成一对密钥并将其中的一把作为公用密钥向其它方公开;得到该公用密钥的乙方使用该密钥对机密信息进行加密后再发送给甲方;甲方再用自己保存的另一把专用密钥对加密后的信息进行解密。
另一方面,甲方可以使用自己的私密钥对机密信息进行加密后再发送给乙方;乙方再用甲方的公钥对加密后的信息进行解密。

使用过程:
乙方生成两把密钥(公钥和私钥)
甲方获取乙方的公钥,然后用它对信息加密。
乙方得到加密后的信息,用私钥解密,乙方也可用私钥加密字符串
甲方获取乙方私钥加密数据,用公钥解密

非对称加密的好处在于,现在A可以保留private key,通过网络传递public key。这样,就算public key被C拦截了,因为没有private key,C还是没有办法完成信息的破解。既然不怕C知道public key,那现在A和B不用再见面商量密钥,直接通过网络传递public key就行。

常见的非对称加密算法:RSA,ECC

对称加密和非对称加密对比

1、对称加密算法加密速度快、加密效率高
非对称加密算法速度慢

2、对称加密的密钥管理不安全,尤其是涉及到网络通信时。
在数据传送前,发送方和接收方必须商定好秘钥,然后双方都必须要保存好秘钥,如果一方的秘钥被泄露,那么加密信息也就不安全了。

对称加密和非对称加密结合使用

在实际的网络环境中,会将两者混合使用:
例如针对C/S模型,
1、服务端计算出一对秘钥pub/pri。将私钥保密,将公钥公开。
2、客户端请求服务端时,拿到服务端的公钥pub。
3、客户端通过AES计算出一个对称加密的秘钥X。 然后使用pub将X进行加密。
4、客户端将加密后的密文发送给服务端。服务端通过pri解密获得X。
5、然后两边的通讯内容就通过对称密钥X以对称加密算法来加解密。

非对称加密算法比对称加密算法要复杂的多,处理起来也要慢得多。如果所有的网络数据都用非对称加密算法来加密,那效率会很低。所以在实际中,非对称加密只会用来传递一条信息,那就是用于对称加密的密钥。当用于对称加密的密钥确定了,A和B还是通过对称加密算法进行网络通信。这样,既保证了网络通信的安全性,又不影响效率,A和B也不用见面商量密钥了。

对称加密与非对称加密,以及RSA的原理
https://blog.csdn.net/u014079662/article/details/61169607


信息摘要(MD5/SHA)

信息摘要是一个唯一对应一个消息或文本的固定长度的值,它由一个单向Hash加密函数对消息进行作用而产生。如果消息在途中改变了,则接收者通过对收到消息的新产生的摘要与原摘要比较,就可知道消息是否被改变了。因此消息摘要保证了消息的完整性。消息摘要采用单向Hash 函数将需加密的明文”摘要”成一串密文,这一串密文亦称为数字指纹(Finger Print)。它有固定的长度,且不同的明文摘要成密文,其结果总是不同的,而同样的明文其摘要必定一致。这样这串摘要便可成为验证明文是否是”真身”的”指纹”了。

消息摘要,其实就是将需要摘要的数据作为参数,经过哈希函数(Hash)的计算,得到的散列值。

消息摘要具有以下特点:
(1)唯一性:数据只要有一点改变,那么再通过消息摘要算法得到的摘要也会发生变化。虽然理论上有可能会发生碰撞,但是概率极其低。
(2)不可逆:消息摘要算法的密文无法被解密。
(3)不需要密钥,可使用于分布式网络。
(4)无论输入的明文有多长,计算出来的消息摘要的长度总是固定的。

常用算法
消息摘要算法包括MD(Message Digest,消息摘要算法)、SHA(Secure Hash Algorithm,安全散列算法)、MAC(Message AuthenticationCode,消息认证码算法)共3大系列,常用于验证数据的完整性,是数字签名算法的核心算法。

MD5和SHA1分别是MD、SHA算法系列中最有代表性的算法。

如今,MD5已被发现有许多漏洞,从而不再安全。SHA算法比MD算法的摘要长度更长,也更加安全。

Java生成MD5和SHA信息摘要

JDK中使用MD5和SHA这两种消息摘要的方式基本一致,步骤如下:
(1)初始化MessageDigest对象
(2)更新要计算的内容
(3)生成摘要

import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

import org.apache.commons.codec.binary.Base64;

public class MsgDigestDemo{
    public static void main(String args[]) throws NoSuchAlgorithmException, UnsupportedEncodingException {
        String msg = "Hello World!";

        MessageDigest md5Digest = MessageDigest.getInstance("MD5");
        // 更新要计算的内容
        md5Digest.update(msg.getBytes());
        // 完成哈希计算,得到摘要
        byte[] md5Encoded = md5Digest.digest();

        MessageDigest shaDigest = MessageDigest.getInstance("SHA");
        // 更新要计算的内容
        shaDigest.update(msg.getBytes());
        // 完成哈希计算,得到摘要
        byte[] shaEncoded = shaDigest.digest();

        System.out.println("原文: " + msg);
        System.out.println("MD5摘要: " + Base64.encodeBase64URLSafeString(md5Encoded));
        System.out.println("SHA摘要: " + Base64.encodeBase64URLSafeString(shaEncoded));
    }
}

[Java 安全]消息摘要与数字签名
https://www.cnblogs.com/jingmoxukong/p/5700906.html


数字签名(摘要加密后就是签名)

数字签名算法可以看做是一种带有密钥的消息摘要算法,并且这种密钥包含了公钥和私钥。也就是说,数字签名算法是非对称加密算法和消息摘要算法的结合体。

数字签名是由信本身的内容经过hash算法计算得到digest摘要,然后用A的私钥加密而来的。

签名,使用私钥对需要传输的文本的摘要进行加密,得到的密文即被称为该次传输过程的签名

特点
数字签名算法要求能够验证数据完整性、认证数据来源,并起到抗否认的作用。

原理
数字签名算法包含签名和验证两项操作,遵循私钥签名,公钥验证的方式。

签名时要使用私钥和待签名数据,验证时则需要公钥、签名值和待签名数据,其核心算法主要是消息摘要算法。


跳跃表

常用算法
RSA、DSA、ECDSA

Java实现数字签名和验证

签名
用私钥为消息计算签名
验证
用公钥验证摘要

import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;

import org.apache.commons.codec.binary.Base64;

public class DsaCoder{
    public static final String KEY_ALGORITHM = "DSA";

    public enum DsaTypeEn {
        MD5withDSA, SHA1withDSA
    }

    /**
    * DSA密钥长度默认1024位。 密钥长度必须是64的整数倍,范围在512~1024之间
    */
    private static final int KEY_SIZE = 1024;

    private KeyPair keyPair;

    public DsaCoder() throws Exception {
        keyPair = initKey();
    }

    public byte[] signature(byte[] data, byte[] privateKey) throws Exception {
        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKey);
        KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
        PrivateKey key =keyFactory.generatePrivate(keySpec);

        Signature signature = Signature.getInstance(DsaTypeEn.SHA1withDSA.name());
        signature.initSign(key);
        signature.update(data);
        return signature.sign();
    }

    public boolean verify(byte[] data, byte[] publicKey, byte[] sign) throws Exception {
        X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicKey);
        KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
        PublicKey key =keyFactory.generatePublic(keySpec);

        Signature signature = Signature.getInstance(DsaTypeEn.SHA1withDSA.name());
        signature.initVerify(key);
        signature.update(data);
        return signature.verify(sign);
    }

    private KeyPair initKey() throws Exception {
        // 初始化密钥对生成器
        KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance(KEY_ALGORITHM);
        // 实例化密钥对生成器
        keyPairGen.initialize(KEY_SIZE);
        // 实例化密钥对
        return keyPairGen.genKeyPair();
    }

    public byte[] getPublicKey() {
        return keyPair.getPublic().getEncoded();
    }

    public byte[] getPrivateKey() {
        return keyPair.getPrivate().getEncoded();
    }

    public static void main(String[] args) throws Exception {
        String msg = "Hello World";
        DsaCoder dsa = new DsaCoder();
        byte[] sign = dsa.signature(msg.getBytes(), dsa.getPrivateKey());
        boolean flag = dsa.verify(msg.getBytes(), dsa.getPublicKey(), sign);
        String result = flag ? "数字签名匹配" : "数字签名不匹配";
        System.out.println("数字签名:" + Base64.encodeBase64URLSafeString(sign));
        System.out.println("验证结果:" + result);
    }
}

[Java 安全]消息摘要与数字签名
https://www.cnblogs.com/jingmoxukong/p/5700906.html


数字证书(经过CA认证的加密的公钥)

数字签名是由信本身的内容经过hash算法计算得到digest摘要,然后用A的私钥加密而来的。
数字证书是A向数字证书中心(CA)申请的,是由A的个人信息,公钥等经过CA的私钥加密而来的。

什么是数字证书?
经CA认证加密后的公钥,即是证书,又称为CA证书,证书中包含了很多信息,最重要的是申请者的公钥。

假设A给B写一份信。那么这封将包含如下三部分内容:
1.信本身的内容(直接可以看到,未加密)
2.A的数字签名
3.A的数字证书

然后B收到了这封信。B会想这封确定是A发过来的吗?这封信在发送过程中有被篡改,还是完整的吗?只有当B确认清楚,才能判断出信的内容是否可靠。

然后B先用CA提供的公钥解开数字证书,根据得到的内容如A的个人信息,确定是A发过来的,然后拿到了A的公钥。

接着,用A的公钥解开A的数字签名就能得到,信本身内容的摘要。然后将信的第一部分即信的本身内容经hash计算得到又一个摘要,将两个摘要比较,如果相同说明信的内容没有被篡改。

最后,便能确定信的可靠性了。

公钥,私钥,数字签名,数字证书个人总结
https://blog.csdn.net/sum_rain/article/details/36897095

为什么需要数字证书?

首先要知道数字签名的验证过程:
签名验证,数据接收端,拿到传输文本,但是需要确认该文本是否就是发送发出的内容,中途是否曾经被篡改。因此拿自己持有的发送方公钥对签名进行解密,得到了文本的摘要,然后使用与发送方同样的HASH算法计算摘要值,再与解密得到的摘要做对比,发现二 者完全一致,则说明文本没有被篡改过

1、在签名验证的过程中,有一点很关键,收到数据的一方,需要自己保管好公钥,但是要知道每一个发送方都有一个公钥,那么接收数据的人需要保存非常多的公钥,这根本 就管理不过来。
2、并且本地保存的公钥有可能被篡改替换,无从发现。

怎么解决这一问题了?
由一个统一的证书管理机构来管理所有需要发送数据方的公钥,对公钥进 行认证和加密。这个机构也就是我们常说的CA。认证加密后的公钥,即是证书,又称为CA证书,证书中包含了很多信息,最重要的是申请者的公钥。

CA 机构在给公钥加密时,用的是一个统一的密钥对,在加密公钥时,用的是其中的私钥。这样,申请者拿到证书后,在发送数据时,用自己的私钥生成签名,将签名、 证书和发送内容一起发给对方,对方拿到了证书后,需要对证书解密以获取到证书中的公钥,解密需要用到CA机构的”统一密钥对“中的公钥,这个公钥也就是我 们常说的CA根证书,通常需要我们到证书颁发机构去下载并安装到相应的收取数据的客户端,如浏览器上面。这个公钥只需要安装一次。有了这个公钥之后,就可 以解密证书,拿到发送方的公钥,然后解密发送方发过来的签名,获取摘要,重新计算摘要,作对比,以验证数据内容的完整性。

公钥私钥加密解密数字证书数字签名详解
https://www.cnblogs.com/kex1n/p/5582530.html


HTTPS/SSL/TLS

HTTPS 其实就是将 HTTP 的数据包再通过 SSL/TLS 加密后再传输。

SSL(Secure Sockets Layer) 安全套接层和 TLS(Transport Layer Security) 传输层安全协议其实是一套东西。
网景公司在1994年提出HTTPS协议时,使用的是SSL进行加密。后来IETF(Internet Engineering Task Force)互联网工程任务组将SSL进一步标准化,于1999年公布第一版TLS协议文件TLS 1.0。目前最新版的TLS协议是TLS 1.3,于2018年公布。

HTTPS通信过程


HTTPS通信过程

1、用户在浏览器发起 HTTPS 请求,默认使用服务端的 443 端口进行连接;HTTPS 需要使用一套 CA 数字证书,证书内会附带一个公钥 Pub,而与之对应的私钥 Private 保留在服务端不公开;服务端收到请求,返回配置好的包含公钥Pub的证书给客户端;
2、客户端收到证书,校验合法性,主要包括是否在有效期内、证书的域名与请求的域名是否匹配,上一级证书是否有效(递归判断,直到判断到系统内置或浏览器配置好的根证书),如果不通过,则显示 HTTPS 警告信息,如果通过则继续;
3、客户端生成一个用于对称加密的随机 Key,并用证书内的公钥 Pub 进行加密,发送给服务端;
4、服务端收到随机 Key 的密文,使用与公钥 Pub 配对的私钥 Private 进行解密,得到客户端真正想发送的随机 Key;服务端使用客户端发送过来的随机 Key 对要传输的 HTTP 数据进行对称加密,将密文返回客户端;
5、客户端使用随机 Key 对称解密密文,得到 HTTP 数据明文;
后续 HTTPS 请求使用之前交换好的随机 Key 进行对称加解密。

复述一遍简化流程,方便后续讨论
服务端有非对称加密的公钥 A1,私钥 A2;
1、客户端发起请求,服务端将公钥 A1 返回给客户端;
2、客户端随机生成一个对称加密的密钥 K,用公钥 A1 加密后发送给服务端;
3、服务端收到密文后用自己的私钥 A2 解密,得到对称密钥 K,此时完成了安全的对称密钥交换,解决了对称加密时密钥传输被人窃取的问题;
之后双方通信都使用密钥K进行对称加解密。

通过非对称加密协商一个对称加密的密钥,然后数据传输通过对称加密进行
由于非对称加解密耗时要远大于对称加解密,所以它一般用于密钥交换,双方通过公钥算法协商出一份密钥,然后通过对称加密来通信。

为什么需要CA

假如没有 CA 做公钥认证,会存在中间人攻击的情况:
非对称加密的算法都是公开的,所有人都可以自己生成一对公钥私钥。
1、当服务端向客户端返回公钥 A1 的时候,中间人将其替换成自己的公钥 B1 传送给浏览器。
2、而浏览器此时一无所知,傻乎乎地使用公钥 B1 加密了密钥 K 发送出去,又被中间人截获,中间人利用自己的私钥 B2 解密,得到密钥 K,再使用服务端的公钥 A1 加密传送给服务端,完成了通信链路,而服务端和客户端毫无感知。

出现这一问题的核心原因是客户端无法确认收到的公钥是不是真的是服务端发来的。为了解决这个问题,互联网引入了一个公信机构,这就是CA。

服务端在使用 HTTPS 前,去经过认证的 CA 机构申请颁发一份数字证书,数字证书里包含有证书持有者、证书有效期、公钥等信息,服务端将证书发送给客户端,客户端校验证书身份和要访问的网站身份确实一致后再进行后续的加密操作。

CA通过数字签名防止公钥被篡改

只有 CA 颁发数字证书还是有问题,因为数字证书中的公钥还是可能被拦截后篡改,要解决这个问题需要 CA 对公钥做数字签名。

1、CA 机构拥有自己的一对公钥和私钥
2、CA 机构在颁发证书时对证书明文信息进行哈希
3、将哈希值用私钥进行加签,得到数字签名
明文数据和数字签名组成证书,传递给客户端。

1、客户端得到证书,分解成明文部分 Text 和数字签名 Sig1
2、用 CA 机构的公钥进行解签,得到 Sig2(由于CA机构是一种公信身份,因此在系统或浏览器中会内置 CA 机构的证书和公钥信息)
3、用证书里声明的哈希算法对明文 Text 部分进行哈希得到 H
4、当自己计算得到的哈希值 H 与解签后的 Sig2 相等,表示证书可信,没有被篡改

签名是由 CA 机构的私钥生成的,中间人篡改信息后无法拿到 CA 机构的私钥,保证了证书可信。


Base64编码

为什么要使用Base64(任意数据转换为asc字符)

Base64编码的作用:由于某些系统中只能使用ASCII字符。Base64就是用来将非ASCII字符的数据转换成ASCII字符的一种方法。

Base64是一种很常见的编码规范,其作用是将二进制序列转换为人类可读的ASCII字符序列,常用在需用通过文本协议(比如HTTP和SMTP)来传输二进制数据的情况下。Base64并不是加密解密算法,尽管我们有时也听到使用Base64来加密解密的说法,但这里所说的加密与解密实际是指编码(encode)和解码(decode)的过程,其变换是非常简单的,仅仅能够避免信息被直接识别。

[Java 安全]加密算法
http://www.cnblogs.com/jingmoxukong/p/5688306.html

Base64编码原理

那么Base64到底是怎样编码的呢?

简单来说,任何一个数据无非可以看作一个比特流,如01000100010011101100111010111100011001010……那么我们取6个比特为一组,计算它的ascii值,得到一个字符,这个字符肯定是可见字符,好,把它对应的字符写出来,再取6个比特,计算…,如此下去,直到最后,就完成了编码。

1、标准base64只有64个字符(英文大小写、数字和+、/)以及用作后缀等号;
2、base64是把3个字节变成4个可打印字符,所以base64编码后的字符串一定能被4整除(不算用作后缀的等号);
3、等号一定用作后缀,且数目一定是0个、1个或2个。这是因为如果原文长度不能被3整除,base64要在后面添加\0凑齐3n位。为了正确还原,添加了几个\0就加上几个等号。显然添加等号的数目只能是0、1或2;
4、严格来说base64不能算是一种加密,只能说是编码转换。使用base64的初衷。是为了方便把含有不可见字符串的信息用可见字符串表示出来,以便复制粘贴;Base64是一种可逆的编码方式。

为什么要使用base64编码,有哪些情景需求? - 郭无心的回答 - 知乎
https://www.zhihu.com/question/36306744/answer/71626823

Base64使用场景

它的使用场景有很多,比如:
1、将图片等资源文件以Base64编码形式直接放于代码中,使用的时候反Base64后转换成Image对象使用;
2、有些文本协议不支持不可见字符的传递,只能转换成可见字符来传递信息。
3、有时在一些特殊的场合,大多数消息是纯文本的,偶尔需要用这条纯文本通道传一张图片之类的情况发生的时候,就会用到Base64,比如多功能Internet 邮件扩充服务(MIME)就是用Base64对邮件的附件进行编码的。
4、base64 最早就是用来邮件传输协议中的,原因是邮件传输协议只支持 ascii 字符传递,因此如果要传输二进制文件,如:图片、视频是无法实现的。因此 base64 就可以用来将二进制文件内容编码为只包含 ascii 字符的内容,这样就可以传输了

Java中使用Base64编码

1、在JDK1.6之前,JDK核心类一直没有Base64的实现类,有人建议用JDK里面的【sun.misc.BASE64Encoder】和 【sun.misc.BASE64Decoder】,使用它们的优点就是不需要依赖第三方类库,缺点就是可能在未来版本会被删除(用maven编译会发出警告),而且性能不佳。

2、JDK1.6中添加了另一个Base64的实现【javax.xml.bind.DatatypeConverter】两个静态方法【parseBase64Binary】和【printBase64Binary】,隐藏在javax.xml.bind包下面,不被很多开发者知道。

3、在Java8在java.util包下面实现了BASE64编解码API,而且性能不俗,API也简单易懂
该类提供了一套静态方法获取下面三种BASE64编解码器:
Basic编码:是标准的BASE64编码,用于处理常规的需求
URL编码:使用下划线替换URL里面的反斜线“/”
MIME编码:使用基本的字母数字产生BASE64输出,而且对MIME格式友好:每一行输出不超过76个字符,而且每行以“\r\n”符结束。

4、第三方实现Base64的API
常用的是Apache Commons Codec library里面的【org.apache.commons.codec.binary.Base64】

Base64编码
http://www.cnblogs.com/baiqiantao/p/d0bbba14f1b942af618226893ee83f1b.html


上一篇 面试准备13-算法

下一篇 面试准备11-设计模式

阅读
评论
30,942
阅读预计119分钟
创建日期 2018-05-24
修改日期 2020-09-13
类别
目录
  1. Shell和Linux命令
    1. 常用Linux命令
      1. lsof列出当前系统打开文件
      2. top结果中load过高可能原因?
      3. kill -2,-9,-15
    2. Shell脚本
      1. 打印nginx的进程号
      2. 统计单词出现的次数
      3. 统计nginx日志中出现次数最多的10个ip
  2. 组成原理
    1. 原码反码补码
      1. 机器数和真值
      2. 原码
      3. 反码
      4. 补码
  3. 操作系统
    1. 内存
      1. 虚拟内存
      2. 驻留内存
      3. 共享内存
      4. top命令中的VIRT/RES/SHR
      5. 用户空间与内核空间
      6. 内存分页
        1. 为什么要分页?(内存碎片/虚拟内存)
        2. 一般一个内存页大小为4KB
      7. c/c++中delete/free如何知道释放多少内存?
    2. IO
      1. 缓冲IO(传统IO)
      2. 零拷贝技术
        1. mmap() 跳过用户空间
        2. sendfile() 文件到socket零拷贝
        3. splice() 任意文件描述符间零拷贝
        4. 写时复制(Copy On Write,COW)
      3. 5种IO模型
        1. 阻塞IO(blocking IO)
        2. 非阻塞IO(nonblocking IO)
        3. 多路复用IO(IO multiplexing)
        4. 信号驱动IO(signal driven IO)
        5. 异步IO(asynchronous IO)
      4. 3种多路复用IO(NIO)模型select/poll/epoll
        1. select()
        2. poll()
        3. epoll()
          1. epoll_create()
          2. epoll_ctl()
          3. epoll_wait()
          4. 水平触发(Level Trigger)和边缘触发(Edge Trigger)
        4. epoll相对于select/poll有哪些优化
    3. 进程/线程/并发
      1. 进程和线程的区别
      2. 并行(Parallel)与并发(Concurrent)的区别
      3. 多线程是否一定比单线程快?
      4. 进程间通信(IPC)方式
        1. UDS 进程间Socket
      5. 线程间通信方式
      6. 协程Coroutine
        1. go协程
        2. python协程
      7. 死锁的四个必要条件
      8. 守护进程
        1. 进程组与会话期
        2. setsid()
        3. Linux如何创建守护进程?
  4. 计算机网络
    1. IP地址
      1. localhost 和 127.0.0.1 的区别
      2. mysql localhost 和 127.0.0.1 的区别
      3. 0.0.0.0 本机所有ip
      4. 私有IP
    2. UDP协议
      1. TCP和UDP对比
    3. TCP协议
      1. 三次握手
        1. 为什么需要3次握手?
      2. 四次挥手
        1. 为什么连接是三次握手而关闭时需要四次握手?
      3. TCP状态转换图
      4. TIME_WAIT 状态
        1. 什么情况下会进入 TIME_WAIT 状态?
        2. 为什么要保持 TIME_WAIT 状态一段时间?
        3. TIME_WAIT 时长是多少?
        4. 为什么压测时server端会出现大量TIME_WAIT状态?
        5. 如何解决服务器的大量TIME_OUT状态?
      5. RST 标志
      6. SO_LINGER 选项
        1. 设置SO_LINGER为0来减少TIME_WAIT状态
      7. TCP长连接/短连接
    4. Socket通信
      1. Socket简介
      2. 网络字节序与主机字节序
      3. socket通信流程
        1. socket() 创建socket
        2. bind() 给socket绑定地址
        3. listen() 服务端监听socket
        4. connect() 客户端连接
        5. accept() 服务器接受请求
        6. read()/write() I/O操作
        7. close() 关闭socket
          1. shutdown()和close()的区别
      4. Socket通信示例代码
    5. ping命令原理
    6. 网络安全
      1. 加密算法
        1. 对称加密(DES/AES)
        2. 非对称加密(RSA)
        3. 对称加密和非对称加密对比
        4. 对称加密和非对称加密结合使用
      2. 信息摘要(MD5/SHA)
        1. Java生成MD5和SHA信息摘要
      3. 数字签名(摘要加密后就是签名)
        1. Java实现数字签名和验证
      4. 数字证书(经过CA认证的加密的公钥)
        1. 为什么需要数字证书?
      5. HTTPS/SSL/TLS
        1. HTTPS通信过程
        2. 为什么需要CA
        3. CA通过数字签名防止公钥被篡改
      6. Base64编码
        1. 为什么要使用Base64(任意数据转换为asc字符)
        2. Base64编码原理
        3. Base64使用场景
        4. Java中使用Base64编码

页面信息

location:
protocol:
host:
hostname:
origin:
pathname:
href:
document:
referrer:
navigator:
platform:
userAgent:

评论