有些人闯进你的生活,只是为了给你上一课,然后转身离开。
各种技术
一、各种技术
写实复制
- 在Linux程序中,fork会产生一个和父进程完全相同的子进程,但子进程在此后大多会有exec系统调用。出于效率考虑,Linux中引入了“写时复制”技术,也就是只有进程空间的各段的内容要发生变化时,才将父进程的内容复制一份给子进程。那么子进程的物理空间没有代码,怎么去取指令执行exec系统调用呢?在fork之后exec之前两个进程用的是相同的物理空间(内存区),子进程的代码段、数据段、堆栈都是指向父进程的物理空间。也就是说两者的虚拟空间不同,其对应的物理空间是一个。当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间。如果不是因为exec,内核会给子进程的数据段、堆栈段分配相应的物理空间(至此两者都有各自的进程空间,互不影响),而代码段继续共享父进程的物理空间(两者的代码完全相同)。而如果是因为exec,由于两者执行的代码不同,子进程的代码段也会分配单独的物理空间。在网上看到的还有个细节问题是:fork之后内核会将子进程排在队列的前面,以让子进程先执行,以免父进程执行导致写时复制,而后子进程执行exec系统调用,因无意义的复制而造成效率的下降。
- 写时复制技术(copy on write):内核只为新生成的子进程创建虚拟空间结构,它们复制于父进程的虚拟空间结构,但是不为这些段分配物理内存,它们共享父进程的物理空间,当父子进程中有更改相应的段的行为发生时,再为子进程相应的段分配物理空间。概括的说就是更新源数据卷中的原始数据时, 将原始数据 Copy 到快照卷中。
- 写时重定向技术(redirect on write):使用指针指向protected entity的所有block。若一个block将被改写,存储系统将指向该block的指针指向一个新位置,然后将新数据写到该新位置。而快照系统知道在哪里找到原block内容,还是使用指针。概括的说就是更新源数据卷中的原始数据时, 将源数据卷数据指针表中的被更新原始数据指针重定向到新的存储空间。读取一个redirect-on-write快照没有任何的计算量。
零拷贝:零拷贝就是一种避免CPU将数据从一块存储拷贝到另外一块存储的技术。
- 对java来说,零拷贝是sendfile和mmap2函数
虚拟机(Virtual Machine)指通过软件模拟的具有完整硬件系统功能的、运行在一个完全隔离环境中的完整计算机系统。在实体计算机中能够完成的工作在虚拟机中都能够实现。在计算机中创建虚拟机时,需要将实体机的部分硬盘和内存容量作为虚拟机的硬盘和内存容量。每个虚拟机都有独立的CMOS、硬盘和操作系统,可以像使用实体机一样对虚拟机进行操作。
- 虚拟化,是指通过虚拟化技术将一台计算机虚拟为多台逻辑计算机。在一台计算机上同时运行多个逻辑计算机,每个逻辑计算机可运行不同的操作系统,并且应用程序都可以在相互独立的空间内运行而互不影响,从而显著提高计算机的工作效率。虚拟化使用软件的方法重新定义划分IT资源,可以实现IT资源的动态分配、灵活调度、跨域共享,提高IT资源利用率,使IT资源能够真正成为社会基础设施,服务于各行各业中灵活多变的应用需求。
二、连接
- Linux写时拷贝技术
- COW技术初窥
- sendfile“零拷贝”、mmap内存映射、DMA
- 零拷贝mmap和sendFile
- 什么是零拷贝?mmap与sendFile的区别是什么?
- redis17- MMAP内存映射与零拷贝sendFile
- 虚拟机是怎么实现的?
- 操作系统面试题
- 查看Linux的CPU个数、核数
- iperf2/3
- 8张图搞懂零拷贝
字节序大小端
一、概念
计算机硬件有两种储存数据的方式:大端字节序(big endian)和小端字节序(little endian),大部分计算机都是大端序。
- 大端字节序:高位字节在前,低位字节在后(这是人类读写数值的习惯)。
- 小端字节序:低位字节在前,高位字节在后。
举例来说,对于数值
0x1234
,高位字节是0x12
,低位字节是0x34
使用大端模式存储如下:
1 | 内存低地址 ————————————————————> 内存高地址 |
使用小端模式存储如下:
1 | 内存低地址 ————————————————————> 内存高地址 |
二、判断大小端
- union方式
- 联合体类型数据所占的内存空间等于其最大的成员所占的空间,对联合体内部所有成员的存取都是相对于该联合体基地址的偏移量为
0
处开始
- 联合体类型数据所占的内存空间等于其最大的成员所占的空间,对联合体内部所有成员的存取都是相对于该联合体基地址的偏移量为
1 | #include <stdio.h> |
- 对联合体成员a赋值1,十六进制为
0x00000001
,其中1为数据的低字节位- 如果1被存储在data所占内存的低地址中,那data.b的值将会是1,就是小端模式。
- 如果1被存储在data所占内存的高地址中,那data.b的值将会是0,就是大端模式。
- int强制类型转换char
1 | #include <stdio.h> |
三、参考
原码反码补码
一、概念
- 机器数:一个数在计算机中的表现形式叫做机器数,它有正负之分。在计算机中用一个数的最高位(符号位)用来表示它的正负,其中0表示正数,1表示负数。
假设计算机字长为
8
位,正数2
的二进制数来是00000010
,而负数-2
则用10000010
表示,这里的00000010
和10000010
是机器数。
- 在同一时间中处理二进制数的位数叫字长。
- 计算机字长是指计算机中CPU在一次操作中能处理的单位字的长度,即运算器能够并行处理和存储器每次读写操作时能包含的二进制码的位数,常见有8、16、32、64位字长。
- 真数:计算机中的机器数对应的真实的值就是真数,也叫真值。对最高位(符号位)后面的二进制数转换成十进制,并根据最高位来确定这个数的正负。
对于上面的
00000010
和10000010
来说,不考虑最高位的值,转换成十进制分别是2
和-2
,即其真数分别是2
和-2
原码:用第一位表示符号,其余位表示值。
- 因为第一位是符号位,所以8位二进制数的取值范围就是:
[1111 1111, 0111 1111]
,即[-127, 127]
- 原码是容易被人脑所理解的表达方式
- 因为第一位是符号位,所以8位二进制数的取值范围就是:
反码:正数的反码是其本身,负数的反码是符号位保持不变,其余位取反。
- 正数
1
的原码是00000001
,它的反码是其本身00000001
- 负数
-1
的原码是10000001]
,其反码是11111110
- 正数
补码:正数的补码是其本身,负数的补码是在其反码的基础上
+1
。- 正数
1
的原码是00000001
,它的补码是其本身00000001
- 负数
-1
的原码是10000001
,其反码是11111110
,其补码是11111111
- 正数
二、为什么引入反码补码
- 人脑可以知道第一位是符号位,在计算的时候我们会根据符号位选择对真值区域的加减。计算机如要辨别符号位显然会让计算机的基础电路设计变得十分复杂,于是人们想出了将符号位也参与运算的方法。
- 根据运算法则减去一个正数等于加上一个负数,即:
1-1=1+(-1)=0
,所以机器可以只有加法而没有减法,这样计算机运算的设计就更简单了。
- 根据运算法则减去一个正数等于加上一个负数,即:
于是人们开始探索将符号位参与运算,并且只保留加法的方法。
- 探索开始(默认十进制加减)
😁只用原码
1 | 1 - 1 |
如果用原码表示且让符号位也参与计算,对于减法来说结果显然是不正确的,这也就是为何计算机内部不使用原码表示一个数。
😁引入反码:解决了原码做减法的问题
1 | 1 - 1 |
用反码计算减法结果的真值部分是正确的,而唯一的问题出现在
0
这个特殊的数值上。虽然理解上+0
和-0
是一样的,但是0
带符号是没有任何意义的,而且会有[0000 0000]原
和[1000 0000]原
两个编码表示0
。
😁引入补码: 解决了0带符号以及两个编码表示0的问题
1 | 1 - 1 |
这样
0
用[0000 0000]
表示且不存在-0
的情况了。
😁-128问题:
1 | -128 |
在用补码运算的结果中,
[1000 0000]补
就是-128
,实际上是使用以前的-0
的补码来表示-128
,所以-128并没有原码和反码表示。(对-128
的补码表示[1000 0000]补
算出来的原码是[0000 0000]原
,这是不正确的)
使用补码,不仅仅修复了0的符号以及存在两个编码的问题,而且还能够多表示一个最低数。
- 这就是为什么
8
位二进制,使用原码或反码表示的范围为[-127, +127]
,而使用补码表示的范围为[-128, 127]
。同理可得MySQL中int有符号型取值范围[-2^31, 2^32-1]
。
三、参考
静态链接和动态链接
一、基础
对于C语言开发,编写好的程序代码一般保存在
.c
文件中,此时.c
文件就叫做源文件。要将编写好的程序运行起来,或者说从源文件到目标文件,一般还要经过以下几个阶段:预编译、编译、汇编、链接、加载(装载)、运行,静态链接和动态链接就是链接这一阶段的产物。- 链接器是一个独立程序,将一个或多个库或目标文件(先前由编译器或汇编器生成)链接到一块生成可执行程序。
静态链接
- 在实际开发过程中,不可能将所有源代码放在一个文件中,一般会根据模块或功能等分类方式将代码保存在多个源文件中。对于大部分应用来说,多个源文件之间不是独立的,而是会存在这相互依赖的关系,如一个源文件可能要调用另一个源文件中定义的函数等。在编译阶段,由于每个源文件都是独立编译的,即每个
.c
文件会形成一个.o
文件,为了满足相互依赖的关系,需要将这些源文件产生的目标文件进行链接从而形成一个可以执行的程序,这个链接的过程就是静态链接,其后缀名一般为.a
,win环境下为.lib
。简而言之就是在程序执行之前完成所有的组装工作,生成一个可执行的目标文件(win环境下的.exe
文件或linux下的可执行文件)。 - 链接器在链接静态链接库的时候是以目标文件为单位的,比如引用了静态库中的
printf()
函数,那么链接器就会把库中包含printf()
函数的那个目标文件链接进来。一般情况下,静态链接库的的目标文件会包含很多函数,如print、sprintf
(实际不一定在同一个文件,只为举例用),如果很多函数都放在一个目标文件中,很多没用的函数都被一起链接进了输出结果中,进而导致空间的浪费。 - 优缺点
- 浪费空间
- 更新困难
- 执行速度快
- 在实际开发过程中,不可能将所有源代码放在一个文件中,一般会根据模块或功能等分类方式将代码保存在多个源文件中。对于大部分应用来说,多个源文件之间不是独立的,而是会存在这相互依赖的关系,如一个源文件可能要调用另一个源文件中定义的函数等。在编译阶段,由于每个源文件都是独立编译的,即每个
动态链接
- 动态链接就是把程序按照模块拆分成多个相对独立的部分,运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件,其后缀名一般为
.so
,win环境下为.dll
。简而言之就是在程序已经为了执行被装入内存之后完成链接工作,并且在内存中一般只保留该编译单元的一份拷贝。- 装载时动态链接(Load-time Dynamic Linking):在编译之前已经明确知道要调用DLL中的哪几个函数,编译时在目标文件中只保留必要的链接信息而不含DLL函数的代码;当程序执行时,调用函数的时候利用链接信息加载DLL函数代码并在内存中将其链接入调用程序的执行空间中(全部函数加载进内存),其主要目的是便于代码共享。
- 运行时动态链接(Run-time Dynamic Linking):在编译之前不知道将会调用哪些DLL函数,在运行过程中根据需要决定应调用哪个函数,将其加载到内存中(只加载调用的函数进内存),并标识内存地址,其他程序也可以使用该程序,并用LoadLibrary和GetProcAddress动态获得DLL函数的入口地址(dll在内存中只存在一份)。
- 优缺点
- 节省空间
- 更新简单
- 执行速度慢
- 动态链接就是把程序按照模块拆分成多个相对独立的部分,运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件,其后缀名一般为
静态链接和动态链接两者最大的区别就在于链接的时机不一样,静态链接是在形成可执行程序前,而动态链接的进行则是在程序执行时。