源码地址
https://github.com/syndtr/goleveldb
参考献
1.https://leveldb-handbook.readthedocs.io/zh/latest/index.html
使用epoll_create创建一个eventpoll结构体,每一个epoll对象都有一个eventpoll结构体,用于存放epoll_ctl添加的event,这样子可以识别重复的event,添加到epollpoll的event都会和设备驱动程序建立会调用关系,当相应的事件发生时调用相应的回调方法,将发生的event添加到一个rdlist双向链表中。
每次轮询只要查看rdlist是否为空即可。
在2.6.8版本之前,epoll的底层实现是hash,所以需要指定一个size大小,从2.6.8版本开始,epoll的底层实现是红黑树,size大小被忽略,但是必须比零大。
epoll_create返回一个文件描述符,用完之后要调用close。
epoll_ctl用于注册事件。1
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll_ctl有四个参数:
1 | uint32_t events; // epoll事件 |
其中events可以是
EPOLLIN, 对应的描述符可读。
EPOLLOUT, 对应的描述符可写。
EPOLLPRI,对应的描述符有紧急数据可读。
EPOLLERR, 对应的描述符发生错误。
EPOLLET,设置为ET模式。
EPOLLHUP,对应的描述符被挂断。
EPOLLRDHUP,。
等
epoll_wait监听使用epl_ctl添加的事件。1
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
其中有四个参数:
epoll有两个工作模式,一个是edge-triggered,另一个是level-triggered。为了区别它们的不同,给出一个场景:
如果添加这个描述符使用的是EPOLLET模式,那么对于epoll_wait的调用会继续阻塞,尽管它的输入缓冲区中还有数据。同时pipe的写端可能在等着对它已经发送数据的一个回复。这是因为ET模式只会在它监视的fd发生变化时才会报告event。
在上面的例子中,因为2中的写完成会产生一个event,然后这个evenet在3中被使用。因为4中的读没有把buffer中的所有数据读完,对于5中的epoll_wait可能会永远阻塞。
为了避免处理多个文件描述符的永远阻塞,建议使用epollET模式的方式,这两个应该是一块设置的:
而epoll的LT模式,比poll快,可以用在任何出现在poll出现的地方。
什么时候使用select而不使用epoll,epoll适用于连接特别多但是活跃连接少的场景,而select适用于连接多活跃连接也多的场景。
1.《UNIX程序员手册》
2.https://www.cnblogs.com/tianzeng/p/9997432.html
3.https://stackoverflow.com/questions/21111003/epoll-tcp-edge-triggered-necessity-of-last-read2-call
tcpdump是unix系统下一个命令行抓包工具,需要使用root权限。
host,指定一台主机。
port,指定端口。
net,指定网段。
src,指定目标地址。
dst,指定源地址。
tcp/udp/arp等,指定通信协议
-i指定网络接口
-D列出所有的网口,linux网口命名(en开头的是以太网口,wl开头的是无线网卡)。
-S用绝对值列出TCP的序号。
-n使用数字名字而不是主机名字(比如说https的端口号使用443而不是https)。
-c num指定接收num个包。
查看man手册。
Flag中
S是SYN,
F是FIN,
P是PUSH,(表示发送方通知接收方通讯层应该尽快的将这个报文段交给应用层。一般传输层都是隔几个报文统一上交数据。设置了PUSH就是尽快上交。)
R是RST,
U是URG,
W是ECN CWR
E是ECN-Echo
.是ACK。
win是对方接收缓冲区的大小。
在本机上运行sudo tcpdump udp dst port 53
,即指定目标端口号为53即可。
使用命令sudo tcpdump host www.baidu.com -c 10 -n -vv
可以查看TCP连接的前10个报文。
1.https://blog.csdn.net/renrenhappy/article/details/5929702
2.https://segmentfault.com/a/1190000002554673
什么是hash,给定不定长的输入,得到定长的输出。
哈希的实现方式:数组,链表等。
hash可以用来干嘛?
碰撞:给定不同的输入,得到相同的输入。这是一定会发生的,因为输入的范围是无穷大的。
解决碰撞的方法:
线性探测法:
二次散列法:
开链法:SGI STL就采用这种方法。
SGI STL中,通过维护一个链表的数组。每个链表叫做一个bucket,通过KEY对buckets取余计算bucket_number。所以不同的KEY可能会被映射到同一个bucket中,这就是冲突。
为了避免冲突太严重,STL中采用了一个方法,当元素的数量大于bucket的数量时,就要进行rehash。插入一个对象时,先判断是否需要rehash。如果需要rehash,需要对元素中的所有元素重新进行hash。
count怎么实现?find怎么实现?比如找某一个val,先找到val对应的key,找到相应bucket中的序号,判断这个bucket中的元素的key是否和要找的key相同。
hashtable的迭代器是ForwardIterator。
一致性hash经常用在服务器增加或者减少时,服务器失效的问题。为什么要取2的32次方?我的理解是,这样子的话,对于同一个数据key,不管有服务器的个数是多少,得到的数据的hash值都是一样的。所以增加服务器不影响数据的hash值。
1.《STL源码剖析》
2.https://zhuanlan.zhihu.com/p/34985026
C++ 对象模型:所有的nonstatic data member放在每一个类对象内。所有的static data member, staitc member function和member function都放在所有类对象之外。而对于虚函数,每一个类产生一堆指向虚函数表的指针,放在一个表中,叫做虚表。每一个类对象都会有一个虚指针指向虚表。如果加上继承的话,每一个类对象中还要放着一个虚基类对象的指针。
指向不同类型指针的差异,既不在指针表示不同,也不再其内容(代表一个地址)不同,而是在其所寻址出来的object类型不同。指针类型起到的作用是告诉编译器如果解释某个特定地址中内存内容和其大小。
转型其实只是一种编译器指令,大部分情况下他并不改变一个指针所含的真正地址,它只影响被指向内存的大小和其内容的解释方式。
一个指针或者引用之所以支持多态,是因为它们并不会引发内存中任何与类型有关的内存委托操作,会受到影响的只是它们所指向内存的“大小和内容解释方式”而已。
为什么引入explicit关键字,避免将一个单一参数的构造函数当成一个转换运算符。
默认构造函数会在编译器需要的时候被编译器产生出来。注意,这是编译器的需要,不是程序的需要。程序的需要需要程序员进行负责。
如果没有任何用户声明的构造函数,在合适的时候编译器会暗中声明一个non-trivial的构造函数。什么时候会合成non-trivil的构造函数?
四种情况:
在这四种情况之外的话并且没有声明任何构造函数的类,它们拥有implicit trivial default constructors,实际上并不会被合成出来。
三种情况,会调用拷贝构造函数:
default memberwise initialization是把每一个内建的或者派生的数据成员(例如指针和数组,就是一个派生的数据成员)的值,从某个object拷贝到另一个object。对于成员类对象,以递归的方式执行memberwise initialization(其实就是深拷贝)。
C++ 标准同样把拷贝构造函数分为trivial和non-trivial的,只有nontrivial的构造函数才会被合成到程序中,决定一个拷贝构造函数是不是nontrivial的,取决于类是否会展现bitwise copy semantics时。bitwise copy其实就是浅拷贝。
什么时候不展现处bitwise copy semantics:
调用拷贝构造函数,构造一个参数传递给函数,并且把函数原型改成接受引用参数。
函数返回值怎么获得?一般都是在函数内部声明一个临时对象,然后返回这个临时对象。编译器在处理的时候使用了:
双阶段转换:
直接使用一个result参数代替函数内部的临时对象,少了一次拷贝构造,这个也叫作name return value(NRV)优化。
为什么没有拷贝构造函数就不能实行NRV优化?(可能是因为NRV优化就是针对拷贝构造函数进行的,你连优化对象都没有了,还优化个毛线。)
我在gcc下进行测试,默认是打开了NRV优化的,但是如果return 指令没有发生在top level就会失效(可看文章最后的代码)。
如果某个类的拷贝构造函数被视为trivial(即没有带有拷贝构造函数的成员对象,或者基类对象,也没有虚函数和虚基类)。
那么memberwise的初始化会导致bitwisecopy,很快速很安全。
如果单从是否复制的角度来看,不用提供显式的拷贝构造函数。但是!!!如果需要大量的memberwise初始化操作,比如值传递,那么提供拷贝构造函数就可以使用NRV优化。
构造函数用来给函数设置初值,除了以下的四种情况,必须使用初始化列表进行初始化,其他情况下既可以在构造函数体内,还可以使用初始化列表。
初始化列表中到底发生了什么?
编译器会一一操作初始化列表,用适当的顺序在构造函数内任何用户显式的代码之前安插初始化操作。
还有,就是初始化顺序和初始化列表中的顺序无关,和在类中声明的顺序相关。
空类的大小为1,因为编译器在空类中安插了一个char字节,使得这两个对象在内存中的位置是独一无二的。标准规定空类的大小大于0。虚基类的大小是8字节,因为虚指针。
如果没有虚函数,但是有虚基类,派生类对一个空类进行继承,它的大小受到三个因素的影响(因为没有虚函数,所以没有虚指针):
在菱形继承中,X是空的虚基类,A和B派生自X,没有自定义的数据,Y派生自A和B。
在没有优化的时候派生类(A和B)的大小是虚基类指针大小,一个char,一个对齐,也就是8+1+3 = 12字节。A的大小是(虚基类指针大小,一个char,和派生自A和B指向,最后加起来总共是1+8+8+3 = 20。
空虚基类(empty virtual base class),它提供了一个虚接口,一个空的虚基类被看成派生类的最开始的一部分,也就是说派生类不再是空类了。所以派生自虚基类的空类的大小就是一个指针的大小。在这里,A和B的大小都是一个指向虚基类指针的大小,是8字节。而Y是16字节。
两个防御性规则
这一节最重要的就是为什么要把类型的typedef放在类的最前面。虽然对于函数的定义来说,可以使用任何类中声明的对象,但是对函数的声明是按照类中的声明顺序进行解析的!所以如果把类型的typedef放在最后,在解析函数声明的时候就可能出错(如果在函数声明出现之前没有找到某个类型,就会查找类外部的作用域)。
**非静态数据成员直接存放在每一个类对象中,只能通过class object(不管是直接的还是简介的)访问。**比如在member function中,访问nonstatic data member,实际上是经过一个隐式的this指针进行的。
通过指针和.运算访问nontstaic data member,访问的data member是一个struct member,一个clas member,单一继承或者多重继承的情况下效率都相同。
对一个nonstatic data member进行存取操作,编译器需要把类对象的起始地址加上偏移量。每一个nonstatic data member的偏移量在编译时就知道,即使它属于一个基类子对象。因此,存取一个nonstatic data member的效果和存取C结构体或者一个费派生类的成员是一样的。
但如果某个data member是虚基类的成员,指针的存取速度会慢一些。
在C++ 继承模型中,一个派生类对象表现出来的,是自己的members加上基类data member的总和。它们的排列顺序没有要求,在大部分编译器中,base class member总是出现在上面。属于虚基类的data member除外。
对于虚继承来说,无论一个虚基类在继承体系中出现了多少次,派生类中只会含有一个虚基类子对象。
类的member function有三种:static member function, nonstatic member function和virtual function。
static member funciton的主要特性是没有this指针,它的其他特性统统来自主要特性:
nonstatic member function的访问效率必须和一般的nonmember function效率相同。这个是怎么实现的呢?编译器通过将nonstatic member function函数实体转换成对等的nonmember function函数实体:
对于一个virtual函数来说,比如1
2Point3d obj, *ptr = &obj;
ptr->normalize(); //normalized虚函数
会被转化成:1
(*ptr->vptr[1])(ptr);
其中第一个ptr表示指针ptr,vptr是虚指针,指向一个虚表,1是normalize()虚函数在虚函数表中的位置,ptr表示传递给this指针的实参。
在一个虚函数内调用另一个虚函数,会比较快。
而对于通过成员访问运算符调用的虚函数,因为它不支持多态,所以会把虚函数当做普通函数进行解析。
对于static memeber function来说,通过指针或者成员访问运算符调用,编译器会将它们转换成一般的nonmember function调用。
如果取一个static member funciton的地址,获得的是一个地址。因为static member function没有this指针,所以地址类型不是一个指向class member function的地址,而是一个nonmember function指针。
virtual function的一般实现模型:每一个class有一个虚表,包含该class中的virtual function地址,每个对象都有一个vptr,指向virtual table的所在。
NRV被关闭:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51#include <iostream>
using std::cout;
using std::endl;
class X
{
public:
X():_val(0)
{
cout << "X()" << endl;
}
X(int val): _val(val)
{
cout << "X()" << endl;
}
X(const X& x)
{
cout << "X(const X& x)" << endl;
}
private:
int _val;
};
X bar(int a)
{
if(a % 2)
{
X x = X(a);
return x;
}
X x;
return x;
}
int main()
{
X t = bar(3);
return 0;
}
上述代码会调用一次构造函数,一次拷贝构造。注意,这个构造函数是临时对象x调用的。t调用的是拷贝构造函数。
1.《深入探索C++对象模型》
一般来说,C++ 对象的分配都是通过new expression来实现的,new expression通过调用operator new函数分配空间,然后调用相应的构造函数。同理释放的时候,通过delete,先调用operator delete,然后调用相应的析构函数。
STL allocator的把这两部分给分开,调用allocate()分配空间,然后调用construct()进行构建,construct()的话一般来说是负责调用构造函数。
而allocate的工作就要多一些,当然也可以少一些,比如直接调用operator new或者malloc分配。这样子的话可能效率要低一下,有内存碎片,overload比较大。
所以就有了下面的分配器。
SGI alloc allocator的实现是一个两级空间配置器,第一级配置器负责分配128字节以上的空间,以及异常处理。第一级配置器使用malloc而不是operator new进行内存分配的原因,可能是C++没有realloc,因为没有使用new,所以就不能使用new_handler,需要自己实现。需要客户端自己设置一个oom_handler。
第二级分配器采用内存池的方式,维护16个链表,每个链表负责分配一个小于128字节的8字节整数倍(8, 16, …, 128个字节)的小区块。初始时是它们都是空指针。(采用嵌入式指针节约空间)
接下来,如果要分配1个64字节大小对象的空间。它会向负责64字节的链表要空间,如果链表上没有足够的空间。它会向内存池要空间,而且要的不止一个,设置为20。如果内存池中有足够的空间,就返回,没有的话,满足一个也行,就返回一个对象。如果一个也没有的话,就向malloc要空间。malloc也没有的话,检测大于64字节的那些链表,是不是有空间。如果还没有的话,就调用第一级空间配置器,调用oom异常处理对象。
这里使用到了type_traits进行重载。(还有哪里使用了type_traits?)
uninitialized_copy(InputForwardIterator, InputIterator, ForwardIteartor);
uninitialized_fill(ForwardIterator, ForwardIterator, const T& x);
uninitialized_fill_n(ForwardIterator , size_type n, const T& x )
这三个函数,会负责对相应的位置进行初始化,通过type_traits判断是否是POD类型,执行内存,或者是调用构造函数。
1.《STL源码剖析》
套接字是通信端点的抽象,是软件接口,是传输层和网络层之间的接口。正如使用文件描述符访文件,使用套接字描述符访问套接字。套接字描述符本质上就是一个文件描述符,但是不是所有参数为文件描述符的函数都可以接收套接字描述符(比如说套接字不支持文件偏移量,所以lseek等函数就不支持)。
大端:最大字节出现在最低地址位。
小端:最小字节出现在最低地址位。
TCP/IP协议栈使用大端字节序,Linux x86使用小端。
四个字节序转换函数:
h表示host,n表示network,l表示long(32位整数), s表示short(16位整数)
inet_pton
inet_ntop
bind一般是在服务器端需要调用,要指定周知端口号(01023),一般服务器的IP地址设置为INADDR。而客户端一般是由内核指定一个端口号,这个端口号是动态或者私用的(4915265536)。还有中间的已登记端口号。
如果地址已经被使用,就会返回EADDRINUSE,可以使用SO_REUSEADD和SO_REUSEPORT。
listen,服务器执行被动打开(LISTEN)。listen的第二个参数是已完成的等待被accept的最大数量(不知道怎么回事,我在linux测试没有成功)。
SYN泛洪攻击。
connect,调用时从CLOSED转到SYN_SENT(从套接字创建以来是CLOSED),如果返回成功是ESTABLISHED。如果connect失败,这个套接字不可用,需要close它,然后重新打开。
connect出错返回的三个可能:
accept从已完成连接的队列取一个连接回来。可能导致accept返回错误的情况:
刚开始,一直以为accept返回的一个套接字描述符,使用了新的IP地址,因为使用了fork。后来才发现自己好傻逼,fork是创建了一个子进程,有一个新的pid,而并不影响这个套接字的四元组。。
这也是为什么如果主机重启之后,对面会发送一个RST,因为你连接的还是那个端口号,只不过是刚才的那个连接已经断开了,这就是发送一个RST的三个条件之一。
close关闭一个套接字,默认行为是把该套接字标记成关闭,然后立即返回到调用进程,该套接字不能在由调用进程使用,也就是说他不能作为read和write的第一个参数。但是TCP将尝试发送已经排队等待发送到对端的任何数据,发送完毕后执行正常的四次挥手。
如果客户端和服务器连接,杀死服务器子进程。这时,服务器子进程调用exit,关闭所有文件描述符,导致服务器子进程向客户端发送一个FIN报文。然后客户端就处于CLOSE_WAIT状态了。
如果客户端继续向服务器发送报文,是允许的。这个时候TCP会返回一个RST(因为这个时候TCP收到了一个根本不存在的连接上的报文)。这个RST会不会被客户端看到取决于接下来用户从套接字读时,取决于客户端先收到由于FIN产生的EOF还是RST产生的。
如果进程第二次写,也就是收到了RST之后,内核会向这个进程发送一个SIGPIPE信号。SIGPIPE的默认行为是终止进程,所以进程为了不让程序崩溃,必须捕获它。无论捕获的操作是设置一个信号处理程序还是简单的忽略,写操作都会收到一个EPIPE错误。
假设主机崩溃,已有的网络连接上发不出任何东西。
主机崩溃了,然后重启,即客户端完全不知道服务端出过事情。
如果客户端不给服务端发送数据,客户就不知道服务器崩溃。
如果客户发送了数据,这时服务器TCP会返回一个RST,因为服务器丢失了崩溃前的所有连接,就会返回ECONNRESET。
和服务器进程被终止是一样的,因为服务器子进程被关闭,文件描述符被关闭。
1.《APUE》
2.《UNP》卷一
Linux及城建通信可以分为两类,一类是同一台主机之间的进程间通信,另一类是网络之间的进程通信。
同一台主机之间的进程通信有以下几种:
而网络通信主要就是套接字通信。
管道的局限性:
FIFO没有第二种局限性,UNIX域套接字两种局限性都没有。
popen
和pclose
popen创建一个FILE stream指针。
指定参数’r’从cmd的标准输出读,指定参数’w’向cmd的标准输入写。
UNIX过滤程序从标准输入读取数据,向标准输出读取数据。
当一个过滤程序即产生某个过滤程序的输入,又读取该过滤程序的输出时,它就变成了协同进程。
popen只提供连接到另一个进程的标准输入或者标准输出的单向通道,而协同新城有连接到另一个进程的两个单向管道:一个连接到其标准输入,另一个则连接到它的标准输出。
相当于我们把数据写入一个进程的标准输出,经过它的处理后,又从它的标准输出读出。(可以通过创建两个管道来实现,一个管道向该进程的标准输入写,另一个管道从该进程的标准输出读。)
注意如果使用标准IO函数测试的话,可能要注意缓冲区的设置。
FIFO也叫命名管道。但是管道只能在两个相关的进程之间使用,而FIFO可以在任意两个进程之间交换数据。
创建一个FIFO之后,需要使用open打开它。
如果不指定O_NONBLOCK,只读open会阻塞到别的进程为写打开这个FIFO为止,而只写open会阻塞到别的进程为读而打开这个FIFO为止。
如果指定O_NONBLOCK,只读open总是返回成功。而如果没有其他进程为读打开这个FIFO,返回-1,置位errno为ENXIO。
FIFO的用处:
XSI IPC包含三种,分别是消息队列,信号量,共享内存。IPC通过IPC描述符进行访问(和文件描述很像)。但是和文件不同的是,IPC都没有名字,所以要创建多个IPC,怎么区分它们,这个就是key(键)的作用,每一个IPC都有一个键(和文件名字很像)。
问题就是怎么让通信的进程知道它们要使用的IPC描述符?
不能使用IPC_PRIVATE作为一个键来引用消息队列,引用消息队列时要绕过get函数。
消息队列
XSI信号量是一个计数器,用于为多个进程提供对共享数据的访问。但是XSI信号量不是单个信号量,而是一个信号量的集合。
需要使用semget
创建一个信号量集合(数量可以等于1)。
然后使用semget
获得一个信号量描述符(成功创建时返回的也是信号量描述符)。
接下来使用semctl
设置每个信号量的值。
使用semop
控制信号量值的增和减。
XSI信号量的缺点:
共享内存允许两个或者多个进程共享一个给定的存储区。因为数据不需要在客户进程和服务器进程之间进行复制,这是最快的一种IPC。
使用共享内存时,需要注意的是,在多个进程之间同步访问一个给定的存储区。通常使用信号量同步共享内存,当然也可以使用互斥量和记录锁,线程锁共享。
共享内存和存储IO映射的区别,用mmap映射的存储段是和文件相关的,而XSI共享内存并没有这种限制。
若果需要两个进程间的双向数据流,可以使用消息队列和全双工管道。
信号量,记录锁和互斥量的比较。如果在多个进程中共享同一个资源,可以使用这三种方法的任意一种来实现。
1.《APUE》第三版
2.https://www.cnblogs.com/my_life/articles/4538299.html