计算机相关知识点整理
计算机相关必须要知道的知识点
持续更新中
一致性HASH算法
https://www.cnblogs.com/lpfuture/p/5796398.html
一致性哈希将整个哈希值空间组织成一个虚拟的圆环
缓存服务器端本身不提供分布式cache的一致性,而是由客户端来提供,具体在计算一致性hash时采用如下步骤:
- 先求出缓存服务器节点的哈希值,将其配置到0~2^32的圆(continuum)上
- 采用同样的方法求出存储数据的键的哈希值,并映射到相同的圆上
- 然后从数据映射到的位置开始顺时针查找,将数据保存到找到的第一个服务器上。如果超过2^32仍然找不到服务器,就会保存到第一台缓存服务器上
从上图的状态中添加一台服务器。余数分布式算法由于保存键的服务器会发生巨大变化而影响缓存的命中率,但在一致性HASH算法中,只有在圆上增加服务器的地点逆时针方向的第一台服务器上的键会受到影响
综上所述,一致性哈希算法对于节点的增减都只需重定位环空间中的一小部分数据,具有较好的容错性和可扩展性。
缺点:一致性哈希算法在服务节点太少时,容易因为节点分部不均匀而造成数据倾斜问题
解决办法:引入虚拟节点机制,即对每一个服务节点计算多个哈希,每个计算结果位置都放置一个此服务节点
Redis分布式锁
单机
获取锁
SET resource_name my_random_value NX PX 30000
my_random_value
是由客户端生成的一个随机字符串,它要保证在足够长的一段时间内在所有客户端的所有获取锁的请求中都是唯一的
NX表示只有当resource_name
对应的key值不存在的时候才能SET成功。保证只有第一个请求的客户端才能获得锁,其它客户端在锁被释放之前都无法获得锁
PX 30000表示这个锁有一个30秒的自动过期时间
释放锁
使用 Lua 脚本:可以保证操作的原子性
if redis.call("get",KEYS[1]) == ARGV[1] thenreturn redis.call("del",KEYS[1])
elsereturn 0
end
my_random_value
作为参数传到Lua脚本里面,作为:ARGV[1]
,而 resource_name
作为KEYS[1]
问题讨论
为什么不用如下命令?
SETNX resource_name my_random_value
EXPIRE resource_name 30
由于这样写不是原子的,如果客户端在执行完SETNX
后crash
了,那么就没有机会执行EXPIRE
了,导致它一直持有这个锁,其他的客户端就永远获取不到这个锁了
为什么不用如下命令?
SETNX lock.foo <current Unix time + lock timeout + 1>
Redis官网文档建议用Set命令来代替,主要原因是SETNX不支持超时时间的设置
为什么my_random_value
要设置成随机值?
保证了一个客户端释放的锁是自己持有的那个锁
客户端1获取锁成功
客户端1在某个操作上阻塞了很长时间
过期时间到了,锁自动释放了
客户端2获取到了对应同一个资源的锁
客户端1从阻塞中恢复过来,释放掉了客户端2持有的锁
集群
https://segmentfault.com/a/1190000018826044
当redis为集群模式时,需要考虑如下情况:
客户端1从Master获取了锁
Master宕机了,存储锁的key还没有来得及同步到Slave上
Slave升级为Master
客户端2从新的Master获取到了对应同一个资源的锁
客户端1和客户端2同时持有了同一个资源的锁,锁不再具有安全性
RedLock获取锁
- 获取当前时间
- 按顺序依次向N个Redis节点执行获取锁的操作。这个获取操作跟前面基于单Redis节点的获取锁的过程相同,包含随机字符串my_random_value,也包含锁的过期时间。为了保证在某个Redis节点不可用的时候算法能够继续运行,这个获取锁的操作还有一个超时时间,它要远小于锁的有效时间(几十毫秒量级)。客户端在向某个Redis节点获取锁失败以后,立即尝试下一个Redis节点
- 计算整个获取锁的过程总共消耗了多长时间,用当前时间减去第1步记录的时间。如果客户端从大多数Redis节点(>= N/2+1)成功获取到了锁,并且获取锁总消耗时间没有超过锁的有效时间,那么这时客户端才认为最终获取锁成功
- 如果最终获取锁成功了,那么这个锁的有效时间应该重新计算,它等于最初的锁的有效时间减去第3步计算出来的获取锁消耗的时间
- 如果最终获取锁失败了(可能由于获取到锁的Redis节点个数少于N/2+1,或者整个获取锁的过程消耗的时间超过了锁的最初有效时间),那么客户端应该立即向所有Redis节点发起释放锁的操作(即前面介绍的单机Redis Lua脚本释放锁的方法)
RedLock释放锁
客户端向所有Redis节点发起释放锁的操作,不管这些节点当时在获取锁的时候成功与否
问题讨论
有节点发生崩溃重启
假设一共有5个Redis节点:A, B, C, D, E。发生了如下的事件序列:
客户端1成功锁住了A, B, C,获取锁成功(但D和E没有锁住)。
节点C崩溃重启了,但客户端1在C上加的锁没有持久化下来,丢失了。
节点C重启后,客户端2锁住了C, D, E,获取锁成功。
客户端1和客户端2同时获得了锁。
提出延迟重启,也就是说,一个节点崩溃后,先不立即重启它,而是等待一段时间再重启,这段时间应该大于锁的有效时间。这样的话,这个节点在重启前所参与的锁都会过期,它在重启后就不会对现有的锁造成影响
客户端长期阻塞导致锁过期
客户端1在获得锁之后发生了很长时间的GC pause,在此期间,它获得的锁过期了,而客户端2获得了锁。当客户端1从GC pause中恢复过来的时候,它不知道自己持有的锁已经过期了,它依然向共享资源发起了写数据请求,而这时锁实际上被客户端2持有,因此两个客户端的写请求就有可能冲突
引入fencing token的概念:
客户端1先获取到的锁,因此有一个fencing token,等于33,而客户端2后获取到的锁,有一个的fencing token,等于34。客户端1从GC pause中恢复过来之后,依然是向存储服务发送访问请求,但是带了fencing token = 33。存储服务发现它之前已经处理过34的请求,所以会拒绝掉这次33的请求。这样就避免了冲突
但是其实这已经超出了Redis实现分布式锁的范围,单纯用Redis没有命令来实现生成Token
时钟跳跃问题
假设有5个Redis节点A, B, C, D, E
客户端1从Redis节点A, B, C成功获取了锁(多数节点)。由于网络问题,与D和E通信失败
节点C上的时钟发生了向前跳跃,导致它上面维护的锁快速过期
客户端2从Redis节点C, D, E成功获取了同一个资源的锁(多数节点)
客户端1和客户端2现在都认为自己持有了锁
这个问题用Redis实现分布式锁暂时无解。而生产环境这种情况是存在的
结论
Redis并不能实现严格意义上的分布式锁。如果你的应用场景为了效率,协调各个客户端避免做重复的工作,即使锁失效了,只是可能把某些操作多做一遍而已,不会产生其它的不良后果。但是如果你的应用场景是为了正确性,那么用Redis实现分布式锁并不合适,会存在各种各样的问题,为了正确性,需要使用zab、raft共识算法,或者使用带有事务的数据库来实现严格意义上的分布式锁
HTTP缓存机制及原理
https://www.cnblogs.com/chenqf/p/6386163.html
浏览器缓存处于磁盘中,假设他叫缓存数据库
强制缓存
强制缓存的响应header中会有两个字段来标明失效规则:Expires、Cache-Control
Expires
是HTTP 1.0的东西
服务端返回的到期时间,下一次请求时,请求时间小于服务端返回的到期时间,直接使用缓存数据
问题:到期时间是由服务端生成的,可能与客户端时间有误差,这就会导致缓存命中的误差
Cache-Control
是HTTP 1.1的东西
常见的取值有private、public、no-cache、max-age,no-store,默认为private
Cache-Control: max-age=31536000//在365天内再次请求这条数据,都会直接获取缓存数据库中的数据
对比缓存
浏览器第一次请求数据时,服务器会将缓存标识与数据一起返回给客户端,客户端将二者备份至缓存数据库中。
再次请求数据时,客户端将备份的缓存标识发送给服务器,服务器根据缓存标识进行判断,判断成功后,返回304状态码,通知客户端比较成功,可以使用缓存数据
Last-Modified / If-Modified-Since
Last-Modified:服务器在响应请求时,告诉浏览器资源的最后修改时间
If-Modified-Since:再次请求服务器时,通过此字段通知服务器上次请求时,服务器返回的资源最后修改时间
服务器收到请求后发现有头If-Modified-Since 则与被请求资源的最后修改时间进行比对
若资源的最后修改时间大于If-Modified-Since,说明资源又被改动过,则响应整片资源内容,返回状态码200
若资源的最后修改时间小于或等于If-Modified-Since,说明资源无新修改,则响应HTTP 304,告知浏览器继续使用所保存的cache
Etag / If-None-Match
优先级高于Last-Modified / If-Modified-Since
**Etag:**服务器响应请求时,告诉浏览器当前资源在服务器的唯一标识(生成规则由服务器决定)
**If-None-Match:**再次请求服务器时,通过此字段通知服务器客户段缓存数据的唯一标识
服务器收到请求后发现有头If-None-Match 则与被请求资源的唯一标识进行比对
不同,说明资源又被改动过,则响应整片资源内容,返回状态码200
相同,说明资源无新修改,则响应HTTP 304,告知浏览器继续使用所保存的cache
Linux环境变量
对所有用户生效的永久性变量(系统级)
路径:/etc/profile,用export指令添加环境变量
添加完成后使用:source /etc/profile 新文件才会生效
对单一用户生效的永久性变量(用户级)
路径:~/.bash_profile,用export添加环境变量
添加完成后使用:source /etc/profile 新文件才会生效
.bashrc和.bash_profile的区别:.bash_profile文件只会在用户登录的时候读取一次,而.bashrc在每次打开终端进行一次新的会话时都会读取
临时有效的环境变量(只对当前shell有效)
直接使用export指令添加
使用环境变量直接执行程序:
此时在 /root/zhd/myCode 目录下有可执行文件 work
设置环境变量:export PATH=$PATH:/root/zhd/myCode
将work的目录添加到搜索路径中,此时输入 work 直接执行程序
单例模式
为什么不使用全局变量?
全局对象能够保证方便地访问实例,但是不能保证只声明一个对象——也就是说除了一个全局实例外,仍然能创建相同类的本地实例
双重检验
template<typename T>
class Singleton : Nocopyable
{
public:static T& getInstance();private:Singleton();~Singleton();private:static std::unique_ptr<T> instance_;static std::mutex mutex_;
};template<typename T>
std::unique_ptr<T> Singleton<T>::instance_;template<typename T>
std::mutex Singleton<T>::mutex_;template<typename T>
inline T& Singleton<T>::getInstance()
{if (instance_.get() == nullptr){std::unique_lock<std::mutex> lock(mutex_);if (instance_.get() == nullptr){instance_ = std::unique_ptr<T>(new T);}}return *instance_.get();
}//main中调用
class Foo
{
public:void print() { cout << a_ << endl; }
private:int a_ = 10;
};int main()
{Foo foo= Singleton<Foo>::getInstance();foo.print();return 0;
}
问题:CPU指令重排,new一个对象主要被分为三步:通过operator new申请内存、使用placement new调用构造函数、返回内存指针。在程序运行期间可能会出现指令重排问题:通过operator new申请内存、返回内存指针、使用placement new调用构造函数。在这种情况下,第一个线程还未调用完成构造函数,第二个线程进来以后判断,发现指针指向不为NULL,即直接返回。此时线程二拿到的东西并没有对象,造成错误
线程安全的单例模式
template<typename T>
class Singleton2 : Nocopyable
{
public:static T& getInstance(){pthread_once(&once_, &Singleton2::init);return *value_;}private:Singleton2();~Singleton2();static void init(){value_ = new T();}private:static pthread_once_t once_;static T* value_;
};template<typename T>
pthread_once_t Singleton2<T>::once_ = PTHREAD_ONCE_INIT;template<typename T>
T* Singleton2<T>::value_ = nullptr;
内存对齐
CPU 总是以字大小(32 位处理器上常常为 4 个字节)访问数据
执行效率提高
struct mystruct {char c; // one byteint i; // four bytesshort s; // two bytes
}
在 32 位处理器上,上述数据结构可能会被按照下图这样排列,此时处理器访问结构体 mystruct 的任意一个成员都只需一次访问
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-apUj70RO-1613993141274)(https://pics7.baidu.com/feed/cdbf6c81800a19d804b843019907da8ea71e467a.jpeg?token=5e0830fcf60b5cfa2c74bfe6f28e7fdd&s=E510247224BA1E214E968283020050A8)]
如果没有内存对齐,mystruct 的各个成员在内存中紧密排列
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5NcM1wbd-1613993141275)(https://pics4.baidu.com/feed/00e93901213fb80e9ea6c3bd9c2c772bb838946c.jpeg?token=6f742a7aded2996d4a3c1f128e38e35d&s=ED00347226DAFF20434F82C60200C0AA)]
如果处理器需要从 0x05 处读取 16 位数据:处理器不得不从 0x04 处读取一个字(这里等于 4 字节),然后左移一个字节,将结果放入 16 位寄存器中
如果处理器需要从 0x01 处读取 32 位数据:处理器不得不从 0x00 处读取一个字,并且左移一个字节,然后从 0x04 处再读取一个字,并且右移 3 个字节,最终使用 OR 位运算将两次读取结果拼接,才能达成目的
访问范围提高
看不懂
对于任意给定的地址空间,如果体系架构可以确定 2 个 LSB 总是 0(例如 32 位机器),那么它可以访问 4 倍多的内存(2 个位能够表示 4 个不同状态)。从一个地址中去掉 2 个 LSB,将得到 4 字节的内存对齐,或者说“跨距”,因为地址每增加一,它就有效的增加 bit 2,而不是 bit 0。(鉴于低 2 位总是 00)
这甚至会影响系统的物理设计:如果地址总线的需要少 2 位,CPU 上的管脚就可以少 2 个
?
原子性的保障
CPU 每次访问数据的宽度是一个字,如果C语言程序中的数据总是内存对齐的,那么 CPU 访问数据总是原子性的,这对于许多无锁数据结构和其他并发需求的正确操作至关重要
glibc malloc
malloc的实现与物理内存是无关的,内核为每个进程维护一张页表,页表存储进程空间内每页的虚拟地址,页表项中有的虚拟内存页对应着某个物理内存页面,也有的虚拟内存页没有实际的物理页面对应。无论malloc通过sbrk还是mmap实现,分配到的内存只是虚拟内存,而且只是虚拟内存的页号,代表这块空间进程可以用,实际上还没有分配到实际的物理页面。等你的进程访问到这个新分配的内存空间的时候,如果其还没有对应的物理页面分配,就会产生缺页中断,内核这个时候会给进程分配实际的物理页面,以与这个未被映射的虚拟页面对应起来
https://blog.csdn.net/maokelong95/article/details/51989081
glibc malloc 使用 ptmalloc 作为内存分配器
申请堆的系统调用
malloc
内部通过 brk
或 mmap
系统调用向内核申请堆区
「堆」指代用于分配动态内存的虚拟地址空间
「栈」指代用于分配静态内存的虚拟地址空间
堆维护在通过 brk
系统调用申请的「Heap」及通过 mmap
系统调用申请的「Memory Mapping Segment」中
栈维护在通过汇编栈指令动态调整的「Stack」中
在 glibc 里,「Heap」用于分配较小的内存及主线程使用的内存
多线程支持
Linux 早期采用 dlmalloc 作为默认分配器,但因为 ptmalloc2 提供多线程支持,后来 Linux 就采用 ptmalloc2
在 dlmalloc 中,当两个线程同时 malloc
时,只有一个线程能够访问临界区——这是因为所有线程共享用以缓存已释放内存的「空闲列表数据结构」,所以使用 dlmalloc 的多线程应用会在 malloc
上耗费过多时间,导致整个应用性能下降
在 ptmalloc2 中,当两个线程同时调用 malloc
时,内存均会得以立即分配——每个线程都维护着单独的堆,各个堆被独立的空闲列表数据结构管理,因此各个线程可以并发地从空闲列表数据结构中申请内存
案例代码
/* Per thread arena example. */
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>void* threadFunc(void* arg) {printf("Before malloc in thread 1\n");getchar();char* addr = (char*) malloc(1000);printf("After malloc and before free in thread 1\n");getchar();free(addr);printf("After free in thread 1\n");getchar();
}int main() {pthread_t t1;void* s;int ret;char* addr;printf("Welcome to per thread arena example::%d\n",getpid());printf("Before malloc in main thread\n");getchar();addr = (char*) malloc(1000);printf("After malloc and before free in main thread\n");getchar();free(addr);printf("After free in main thread\n");getchar();ret = pthread_create(&t1, NULL, threadFunc, NULL);if(ret){printf("Thread creation error\n");return -1;}ret = pthread_join(t1, &s);if(ret){printf("Thread join error\n");return -1;}return 0;
}
在主线程 malloc 之前
没有堆段也没有每个线程的栈,因为 thread1 还没有创建!
sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$ ./mthread
Welcome to per thread arena example::6501
Before malloc in main thread
...
sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$ cat /proc/6501/maps
08048000-08049000 r-xp 00000000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread
08049000-0804a000 r--p 00000000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread
0804a000-0804b000 rw-p 00001000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread
b7e05000-b7e07000 rw-p 00000000 00:00 0
...
sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$
在主线程 malloc 之后
堆段已经产生,其地址区间正好在数据段(0x0804b000 - 0x0806c000)上,这表明堆内存是移动 Program Break 的位置产生的(也即通过 brk
中断)。尽管用户只申请了 1000 字节的内存,但是实际产生了 132KB 的堆
这个连续的堆区域被称为「arena」。因为这个 arena 是被主线程建立的,因此其被称为「main arena」
接下来的申请会继续分配这个 arena 的 132KB 中剩余的部分。当分配完毕时,它可以通过继续移动 Program Break 的位置扩容。扩容后,「top chunk」的大小随之调整,以将这块新增的空间圈进去;arena 也可以在 top chunk 过大时缩小
注意:top chunk 是一个 arena 位于最顶层的 chunk
sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$ ./mthread
Welcome to per thread arena example::6501
Before malloc in main thread
After malloc and before free in main thread
...
sploitfun@sploitfun-VirtualBox:~/lsploits/hof/ptmalloc.ppt/mthread$ cat /proc/6501/maps
08048000-08049000 r-xp 00000000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread
08049000-0804a000 r--p 00000000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread
0804a000-0804b000 rw-p 00001000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread
0804b000-0806c000 rw-p 00000000 00:00 0 [heap]
b7e05000-b7e07000 rw-p 00000000 00:00 0
...
sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$
在主线程 free 之后
当分配的内存区域 free
掉时,其并不会立即归还给操作系统,而仅仅是移交给了作为库函数的分配器。这块 free
掉的内存添加在了「main arenas bin」中(在 glibc malloc 中,空闲列表数据结构被称为「bin」)
随后当用户请求内存时,分配器就不再向内核申请新堆了,而是先试着各个「bin」中查找空闲内存。只有当 bin 中不存在空闲内存时,分配器才会继续向内核申请内存
sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$ ./mthread
Welcome to per thread arena example::6501
Before malloc in main thread
After malloc and before free in main thread
After free in main thread
...
sploitfun@sploitfun-VirtualBox:~/lsploits/hof/ptmalloc.ppt/mthread$ cat /proc/6501/maps
08048000-08049000 r-xp 00000000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread
08049000-0804a000 r--p 00000000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread
0804a000-0804b000 rw-p 00001000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread
0804b000-0806c000 rw-p 00000000 00:00 0 [heap]
b7e05000-b7e07000 rw-p 00000000 00:00 0
...
sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$
在 thread1 malloc 之前
此时 thread1 的堆尚不存在,但其栈已产生
sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$ ./mthread
Welcome to per thread arena example::6501
Before malloc in main thread
After malloc and before free in main thread
After free in main thread
Before malloc in thread 1
...
sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$ cat /proc/6501/maps
08048000-08049000 r-xp 00000000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread
08049000-0804a000 r--p 00000000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread
0804a000-0804b000 rw-p 00001000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread
0804b000-0806c000 rw-p 00000000 00:00 0 [heap]
b7604000-b7605000 ---p 00000000 00:00 0
b7605000-b7e07000 rw-p 00000000 00:00 0 [stack:6594]
...
sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$
在 thread1 malloc 之后
thread1 的堆段(b7500000 - b7521000,132KB)建立在了内存映射段中,这也表明了堆内存是使用 mmap
系统调用产生的,而非同主线程一样使用 sbrk
系统调用
尽管用户只请求了 1000B,但是映射到程地址空间的堆内存足有 1MB。这 1MB 中,只有 132KB 被设置了读写权限,并成为该线程的堆内存。这段连续内存(132KB)被称为「thread arena」
注意:当用户请求超过 128KB(比如
malloc(132*1024)
) 大小并且此时 arena 中没有足够的空间来满足用户的请求时,内存将通过mmap
系统调用(不再是sbrk
)分配,而不论请求是发自 main arena 还是 thread arena
ploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$ ./mthread
Welcome to per thread arena example::6501
Before malloc in main thread
After malloc and before free in main thread
After free in main thread
Before malloc in thread 1
After malloc and before free in thread 1
...
sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$ cat /proc/6501/maps
08048000-08049000 r-xp 00000000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread
08049000-0804a000 r--p 00000000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread
0804a000-0804b000 rw-p 00001000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread
0804b000-0806c000 rw-p 00000000 00:00 0 [heap]
b7500000-b7521000 rw-p 00000000 00:00 0
b7521000-b7600000 ---p 00000000 00:00 0
b7604000-b7605000 ---p 00000000 00:00 0
b7605000-b7e07000 rw-p 00000000 00:00 0 [stack:6594]
...
sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$
在 thread1 free 之后
free
不会把内存归还给操作系统,而是移交给分配器,然后添加在了「thread arenas bin」中
sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$ ./mthread
Welcome to per thread arena example::6501
Before malloc in main thread
After malloc and before free in main thread
After free in main thread
Before malloc in thread 1
After malloc and before free in thread 1
After free in thread 1
...
sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$ cat /proc/6501/maps
08048000-08049000 r-xp 00000000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread
08049000-0804a000 r--p 00000000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread
0804a000-0804b000 rw-p 00001000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread
0804b000-0806c000 rw-p 00000000 00:00 0 [heap]
b7500000-b7521000 rw-p 00000000 00:00 0
b7521000-b7600000 ---p 00000000 00:00 0
b7604000-b7605000 ---p 00000000 00:00 0
b7605000-b7e07000 rw-p 00000000 00:00 0 [stack:6594]
...
sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$
Arena
Arena 的数量
主线程包含 main arena,thread 1 包含它自己的 thread arena。arena 数量限于系统核数
For 32 bit systems:
Number of arena = 2 * number of cores.
For 64 bit systems:
Number of arena = 8 * number of cores.
main arena 可以使用brk和mmap来分配,而 thread arena 只能使用mmap来映射内存块
Multiple Arena
举例:一个运行在单核计算机上的 32 位操作系统上的多线程应用(4 线程,主线程 + 3 个线程)。这里线程数量(4)> 2 * 核数(1),所以分配器中可能有 Arena 会被所有线程共享
- 当主线程第一次调用
malloc
时,已经建立的 main arena 会被没有任何竞争地使用 - 当 thread 1 和 thread 2 第一次调用
malloc
时,一块新的 arena 将被创建,且将被没有任何竞争地使用。此时线程和 arena 之间存在一一映射关系 - 当 thread3 第一次调用
malloc
时,arena 的数量限制被计算出已超出,因此尝试复用已经存在的 arena - 复用:
- 一旦遍历到可用 arena,就开始自旋申请该 arena 的锁
- 如果上锁成功(比如说 main arena 上锁成功),就将该 arena 返回用户
- 如果没找到可用 arena,thread 3 的
malloc
将被阻塞,直到有可用的 arena 为止
- 当thread 3 调用
malloc
时(第二次),分配器会尝试使用上一次使用的 arena(即 main arena),从而尽量提高缓存命中率。当 main arena 可用时就用,否则 thread 3 就一直阻塞,直至 main arena 空闲。因此现在 main arena 实际上是被 main thread 和 thread 3 所共享
Multiple Heaps
看不懂
在「glibc malloc」中主要有 3 种数据结构:
- heap_info —— Heap Header —— 一个 thread arena 可以维护多个堆。每个堆都有自己的堆 Header(注:也即头部元数据)。什么时候 Thread Arena 会维护多个堆呢? 一般情况下,每个 thread arena 都只维护一个堆,但是当这个堆的空间耗尽时,新的堆(而非连续内存区域)就会被
mmap
到这个 aerna 里 - malloc_state —— Arena header —— 一个 thread arena 可以维护多个堆,这些堆另外共享同一个 arena header。Arena header 描述的信息包括:bins、top chunk、last remainder chunk 等
- malloc_chunk —— Chunk header —— 根据用户请求,每个堆被分为若干 chunk。每个 chunk 都有自己的 chunk header
Main arena 无需维护多个堆,因此也无需 heap_info。当空间耗尽时,与 thread arena 不同,main arena 可以通过
sbrk
拓展堆段,直至堆段「碰」到内存映射段与 thread arena 不同,main arena 的 arena header 不是保存在通过
sbrk
申请的堆段里,而是作为一个全局变量,可以在 libc.so 的数据段中找到
main arena 和 thread arena 的图示如下(单堆段):
thread arena 的图示如下(多堆段):
Chunk
堆段中存在的 chunk 类型如下
- Allocated chunk
- Free chunk
- Top chunk
- Last Remainder chunk
Allocated chunk
已经分配给用户的
chunk指针指向chunk开始的地址,mem指针指向用户内存块开始的地址,next_chunk指向下一个chunck的起始地址
prev_size:若上一个 chunk 可用,则此字段赋值为上一个 chunk 的大小;否则,此字段被用来存储上一个 chunk 的用户数据
size:此字段赋值本 chunk 的大小,其最后三位包含标志信息:
P=0时,表示前一个chunk为空闲,prev_size才有效,P=1时,表示前一个chunk正在使用,prev_size无效 P主要用于内存块的合并操作
ptmalloc 分配的第一个块总是将p设为1, 以防止程序引用到不存在的区域
M=1 为mmap映射区域分配;M=0为heap区域分配
A=1 为非主分区分配;A=0 为主分区分配
malloc_chunk 中的其余结构成员,如 fd、 bk,没有使用的必要而拿来存储用户数据
用户请求的大小被转换为内部实际大小,因为需要额外空间存储 malloc_chunk,此外还需要考虑对齐
Free chunk
是用户已释放的 chunk:
空闲的chunk会被放置到空闲的链表bins上。当用户申请内存malloc的时候,会先去查找空闲链表bins上是否有合适的内存。
- fp和bp分别指向前一个和后一个空闲链表上的chunk
- fp_nextsize和bp_nextsize分别指向前一个空闲chunk和后一个空闲chunk的大小,主要用于在空闲链表上快速查找合适大小的chunk。
- prev_size: 两个相邻 free chunk 会被合并成一个,因此该字段总是保存前一个 allocated chunk 的用户数据
- size: 该字段保存本 free chunk 的大小
Bins
是空闲列表数据结构,用以保存 free chunks。根据其中 chunk 的大小,bins 被分为如下几种类型:
Fast binUnsorted bin、Small bin、Large bin
保存这些 bins 的字段为:
fastbinsY:这个数组用以保存 fast bins
bins:这个数组用于保存 unsorted bin、small bins 以及 large bins,共计可容纳 126 个,其中:
Bin 1:unsorted bin
Bin 2 - 63:small bins
Bin 64 - 126:large bins
Fast Bin
大小为 16~ 80 字节的 chunk 被称为「fast chunk」。在所有的 bins 中,fast bins 路径享有最快的内存分配及释放速度
- 数量:10
- 每个 fast bin 都维护着一条 free chunk 的单链表,采用单链表是因为链表中所有 chunk 的大小相等,增删 chunk 发生在链表顶端即可
- chunk 大小:8 字节递增
- fast bins 由一系列所维护 chunk 大小以 8 字节递增的 bins 组成。即,
fast bin[0]
维护大小为 16 字节的 chunk、fast bin[1]
维护大小为 24 字节的 chunk。依此类推…… - 指定 fast bin 中所有 chunk 大小相同
- 在 malloc 初始化过程中,最大的 fast bin 的大小被设置为 64 而非 80 字节。因为默认情况下只有大小 16 ~ 64 的 chunk 被归为 fast chunk
- 无需合并 —— 两个相邻 chunk 不会被合并。虽然这可能会加剧内存碎片化,但也大大加速了内存释放的速度
malloc(fast chunk)
- 初始情况下 fast chunck 最大尺寸以及 fast bin 相应数据结构均未初始化,因此即使用户请求内存大小落在 fast chunk 相应区间,服务用户请求的也将是 small bin 路径而非 fast bin 路径
- 初始化后,将在计算 fast bin 索引后检索相应 bin
- 相应 bin 中被检索的第一个 chunk 将被摘除并返回给用户
free(fast chunk)
- 计算 fast bin 索引以索引相应 bin
free
掉的 chunk 将被添加到上述 bin 的顶端
Unsorted Bin
当 small chunk 和 large chunk 被 free
掉时,它们并非被添加到各自的 bin 中,而是被添加在 「unsorted bin」 中。这使得分配器可以重新使用最近 free
掉的 chunk,从而消除了寻找合适 bin 的时间开销,进而加速了内存分配及释放的效率
这里应补充说明「Unsorted Bin 中的 chunks 何时移至 small/large chunk 中」。在内存分配的时候,在前后检索 fast/small bins 未果之后,在特定条件下,会将 unsorted bin 中的 chunks 转移到合适的 bin 中去,small/large
- 数量:1
- unsorted bin 包括一个用于保存 free chunk 的双向循环链表(又名 binlist);
- chunk 大小:无限制,任何大小的 chunk 均可添加到这里。
Small Bin
大小小于 512 字节的 chunk 被称为 「small chunk」,而保存 small chunks 的 bin 被称为 「small bin」。在内存分配回收的速度上,small bin 比 large bin 更快
- 数量:62
- 每个 small bin 都维护着一条 free chunk 的双向循环链表。采用双向链表的原因是,small bins 中的 chunk 可能会从链表中部摘除。这里新增项放在链表的头部位置,而从链表的尾部位置移除项。—— FIFO
- chunk 大小:8 字节递增
- Small bins 由一系列所维护 chunk 大小以 8 字节递增的 bins 组成。举例而言,
small bin[0]
(Bin 2)维护着大小为 16 字节的 chunks、small bin[1]
(Bin 3)维护着大小为 24 字节的 chunks ,依此类推…… - 指定 small bin 中所有 chunk 大小均相同,因此无需排序;
- Small bins 由一系列所维护 chunk 大小以 8 字节递增的 bins 组成。举例而言,
- 合并 —— 相邻的 free chunk 将被合并,这减缓了内存碎片化,但是减慢了
free
的速度; malloc(small chunk)
- 初始情况下,small bins 都是 NULL,因此尽管用户请求 small chunk ,提供服务的将是 unsorted bin 路径而不是 small bin 路径;
- 第一次调用
malloc
时,维护在 malloc_state 中的 small bins 和 large bins 将被初始化,它们都会指向自身以表示其为空; - 此后当 small bin 非空,相应的 bin 会摘除其中最后一个 chunk 并返回给用户;
free(small chunk)
free
chunk 的时候,检查其前后的 chunk 是否空闲,若是则合并,也即把它们从所属的链表中摘除并合并成一个新的 chunk,新 chunk 会添加在 unsorted bin 的前端。
Large Bin
大小大于等于 512 字节的 chunk 被称为「large chunk」,而保存 large chunks 的 bin 被称为 「large bin」。在内存分配回收的速度上,large bin 比 small bin 慢。
数量
:63
- 每个 large bin 都维护着一条 free chunk 的双向循环链表。采用双向链表的原因是,large bins 中的 chunk 可能会从链表中的任意位置插入及删除。
- 这 63 个 bins
- 32 个 bins 所维护的 chunk 大小以 64B 递增,也即
large chunk[0]
(Bin 65) 维护着大小为 512B ~ 568B 的 chunk 、large chunk[1]
(Bin 66) 维护着大小为 576B ~ 632B 的 chunk,依此类推…… - 16 个 bins 所维护的 chunk 大小以 512 字节递增;
- 8 个 bins 所维护的 chunk 大小以 4096 字节递增;
- 4 个 bins 所维护的 chunk 大小以 32768 字节递增;
- 2 个 bins 所维护的 chunk 大小以 262144 字节递增;
- 1 个 bin 维护所有剩余 chunk 大小;
- 32 个 bins 所维护的 chunk 大小以 64B 递增,也即
- 不像 small bin ,large bin 中所有 chunk 大小不一定相同,各 chunk 大小递减保存。最大的 chunk 保存顶端,而最小的 chunk 保存在尾端;
合并 —— 两个相邻的空闲 chunk 会被合并;
malloc(large chunk)
- 初始情况下,large bin 都会是 NULL,因此尽管用户请求 large chunk ,提供服务的将是 next largetst bin 路径而不是 large bin 路劲 。
- 第一次调用
malloc
时,维护在 malloc_state 中的 small bin 和 large bin 将被初始化,它们都会指向自身以表示其为空; - 此后当 large bin 非空,如果相应 bin 中的最大 chunk 大小大于用户请求大小,分配器就从该 bin 顶端遍历到尾端,以找到一个大小最接近用户请求的 chunk。一旦找到,相应 chunk 就会被切分成两块:
- User chunk(用户请求大小)—— 返回给用户;
- Remainder chunk (剩余大小)—— 添加到 unsorted bin。
- 如果相应 bin 中的最大 chunk 大小小于用户请求大小,分配器就会扫描 binmaps,从而查找最小非空 bin。如果找到了这样的 bin,就从中选择合适的 chunk 并切割给用户;反之就使用 top chunk 响应用户请求。
free(large chunk)
—— 类似于 small chunk 。
Top Chunk
一个 arena 中最顶部的 chunk 被称为「top chunk」。它不属于任何 bin 。当所有 bin 中都没有合适空闲内存时,就会使用 top chunk 来响应用户请求。
当 top chunk 的大小比用户请求的大小大的时候,top chunk 会分割为两个部分:
- User chunk,返回给用户;
- Remainder chunk,剩余部分,将成为新的 top chunk。
当 top chunk 的大小比用户请求的大小小的时候,top chunk 就通过 sbrk
(main arena)或 mmap
( thread arena)系统调用扩容。
Last Remainder Chunk
「last remainder chunk」即最后一次 small request 中因分割而得到的剩余部分,它有利于改进引用局部性,也即后续对 small chunk 的 malloc
请求可能最终被分配得彼此靠近。
那么 arena 中的若干 chunks,哪个有资格成为 last remainder chunk 呢?
当用户请求 small chunk 而无法从 small bin 和 unsorted bin 得到服务时,分配器就会通过扫描 binmaps 找到最小非空 bin。正如前文所提及的,如果这样的 bin 找到了,其中最合适的 chunk 就会分割为两部分:返回给用户的 User chunk 、添加到 unsorted bin 中的 Remainder chunk。这一 Remainder chunk 就将成为 last remainder chunk。
那么引用局部性是如何达成的呢?
当用户的后续请求 small chunk,并且 last remainder chunk 是 unsorted bin 中唯一的 chunk,该 last remainder chunk 就将分割成两部分:返回给用户的 User chunk、添加到 unsorted bin 中的 Remainder chunk(也是 last remainder chunk)。因此后续的请求的 chunk 最终将被分配得彼此靠近。
内存分配malloc流程
- 获取分配区的锁,防止多线程冲突。
- 计算出需要分配的内存的chunk实际大小。
- 判断chunk的大小,如果小于max_fast(64b),则取fast bins上去查询是否有适合的chunk,如果有则分配结束。
- chunk大小是否小于512B,如果是,则从small bins上去查找chunk,如果有合适的,则分配结束。
- 继续从 unsorted bins上查找。如果unsorted bins上只有一个chunk并且大于待分配的chunk,则进行切割,并且剩余的chunk继续扔回unsorted bins;如果unsorted bins上有大小和待分配chunk相等的,则返回,并从unsorted bins删除;如果unsorted bins中的某一chunk大小 属于small bins的范围,则放入small bins的头部;如果unsorted bins中的某一chunk大小 属于large bins的范围,则找到合适的位置放入。
- 从large bins中查找,找到链表头后,反向遍历此链表,直到找到第一个大小 大于待分配的chunk,然后进行切割,如果有余下的,则放入unsorted bin中去,分配则结束。
- 如果搜索fast bins和bins都没有找到合适的chunk,那么就需要操作top chunk来进行分配了(top chunk相当于分配区的剩余内存空间)。判断top chunk大小是否满足所需chunk的大小,如果是,则从top chunk中分出一块来。
- 如果top chunk也不能满足需求,则需要扩大top chunk。主分区上,如果分配的内存小于分配阀值(默认128k),则直接使用brk()分配一块内存;如果分配的内存大于分配阀值,则需要mmap来分配;非主分区上,则直接使用mmap来分配一块内存。通过mmap分配的内存,就会放入mmap chunk上,mmap chunk上的内存会直接回收给操作系统。
内存释放free流程
获取分配区的锁,保证线程安全。
如果free的是空指针,则返回,什么都不做。
判断当前chunk是否是mmap映射区域映射的内存,如果是,则直接munmap()释放这块内存。前面的已使用chunk的数据结构中,我们可以看到有****M****来标识是否是mmap映射的内存。
判断chunk是否与top chunk相邻,如果相邻,则直接和top chunk合并(和top chunk相邻相当于和分配区中的空闲内存块相邻)。转到步骤8
如果chunk的大小大于max_fast(64b),则放入unsorted bin,并且检查是否有合并,有合并情况并且和top chunk相邻,则转到步骤8;没有合并情况则free。
如果chunk的大小小于 max_fast(64b),则直接放入fast bin,fast bin并没有改变chunk的状态。没有合并情况,则free;有合并情况,转到步骤7
在fast bin,如果当前chunk的下一个chunk也是空闲的,则将这两个chunk合并,放入unsorted bin上面。合并后的大小如果大于64KB,会触发进行fast bins的合并操作,fast bins中的chunk将被遍历,并与相邻的空闲chunk进行合并,合并后的chunk会被放到unsorted bin中,fast bin会变为空。合并后的chunk和topchunk相邻,则会合并到topchunk中。转到步骤8
判断top chunk的大小是否大于mmap收缩阈值(默认为128KB),如果是的话,对于主分配区,则会试图归还top chunk中的一部分给操作系统。free结束。
brk与sbrk
malloc所申请的内存主要从Heap区域分配(暂不考虑通过mmap申请大块内存的情况)
进程所面对的虚拟内存地址空间,只有按页映射到物理内存地址,才能真正使用。受物理存储容量限制,整个堆虚拟内存空间不可能全部映射到实际的物理内存
Linux对堆的管理示意如下:
Linux维护一个break指针,这个指针指向堆空间的某个地址。从堆起始地址到break之间的地址空间为映射好的,可以供进程访问;而从break往上,是未映射的地址空间,如果访问这段空间则程序会报错
要增加一个进程实际的可用堆大小,需要将break指针向高地址移动。Linux通过brk和sbrk系统调用操作break指针。两个系统调用的原型如下:
int brk(void *addr);
void *sbrk(intptr_t increment);
brk将break指针直接设置为某个地址,而sbrk将break从当前位置移动increment所指定的增量。brk 在执行成功时返回0,否则返回-1并设置errno为ENOMEM;sbrk成功时返回break移动之前所指向的地址,否则返回(void *) -1,如果将increment设置为0,则可以获得当前break的地址
由于Linux是按页进行内存映射的,所以如果break被设置为没有按页大小对齐,则系统实际上会在最后映射一个完整的页,从而实际已映射的内存空间比break指向的地方要大一些。但是使用break之后的地址是很危险的(尽管也许break之后确实有 一小块可用内存地址)
Fork的写时复制
fork()会产生一个和父进程完全相同的子进程,但子进程在此后大多会exec系统调用,出于效率考虑,linux中引入了“写时复制“技术,也就是只有进程空间的各段的内容要发生变化时,才会将原来的内容复制一份给这个进程使用。在fork之后exec之前两个进程用的是相同的物理空间,子进程的代码段、数据段、堆栈都是指向父进程的物理空间。当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间。如果不是因为exec,内核会给子进程的数据段、堆栈段分配相应的物理空间,而代码段继续共享父进程的物理空间。而如果是因为exec,由于两者执行的代码不同,子进程的代码段也会分配单独的物理空间
fork之后内核会通过将子进程放在队列的前面,以让子进程先执行,以免父进程执行导致写时复制(想一想为什么)
简单原理:
pid=fork(),当执行这一句时,当前进程进入fork()运行,此时,fork()内会用一段嵌入式汇编进行系统调用:int 0x80。进入内核根据此前写入eax的系统调用功能号便会运行sys_fork系统调用。接着,sys_fork中首先会调用C函数find_empty_process产生一个新的进程,然后会调用C函数 copy_process将父进程的内容复制给子进程,但是子进程tss中的eax值赋值为0(这也是为什么子进程中返回0的原因),当赋值完成后, copy_process会返回新进程(该子进程)的pid,这个值会被保存到eax中。这时子进程就产生了,此时子进程与父进程拥有相同的代码空间,程序指针寄存器eip指向相同的下一条指令地址,当fork正常返回调用其的父进程后,因为eax中的值是新创建的子进程号,所以,fork()返回子进程号,执行else(pid>0);当产生进程切换运行子进程时,首先会恢复子进程的运行环境即装入子进程的tss任务状态段,其中的eax 值(copy_process中置为0)也会被装入eax寄存器,所以,当子进程运行时,fork返回的是0执行if(pid==0)
Mysql使用规范
数据库设计规范
- 若无特殊说明,建表时一律采用Innodb存储引擎
- 数据库和表的字符集统一使用UTF8
数据库和表的字符集统一使用utf8,若是有字段需要存储emoji表情之类的,则将表或字段设置成utf8mb4;因为,utf8号称万国码,其无需转码、无乱码风险且节省空间,而utf8mb4又向下兼容utf8 - 单个表的数据量大小控制在500万以内
- 使用MySQL分区表需谨慎
- 数据库中禁止存储图片、文件等大的二进制数据
若往数据库表中存储文件,而文件通常很大,当数据库进行读取操作时,会进行大量的随机IO操作,大文件使得IO操作很耗时耗性能,造成短时间内数据量快速增长;所以,通常将图片、文件存储在文件服务器中,数据库只用于存储文件地址信息
建表规范
表达是与否概念的字段,必须使用 is _ xxx 的方式命名,数据类型是 unsigned tinyint( 1 表示是,0 表示否 )
说明:任何字段如果为非负数,必须是 unsigned 。
正例:表达逻辑删除的字段名 is_deleted ,1 表示删除,0 表示未删除
表名、字段名必须使用小写字母或数字 , 禁止出现数字开头,禁止两个下划线中间只出现数字。数据库字段名的修改代价很大,因为无法进行预发布,所以字段名称需要慎重考虑。
说明:MySQL 在 Windows 下不区分大小写,但在 Linux 下默认是区分大小写。因此,数据库名、表名、字段名,都不允许出现任何大写字母,避免节外生枝
禁用保留字,如 desc 、 range 、 match 、 delete等
主键索引名为 pk_ 字段名;唯一索引名为 uk _字段名 ;普通索引名则为 idx _字段名。
说明: pk_ 即 primary key;uk _ 即 unique key;idx _ 即 index 的简称
小数类型为 decimal ,禁止使用 float 和 double 。
说明: float 和 double 在存储的时候,存在精度损失的问题,很可能在值的比较时,得到不正确的结果。如果存储的数据范围超过 decimal 的范围,建议将数据拆成整数和小数分开存储
varchar 是可变长字符串,不预先分配存储空,长度不要超过 5000,如果存储长度大于此值,定义字段类型为 text ,独立出来一张表,用主键来对应,避免影响其它字段索引效率
表必备三字段: id , create_time , update_time 。
说明:其中 id 必为主键,类型为 uuid。create_time、update_time 的类型均为 bigint(20) 类型
枚举型字段,逻辑型字段必须要有默认值
字段设计规范
先选择符合存储需要的最小的数据类型。
主要是考虑索引的性能,因为列的字段越大,建立索引时所需要的空间也越大,这样一页中能存储的索引节点的数量也就越少,在遍历时需要的IO次数也就越多,索引的性能也就越差
避免使用TEXT、BLOB数据类型
避免使用TEXT和BLOB数据类型,其中最常见的TEXT类型可以存储64K数据,MySQL内存临时表不支持TEXT、BLOB这样的大数据类型,若查询中包含这样的数据,在执行排序等操作时就不能使用内存临时表,必须使用磁盘临时表执行操作;
TEXT和BLOB类型只能使用前缀索引(当索引是很长的字符序列时,这个索引将会很占内存,而且会很慢,这时候就会用到前缀索引了;所谓的前缀索引就是去索引的前面几个字母作为索引,但是要降低索引的重复率,所以我们还必须要判断前缀索引的重复率;),因为MySQL对索引字段长度是有限的,所以TEXT类型只能使用前缀索引,并且TEXT列上是不能有默认值的;
若需要使用,建议把BLOB或TEXT列分离到单独的的扩展表中,且查询时一定不要使用select *,只需取出必要的列即可
避免使用ENUM枚举类型
修改ENUM 值需要使用ALTER 语句;ENUM 类型的ORDER BY 操作效率低;禁止使用数值作为ENUM 的枚举值
所有列的默认值定义为NOT NULL
数据库所有为NULL 的列需要额外的空间来存储,因此会占用更多的空间;
数据库在进行比较和计算时需要对NULL 值做特别处理
使用TIMESTAMP(4字节)或DATETIME(8字节)类型存储时间TIMESTAMP 存储的时间范围为:1970-01-01 00:00:01 ~ 2038-01-19-03:14:07;
TIMESTAMP 占用4字节和INT相同,但可读性比INT 类型的高,若是超出TIMESTAMP 取值范围的则使用DATETIME 类型存储;
用字符串类型存储时间的缺点:无法使用日期函数进行比较计算、字符串存储占有更多的空间
财务相关的金额类数据必须使用decimal 类型
精准浮点:decimal
非精准浮点:float、double
Decimal类型为精准浮点数,在计算时不会丢失精度;占有空间大小由定义的宽度决定,每4个字节可以存储9位数字,且小数点也要占有一个字节;另外,Decimal类型可用于存储比bigint更大的数据类型。
索引规范
业务上具有唯一特性的字段,即使是多个字段的组合,也必须建成唯一索引
超过三个表禁止 join 。需要 join 的字段,数据类型必须绝对一致;多表关联查询时,保证被关联的字段需要有索引
在 varchar 字段上建立索引时,必须指定索引长度,没必要对全字段建立索引,根据实际文本区分度决定索引长度即可
页面搜索严禁左模糊或者全模糊,索引失效
【推荐】如果有 order by 的场景,请注意利用索引的有序性。 order by 最后的字段是组合索引的一部分,并且放在索引组合顺序的最后,避免出现 file _ sort 的情况,影响查询性能。
正例: where a =? and b =? order by c; 索引: a _ b _ c
反例:索引中有范围查找,那么索引有序性无法利用,如: WHERE a >10 ORDER BY b; 索引a _ b 无法排序。2.【推荐】利用延迟关联或者子查询优化超多分页场景。
说明: MySQL 并不是跳过 offset 行,而是取 offset + N 行,然后返回放弃前 offset 行,返回N 行,那当 offset 特别大的时候,效率就非常的低下,要么控制返回的总页数,要么对超过特定阈值的页数进行 SQL 改写。
正例:先快速定位需要获取的 id 段,然后再关联:
SELECT a.* FROM 表 1 a, (select id from 表 1 where 条件 LIMIT 100000,20 ) b where a.id=b.id3.【推荐】 SQL 性能优化的目标:至少要达到 range 级别,要求是 ref 级别,如果可以是 consts最好。
说明:
1 ) consts 单表中最多只有一个匹配行 ( 主键或者唯一索引 ) ,在优化阶段即可读取到数据。
2 ) ref 指的是使用普通的索引 (normal index) 。
3 ) range 对索引进行范围检索。
反例: explain 表的结果, type = index ,索引物理文件全扫描,速度非常慢,这个 index 级
别比较 range 还低,与全表扫描是小巫见大巫。4.【推荐】建组合索引的时候,区分度最高的在最左边。
正例:如果 where a =? and b =? , a 列的几乎接近于唯一值,那么只需要单建 idx _ a 索引即可。说明:存在非等号和等号混合判断条件时,在建索引时,请把等号条件的列前置。如: where a >?
and b =? 那么即使 a 的区分度更高,也必须把 b 放在索引的最前列。5.【推荐】防止因字段类型不同造成的隐式转换,导致索引失效。
sql语句
使用 count( 列名 ) 或 count( 常量 ) 来替代 count( * ) , count( * ) 是 SQL 92 定义的标准统计行数的语法,跟数据库无关,跟 NULL 和非 NULL 无关。
说明: count( * ) 会统计值为 NULL 的行,而 count( 列名 ) 不会统计此列为 NULL 值的行
禁止使用存储过程,存储过程难以调试和扩展,更没有移植性
不得使用外键与级联,一切外键概念必须在应用层解决。
说明:以学生和成绩的关系为例,学生表中的 student _ id 是主键,那么成绩表中的 student _ id
则为外键。如果更新学生表中的 student _ id ,同时触发成绩表中的 student _ id 更新,即为
级联更新。外键与级联更新适用于单机低并发,不适合分布式、高并发集群 ; 级联更新是强阻
塞,存在数据库更新风暴的风险 ; 外键影响数据库的插入速度数据订正(特别是删除、修改记录操作)时,要先 select ,避免出现误删除,确认无误才能执行更新语句
count(distinct col) 计算该列除 NULL 之外的不重复行数,注意 count(distinct col 1, col 2 ) 如果其中一列全为 NULL ,那么即使另一列有不同的值,也返回为 0。
3.【强制】当某一列的值全是 NULL 时, count(col) 的返回结果为 0,但 sum(col) 的返回结果为NULL ,因此使用 sum() 时需注意 NPE 问题。
正例:可以使用如下方式来避免 sum 的 NPE 问题: SELECT IF(ISNULL(SUM(g)) ,0, SUM(g)) FROM table;
4.【强制】使用 ISNULL() 来判断是否为 NULL 值。
说明:NULL 与任何值的直接比较都为 NULL。
1 ) NULL<>NULL 的返回结果是 NULL ,而不是 false 。
2 ) NULL=NULL 的返回结果是 NULL ,而不是 true 。
3 ) NULL<>1 的返回结果是 NULL ,而不是 true 。5.【强制】 在代码中写分页查询逻辑时,若 count 为 0 应直接返回,避免执行后面的分页语句
in 操作能避免则避免,若实在避免不了,需要仔细评估 in 后边的集合元素数量,控制在 1000 个之内。
2.【参考】 TRUNCATE TABLE 比 DELETE 速度快,且使用的系统和事务日志资源少,但 TRUNCATE无事务且不触发 trigger ,有可能造成事故,故不建议在开发代码中使用此语句。
说明: TRUNCATE TABLE 在功能上与不带 WHERE 子句的 DELETE 语句相同
mysql数据库行为规范
1、【强制】超过100万行数据的批量操作(update delete insert),分多次进行
大批量操作可能回造成严重的主从延迟;
binlog日志为row格式时会产生大量的日志;
避免产生大事物操作。
2、【强制】对于大表使用pt-online-schema-change 修改表结构
1)避免大表修改产生的主从延迟、避免在对表字段进行修改时进行锁表;
2)pt-online-schema-change 它首先会建立一个与原表结构相同的新表,并且在新表上进行表结构的修改,然后再把原表中的数据复制到新表中,并在原表中增加一些触发器;然后,把原表中新增的数据也复制到新表中,在行所有数据复制完成之后,把新表命名成原表,并把原来的表删除掉,其是把原来一个DDL操作,分解成多个小的批次执行。
3、【强制】禁止给程序使用的账号授予super 权限
当达到最大连接数限制时,还运行1个有super权限的用户连接super权限只能留给DBA处理问题的账号使用。
4、【强制】对于程序连接数据库账号,遵循权限最小原则
程序使用数据库账号只能在一个数据库下使用,且程序使用的账号原则上不授予drop 权限
线程安全和可重入的区别
线程安全:当函数被多个并发线程反复调用时,它会一直产生正确的结果
线程安全需要考虑的是线程之间的共享变量。同一进程的不同线程会共享进程内存空间中的全局区和堆,而私有的线程空间则主要包括栈和寄存器。对于同一进程的不同线程来说,每个线程的局部变量都是私有的,而全局变量、局部静态变量、分配于堆的变量都是共享的。在对这些共享变量进行访问时,必须通过加锁的方式保证线程安全
可重入:当程序执行到某个函数foo()时,收到信号,于是暂停目前正在执行的函数,转到信号处理函数,而这个信号处理函数的执行过程中,又进入到刚刚执行的函数foo(),这样便发生了所谓的重入。此时如果foo()能够正确的运行,而且处理完成后,之前暂停的foo()也能够正确运行,则说明它是可重入的。针对的较多的应该是单线程(个人理解)
即使对于可重入函数,在信号处理函数中使用也需要注意一个问题就是errno。一个线程中只有一个errno变量,信号处理函数中使用的可重入函数也有可能会修改errno。例如,read函数是可重入的,但是它也有可能会修改errno。因此,正确的做法是在信号处理函数开始,先保存errno;在信号处理函数退出的时候,再恢复errno
如程序正在调用printf输出,但是在调用printf时,出现了信号,对应的信号处理函数也有printf语句,就会导致两个printf的输出混杂在一起
如果给printf加锁的话,上面的情况就会导致死锁。这种情况,采用的方法一般是在特定的区域屏蔽一定的信号
要确保函数可重入,需满足以下几个条件:
- 不在函数内部使用静态或全局数据
- 不返回静态或全局数据,所有数据都由函数的调用者提供
- 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据
- 不调用不可重入函数
可重入的函数一定是线程安全的,但反过来不一定成立
- 如果一个函数中用到了全局或静态变量,那么它不是线程安全的,也不是可重入的
- 如果我们对它加以改进,在访问全局或静态变量时使用互斥量或信号量等方式加锁,则可以使它变成线程安全的,但此时它仍然是不可重入的,因为通常加锁方式是针对不同线程的访问,而对同一线程可能出现问题
- 如果将函数中的全局或静态变量去掉,改成函数参数等其他形式,则有可能使函数变成既线程安全,又可重入
B-树
MySQL索引背后的数据结构及算法原理
B树属于多叉树又名平衡多路查找树
规则:
定义一条数据记录为一个二元组[key, data]:key为记录的键值,data为数据记录除key外的数据
每个节点都存储key和data
d为大于1的一个正整数,称为B-Tree的度
每个非叶子节点由n-1个key和n个指针组成,其中d<=n<=2d
每个叶子节点最少包含一个key和两个指针,最多包含2d-1个key和2d个指针
key和指针互相间隔,节点两端是指针
一个节点中的key从左到右非递减排列
每个指针要么为null(叶子节点),要么指向另外一个节点
B-Tree查找算法伪代码
BTree_Search(node, key) {if(node == null) return null;foreach(node.key){if(node.key[i] == key) return node.data[i];if(node.key[i] > key) return BTree_Search(point[i]->node);}return BTree_Search(point[i+1]->node);
}
B+树
B+树为B-树的变种
与B-树相比,有一下不同:
- 每个节点的指针上限为2d-1而不是2d
- 内节点不存储data,只存储key
- 叶子节点不存储指针
由于并不是所有节点都具有相同的域,因此B+Tree中叶节点和内节点一般大小不同
这点与B-Tree不同,虽然B-Tree中不同节点存放的key和指针可能数量不一致,但是每个节点的域和上限是一致的,所以在实现中B-Tree往往对每个节点申请同等大小的空间
带有顺序访问指针
在B+Tree的每个叶子节点增加一个指向相邻叶子节点的指针,形成带有顺序访问指针的B+Tree
目的是为了提高区间访问的性能,极大提到了区间查询效率
为什么使用B(+)树
索引本身也很大,往往以文件的形式存储的磁盘上。索引查找过程中要产生磁盘I/O消耗,而I/O存取的消耗较内存要高几个数量级,所以索引的结构组织要尽量减少查找过程中磁盘I/O的存取次数
根据B-Tree的定义,可知检索一次最多需要访问h个节点。数据库系统的设计者巧妙利用了磁盘预读原理,将一个节点的大小设为等于一个页,这样每个节点只需要一次I/O就可以完全载入:
- 每次新建节点时,直接申请一个页的空间,这样就保证一个节点物理上也存储在一个页里,加之计算机存储分配都是按页对齐的,就实现了一个node只需一次I/O
- B-Tree中一次检索最多需要h-1次I/O(根节点常驻内存)。一般实际应用中,出度d是非常大的数字,通常超过100,因此h非常小(通常不超过3)
而红黑树h明显要深很多,效率明显比B-Tree差很多
B+Tree更适合外存索引,和内节点出度d有关,d越大索引的性能越好,由于B+Tree内节点去掉了data域,因此可以拥有更大的出度,拥有更好的性能
MyISAM索引实现
MyISAM引擎使用B+Tree作为索引结构,叶节点的data域存放的是数据记录的地址
假设表有三列,且以Col1为主键,则上图是一个MyISAM表的主索引示意。MyISAM的索引文件仅仅保存数据记录的地址
在MyISAM中,主索引和辅助索引在结构上没有任何区别,只是主索引要求key是唯一的,而辅助索引的key可以重复。若在Col2上建立一个辅助索引,则此索引的结构如下图所示:
同样是一棵B+Tree,data域保存数据记录的地址
因此,MyISAM中索引检索的算法为首先按照B+Tree搜索索引,如果指定的Key存在,则取出其data域的值,然后以data域的值为地址,读取相应数据记录
MyISAM索引文件和数据文件是分离的,索引文件仅保存数据记录的地址
MyISAM的索引也叫做非聚集索引,InnoDB的索引叫聚集索引
InnoDB索引实现
InnoDB使用B+Tree作为索引结构
InnoDB的数据文件本身就是按B+Tree组织的索引文件,这棵树的叶节点data域保存了完整的数据记录。InnoDB表数据文件本身就是主索引,索引的key是数据表的主键
叶节点包含了完整的数据记录,这种索引叫做聚集索引。因为InnoDB的数据文件本身要按主键聚集,所以InnoDB要求表必须有主键(MyISAM可以没有),如果没有显式指定,会自动选择一个可以唯一标识数据记录的列作为主键,如果不存在这种列,则会自动为InnoDB表生成一个隐含字段作为主键,这个字段长度为6个字节,类型为长整形
InnoDB的辅助索引data域存储相应记录主键的值而不是地址(InnoDB的所有辅助索引都引用主键作为data域)
聚集索引使得按主键的搜索十分高效,但是辅助索引需要检索两遍:首先检索辅助索引获得主键,然后用主键到主索引中检索获得记录
所以,不建议使用过长的字段作为主键,因为所有辅助索引都引用主索引,过长的主索引会令辅助索引变得过大;使用非单调的字段作为主键在InnoDB中十分低效(非单调的主键会造成在插入新记录时数据文件为了维持B+Tree的特性而频繁的分裂调整),使用自增字段作为主键比较合适
索引使用策略及优化
最左前缀原理与相关优化
联合索引:按一定顺序引用多个列
全列匹配:当按照索引中所有列进行精确匹配("=“或"IN”)时,索引可以被用到。MySQL的查询优化器会自动调整WHERE子句的条件顺序以使用适合的索引,可以颠倒WHERE中条件顺序
最左前缀匹配:当查询条件精确匹配索引的左边连续一个或几个列时,索引可以被用到一部分
查询条件用到了部分索引列,但中间某个条件未提供:(例如这个联合索引是由<row1,row2,row3>构成的,此时WHERE条件只有row1和row3)由于row2不存在无法和最前缀连接,因此需要对结果扫描过滤row3
若想让row3也用索引,可以添加一个辅助索引<row1,row3>,此时,这两个条件的查询会用这个索引
使用隔离列的优化方法:若row2只有(a,b,c,d,e)这几个值(多了就不合适),使用IN来填坑:
SELECT * FROM tables WHERE row1 = '00001' AND row2 IN ('a', 'b', 'c', 'd', 'e') AND row3 = 'afwfaw';
查询条件没有指定索引第一列:不是最左前缀,用不到索引
匹配某列的前缀字符串:此时可以用到索引,通配符%不出现在开头,就可以用到
范围查询:范围列可以用到索引(必须是最左前缀),但是范围列后面的列无法用到索引,且索引最多用于一个范围列(查询条件有两个范围列无法全用到索引)
查询条件中含有函数或表达式:查询条件中含有函数或表达式,MySQL不会为这列使用索引(row1无表达式,row2有表达式,只会用到row1的索引)
索引选择性与前缀索引
索引文件本身要消耗存储空间,同时会加重插入、删除和修改记录时的负担,且MySQL在运行时也要消耗资源维护索引
不建议建索引的情况:
表记录比较少,没必要建索引,全表扫描就好(超过两千条可以考虑建索引)
索引的选择性较低
索引的选择性:不重复的索引值(Cardinality)与表记录数(#T)的比值(Index Selectivity = Cardinality / #T)
选择性的取值范围为(0,1],选择性越高索引价值越大,例如前面的row2,假设row2经常被单独查询(但是只有a,b,c,d,e这几个值)
SELECT count(DISTINCT(row2))/count(*) AS Selectivity FROM tables; +-------------+ | Selectivity | +-------------+ | 0.0000 | +-------------+
row2的选择性不足0.0001(精确值为0.00001579),没有必要为其单独建索引
优化策略:前缀索引,用列的前缀代替整个列作为索引key,当前缀长度合适时,可以做到既使得前缀索引的选择性接近全列索引,同时因为索引key变短而减少了索引文件的大小和维护开销
例如:employees表只有一个索引<emp_no>,如果想按名字搜索一个人,只能全表扫描
EXPLAIN SELECT * FROM employees WHERE first_name='Eric' AND last_name='Anido'; +----+-------------+-----------+------+---------------+------+---------+------+--------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-----------+------+---------------+------+---------+------+--------+-------------+ | 1 | SIMPLE | employees | ALL | NULL | NULL | NULL | NULL | 300024 | Using where | +----+-------------+-----------+------+---------------+------+---------+------+--------+-------------+
若频繁按名字搜索员工,效率很低,因此可以考虑建索引。可以选择建<first_name>或<first_name, last_name>,看下两个索引的选择性:
SELECT count(DISTINCT(first_name))/count(*) AS Selectivity FROM employees; +-------------+ | Selectivity | +-------------+ | 0.0042 | +-------------+ SELECT count(DISTINCT(concat(first_name, last_name)))/count(*) AS Selectivity FROM employees; +-------------+ | Selectivity | +-------------+ | 0.9313 | +-------------+
<first_name>显然选择性太低,<first_name, last_name>选择性好,但是加起来长度太长。可以考虑用first_name和last_name的前几个字符建立索引,例如<first_name, left(last_name, 4)>
SELECT count(DISTINCT(concat(first_name, left(last_name, 4))))/count(*) AS Selectivity FROM employees; +-------------+ | Selectivity | +-------------+ | 0.9007 | +-------------+
这是已经比较理想,且索引长度缩短很多
建索引:
ALTER TABLE employees ADD INDEX `first_name_last_name4` (first_name, last_name(4));
前缀索引兼顾索引大小和查询速度,但是其缺点是不能用于ORDER BY和GROUP BY操作,也不能用于Covering index(即当索引本身包含查询所需全部数据时,不再访问数据文件本身)
InnoDB的主键选择与插入优化
在使用InnoDB存储引擎时,若没有特别需要,请使用一个与业务无关的自增字段作为主键
InnoDB使用聚集索引,数据记录本身被存于主索引(一棵B+Tree)的叶子节点上。这要求同一个叶子节点内(大小为一个内存页或磁盘页)的各条数据记录按主键顺序存放,因此每当有一条新的记录插入时,MySQL会根据其主键将其插入适当的节点和位置,如果页面达到装载因子,则会开辟一个新的页(节点)
如果使用自增主键,那么每次插入新的记录,记录就会顺序添加到当前索引节点的后续位置,当一页写满,会自动开辟一个新的页:
这样会形成一个紧凑的索引结构,近似顺序填满。由于每次插入时也不需要移动已有数据,因此效率很高,也不会增加很多开销在维护索引上
如果使用非自增主键(身份证号等),由于每次插入主键的值近似于随机,因此每次新纪录都要被插到现有索引页得中间某个位置:
MySQL为了将新记录插到合适位置而移动数据,甚至目标页面可能已经被回写到磁盘上而从缓存中清掉,此时又要从磁盘上读回来,这增加了很多开销,同时频繁的移动、分页操作造成了大量的碎片,得到了不够紧凑的索引结构,后续不得不通过OPTIMIZE TABLE来重建表并优化填充页面
红黑树
STL的组成
- 容器:由STL定义的数据结构组成的模板类,包括vector、list、queue、set、map、multimap、stack、hash_set等
- 算法:对具有泛型数据结构的容器进行数据处理,以模板函数的方式提供
- 迭代器:通过迭代器可以定位和操纵容器,从而实现算法和容器关联(类似指针)
- 仿函数:通过重载operator(),使这个类看起来像函数。使这个类的对象可以作为参数传递给合适的STL算法,拓展算法的功能
- 适配器:是一种修改容器、函数对象或迭代器接口的STL组件,其作用相当于类型转换器
- 容器适配器:stack可以被实现为基本容器类型(vector、dequeue、list)的适配,可以把stack看作是某种特殊的vector、deque或者list容器,只是其操作受到stack本身属性的限制
- 迭代器适配器:修改为某些基本容器定义的迭代器的接口的一种STL组件。反向迭代器和插入迭代器都属于迭代器适配器,迭代器适配器扩展了迭代器的功能
- 函数对象适配器:通过转换或者修改其他函数对象使其功能得到扩展。使函数转化为函数对象,或是将多参数的函数对象转化为少参数的函数对象
- 内存分配器:为容器分配并管理内存。默认的分配器仅简单封装了malloc()和free()函数。有其他分配器,运用内存池技术,对小块的内存进行管理
浏览器输入URL执行过程
https://www.cnblogs.com/KeleLLXin/p/14020283.html
https://www.cnblogs.com/ffdsj/p/12375290.html
域名解析
DNS协议:域名解析协议
例如要查询www.baidu.com的IP地址:
- 浏览器搜索自己的DNS缓存
- 搜索操作系统中的DNS缓存
- 搜索操作系统的hosts文件
- 操作系统将域名发送至本地域名服务器 ——(递归查询),本地域名服务器查询自己的DNS缓存,查找成功则返回结果,否则:(迭代查询)
- 本地域名服务器向根域名服务器发起请求,此处,根域名服务器返回com域的顶级域名服务器的地址
- 本地域名服务器向com域的顶级域名服务器发起请求,返回baidu.com权限域名服务器地址
- 本地域名服务器向baidu.com权限域名服务器发起请求,得到www.baidu.com的IP地址
- 本地域名服务器将得到的IP地址返回给操作系统,同时自己也将IP地址缓存起来
- 操作系统将IP地址返回给浏览器,同时自己也将IP地址缓存起来
- 浏览器得到域名对应的IP地址
将消息发送到服务器
为了将消息从PC传到服务器上,需要用到IP协议、ARP协议和OSPF协议
ARP协议:地址解析协议,用于实现从IP地址到MAC地址的映射,即询问目标IP对应的MAC地址
如果源主机和目的主机在同一个局域网内,就可以用ARP找到目的主机的MAC地址;如果不在一个局域网内,用ARP协议找到本网络内的一个路由器的MAC地址,剩下的工作由这个路由器来完成
- 每个主机都有ARP高速缓存,存储本局域网内IP地址和MAC地址之间的对应关系(重启失效)
- 首先检查ARP高速缓存中是否有对应IP地址目的主机的MAC地址,如果有,则直接发送数据。如果没有,就向本网段的所有主机发送ARP请求分组(广播),该数据包包括的内容有:(源主机IP地址,源主机MAC地址,目的主机的IP地址)
- 当本网络的主机收到该ARP请求分组时,首先检查数据包中的IP地址是否是自己的IP地址,如果不是,则忽略该数据包;如果是,则首先从数据包中取出源主机的IP地址和MAC地址写入到ARP高速缓存中,如果已经存在,则覆盖,然后将自己的MAC地址写入ARP响应包中,告诉源主机自己是它想要找的MAC地址(单播)
- 源主机收到ARP响应分组后,将目的主机的IP和MAC地址写入ARP高速缓存中,并利用此信息发送数据。如果源主机一直没有收到ARP响应分组,表示ARP查询失败
ARP协议属于哪一层?
从功能角度来说,目的是获取MAC地址,服务于链路层,所以属于链路层协议
从层次角度来说,ARP协议属于TCP/IP协议族,而TCP/IP模型中所有协议的定义至少在网络层
路由选择协议:包括很多,这里只说OSPF
OSPF协议:最短路径优先协议(网络层)
IP协议:在相互连接的网络之间传递IP数据报
寻址与路由
用IP地址来标识Internet的主机:在每个IP数据报中,都会携带源IP地址和目标IP地址来标识该IP数据报的源和目的主机。IP数据报在传输过程中,每个中间节点(IP 网关)还需要为其选择从源主机到目的主机的合适的转发路径(即路由)。IP协议可以根据路由选择协议提供的路由信息对IP数据报进行转发,直至抵达目的主机
IP地址和MAC地址的匹配,ARP协议。数据链路层使用MAC地址来发送数据帧,因此在实际发送IP报文时,还需要进行IP地址和MAC地址的匹配,由TCP/IP协议簇中的ARP(地址解析协议)完成
分段与重组
IP数据报通过不同类型的通信网络发送,IP数据报的大小会受到这些网络所规定的最大传输单元的限制
将IP数据报拆分成一个个能够适合下层技术传输的小数据报,被分段后的IP数据报可以独立地在网络中进行转发,在到达目的主机后被重组,恢复成原来的IP数据报
发起TPC三次握手
发起HTTP请求
服务器响应HTTP请求
浏览器解析HTML代码
断开TCP连接
浏览器渲染页面
为什么同时需要IP地址与MAC地址
https://www.zhihu.com/question/21546408/answer/149670503
memcpy
需注意地址重叠的情况(部分函数库实现memcpy时未考虑地址重叠)
void *memcpy(void *dest, const void *src, size_t n);
// 从源src所指的内存地址的起始位置开始拷贝n个字节到目标dest所指的内存地址的起始位置中
标准库提供了地址重叠时的内存拷贝函数:memmove(),但该函数是把源字符串拷贝到临时buf里,然后再从临时buf里写到目的地址,增加了一次不必要的开销。在我们重载memcpy时,需要考虑地址重叠情况
未考虑地址重叠情况的函数库:
#include <iostream>
#include <cstring>
using namespace std;int main(int argc, char *argv[])
{char buf[100] = "abcdefghijk";memcpy(buf + 2, buf, 5);printf("%s\n", buf + 2);return 0;
}// 输出abadehijk,结果被污染
自行重载:
void* Memcpy(void* dest, const void* src, int size)
{char* pdest;char* psrc;if (NULL == dest || NULL == src){return NULL;}if (dest > src && (char*)src + size > (char*)dest) // 重叠情况自后向前{pdest = (char*)dest + size - 1;psrc = (char*)src + size - 1;while (size--){*pdest-- = *psrc--;}}else{pdest = (char*)dest;psrc = (char*)src;while (size--){*pdest++ = *psrc++;}}return dest;
}
shared_ptr的线程安全
http://www.cppblog.com/Solstice/archive/2013/01/28/197597.html
shared_ptr用来实现线程安全的释放对象
shared_ptr的引用计数本身是安全且无锁的,但对象的读写则不是,因为 shared_ptr 有两个数据成员,读写操作不能原子化
类的空指针访问类的成员函数
class MyClass
{
public:int i;void hello() { printf("hello/n"); }void print() { printf("%d/n", i); }
};
int main()
{MyClass* pmy = NULL;pmy->hello();return 0;
}
这段代码看似诡异,但是是可以正常执行的
每个对象都有this指针,this指针主要的作用就是用来区分不同对象,可以根据它来访问不同对象的成员变量
当前代码hello函数并无成员变量,所以不会用到this指针。相反,print函数用到了成员变量,在调用该函数时需要用来this指针来确定i的值,此时会报段错误
100亿个数找出最大10个
小顶堆
在内存中维护一个长度为N的数组,先取出前N个数并构建小顶堆,再将剩下所有数与堆顶比较,比堆顶大则替换,比堆顶小则丢弃,并重建这个堆
MapReduce
将100亿个分段存储在不同的机器上,每台机器计算各自的TopN,最后汇总
分治法
随机选一个数t,对数组进行划分,前一部分大于t,后一部分小于t。如果前一部分数据个数等于N,直接结束;如果前一部分数据个数大于N,那么在前一部分中继续划分;如果前一部分数据个数小于N,那么在后一部分中划分。可能出现内存不够的情况,则将数据写入文件中
海量数据查找中位数
前提条件:无法将数字一次性加载到内存中
采用基于二进制位比较和快速排序算法中的分割思想
将10亿数据读取一部分到内存中(不超过内存限制),将每个数字用二进制表示,比较二进制的最高位(第32位),如果最高位为0,则将数字写入file_0文件中,如果最高位为1,将数字写入file_1文件中。此时将10亿个数字分成了两个文件。假设file_0有6亿数字,file_1有4亿数字,中位数在file_0的从大到小排序的第1亿个数。在比较file_0的二进制次高位(第31位),如果次高位为0,则将数字写入file_0_0文件中,如果次高位为1,将数字写入file_0_1文件中。假设 file_0_0文件中有3亿个数字,file_0_1中也有3亿个数字,则中位数就是:file_0_0文件中的数字从小到大排序之后的第1亿个数字…
leetcode 165
如果电脑是2G的,能写出4G的数组吗
TLS和SSL握手
空类默认产生哪些函数。能产生虚函数吗
如何判断是大端还是小端
异步日志的实现原理
构造函数可否调用虚函数,会有什么后果
Epoll/select 区别
定时器怎么实现
C++内存分区
Const作用,const对象在哪个段
多态/继承
空类对象大小/有虚函数的类对象大小
父类和子类是不是同一个虚函数表
线程和进程
虚拟内存的作用
分页分段的优点
CPU密集型和I/O密集型指令同时到来先执行哪个
URL输入到页面返回发生了什么,用了什么协议
CVTE
计算机相关知识点整理相关推荐
- 计算机图形学知识点整理(一)
计算机图形学知识点整理 八叉树空间划分: 八叉树是一个存储结构,我们的游戏空间存储的是三维的实体,那么这个实体是如何表示的呢?八叉树用一个正方体包含了这个对象,并且将正方体切分成八个小的正方体.八叉树 ...
- Unity 之 解决包体过大问题记录和纹理相关知识点整理
Unity 之 解决包体过大问题记录和纹理相关知识点整理 一,发现问题: 二,分析问题: 三,解决问题 3.1 问题分析 3.2 解决方案一 3.3 解决方案二 四,相关知识: 4.1 纹理导入: 4 ...
- JS事件相关知识点整理
JS事件相关知识点整理 JS事件的驱动机制 常见JS事件 点击事件---onclick 焦点事件 获取焦点事件---onfocus 失去焦点事件----onblur 域内容改变事件---onchang ...
- 计算机基础计算机网络相关知识点整理
计算机基础 计算机分类 超级计算机 工业控制计算机 网络计算机 个人计算机 嵌入式计算机 计算机硬件-CPU 计算机指标 参考指标:主频.缓存.核数 CPU分类 按厂商:intel AMD 按接口:L ...
- 计算机导论知识点整理笔记(一.数据结构)
数据的逻辑结构.物理结构 数据的逻辑结构:描述的是数据元素之间的逻辑关系.数据的逻辑结构分为三种典型结构: 集合:元素间为松散的关系,只是同属于一个集合而已. 线性结构 线性结构的逻辑特征是有且仅有一 ...
- Sentinel 相关知识点整理
1.Sentinel (分布式系统的流量防卫兵) 是阿里开源的一套用于服务容错的综合性解决方案. 它以流量为切入点, 从流量控制.熔断降级.系统负载保护等多个维度来保护服务的稳定性. 2.使 ...
- Keras相关知识点整理(tensorflow2.4)
具体的一些知识点还是从官方文档获取, 博主只列举几个常用的.毕竟17年的时候就曾用keras落地过实际项目,后来被集成到tensorflow2.x里了,对此框架还是有一定的了解. 应用 Applic ...
- python求正方体体积_「高中数学」简单几何体的面积与体积相关知识点整理+例题...
一.知识要点 (一)圆柱.圆锥.圆台的侧面积 将侧面沿母线展开在平面上,则其侧面展开图的面积即为侧面面积. 1.圆柱的侧面展开图--矩形 圆柱的侧面积 2.圆锥的侧面展开图--扇形 圆锥的侧面积 3. ...
- 进程、线程相关知识点整理
什么是进程 进程是一个具有独立功能的程序关于某个数据集合的一次运行活动.它可以申请和拥有系统资源,是一个动态的概念,是一个活动的实体. 进程是一个"执行中的程序".程序是一个没有生 ...
最新文章
- fullPage教程 -- 整屏滚动效果插件 fullpage详解
- ssm商务会员管理系统_会员管理商城开发
- javascript感叹号1_「翻译」JavaScript的可视化学习之三:作用域(链)
- Vue生命周期与自定义组件
- 解决MySQL下把结果导出到文件权限不足问题
- linux重定向命令语法,linux重定向命令应用及语法
- 面试官:换人!赶快换人!连CopyOnWriteArrayList都没听过!确实没听过
- gns3gns3下载_如何将Gns3安装到Fedora?
- C#list转JSON(Newtonsoft.Json.dll)(仅做记录)
- 第一章 计算机网络概述(计算机网络韩立刚)
- 打太极不协调的二三事
- go语言入门(转载自开源社区)
- 音乐指纹识别(一):音乐波形
- 【Practical】ZFC七公理
- 在recovery下挂载/system以使用adb
- 微信小程序如何从数组里取值_微信小程序 怎么数组里面值
- oracle11g回闪,oracle11gdroptable后闪回-Oracle
- Postgresql skip locked跳过行锁消除行锁冲突等待
- jQuery插件开发全解析,jQuery.extend , (function($){ , $.fn.pluginName
- 抖音上很火的3D立体动态相册
热门文章
- Proteus 7.8 实现时钟 由6片74LS60组成
- 【算法题解】 Day1 前缀和
- IDEA2020创建Maven项目卡在[INFO] Generating project in Batch mode状态(Maven配置阿里镜像)
- pycharm配置python环境变量详细步骤
- 烟台.Net俱乐部10月活动Asp.Net实战之旅 威海行
- html password 默认值,html slelect 标签默认值
- 用Python来设计二维码(表白神器)
- 河南省3加2计算机学校,河南省有哪些3+2学校
- adsp21489 CCES修改kernel file使得SPI启动速度加快
- ISO20000认证去哪好,ISO20000认证条件