├── .gitignore ├── C-Program ├── "static" in c program.md └── README.md ├── CPlusPlus-Program ├── README.md ├── new_vs_malloc.md └── virtual_function_in_c++.md ├── Database ├── README.md └── redis.md ├── Interprocess-communication ├── Half-duplex UNIX Pipes.md ├── README.md └── fifo.md ├── Network-programming ├── Netty.md ├── README.md ├── RPC-concept.md └── rpc.md ├── Network ├── README.md ├── tcp-1.md ├── tcp-2.md ├── tcp-3.md └── tcp-4.md ├── README.md ├── creative └── image │ ├── concurrency.jpg │ ├── network-programing │ ├── netty-1.png │ ├── netty-2.jpeg │ ├── rpc.jpeg │ ├── rpc01.png │ ├── rpc02.png │ └── rpc03.png │ ├── network │ ├── tcp-windows.png │ ├── tcp_client_window.png │ ├── tcp_finish.png │ ├── tcp_flag.png │ ├── tcp_hand.png │ ├── tcp_header.png │ ├── tcp_kp.png │ ├── tcp_new_reno.png │ ├── tcp_resend.png │ ├── tcp_sack.png │ ├── tcp_send.png │ ├── tcp_seq.png │ ├── tcp_slide.png │ ├── tcp_slow_start.png │ ├── tcp_status.png │ ├── tcp_vegas.png │ └── thread_lock.png │ ├── parallelism.jpg │ └── thread_status.png └── multithreaded programming ├── README.md ├── posix_thread_basic_01.md ├── posix_thread_basic_02.md ├── posix_thread_basic_03.md ├── pthread_api.md ├── pthread_cond_basic.md ├── pthread_examples.md └── pthread_mutex_basic.md /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | 3 | *.DS_Store 4 | -------------------------------------------------------------------------------- /C-Program/"static" in c program.md: -------------------------------------------------------------------------------- 1 | 2 | #### 程序结构 3 | C 程序由下列部分组成: 4 | 5 | * 正文段 —— CPU 执行的机器指令部分;一个程序只有一个副本;只读,防止程序由于意外事故而修改自身指令 6 | * 初始化数据段(数据段)—— 在程序中所有赋了初值的全局变量,存放在这里。 7 | * 非初始化数据段(bss段)—— 在程序中没有初始化的全局变量;内核将此段初始化为0。 8 | * 栈 —— 增长方向:自顶向下增长;自动变量以及每次函数调用时所需要保存的信息(返回地址;环境信息) 9 | * 堆 —— 动态存储分。 10 | 11 | #### 变量类型 12 | 变量可以分为:全局变量、静态全局变量、静态局部变量和局部变量 13 | 14 | 按存储区域分,全局变量、静态全局变量和静态局部变量都存放在内存的静态存储区域,局部变量存放在内存的栈区。 15 | 16 | 按作用域分,全局变量在整个工程文件内都有效;静态全局变量只在定义它的文件内有效;静态局部变量只在定义它的函数内有效,只是程序仅分配一次内存,函数返回后,该变量不会消失;局部变量在定义它的函数内有效,但是函数返回后失效。 17 | 18 | 19 | #### 静态全局变量 20 | 在全局变量之前加上关键字 static,全局变量就被定义成为一个静态全局变量。 21 | 22 | 1. 内存中的位置:静态存储区(静态存储区在整个程序运行期间都存在) 23 | 2. 初始化:未经初始化的静态变全局量会被程序自动初始化为0(自动对象的值是任意的,除非他被显示初始化) 24 | 3. 作用域:静态全局变量在声明他的文件之外是不可见的。准确地讲从定义之处开始到文件结尾。 25 | 26 | 27 | 定义静态全局变量的好处: 28 | 29 | 1. 不会被其他文件所访问,修改,降低模块间的耦合度 30 | 2. 其他文件中可以使用相同名字的变量,不会发生冲突。 31 | 32 | 33 | #### 静态局部变量 34 | 在局部变量之前加上关键字 static,局部变量就被定义成为一个静态局部变量。 35 | 36 | 1. 内存中的位置:静态存储区 37 | 2. 初始化:未经初始化的静态局部变量会被系统初始化为0 38 | 3. 作用域:作用域仍为局部作用域,当定义它的函数或者语句块结束的时候,作用域随之结束。 39 | 40 | 注:当 static 用来修饰局部变量的时候,它就改变了局部变量的存储位置,从原来的栈中存放改为静态存储区。但是静态局部变量在离开作用域之后,并没有被销毁,而是仍然驻留在内存当中,直到程序结束,只不过我们不能再对他进行访问。当 static 用来修饰全局变量的时候,它就改变了全局变量的作用域(在声明他的文件之外是不可见的),但是没有改变它的存放位置,还是在静态存储区中。 41 | 42 | #### 静态函数 43 | 在函数的返回类型前加上关键字static,函数就被定义成为静态函数,也称内部函数。 44 | 45 | static 函数类型 函数名(函数参数表) 46 | {……} 47 | 48 | 函数的定义和声明默认情况下是 extern 的,但静态函数只是在声明他的文件当中可见,不能被其他文件所用。 49 | 50 | 定义静态函数的好处: 51 | 52 | 1. 其他文件中可以定义相同名字的函数,不会发生冲突 53 | 2. 静态函数不能被其他文件所用。 54 | 55 | 存储说明符 auto,register,extern,static,对应两种存储期:自动存储期和静态存储期。auto 和 register 对应自动存储期,具有自动存储期的变量在进入声明该变量的程序块时被建立,它在该程序块活动时存在,退出该程序块时撤销。关键字 extern 和 static 用来说明具有静态存储期的变量和函数。用 static 声明的局部变量具有静态存储持续期(static storage duration),或静态范围(static extent)。虽然他的值在函数调用之间保持有效,但是其名字的可视性仍限制在其局部域内。静态局部对象在程序执行到该对象的声明处时被首次初始化。 56 | 57 | #### 外部函数 58 | 59 | 外部函数的定义:在定义函数时,如果没有加关键字“static”,或冠以关键字“extern”,表示此函数是外部函数: 60 | 61 | [extern] 函数类型 函数名(函数参数表) 62 | {……} 63 | 64 | 调用外部函数时,需要对其进行说明: 65 | 66 | [extern] 函数类型 函数名(参数类型表)[,函数名2(参数类型表2)……]; 67 | 68 | static 函数与普通函数作用域不同。仅在本文件。只在当前源文件中使用的函数应该说明为内部函数(static),内部函数应该在当前源文件中说明和定义。 69 | 对于可在当前源文件以外使用的函数,应该在一个头文件中说明,要使用这些函数的源文件要包含这个头文件。 70 | 71 | 72 | #### 区别 73 | 74 | static 全局变量与普通的全局变量:static 全局变量只初使化一次,防止在其他文件单元中被引用 75 | 76 | static 局部变量和普通局部变量:static 局部变量只被初始化一次,下一次依据上一次结果值 77 | 78 | static 函数与普通函数:static 函数在内存中只有一份,普通函数在每个被调用中维持一份拷贝 79 | 80 | 全局变量和静态变量如果没有手工初始化,则由编译器初始化为0。局部变量的值不可知。 81 | -------------------------------------------------------------------------------- /C-Program/README.md: -------------------------------------------------------------------------------- 1 | ## C语言编程 2 | 3 | * [C程序中的Static关键字](https://github.com/AngryHacker/Rookie-Note/blob/master/C-Program/%22static%22%20in%20c%20program.md) 4 | 5 | -------------------------------------------------------------------------------- /CPlusPlus-Program/README.md: -------------------------------------------------------------------------------- 1 | ## C++语言编程 2 | 3 | * [new VS malloc](https://github.com/AngryHacker/Rookie-Note/blob/master/CPlusPlus-Program/new_vs_malloc.md) 4 | 5 | * [Virtual工作原理](https://github.com/AngryHacker/Rookie-Note/blob/master/CPlusPlus-Program/virtual_function_in_c++.md) 6 | 7 | -------------------------------------------------------------------------------- /CPlusPlus-Program/new_vs_malloc.md: -------------------------------------------------------------------------------- 1 | ### malloc 2 | 3 | 全称:memory allocation(动态内存分配) 4 | 5 | 原型: `extern void *malloc(unsigned int num_bytes); ` 6 | 7 | 作用:分配长度为 num_bytes 字节的内存块。如果分配成功则返回指向被分配内存的指针,分配失败返回空指针 NULL。当内存不再使用时,应使用 free() 函数将内存块释放。 8 | 9 | `void *malloc(int size); ` 说明:malloc 向系统申请分配指定 size 个字节的内存空间,返回类型是 void* 类型。void* 表示未确定类型的指针。C/C++规定,void* 类型可以强制转换为任何其它类型的指针。 10 | 11 | 备注:void* 表示未确定类型的指针,因为申请内存空间时还不知道用户是用这段空间来存储什么类型的数据。 12 | 13 | `void free(void *FirstByte);` 该函数是将之前用 malloc 分配的空间还给程序或者是操作系统,也就是释放了这块内存,让它重新得到自由。 14 | 15 | #### 注意事项: 16 | 17 | 1. 申请了内存空间后,必须检查是否分配成功。 18 | 2. 不需要再使用申请的内存时,必须释放;释放后应该把指向这块内存的指针指向 NULL,防止程序后面不小心使用了它。 19 | 3. 如果申请后不释放就是内存泄露;如果无故释放那就是什么也没有做。释放只能一次,如果释放两次及两次以上会出现错误(释放空指针例外,释放空指针其实也等于啥也没做,所以释放空指针释放多少次都没有问题)。 20 | 4. 虽然 malloc() 函数的类型是 (void *), 任何类型的指针都可以转换成 (void *), 但是最好还是在前面进行强制类型转换,因为这样可以躲过一些编译器的检查。 21 | 22 | #### 执行时具体行为: 23 | 24 | malloc 从堆里面获得空间。也就是说函数返回的指针是指向堆里面的一块内存。操作系统中有一个记录空闲内存地址的链表。当操作系统收到程序的申请时,就会遍历该链表,然后就寻找第一个空间大于所申请空间的堆结点,然后就将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。 25 | 26 | ### new 27 | 28 | C++中,用 new 和 delete 动态创建和释放数组或单个对象。 29 | 30 | ```c++ 31 | int *pi = new int; // 这个 new 表达式在堆区中分配创建了一个整型对象,并返回此对象的地址,并用该地址初始化指针 pi 。 32 | ``` 33 | 34 | 初始化: 动态创建的对象可以用初始化变量的方式初始化。 35 | 36 | ```c++ 37 | int *pi = new int(100); // 指针pi所指向的对象初始化为100 38 | string *ps = new string(10,’9’); // *ps 为“9999999999” 39 | ``` 40 | 41 | 如果不提供显示初始化,对于类类型,用该类的默认构造函数初始化;而内置类型的对象则无初始化。 42 | 43 | 也可以对动态创建的对象做值初始化: 44 | 45 | ```c++ 46 | int *pi = new int( ); // 初始化为0 47 | int *pi = new int; // pi 指向一个没有初始化的int 48 | string *ps = new string( ); // 初始化为空字符串 (对于提供了默认构造函数的类类型,没有必要对其对象进行值初始化) 49 | ``` 50 | 51 | 撤销动态创建的对象: 52 | 53 | ```c++ 54 | delete 表达式释放指针指向的地址空间。 55 | delete pi ; // 释放单个对象 56 | delete [ ]pi; // 释放数组 57 | ``` 58 | 59 | 如果指针指向的不是 new 分配的内存地址,则使用 delete 是不合法的。 60 | 61 | `delete p;` 执行完该语句后,p 变成了不确定的指针,在很多机器上,尽管 p 值没有明确定义,但仍然存放了它之前所指对象的地址,然后 p 所指向的内存已经被释放了,所以 p 不再有效。此时,该指针变成了悬垂指针(悬垂指针指向曾经存放对象的内存,但该对象已经不存在了)。悬垂指针往往导致程序错误,而且很难检测出来。 62 | 63 | 一旦删除了指针所指的对象,立即将指针置为0,这样就非常清楚的指明指针不再指向任何对象。(零值指针:`int *ip=0;` ) 64 | 65 | #### 零值指针和 NULL 指针: 66 | 67 | 零值指针,是值是 0 的指针,可以是任何一种指针类型,可以是通用变体类型 void* 也可以是 char,int 等等。 68 | 空指针,其实空指针只是一种编程概念,就如一个容器可能有空和非空两种基本状态,而在非空时可能里面存储了一个数值是0,因此空指针是人为认为的指针不提供任何地址讯息。 参考:http://www.cnblogs.com/fly1988happy/archive/2012/04/16/2452021.html 69 | 70 | #### new 分配失败: 71 | 72 | 1993 年前,c++ 一直要求在内存分配失败时 operator new 要返回 0,现在则是要求 operator new 抛出 std::bad_alloc 异常。很多 c++ 程序是在编译器开始支持新规范前写的。c++ 标准委员会不想放弃那些已有的遵循返回 0 规范的代码,所以他们提供了另外形式的 operator new (以及 operator new[])以继续提供返回 0 功能。这些形式被称为“无抛出”,因为他们没用过一个 throw,而是在使用 new 的入口点采用了 nothrow 对象: 73 | 74 | ```c++ 75 | class widget { something; }; 76 | widget *pw1 = new widget; //分配失败抛出std::bad_alloc 77 | if (pw1 == 0) do something; // 这个检查一定失败 78 | widget *pw2 = new(nothrow)widget; //若分配失败返回0 79 | if (pw2 == 0) do something; //这个检查可能会成功 80 | ``` 81 | 82 | ### malloc 和 new 的区别 83 | 84 | new 返回指定类型的指针,并且可以自动计算所需要大小。 比如:  85 | 86 | ```c++ 87 | int *p;    88 | p = new int; //返回类型为int* 类型(整数型指针),分配大小为 sizeof(int);      89 | int* parr;    90 | parr = new int [100]; //返回类型为 int* 类型(整数型指针),分配大小为 sizeof(int) * 100; 91 | ``` 92 | 93 | 而 malloc 则必须要由我们计算字节数,并且在返回后强行转换为实际类型的指针。   94 | 95 | ```c++ 96 | int* p;    97 | //分配128个(可根据实际需要替换该数值)整型存储单元,并将这128个连续的整型存储单元的首地址存储到指针变量p中 98 | p = (int *) malloc (sizeof(int)*128); 99 | //分配12个double型存储单元,并将首地址存储到指针变量pd中 100 | double *pd = (double *) malloc (sizeof(double)*12); 101 | ``` 102 | 103 | malloc 只管分配内存,并不能对所得的内存进行初始化,所以得到的一片新内存中,其值将是随机的。 除了分配及最后释放的方法不一样以外,通过malloc或new得到指针,在其它操作上保持一致。 104 | 105 | #### 有了malloc/free为什么还要new/delete? 106 | 107 | * malloc 与 free 是 C++/C 语言的标准库函数,new/delete 是C++的运算符。它们都可用于申请动态内存和释放内存。 108 | * 对于非内部数据类型的对象而言,光用 maloc/free 无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于 malloc/free 是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于 malloc/free。因此C++语言需要一个能完成动态内存分配和初始化工作的运算符 new,以及一个能完成清理与释放内存工作的运算符 delete。注意 new/delete 不是库函数。我们不要企图用 malloc/free 来完成动态对象的内存管理,应该用 new/delete。由于内部数据类型的“对象”没有构造与析构的过程,对它们而言 malloc/free 和 new/delete 是等价的。 109 | * 既然 new/delete 的功能完全覆盖了 malloc/free,为什么 C++ 不把 malloc/free 淘汰出局呢?这是因为 C++ 程序经常要调用 C 函数,而 C 程序只能用 malloc/free 管理动态内存。如果用 free 释放“new创建的动态对象”,那么该对象因无法执行析构函数而可能导致程序出错。如果用 delete 释放 “malloc申请的动态内存”,结果也会导致程序出错,但是该程序的可读性很差。所以 new/delete 必须配对使用,malloc/free 也一样。 110 | 111 | 整理来自:网络,具体地址已不可考 -------------------------------------------------------------------------------- /CPlusPlus-Program/virtual_function_in_c++.md: -------------------------------------------------------------------------------- 1 | ## 虚函数的工作原理 2 | 3 | 虚函数的实现要求对象携带额外的信息,这些信息用于在运行时确定该对象应该调用哪一个虚函数。典型情况下,这一信息具有一种被称为 vptr(virtual table pointer,虚函数表指针)的指针的形式。vptr 指向一个被称为 vtbl(virtual table,虚函数表)的函数指针数组,每一个包含虚函数的类都关联到 vtbl。当一个对象调用了虚函数,实际的被调用函数通过下面的步骤确定:找到对象的 vptr 指向的 vtbl,然后在 vtbl 中寻找合适的函数指针。 4 | 5 | 虚拟函数的地址翻译取决于对象的内存地址,而不取决于数据类型(编译器对函数调用的合法性检查取决于数据类型)。如果类定义了虚函数,该类及其派生类就要生成一张虚拟函数表,即vtable。而在类的对象地址空间中存储一个该虚表的入口,占4个字节,这个入口地址是在构造对象时由编译器写入的。所以,由于对象的内存空间包含了虚表入口,编译器能够由这个入口找到恰当的虚函数,这个函数的地址不再由数据类型决定了。故对于一个父类的对象指针,调用虚拟函数,如果给他赋父类对象的指针,那么他就调用父类中的函数,如果给他赋子类对象的指针,他就调用子类中的函数(取决于对象的内存地址)。 6 | 7 | 每当创建一个包含有虚函数的类或从包含有虚函数的类派生一个类时,编译器就会为这个类创建一个虚函数表(VTABLE)保存该类所有虚函数的地址,其实这个 VTABLE 的作用就是保存自己类中所有虚函数的地址,可以把 VTABLE 形象地看成一个函数指针数组,这个数组的每个元素存放的就是虚函数的地址。在每个带有虚函数的类中,编译器秘密地置入一指针,称为 vpointer(缩写为 VPTR),指向这个对象的 VTABLE。 当构造该派生类对象时,其成员 VPTR 被初始化指向该派生类的 VTABLE,所以可以认为 VTABLE 是该类的所有对象共有的,在定义该类时被初始化;而 VPTR 则是每个类对象都有独立一份的,且在该类对象被构造时被初始化。 8 | 9 | 通过基类指针做虚函数调用时(也就是做多态调用时),编译器静态地插入取得这个 VPTR,并在 VTABLE 表中查找函数地址的代码,这样就能调用正确的函数使晚捆绑发生。为每个类设置 VTABLE、初始化 VPTR、为虚函数调用插入代码,所有这些都是自动发生的,所以我们不必担心这些。 10 | 11 | ```C++ 12 | #include 13 | 14 | using namespace std; 15 | 16 | class A 17 | { 18 | public: 19 | virtual void fun1() 20 | { 21 | cout << "A::fun1()" << endl; 22 | } 23 | virtual void fun2() 24 | { 25 | cout << "A::fun2()" << endl; 26 | } 27 | }; 28 | 29 | class B : public A 30 | { 31 | public: 32 | void fun1() 33 | { 34 | cout << "B::fun1()" << endl; 35 | } 36 | void fun2() 37 | { 38 | cout << "B::fun2()" << endl; 39 | } 40 | }; 41 | 42 | int main() 43 | { 44 | A *pa = new B; 45 | pa->fun1(); 46 | delete pa; 47 | 48 | system("pause"); 49 | return 0; 50 | } 51 | ``` 52 | 53 | 毫无疑问,调用了 B::fun1(),但是 B::fun1() 不是像普通函数那样直接找到函数地址而执行的。真正的执行方式是:首先取出 pa 指针所指向的对象的 vptr 的值,这个值就是 vtbl 的地址,由于调用的函数 B::fun1() 是第一个虚函数,所以取出 vtbl 第一个表项里的值,这个值就是 B::fun1() 的地址了,最后调用这个函数。因此只要 vptr 不同,指向的 vtbl 就不同,而不同的 vtbl 里装着对应类的虚函数地址,所以这样虚函数就可以完成它的任务,多态就是这样实现的。 54 | 55 | 而对于 class A 和 class B 来说,他们的 vptr 指针存放在何处?其实这个指针就放在他们各自的实例对象里。由于 class A 和 class B 都没有数据成员,所以他们的实例对象里就只有一个 vptr 指针。 56 | 57 | 优点讲了一大堆,现在谈一下缺点,虚函数最主要的缺点是执行效率较低,看一看虚拟函数引发的多态性的实现过程,你就能体会到其中的原因,另外就是由于要携带额外的信息(VPTR),所以导致类多占的内存空间也会比较大,对象也是一样的。 58 | 59 | 含有虚函数的对象在内存中的结构如下: 60 | 61 | ```c++ 62 | class A 63 | { 64 | private: 65 | int a; 66 | int b; 67 | public: 68 | virtual void fun0() 69 | { 70 | cout<<"A::fun0"< &fun0() 78 | a 79 | b 80 | ``` 81 | 82 | #### 直接继承 83 | 那我们来看看编译器是怎么建立 VPTR 指向的这个虚函数表的,先看下面两个类: 84 | 85 | ```C++ 86 | class base 87 | { 88 | private: 89 | int a; 90 | public: 91 | void bfun() 92 | { 93 | } 94 | virtual void vfun1() 95 | { 96 | } 97 | virtual void vfun2() 98 | { 99 | } 100 | }; 101 | 102 | class derived : public base 103 | { 104 | private: 105 | int b; 106 | public: 107 | void dfun() 108 | { 109 | } 110 | virtual void vfun1() 111 | { 112 | } 113 | virtual void vfun3() 114 | { 115 | } 116 | }; 117 | ``` 118 | 119 | 两个类的VPTR指向的虚函数表(VTABLE)分别如下: 120 | 121 | ```text 122 | base类 123 |             —————— 124 | VPTR——>    |&base::vfun1 | 125 |             —————— 126 |            |&base::vfun2 | 127 |             —————— 128 |        129 | derived类 130 |             ——————— 131 | VPTR——>    |&derived::vfun1 | 132 |             ——————— 133 |            |&base::vfun2    | 134 |             ——————— 135 |            |&derived::vfun3 | 136 |             ——————— 137 | ``` 138 | 每当创建一个包含有虚函数的类或从包含有虚函数的类派生一个类时,编译器就为这个类创建一个 VTABLE,如上图所示。在这个表中,编译器放置了在这个类中或在它的基类中所有已声明为 virtual 的函数的地址。如果在这个派生类中没有对在基类中声明为 virtual 的函数进行重新定义,编译器就使用基类 的这个虚函数地址。(在 derived 的 VTABLE 中,vfun2 的入口就是这种情况。)然后编译器在这个类中放置 VPTR。当使用简单继承时,对于每个对象只有一个 VPTR。VPTR 必须被初始化为指向相应的 VTABLE,这在构造函数中发生。 139 | 140 | 一旦 VPTR 被初始化为指向相应的 VTABLE,对象就"知道"它自己是什么类型。但只有当虚函数被调用时这种自我认知才有用。 141 | 142 | 没有虚函数类对象的大小正好是数据成员的大小,包含有一个或者多个虚函数的类对象编译器向里面插入了一个 VPTR 指针(void *),指向一个存放函数地址的表就是我们上面说的 VTABLE,这些都是编译器为我们做的我们完全可以不关心这些。所以有虚函数的类对象的大小是数据成员的大小加上一个VPTR指针(void *)的大小。 143 | 144 | ##### 总结一下VPTR 和 VTABLE 和类对象的关系: 145 | 每一个具有虚函数的类都有一个虚函数表 VTABLE,里面按在类中声明的虚函数的顺序存放着虚函数的地址,这个虚函数表 VTABLE 是这个类的所有对象所共有的,也就是说无论用户声明了多少个类对象,但是这个 VTABLE 虚函数表只有一个。 146 | 147 | 在每个具有虚函数的类的对象里面都有一个 VPTR 虚函数指针,这个指针指向 VTABLE 的首地址,每个类的对象都有这么一种指针。 148 | 149 | #### 虚继承 150 |      这个是比较不好理解的,对于虚继承,若派生类有自己的虚函数,则它本身需要有一个虚指针,指向自己的虚表。另外,派生类虚继承父类时,首先要通过加入一个虚指针来指向父类,因此有可能会有两个虚指针。 151 | 152 | ## 继承类的内存占用大小 153 | 154 | 首先,平时所声明的类只是一种类型定义,它本身是没有大小可言的。 因此,如果用sizeof运算符对一个类型名操作,那得到的是具有该类型实体的大小。 155 | 156 | 计算一个类对象的大小时的规律: 157 | 1. 空类、单一继承的空类、多重继承的空类所占空间大小为:1(字节,下同); 158 | 2. 一个类中,虚函数本身、成员函数(包括静态与非静态)和静态数据成员都是不占用类对象的存储空间的; 159 | 3. 因此一个对象的大小 ≥ 所有非静态成员大小的总和; 160 | 4. 当类中声明了虚函数(不管是1个还是多个),那么在实例化对象时,编译器会自动在对象里安插一个指针 vPtr 指向虚函数表 VTable; 161 | 5. 虚承继的情况:由于涉及到虚函数表和虚基表,会同时增加一个(多重虚继承下对应多个)vfPtr 指针指向虚函数表 vfTabl e和一个 vbPtr 指针指向虚基表 vbTable,这两者所占的空间大小为:8(或8乘以多继承时父类的个数); 162 | 6. 在考虑以上内容所占空间的大小时,还要注意编译器下的“补齐”padding的影响,即编译器会插入多余的字节补齐; 163 | 7. 类对象的大小 = 各非静态数据成员(包括父类的非静态数据成员但都不包括所有的成员函数)的总和 + vfptr 指针(多继承下可能不止一个) + vbptr指针(多继承下可能不止一个) + 编译器额外增加的字节。 164 | 165 | ##### 示例一:含有普通继承 166 | 167 | ```c++ 168 | class A 169 | { 170 | }; 171 | 172 | class B 173 | { 174 | char ch; 175 | virtual void func0() { } 176 | }; 177 | 178 | class C 179 | { 180 | char ch1; 181 | char ch2; 182 | virtual void func() { } 183 | virtual void func1() { } 184 | }; 185 | 186 | class D: public A, public C 187 | { 188 | int d; 189 | virtual void func() { } 190 | virtual void func1() { } 191 | }; 192 | 193 | class E: public B, public C 194 | { 195 | int e; 196 | virtual void func0() { } 197 | virtual void func1() { } 198 | }; 199 | 200 | int main(void) 201 | { 202 | cout<<"A="<vptr[1])` ),所以运行速度比较快;缺点是,当 A 的 virtual 成员比较多(比如1000个),而 B 重写的成员比较少(比如2个),这种时候,B 的 vtableB 的剩下的 998 个表项都是放 A 中的 virtual 成员函数的指针,如果这个派生体系比较大的时候,就浪费了很多的空间。 317 | 2. 按照函数名称查表,这种方案可以避免如上的问题;但是由于要比较名称,有时候要遍历所有的继承结构,时间效率性能不是很高。 318 | 3. 如果继承体系的基类的 virtual 成员不多,而且在派生类要重写的部分占了其中的大多数时候,用 C++ 的虚函数机制是比较好的; 319 | 但是如果继承体系的基类的 virtual 成员很多,或者是继承体系比较庞大的时候,而且派生类中需要重写的部分比较少,那就用名称查找表,这样效率会高一些,很多的 GUI 库都是这样的,比如 MFC,QT。 320 | 321 | ##### C++如何不用虚函数实现多态 322 | 可以考虑使用函数指针来实现多态 323 | 324 | ```c++ 325 | #include 326 | using namespace std; 327 | 328 | typedef void (*fVoid)(); 329 | 330 | class A 331 | { 332 | public: 333 | static void test() 334 | { 335 | printf("hello A\n"); 336 | } 337 | 338 | fVoid print; 339 | 340 | A() 341 | { 342 | print = A::test; 343 | } 344 | }; 345 | 346 | class B : public A 347 | { 348 | public: 349 | static void test() 350 | { 351 | printf("hello B\n"); 352 | } 353 | 354 | B() 355 | { 356 | print = B::test; 357 | } 358 | }; 359 | 360 | 361 | int main(void) 362 | { 363 | A aa; 364 | aa.print(); 365 | 366 | B b; 367 | A* a = &b; 368 | a->print(); 369 | 370 | return 0; 371 | } 372 | ``` 373 | 374 | 这样做的好处主要是绕过了 vtable。我们都知道虚函数表有时候会带来一些性能损失。 375 | 376 | 整理来自:[Reference](https://blog.csdn.net/hackbuteer1/article/details/7883531) 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | -------------------------------------------------------------------------------- /Database/README.md: -------------------------------------------------------------------------------- 1 | ## 数据库 2 | 3 | * [Redis](https://github.com/AngryHacker/Rookie-Note/blob/master/Database/redis.md) 4 | -------------------------------------------------------------------------------- /Database/redis.md: -------------------------------------------------------------------------------- 1 | ## Redis 2 | ### 数据库类型 3 | KV 存储,非关系型(NoSQL) 4 | 5 | ### 存储量级 6 | 在内存存储,存储数量不多,配置中的 maxmemory 控制使用的物理内存。单例能存储 Key 2.5亿个( https://redis.io/topics/faq ),一个key或是value大小最大是512M。 7 | 8 | 容量评估:[Redis容量评估模型](https://cloud.tencent.com/developer/article/1004898) 9 | 10 | 超出使用内存:受配置中 maxmemory-policy 策略控制,默认是 noeviction(访问出错,不做淘汰) 11 | 12 | ### 内存模型 13 | [Redis内存模型](https://www.cnblogs.com/kismetv/p/8654978.html) 14 | 15 | ### 线程模型 16 | 单线程,IO多路复用 17 | 18 | ### 读写速度 19 | 20 | 21 | ### 持久化机制 22 | [Redis 持久化](https://www.cnblogs.com/kismetv/p/9137897.html) 23 | 24 | ### 锁机制 25 | 因为 redis 是单线程,对 redis 的数据读取不需要锁。但可以利用 redis 提供的锁机制做并发控制。 26 | -------------------------------------------------------------------------------- /Interprocess-communication/ Half-duplex UNIX Pipes.md: -------------------------------------------------------------------------------- 1 | ### 管道概述及相关API应用 2 | #### 管道相关的关键概念 3 | 管道是 Linux 支持的最初 Unix IPC 形式之一,具有以下特点: 4 | 5 | * 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道; 6 | * 只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程); 7 | * 单独构成一种独立的文件系统:管道对于管道两端的进程而言,就是一个文件,但它不是普通的文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统,并且只存在与内存中。 8 | * 数据的读出和写入:一个进程向管道中写的内容被管道另一端的进程读出。写入的内容每次都添加在管道缓冲区的末尾,并且每次都是从缓冲区的头部读出数据。 9 | 10 | #### 管道的创建 11 | 12 | ```c 13 | #include 14 | int pipe(int fd[2]) 15 | ``` 16 | 该函数创建的管道的两端处于一个进程中间,在实际应用中没有太大意义,因此,一个进程在由 pipe() 创建管道后,一般再 fork 一个子进程,然后通过管道实现父子进程间的通信(因此也不难推出,只要两个进程中存在亲缘关系,这里的亲缘关系指的是具有共同的祖先,都可以采用管道方式来进行通信)。 17 | 18 | #### 管道的读写规则 19 | 管道两端可分别用描述字 fd[0] 以及 fd[1] 来描述,需要注意的是,管道的两端是固定了任务的。即一端只能用于读,由描述字 fd[0] 表示,称其为管道读端;另一端则只能用于写,由描述字 fd[1] 来表示,称其为管道写端。如果试图从管道写端读取数据,或者向管道读端写入数据都将导致错误发生。一般文件的 I/O 函数都可以用于管道,如 close、read、write 等等。 20 | 21 | ##### 从管道中读取数据: 22 | * 如果管道的写端不存在,则认为已经读到了数据的末尾,读函数返回的读出字节数为 0; 23 | * 当管道的写端存在时,如果请求的字节数目大于 `PIPE_BUF`,则返回管道中现有的数据字节数,如果请求的字节数目不大于 `PIPE_BUF`,则返回管道中现有数据字节数(此时,管道中数据量小于请求的数据量);或者返回请求的字节数(此时,管道中数据量不小于请求的数据量)。注:(`PIPE_BUF` 在`include/linux/limits.h` 中定义,不同的内核版本可能会有所不同。Posix.1 要求 PIPE_BUF 至少为512 字节,red hat 7.2 中为 4096)。 24 | 25 | ###### 验证 `PIPE_BUF` 大小: 26 | 27 | ```c 28 | #include 29 | #include 30 | #include 31 | 32 | int main() { 33 | uint32_t pipe_buf_size = pathconf("PIPE_BUF", _PC_PIPE_BUF); 34 | printf("The size of PIPE_BUF is: %d\n", pipe_buf_size); 35 | return 0; 36 | } 37 | ``` 38 | 39 | ###### 关于管道的读规则验证: 40 | 41 | ```c 42 | /* 验证写端关闭后数据一直存在,直到被读出 */ 43 | #include 44 | #include 45 | #include 46 | #include 47 | 48 | int main() 49 | { 50 | int pipe_fd[2]; 51 | pid_t pid; 52 | char r_buf[100]; 53 | char w_buf[4]; 54 | char* p_wbuf; 55 | int r_num; 56 | int cmd; 57 | 58 | memset(r_buf, 0, sizeof(r_buf)); 59 | memset(w_buf, 0, sizeof(r_buf)); 60 | p_wbuf = w_buf; 61 | if (pipe(pipe_fd) < 0) 62 | { 63 | printf("pipe create error\n"); 64 | return -1; 65 | } 66 | 67 | if ((pid = fork()) == 0) 68 | { 69 | printf("\n"); 70 | close(pipe_fd[1]); 71 | /* 确保父进程关闭写端 */ 72 | sleep(3); 73 | r_num = read(pipe_fd[0], r_buf, 100); 74 | printf( "read num is %d, the data read from the pipe is %d\n", r_num, atoi(r_buf)); 75 | close(pipe_fd[0]); 76 | exit(0); 77 | } else if (pid > 0) { 78 | /* 关闭读端 */ 79 | close(pipe_fd[0]); 80 | strcpy(w_buf,"111"); 81 | if (write(pipe_fd[1], w_buf, 4) != -1) 82 | printf("parent write over\n"); 83 | 84 | /* 关闭写端 */ 85 | close(pipe_fd[1]); 86 | 87 | printf("parent close fd[1] over\n"); 88 | 89 | sleep(10); 90 | } 91 | } 92 | ``` 93 | 94 | 输出结果: 95 | 96 | ```shell 97 | ➜ ipc gcc -o pipe1 pipe1.c 98 | ➜ ipc ./pipe1 99 | parent write over 100 | parent close fd[1] over 101 | 102 | read num is 4, the data read from the pipe is 111 103 | ``` 104 | 105 | ##### 向管道中写入数据: 106 | 107 | * 向管道中写入数据大于 `PIPE_BUF`(管道缓冲区大小) 时,linux 将不保证写入的原子性,这里的原子性指的是写操作的数据可能相互穿插。 108 | * 如果读进程不读走管道缓冲区中的数据,那么写操作将一直阻塞。 109 | 110 | 注:只有在管道的读端存在时,向管道中写入数据才有意义。否则,向管道中写入数据的进程将收到内核传来的 `SIFPIPE` 信号,应用程序可以处理该信号,也可以忽略(默认动作则是应用程序终止)。 111 | 112 | ###### 验证写端对读端存在的依赖性: 113 | 114 | ```c 115 | #include 116 | #include 117 | #include 118 | #include 119 | #include 120 | 121 | int main() 122 | { 123 | /* 捕捉管道断裂产生的 SIGPIPE 信号 */ 124 | signal(SIGPIPE, SIG_IGN); 125 | 126 | int pipe_fd[2]; 127 | pid_t pid; 128 | char r_buf[4]; 129 | char* w_buf; 130 | int writenum; 131 | int cmd; 132 | 133 | memset(r_buf, 0, sizeof(r_buf)); 134 | if (pipe(pipe_fd) < 0) 135 | { 136 | printf("pipe create error\n"); 137 | return -1; 138 | } 139 | if ((pid = fork()) == 0) { 140 | close(pipe_fd[0]); 141 | close(pipe_fd[1]); 142 | sleep(10); 143 | exit(0); 144 | } else if (pid > 0) { 145 | sleep(1); /* 等待子进程完成关闭读端的操作 */ 146 | close(pipe_fd[0]); 147 | w_buf = "111"; 148 | if ((writenum = write(pipe_fd[1], w_buf, 4)) == -1) { 149 | printf("write to pipe error\n"); 150 | } else { 151 | printf("the bytes write to pipe is %d \n", writenum); 152 | close(pipe_fd[1]); 153 | } 154 | } 155 | return 0; 156 | } 157 | ``` 158 | 159 | 输出结果: 160 | 161 | ```shell 162 | ➜ ipc gcc -o pipe2 pipe2.c 163 | ➜ ipc ./pipe2 164 | write to pipe error 165 | ``` 166 | 167 | 分析:管道的所有读端都被关闭后,依然向管道写,会产生 SIGPIPE 信号,系统默认是终止应用程序,这样就看不到错误是什么。上面程序加了 `signal(SIGPIPE, SIG_IGN);` 是捕捉该信号,并交给系统处理,这样会产生 write to pipe error 的提示。。因此,在向管道写入数据时,至少应该存在某一个进程,其中管道读端没有被关闭,否则就会出现上述错误。 168 | 169 | ###### 验证写入管道的原子性: 170 | 171 | ```c 172 | #include 173 | #include 174 | #include 175 | #include 176 | #include 177 | #include 178 | #include 179 | 180 | 181 | #define ERR_EXIT(m) \ 182 | do \ 183 | { \ 184 | perror(m); \ 185 | exit(EXIT_FAILURE); \ 186 | } while(0) 187 | 188 | #define TEST_SIZE 68*1024 189 | 190 | int main(void) 191 | { 192 | char a[TEST_SIZE]; 193 | char b[TEST_SIZE]; 194 | char c[TEST_SIZE]; 195 | 196 | memset(a, 'A', sizeof(a)); 197 | memset(b, 'B', sizeof(b)); 198 | memset(c, 'C', sizeof(c)); 199 | 200 | int pipefd[2]; 201 | 202 | int ret = pipe(pipefd); 203 | if (ret == -1) 204 | ERR_EXIT("pipe error"); 205 | 206 | pid_t pid; 207 | pid = fork(); 208 | if (pid == 0)//第一个子进程 209 | { 210 | close(pipefd[0]); 211 | ret = write(pipefd[1], a, sizeof(a)); 212 | printf("process a: pid=%d write %d bytes to pipe\n", getpid(), ret); 213 | exit(0); 214 | } 215 | 216 | pid = fork(); 217 | 218 | 219 | if (pid == 0)//第二个子进程 220 | { 221 | close(pipefd[0]); 222 | ret = write(pipefd[1], b, sizeof(b)); 223 | printf("process b: pid=%d write %d bytes to pipe\n", getpid(), ret); 224 | exit(0); 225 | } 226 | 227 | pid = fork(); 228 | 229 | 230 | if (pid == 0)//第三个子进程 231 | { 232 | close(pipefd[0]); 233 | ret = write(pipefd[1], c, sizeof(c)); 234 | printf("process c: pid=%d write %d bytes to pipe\n", getpid(), ret); 235 | exit(0); 236 | } 237 | 238 | 239 | close(pipefd[1]); 240 | 241 | sleep(1); 242 | int fd = open("test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644); 243 | /* 缓冲区大小 */ 244 | char buf[1024*4] = {0}; 245 | int n = 1; 246 | while (1) 247 | { 248 | ret = read(pipefd[0], buf, sizeof(buf)); 249 | if (ret == 0) 250 | break; 251 | printf("n=%02d pid=%d read %d bytes from pipe buf[4095]=%c\n", n++, getpid(), ret, buf[4095]); 252 | write(fd, buf, ret); 253 | 254 | } 255 | return 0; 256 | } 257 | ``` 258 | 259 | 输出结果: 260 | 261 | ``` 262 | ➜ ipc ./pipe3 263 | n=01 pid=23164 read 4096 bytes from pipe buf[4095]=C 264 | n=02 pid=23164 read 4096 bytes from pipe buf[4095]=C 265 | process c: pid=23167 write 69632 bytes to pipe 266 | n=03 pid=23164 read 4096 bytes from pipe buf[4095]=C 267 | n=04 pid=23164 read 4096 bytes from pipe buf[4095]=C 268 | n=05 pid=23164 read 4096 bytes from pipe buf[4095]=C 269 | n=06 pid=23164 read 4096 bytes from pipe buf[4095]=C 270 | n=07 pid=23164 read 4096 bytes from pipe buf[4095]=C 271 | n=08 pid=23164 read 4096 bytes from pipe buf[4095]=C 272 | n=09 pid=23164 read 4096 bytes from pipe buf[4095]=C 273 | n=10 pid=23164 read 4096 bytes from pipe buf[4095]=C 274 | n=11 pid=23164 read 4096 bytes from pipe buf[4095]=C 275 | n=12 pid=23164 read 4096 bytes from pipe buf[4095]=C 276 | n=13 pid=23164 read 4096 bytes from pipe buf[4095]=C 277 | n=14 pid=23164 read 4096 bytes from pipe buf[4095]=C 278 | n=15 pid=23164 read 4096 bytes from pipe buf[4095]=C 279 | n=16 pid=23164 read 4096 bytes from pipe buf[4095]=C 280 | n=17 pid=23164 read 4096 bytes from pipe buf[4095]=A 281 | n=18 pid=23164 read 4096 bytes from pipe buf[4095]=C 282 | n=19 pid=23164 read 4096 bytes from pipe buf[4095]=B 283 | n=20 pid=23164 read 4096 bytes from pipe buf[4095]=B 284 | n=21 pid=23164 read 4096 bytes from pipe buf[4095]=A 285 | n=22 pid=23164 read 4096 bytes from pipe buf[4095]=B 286 | n=23 pid=23164 read 4096 bytes from pipe buf[4095]=A 287 | n=24 pid=23164 read 4096 bytes from pipe buf[4095]=B 288 | n=25 pid=23164 read 4096 bytes from pipe buf[4095]=A 289 | n=26 pid=23164 read 4096 bytes from pipe buf[4095]=B 290 | n=27 pid=23164 read 4096 bytes from pipe buf[4095]=A 291 | n=28 pid=23164 read 4096 bytes from pipe buf[4095]=B 292 | n=29 pid=23164 read 4096 bytes from pipe buf[4095]=A 293 | n=30 pid=23164 read 4096 bytes from pipe buf[4095]=B 294 | n=31 pid=23164 read 4096 bytes from pipe buf[4095]=A 295 | n=32 pid=23164 read 4096 bytes from pipe buf[4095]=B 296 | n=33 pid=23164 read 4096 bytes from pipe buf[4095]=A 297 | process b: pid=23166 write 69632 bytes to pipe 298 | n=34 pid=23164 read 4096 bytes from pipe buf[4095]=B 299 | process a: pid=23165 write 69632 bytes to pipe 300 | n=35 pid=23164 read 4096 bytes from pipe buf[4095]=A 301 | n=36 pid=23164 read 4096 bytes from pipe buf[4095]=B 302 | n=37 pid=23164 read 4096 bytes from pipe buf[4095]=A 303 | n=38 pid=23164 read 4096 bytes from pipe buf[4095]=B 304 | n=39 pid=23164 read 4096 bytes from pipe buf[4095]=A 305 | n=40 pid=23164 read 4096 bytes from pipe buf[4095]=B 306 | n=41 pid=23164 read 4096 bytes from pipe buf[4095]=A 307 | n=42 pid=23164 read 4096 bytes from pipe buf[4095]=B 308 | n=43 pid=23164 read 4096 bytes from pipe buf[4095]=A 309 | n=44 pid=23164 read 4096 bytes from pipe buf[4095]=B 310 | n=45 pid=23164 read 4096 bytes from pipe buf[4095]=A 311 | n=46 pid=23164 read 4096 bytes from pipe buf[4095]=B 312 | n=47 pid=23164 read 4096 bytes from pipe buf[4095]=A 313 | n=48 pid=23164 read 4096 bytes from pipe buf[4095]=B 314 | n=49 pid=23164 read 4096 bytes from pipe buf[4095]=A 315 | n=50 pid=23164 read 4096 bytes from pipe buf[4095]=B 316 | n=51 pid=23164 read 4096 bytes from pipe buf[4095]=A 317 | ``` 318 | 319 | 分析:这里是开启了三个子进程,分别向管道写入 68×1024 大小的数据,远远大于管道缓冲区 PIPE_BUF,子进程 a 写入的数据都是字符 a,子进程 b 写入的数据都是字符 b,子进程 c 写入的数据都是字符 c。如果管道能保证原子性的话,那么写操作不会穿插进行,保证一种字符写完后才会出现另一种字符,但这里看到的是三种字符 abc 在过程中交叉出现,如果你打开运行目录下生成的 test.txt,也可以看到这一点。 320 | 321 | #### 管道应用实例 322 | 323 | ##### 实例一:用于shell 324 | 管道可用于输入输出重定向,它将一个命令的输出直接定向到另一个命令的输入。比如,当在某个 shell 程序(Bourne shell 或 C shell 等)键入 who│wc -l 后,相应 shell 程序将创建 who 以及 wc 两个进程和这两个进程间的管道。 325 | 326 | ```shell 327 | ➜ ipc who 328 | rookie :0 2016-01-08 09:24 (:0) 329 | rookie pts/1 2016-01-08 09:26 (ubuntu) 330 | rookie pts/19 2016-01-08 15:15 (ubuntu) 331 | rookie pts/20 2016-01-08 15:15 (ubuntu) 332 | ➜ ipc who | wc -l 333 | 4 334 | ``` 335 | 336 | ##### 实例二:用于具有亲缘关系的进程间通信 337 | 下面例子给出了管道的具体应用,父进程通过管道发送一些命令给子进程,子进程解析命令,并根据命令作相应处理。 338 | 339 | ```c 340 | #include 341 | #include 342 | #include 343 | #include 344 | #include 345 | 346 | int handle_cmd(int); 347 | 348 | int main() { 349 | int pipe_fd[2]; 350 | pid_t pid; 351 | char r_buf[4]; 352 | char* w_buf[256]; 353 | int childexit = 0; 354 | int i, cmd; 355 | 356 | memset(r_buf, 0, sizeof(r_buf)); 357 | if (pipe(pipe_fd) < 0) { 358 | printf("pipe create error\n"); 359 | return -1; 360 | } 361 | 362 | /* 子进程:解析从管道中获取的命令,并作相应的处理 */ 363 | if ((pid = fork()) == 0) { 364 | printf("\n"); 365 | close(pipe_fd[1]); 366 | sleep(2); 367 | 368 | while (!childexit) { 369 | read(pipe_fd[0], r_buf, 4); 370 | cmd = atoi(r_buf); 371 | if(cmd == 0) { 372 | printf("child: receive command from parent over\n now child process exit\n"); 373 | childexit = 1; 374 | } else if (handle_cmd(cmd) != 0) { 375 | printf("child end\n"); 376 | return -1; 377 | } 378 | sleep(1); 379 | } 380 | close(pipe_fd[0]); 381 | exit(0); 382 | } else if (pid > 0) { 383 | /* 父进程发送命令 */ 384 | close(pipe_fd[0]); 385 | w_buf[0] = "003"; 386 | w_buf[1] = "005"; 387 | w_buf[2] = "777"; 388 | w_buf[3] = "000"; 389 | for(i = 0;i < 4;i++) 390 | write(pipe_fd[1], w_buf[i], 4); 391 | close(pipe_fd[1]); 392 | exit(0); 393 | } 394 | } 395 | 396 | //下面是子进程的命令处理函数(特定于应用) 397 | int handle_cmd(int cmd) { 398 | /* cmd 应该在 0 到 256 之间 */ 399 | if ((cmd < 0) || (cmd > 256)) { 400 | printf("child: invalid command \n"); 401 | return -1; 402 | } 403 | printf("child: the cmd from parent is %d\n", cmd); 404 | return 0; 405 | } 406 | ``` 407 | 408 | #### 管道的局限性 409 | 管道的主要局限性正体现在它的特点上: 410 | 411 | * 只支持单向数据流 412 | * 只能用于具有亲缘关系的进程之间 413 | * 没有名字 414 | * 管道的缓冲区是有限的(管道制存在于内存中,在管道创建时,为缓冲区分配一个页面大小) 415 | * 管道所传送的是无格式字节流,这就要求管道的读出方和写入方必须事先约定好数据的格式,比如多少字节算作一个消息(或命令、或记录)等等 416 | 417 | 整理来自:[Reference1](http://www.ibm.com/developerworks/cn/linux/l-ipc/part1/index.html#authorN10019) 418 | [Reference2](http://www.cnblogs.com/mickole/p/3192461.html) 419 | 420 | (注:主要来自 Reference1, 但原文的程序示例中有大量错误,所以进行了不少修改和替换。如有问题,请指正。) 421 | -------------------------------------------------------------------------------- /Interprocess-communication/README.md: -------------------------------------------------------------------------------- 1 | ## 进程间通信 2 | * [Linux 进程间通信 —— 管道(一)](https://github.com/AngryHacker/ocean/blob/master/Interprocess-communication/%20Half-duplex%20UNIX%20Pipes.md) 3 | 4 | * [Linux 进程间通信 —— 管道(二)](https://github.com/AngryHacker/ocean/blob/master/Interprocess-communication/fifo.md) 5 | 6 | ### 其他资料 7 | [6 Linux Interprocess Communications](http://tldp.org/LDP/lpg/node7.html) 8 | -------------------------------------------------------------------------------- /Interprocess-communication/fifo.md: -------------------------------------------------------------------------------- 1 | ### 有名管道概述及相关API应用 2 | #### 有名管道相关的关键概念 3 | 管道应用的一个重大限制是它没有名字,因此,只能用于具有亲缘关系的进程间通信,在有名管道(named pipe 或 FIFO)提出后,该限制得到了克服。FIFO 不同于管道之处在于它提供一个路径名与之关联,以 FIFO 的文件形式存在于文件系统中。这样,即使与 FIFO 的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过 FIFO 相互通信(能够访问该路径的进程以及 FIFO 的创建进程之间),因此,通过 FIFO 不相关的进程也能交换数据。值得注意的是,FIFO 严格遵循先进先出(first in first out),对管道及 FIFO 的读总是从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如 `lseek()` 等文件定位操作。 4 | 5 | #### 有名管道的创建 6 | 7 | ```c 8 | #include 9 | #include 10 | int mkfifo(const char * pathname, mode_t mode) 11 | ``` 12 | 13 | 该函数的第一个参数是一个普通的路径名,也就是创建后 FIFO 的名字。第二个参数与打开普通文件的`open()` 函数中的 mode 参数相同。 如果 mkfifo 的第一个参数是一个已经存在的路径名时,会返回 EEXIST 错误,所以一般典型的调用代码首先会检查是否返回该错误,如果确实返回该错误,那么只要调用打开 FIFO 的函数就可以了。一般文件的 I/O 函数都可以用于 FIFO,如 close、read、write 等等。 14 | 15 | #### 有名管道的打开规则 16 | 有名管道比管道多了一个打开操作:open。 17 | 18 | FIFO的打开规则: 19 | 20 | 如果当前打开操作是为读而打开 FIFO 时,若已经有相应进程为写而打开该 FIFO,则当前打开操作将成功返回;否则,可能阻塞直到有相应进程为写而打开该 FIFO(当前打开操作设置了阻塞标志);或者,成功返回(当前打开操作没有设置阻塞标志)。 21 | 22 | 如果当前打开操作是为写而打开 FIFO 时,如果已经有相应进程为读而打开该 FIFO,则当前打开操作将成功返回;否则,可能阻塞直到有相应进程为读而打开该 FIFO(当前打开操作设置了阻塞标志);或者,返回 ENXIO 错误(当前打开操作没有设置阻塞标志)。 23 | 24 | #### 有名管道的读写规则 25 | ##### 从 FIFO 中读取数据: 26 | 27 | 约定:如果一个进程为了从 FIFO 中读取数据而阻塞打开 FIFO,那么称该进程内的读操作为设置了阻塞标志的读操作。 28 | 29 | * 如果有进程写打开 FIFO,且当前 FIFO 内没有数据,则对于设置了阻塞标志的读操作来说,将一直阻塞。对于没有设置阻塞标志读操作来说则返回-1,当前 errno 值为 EAGAIN,提醒以后再试。 30 | * 对于设置了阻塞标志的读操作说,造成阻塞的原因有两种:当前 FIFO 内有数据,但有其它进程在读这些数据;另外就是 FIFO 内没有数据。解阻塞的原因则是 FIFO 中有新的数据写入,不论信写入数据量的大小,也不论读操作请求多少数据量。 31 | * 读打开的阻塞标志只对本进程第一个读操作施加作用,如果本进程内有多个读操作序列,则在第一个读操作被唤醒并完成读操作后,其它将要执行的读操作将不再阻塞,即使在执行读操作时,FIFO 中没有数据也一样(此时,读操作返回0)。 32 | * 如果没有进程写打开 FIFO,则设置了阻塞标志的读操作会阻塞。 33 | 34 | 注:如果 FIFO 中有数据,则设置了阻塞标志的读操作不会因为 FIFO 中的字节数小于请求读的字节数而阻塞,此时,读操作会返回 FIFO 中现有的数据量。 35 | 36 | ##### 向FIFO中写入数据: 37 | 38 | 约定:如果一个进程为了向 FIFO 中写入数据而阻塞打开 FIFO,那么称该进程内的写操作为设置了阻塞标志的写操作。 39 | 40 | 对于设置了阻塞标志的写操作: 41 | * 当要写入的数据量不大于 `PIPE_BUF` 时,linux 将保证写入的原子性。如果此时管道空闲缓冲区不足以容纳要写入的字节数,则进入睡眠,直到当缓冲区中能够容纳要写入的字节数时,才开始进行一次性写操作。 42 | * 当要写入的数据量大于 `PIPE_BUF` 时,linux 将不再保证写入的原子性。FIFO 缓冲区一有空闲区域,写进程就会试图向管道写入数据,写操作在写完所有请求写的数据后返回。 43 | 44 | 对于没有设置阻塞标志的写操作: 45 | * 当要写入的数据量大于 `PIPE_BUF` 时,linux 将不再保证写入的原子性。在写满所有 FIFO 空闲缓冲区后,写操作返回。 46 | * 当要写入的数据量不大于 `PIPE_BUF` 时,linux 将保证写入的原子性。如果当前 FIFO 空闲缓冲区能够容纳请求写入的字节数,写完后成功返回;如果当前 FIFO 空闲缓冲区不能够容纳请求写入的字节数,则返回 EAGAIN 错误,提醒以后再写。 47 | 48 | #### 应用示例 49 | 50 | ##### 生产者程序 51 | 52 | ```c 53 | #include 54 | #include 55 | #include 56 | #include 57 | #include 58 | #include 59 | #include 60 | #include 61 | 62 | #define FIFO_NAME "/tmp/my_fifo" 63 | #define BUFFER_SIZE PIPE_BUF 64 | #define TEN_MEG (1024 * 1024 * 10) 65 | 66 | int main() 67 | { 68 | int pipe_fd; 69 | int res; 70 | int open_mode = O_WRONLY; 71 | 72 | int bytes = 0; 73 | char buffer[BUFFER_SIZE + 1]; 74 | 75 | if (access(FIFO_NAME, F_OK) == -1) 76 | { 77 | res = mkfifo(FIFO_NAME, 0777); 78 | if (res != 0) 79 | { 80 | fprintf(stderr, "Could not create fifo %s\n", FIFO_NAME); 81 | exit(EXIT_FAILURE); 82 | } 83 | } 84 | 85 | printf("Process %d opening FIFO O_WRONLY\n", getpid()); 86 | pipe_fd = open(FIFO_NAME, open_mode); 87 | printf("Process %d open result: the fd is %d\n", getpid(), pipe_fd); 88 | 89 | if (pipe_fd != -1) 90 | { 91 | while (bytes < TEN_MEG) 92 | { 93 | res = write(pipe_fd, buffer, BUFFER_SIZE); 94 | if (res == -1) 95 | { 96 | fprintf(stderr, "Write error on pipe\n"); 97 | exit(EXIT_FAILURE); 98 | } 99 | bytes += res; 100 | } 101 | close(pipe_fd); 102 | } 103 | else 104 | { 105 | exit(EXIT_FAILURE); 106 | } 107 | 108 | printf("Process %d finish\n", getpid()); 109 | exit(EXIT_SUCCESS); 110 | } 111 | ``` 112 | 113 | ##### 消费者程序 114 | 115 | ```c 116 | #include 117 | #include 118 | #include 119 | #include 120 | #include 121 | #include 122 | #include 123 | #include 124 | 125 | #define FIFO_NAME "/tmp/my_fifo" 126 | #define BUFFER_SIZE PIPE_BUF 127 | 128 | int main() 129 | { 130 | int pipe_fd; 131 | int res; 132 | 133 | int open_mode = O_RDONLY; 134 | char buffer[BUFFER_SIZE + 1]; 135 | int bytes = 0; 136 | 137 | memset(buffer, '\0', sizeof(buffer)); 138 | 139 | printf("Process %d opeining FIFO O_RDONLY\n", getpid()); 140 | pipe_fd = open(FIFO_NAME, open_mode); 141 | printf("Process %d open result: the fd is %d\n", getpid(), pipe_fd); 142 | 143 | if (pipe_fd != -1) 144 | { 145 | do{ 146 | res = read(pipe_fd, buffer, BUFFER_SIZE); 147 | bytes += res; 148 | }while(res > 0); 149 | close(pipe_fd); 150 | } 151 | else 152 | { 153 | exit(EXIT_FAILURE); 154 | } 155 | 156 | printf("Process %d finished, %d bytes read\n", getpid(), bytes); 157 | exit(EXIT_SUCCESS); 158 | } 159 | ``` 160 | 161 | ##### 运行结果 162 | 163 | ```shell 164 | ➜ unix gcc -o produce fifo1.c 165 | ➜ unix gcc -o consume fifo2.c 166 | ➜ unix ./produce& #进入后台运行 167 | [1] 7796 168 | Process 7796 opening FIFO O_WRONLY 169 | ➜ unix time ./consume #用 time 统计运行时间 170 | Process 7805 opeining FIFO O_RDONLY 171 | Process 7805 open result: the fd is 3 172 | Process 7796 open result: the fd is 3 173 | Process 7796 finish 174 | Process 7805 finished, 10485760 bytes read 175 | [1] + 7796 done ./produce 176 | ./consume 0.00s user 0.02s system 73% cpu 0.022 total 177 | ``` 178 | 179 | ### 小结 180 | 181 | 管道常用于两个方面: 182 | 183 | 1. 在 shell 中时常会用到管道(作为输入输入的重定向),在这种应用方式下,管道的创建对于用户来说是透明的; 184 | 2. 用于具有亲缘关系的进程间通信,用户自己创建管道,并完成读写操作。 185 | 186 | * FIFO 可以说是管道的推广,克服了管道无名字的限制,使得无亲缘关系的进程同样可以采用先进先出的通信机制进行通信。 187 | * 管道和 FIFO 的数据是字节流,应用程序之间必须事先确定特定的传输"协议",采用传播具有特定意义的消息。 188 | * 要灵活应用管道及 FIFO,理解它们的读写规则是关键。 189 | 190 | 整理来自:[Reference1](http://www.ibm.com/developerworks/cn/linux/l-ipc/part1/index.html#authorN10019) 191 | [Reference2](http://blog.csdn.net/MONKEY_D_MENG/article/details/5651430) 192 | -------------------------------------------------------------------------------- /Network-programming/Netty.md: -------------------------------------------------------------------------------- 1 | 2 | ## Netty 3 | 而Netty框架不局限于RPC,更多的是作为一种网络协议的实现框架,比如HTTP,由于RPC需要高效的网络通信,就可能选择以Netty作为基础。除了网络通信,RPC还需要有比较高效的序列化框架,以及一种寻址方式。如果是带会话(状态)的RPC调用,还需要有会话和状态保持的功能。大体上来说,Netty就是提供一种事件驱动的,责任链式(也可以说是流水线)的网络协议实现方式。 4 | 5 | 网络协议包含很多层次,很多部分组成,如传输层协议,编码解码,压缩解压,身份认证,加密解密,请求的处理逻辑,怎么能够更好的复用,扩展,业界通用的方法就是责任链,一个请求应答网络交互通常包含两条链,一条链(Upstream)是从传输层,经过一系列步骤,如身份认证,解密,日志,流控,最后到达业务层,一条链(DownStream)是业务层返回后,又经过一系列步骤,如加密等,又回到传输层。 6 | 7 | 这样每一层都有一个处理接口,都可以进行不同的操作,比如身份认证,加解密,日志,流控,将不同的处理实现像拼积木那样插接起来就可以实现一个网络协议了(快速开发)。每一层都有自己的实现,上层不需要关注面向网络的操作(可维护)。Netty已经提供了很多实现。 8 | 9 | 当然Netty还有许多好处,比如对非阻塞IO(NIO)的支持,比如在链上传递时最大程度的减少buffer的copy(高性能) 10 | 11 | ## Refer 12 | [谁能用通俗的语言解释一下什么是 RPC 框架?](https://www.zhihu.com/question/25536695/answer/36197244) -------------------------------------------------------------------------------- /Network-programming/README.md: -------------------------------------------------------------------------------- 1 | 2 | ## RPC 3 | * [RPC概念](https://github.com/AngryHacker/Rookie-Note/blob/master/Network-programming/RPC-concept.md) 4 | * [RPC机制介绍](https://github.com/AngryHacker/Rookie-Note/blob/master/Network-programming/rpc.md) 5 | * [Netty](https://github.com/AngryHacker/Rookie-Note/blob/master/Network-programming/Netty.md) 6 | -------------------------------------------------------------------------------- /Network-programming/RPC-concept.md: -------------------------------------------------------------------------------- 1 | 2 | ## RPC概念 3 | Remote Procedure Call,翻译为中文叫“远程过程调用”。也就是说两台服务器A,B,一个应用部署在A服务器上,想要调用B服务器上应用提供的函数/方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。 4 | 5 | 比如说,一个方法可能是这样定义的:`Employee getEmployeeByName(String fullName)` 6 | 7 | 那么: 8 | * 首先,要解决通讯的问题,主要是通过在客户端和服务器之间建立TCP连接,远程过程调用的所有交换的数据都在这个连接里传输。连接可以是按需连接,调用结束后就断掉,也可以是长连接,多个远程过程调用共享同一个连接。 9 | * 第二,要解决寻址的问题,也就是说,A服务器上的应用怎么告诉底层的RPC框架,如何连接到B服务器(如主机或IP地址)以及特定的端口,方法的名称名称是什么,这样才能完成调用。比如基于Web服务协议栈的RPC,就要提供一个endpoint URI,或者是从UDDI服务上查找。如果是RMI调用的话,还需要一个RMI Registry来注册服务的地址。 10 | * 第三,当A服务器上的应用发起远程过程调用时,方法的参数需要通过底层的网络协议如TCP传递到B服务器,由于网络协议是基于二进制的,内存中的参数的值要序列化成二进制的形式,也就是序列化(Serialize)或编组(marshal),通过寻址和传输将序列化的二进制发送给B服务器。 11 | * 第四,B服务器收到请求后,需要对参数进行反序列化(序列化的逆操作),恢复为内存中的表达方式,然后找到对应的方法(寻址的一部分)进行本地调用,然后得到返回值。第五,返回值还要发送回服务器A上的应用,也要经过序列化的方式发送,服务器A接到后,再反序列化,恢复为内存中的表达方式,交给A服务器上的应用 12 | 13 | ![rpc](rpc.jpeg) 14 | 15 | 为什么RPC呢?就是无法在一个进程内,甚至一个计算机内通过本地调用的方式完成的需求,比如比如不同的系统间的通讯,甚至不同的组织间的通讯。由于计算能力需要横向扩展,需要在多台机器组成的集群上部署应用,RPC的协议有很多,比如最早的CORBA,Java RMI,Web Service的RPC风格,Hessian,Thrift,甚至Rest API。 16 | 17 | ## Refer 18 | [谁能用通俗的语言解释一下什么是 RPC 框架?](https://www.zhihu.com/question/25536695/answer/36197244) 19 | 20 | ## 相关推荐 21 | [既然有 HTTP 请求,为什么还要用 RPC 调用?](https://www.zhihu.com/question/41609070/answer/1030913797) 22 | -------------------------------------------------------------------------------- /Network-programming/rpc.md: -------------------------------------------------------------------------------- 1 | ## RPC 详细介绍 2 | #### RPC 是什么 3 | RPC 的全称是 Remote Procedure Call, 是一种进程间通信方式。它允许程序调用另一个地址空间(通常是共享网络的另一台机器上)的过程或函数,而不用程序员显式编码这个远程调用的细节。即程序员无论是调用本地的还是远程的,本质上编写的调用代码基本相同。 4 | 5 | #### RPC 起源 6 | RPC 这个概念术语在上世纪 80 年代由 Bruce Jay Nelson 提出。这里我们追溯下当初开发 RPC 的原动机是什么?在 Nelson 的论文 "Implementing Remote Procedure Calls" 中他提到了几点: 7 | 8 | 1. 简单:RPC 概念的语义十分清晰和简单,这样建立分布式计算就更容易。 9 | 2. 高效:过程调用看起来十分简单而且高效。 10 | 3. 通用:在单机计算中过程往往是不同算法部分间最重要的通信机制。 11 | 12 | 通俗一点说,就是一般程序员对于本地的过程调用很熟悉,那么我们把 RPC 作成和本地调用完全类似,那么就更容易被接受,使用起来毫无障碍。Nelson 的论文发表于 30 年前,其观点今天看来确实高瞻远瞩,今天我们使用的 RPC 框架基本就是按这个目标来实现的。 13 | 14 | #### RPC 结构 15 | Nelson 的论文中指出实现 RPC 的程序包括 5 个部分: 16 | 17 | 1. User 18 | 2. User-stub 19 | 3. RPCRuntime 20 | 4. Server-stub 21 | 5. Server 22 | 23 | 这 5 个部分的关系如下图所示: 24 | 25 | ![RPC结构][rpc01] 26 | 27 | 这里 user 就是 client 端,当 user 想发起一个远程调用时,它实际是通过本地调用 user-stub。user-stub 负责将调用的接口、方法和参数通过约定的协议规范进行编码并通过本地的 RPCRuntime 实例传输到远端的实例。远端 RPCRuntime 实例收到请求后交给 server-stub 进行解码后发起本地端调用,调用结果再返回给 user 端。 28 | 29 | #### RPC 实现 30 | Nelson 论文中给出的这个实现结构也成为后来大家参考的标准范本。大约 10 年前,我最早接触分布式计算时使用的 CORBAR 实现结构基本与此类似。CORBAR 为了解决异构平台的 RPC,使用了 IDL(Interface Definition Language)来定义远程接口,并将其映射到特定的平台语言中。后来大部分的跨语言平台 RPC 基本都采用了此类方式,比如我们熟悉的 Web Service(SOAP),近年开源的 Thrift 等。他们大部分都通过 IDL 定义,并提供工具来映射生成不同语言平台的 user-stub 和 server-stub,并通过框架库来提供 RPCRuntime 的支持。不过貌似每个不同的 RPC 框架都定义了各自不同的 IDL 格式,导致程序员的学习成本进一步上升,Web Service 尝试建立业界标准,无赖标准规范复杂而效率偏低,否则 Thrift 等更高效的 RPC 框架就没必要出现了。 31 | 32 | IDL 是为了跨平台语言实现 RPC 不得已的选择,要解决更广泛的问题自然导致了更复杂的方案。而对于同一平台内的 RPC 而言显然没必要搞个中间语言出来,例如 Java 原生的 RMI,这样对于 Java 程序员而言显得更直接简单,降低使用的学习成本。目前市面上提供的 RPC 框架已经可算是五花八门,百家争鸣了。需要根据实际使用场景谨慎选型,需要考虑的选型因素我觉得至少包括下面几点: 33 | 34 | 1. 性能指标 35 | 2. 是否需要跨语言平台 36 | 3. 内网开放还是公网开放 37 | 4. 开源 RPC 框架本身的质量、社区活跃度 38 | 39 | #### RPC 功能目标 40 | RPC 的主要功能目标是让构建分布式计算(应用)更容易,在提供强大的远程调用能力时不损失本地调用的语义简洁性。为实现该目标,RPC 框架需提供一种透明调用机制让使用者不必显式的区分本地调用和远程调用,前面已经给出了一种 基于 stub 的实现结构。下面我们将具体细化 stub 结构的实现。 41 | 42 | #### RPC 调用分类 43 | RPC 调用分以下两种: 44 | 45 | 1. 同步调用 46 | 客户方等待调用执行完成并返回结果。 47 | 2. 异步调用 48 | 客户方调用后不用等待执行结果返回,但依然可以通过回调通知等方式获取返回结果。 49 | 若客户方不关心调用返回结果,则变成单向异步调用,单向调用不用返回结果。 50 | 51 | 异步和同步的区分在于是否等待服务端执行完成并返回结果。 52 | 53 | #### RPC 结构拆解 54 | ![RPC结构][rpc02] 55 | 56 | RPC 服务方通过 RpcServer 去导出(export)远程接口方法,而客户方通过 RpcClient 去引入(import)远程接口方法。客户方像调用本地方法一样去调用远程接口方法,RPC 框架提供接口的代理实现,实际的调用将委托给代理 RpcProxy 。代理封装调用信息并将调用转交给RpcInvoker 去实际执行。在客户端的 RpcInvoker 通过连接器 RpcConnector 去维持与服务端的通道 RpcChannel,并使用 RpcProtocol 执行协议编码(encode)并将编码后的请求消息通过通道发送给服务方。 57 | 58 | RPC 服务端接收器 RpcAcceptor 接收客户端的调用请求,同样使用 RpcProtocol 执行协议解码(decode)。解码后的调用信息传递给 RpcProcessor 去控制处理调用过程,最后再委托调用给 RpcInvoker 去实际执行并返回调用结果。 59 | 60 | #### RPC 组件职责 61 | 上面我们进一步拆解了 RPC 实现结构的各个组件组成部分,下面我们详细说明下每个组件的职责划分。 62 | 63 | 1. RpcServer
64 | 负责导出(export)远程接口 65 | 2. RpcClient
66 | 负责导入(import)远程接口的代理实现 67 | 3. RpcProxy
68 | 远程接口的代理实现 69 | 4. RpcInvoker
70 | 客户方实现:负责编码调用信息和发送调用请求到服务方并等待调用结果返回 71 | 服务方实现:负责调用服务端接口的具体实现并返回调用结果 72 | 5. RpcProtocol
73 | 负责协议编/解码 74 | 6. RpcConnector
75 | 负责维持客户方和服务方的连接通道和发送数据到服务方 76 | 7. RpcAcceptor
77 | 负责接收客户方请求并返回请求结果 78 | 8. RpcProcessor
79 | 负责在服务方控制调用过程,包括管理调用线程池、超时时间等 80 | 9. RpcChannel
81 | 数据传输通道 82 | 83 | #### RPC 实现分析 84 | 在进一步拆解了组件并划分了职责之后,这里以在 Java 平台实现该 RPC 框架概念模型为例,详细分析下实现中需要考虑的因素。 85 | 86 | ##### 导出远程接口 87 | 导出远程接口的意思是指只有导出的接口可以供远程调用,而未导出的接口则不能。在 Java 中导出接口的代码片段可能如下: 88 | 89 | ```java 90 | DemoService demo = new ...; 91 | RpcServer server = new ...; 92 | server.export(DemoService.class, demo, options); 93 | ``` 94 | 95 | 我们可以导出整个接口,也可以更细粒度一点只导出接口中的某些方法,如: 96 | 97 | ```java 98 | // 只导出 DemoService 中签名为 hi(String s) 的方法 99 | server.export(DemoService.class, demo, "hi", new Class[] { String.class }, options); 100 | ``` 101 | 102 | Java 中还有一种比较特殊的调用就是多态,也就是一个接口可能有多个实现,那么远程调用时到底调用哪个?这个本地调用的语义是通过 jvm 提供的引用多态性隐式实现的,那么对于 RPC 来说跨进程的调用就没法隐式实现了。如果前面 DemoService 接口有 2 个实现,那么在导出接口时就需要特殊标记不同的实现,如: 103 | 104 | ```java 105 | DemoService demo = new ...; 106 | DemoService demo2 = new ...; 107 | RpcServer server = new ...; 108 | server.export(DemoService.class, demo, options); 109 | server.export("demo2", DemoService.class, demo2, options); 110 | ``` 111 | 112 | 上面 demo2 是另一个实现,我们标记为 "demo2" 来导出,那么远程调用时也需要传递该标记才能调用到正确的实现类,这样就解决了多态调用的语义。 113 | 114 | ##### 导入远程接口与客户端代理 115 | 导入相对于导出远程接口,客户端代码为了能够发起调用必须要获得远程接口的方法或过程定义。目前,大部分跨语言平台 RPC 框架采用根据 IDL 定义通过 code generator 去生成 stub 代码,这种方式下实际导入的过程就是通过代码生成器在编译期完成的。我所使用过的一些跨语言平台 RPC 框架如 CORBAR、WebService、ICE、Thrift 均是此类方式。 116 | 117 | 代码生成的方式对跨语言平台 RPC 框架而言是必然的选择,而对于同一语言平台的 RPC 则可以通过共享接口定义来实现。在 Java 中导入接口的代码片段可能如下: 118 | 119 | ```java 120 | RpcClient client = new ...; 121 | DemoService demo = client.refer(DemoService.class); 122 | demo.hi("how are you?"); 123 | ``` 124 | 125 | 在 Java 中 'import' 是关键字,所以代码片段中我们用 refer 来表达导入接口的意思。这里的导入方式本质也是一种代码生成技术,只不过是在运行时生成,比静态编译期的代码生成看起来更简洁些。java 里至少提供了两种技术来提供动态代码生成,一种是 jdk 动态代理,另外一种是字节码生成。动态代理相比字节码生成使用起来更方便,但动态代理方式在性能上是要逊色于直接的字节码生成的,而字节码生成在代码可读性上要差很多。两者权衡起来,个人认为牺牲一些性能来获得代码可读性和可维护性显得更重要。 126 | 127 | ##### 协议编解码 128 | 客户端代理在发起调用前需要对调用信息进行编码,这就要考虑需要编码些什么信息并以什么格式传输到服务端才能让服务端完成调用。出于效率考虑,编码的信息越少越好(传输数据少),编码的规则越简单越好(执行效率高)。我们先看下需要编码些什么信息: 129 | 130 | -- 调用编码 -- 131 | 132 | 1. 接口方法
133 | 包括接口名、方法名 134 | 2. 方法参数
135 | 包括参数类型、参数值 136 | 3. 调用属性
137 | 包括调用属性信息,例如调用附件隐式参数、调用超时时间等 138 | 139 | -- 返回编码 -- 140 | 141 | 1. 返回结果
142 | 接口方法中定义的返回值 143 | 2. 返回码
144 | 异常返回码 145 | 3. 返回异常信息
146 | 调用异常信息 147 | 148 | 除了以上这些必须的调用信息,我们可能还需要一些元信息以方便程序编解码以及未来可能的扩展。这样我们的编码消息里面就分成了两部分,一部分是元信息、另一部分是调用的必要信息。如果设计一种 RPC 协议消息的话,元信息我们把它放在协议消息头中,而必要信息放在协议消息体中。下面给出一种概念上的 RPC 协议消息设计格式: 149 | 150 | ![RPC协议][rpc03] 151 | 152 | 消息头 153 | 154 | 字段 | 解释 | 155 | -----------|-------------------------------------| 156 | magic | 协议魔数,为解码设计 | 157 | header size| 协议头长度,为扩展设计 | 158 | version | 协议版本,为兼容设计 | 159 | st | 消息体序列化类型 | 160 | hb | 心跳消息标记,为长连接传输层心跳设计| 161 | ow | 单向消息标记, | 162 | rp | 响应消息标记,不置位默认是请求消息 | 163 | status code| 响应消息状态码 | 164 | reserved | 为字节对齐保留 | 165 | message id | 消息 id | 166 | body size | 消息体长度 | 167 | 168 | 消息体 169 | 170 | 采用序列化编码,常见有以下格式 171 | 172 | * xml : 如 webservie soap 173 | * json : 如 JSON-RPC 174 | * binary: 如 thrift; hession; kryo 等 175 | 176 | 格式确定后编解码就简单了,由于头长度一定所以我们比较关心的就是消息体的序列化方式。序列化我们关心三个方面: 177 | 178 | 1. 序列化和反序列化的效率,越快越好。 179 | 2. 序列化后的字节长度,越小越好。 180 | 3. 序列化和反序列化的兼容性,接口参数对象若增加了字段,是否兼容。 181 | 182 | 上面这三点有时是鱼与熊掌不可兼得,这里面涉及到具体的序列化库实现细节,就不在本文进一步展开分析了。 183 | 184 | ##### 传输服务 185 | 协议编码之后,自然就是需要将编码后的 RPC 请求消息传输到服务方,服务方执行后返回结果消息或确认消息给客户方。RPC 的应用场景实质是一种可靠的请求应答消息流,和 HTTP 类似。因此选择长连接方式的 TCP 协议会更高效,与 HTTP 不同的是在协议层面我们定义了每个消息的唯一 id,因此可以更容易的复用连接。 186 | 187 | 既然使用长连接,那么第一个问题是到底 client 和 server 之间需要多少根连接?实际上单连接和多连接在使用上没有区别,对于数据传输量较小的应用类型,单连接基本足够。单连接和多连接最大的区别在于,每根连接都有自己私有的发送和接收缓冲区,因此大数据量传输时分散在不同的连接缓冲区会得到更好的吞吐效率。所以,如果你的数据传输量不足以让单连接的缓冲区一直处于饱和状态的话,那么使用多连接并不会产生任何明显的提升,反而会增加连接管理的开销。 188 | 189 | 连接是由 client 端发起建立并维持。如果 client 和 server 之间是直连的,那么连接一般不会中断(当然物理链路故障除外)。如果 client 和 server 连接经过一些负载中转设备,有可能连接一段时间不活跃时会被这些中间设备中断。为了保持连接有必要定时为每个连接发送心跳数据以维持连接不中断。心跳消息是 RPC 框架库使用的内部消息,在前文协议头结构中也有一个专门的心跳位,就是用来标记心跳消息的,它对业务应用透明。 190 | 191 | ##### 执行调用 192 | client stub 所做的事情仅仅是编码消息并传输给服务方,而真正调用过程发生在服务方。server stub 从前文的结构拆解中我们细分了 RpcProcessor 和 RpcInvoker 两个组件,一个负责控制调用过程,一个负责真正调用。这里我们还是以 java 中实现这两个组件为例来分析下它们到底需要做什么? 193 | java 中实现代码的动态接口调用目前一般通过反射调用。除了原生的 jdk 自带的反射,一些第三方库也提供了性能更优的反射调用,因此 RpcInvoker 就是封装了反射调用的实现细节。 194 | 195 | 调用过程的控制需要考虑哪些因素,RpcProcessor 需要提供什么样地调用控制服务呢?下面提出几点以启发思考: 196 | 197 | 1. 效率提升 198 | 每个请求应该尽快被执行,因此我们不能每请求来再创建线程去执行,需要提供线程池服务。 199 | 2. 资源隔离 200 | 当我们导出多个远程接口时,如何避免单一接口调用占据所有线程资源,而引发其他接口执行阻塞。 201 | 3. 超时控制 202 | 当某个接口执行缓慢,而 client 端已经超时放弃等待后,server 端的线程继续执行此时显得毫无意义。 203 | 204 | #### RPC 异常处理 205 | 无论 RPC 怎样努力把远程调用伪装的像本地调用,但它们依然有很大的不同点,而且有一些异常情况是在本地调用时绝对不会碰到的。在说异常处理之前,我们先比较下本地调用和 RPC 调用的一些差异: 206 | 207 | 1. 本地调用一定会执行,而远程调用则不一定,调用消息可能因为网络原因并未发送到服务方。 208 | 2. 本地调用只会抛出接口声明的异常,而远程调用还会跑出 RPC 框架运行时的其他异常。 209 | 3. 本地调用和远程调用的性能可能差距很大,这取决于 RPC 固有消耗所占的比重。 210 | 211 | 正是这些区别决定了使用 RPC 时需要更多考量。当调用远程接口抛出异常时,异常可能是一个业务异常,也可能是 RPC 框架抛出的运行时异常(如:网络中断等)。业务异常表明服务方已经执行了调用,可能因为某些原因导致未能正常执行,而 RPC 运行时异常则有可能服务方根本没有执行,对调用方而言的异常处理策略自然需要区分。 212 | 213 | 由于 RPC 固有的消耗相对本地调用高出几个数量级,本地调用的固有消耗是纳秒级,而 RPC 的固有消耗是在毫秒级。那么对于过于轻量的计算任务就并不合适导出远程接口由独立的进程提供服务,只有花在计算任务上时间远远高于 RPC 的固有消耗才值得导出为远程接口提供服务。 214 | 215 | 整理来自:[Reference1](http://blog.csdn.net/mindfloating/article/details/39473807) 216 | [Reference2](http://blog.csdn.net/mindfloating/article/details/39474123) 217 | 218 | #### 扩展阅读 219 | [知乎 - 什么是RPC](https://www.zhihu.com/question/25536695) 220 | 221 | 222 | [rpc01]: https://github.com/AngryHacker/Rookie-Note/blob/master/creative/image/rpc01.png 223 | [rpc02]: https://github.com/AngryHacker/Rookie-Note/blob/master/creative/image/rpc02.png 224 | [rpc03]: https://github.com/AngryHacker/Rookie-Note/blob/master/creative/image/rpc03.png 225 | 226 | -------------------------------------------------------------------------------- /Network/README.md: -------------------------------------------------------------------------------- 1 | ## 网络基础 2 | 3 | ### TCP 4 | * [TCP 那些事(一)格式/状态机](https://github.com/AngryHacker/Rookie-Note/blob/master/Network/tcp-1.md) 5 | * [TCP 那些事(二)重传机制和RTT算法](https://github.com/AngryHacker/Rookie-Note/blob/master/Network/tcp-2.md) 6 | * [TCP 那些事(三)滑动窗口](https://github.com/AngryHacker/Rookie-Note/blob/master/Network/tcp-3.md) 7 | * [TCP 那些事(四)拥塞处理](https://github.com/AngryHacker/Rookie-Note/blob/master/Network/tcp-4.md) -------------------------------------------------------------------------------- /Network/tcp-1.md: -------------------------------------------------------------------------------- 1 | # TCP 那些事(一) 格式/状态机 2 | ## 简介 3 | TCP在网络 OSI 七层模型中的第四层——Transport层,IP在第三层——Network层,ARP在第二层——Data Link层,在第二层上的数据,我们叫Frame,在第三层上的数据叫Packet,第四层的数据叫Segment。 4 | 5 | 首先,我们需要知道,我们程序的数据首先会打到 TCP 的 Segment 中,然后 TCP 的 Segment 会打到 IP 的 Packet 中,然后再打到以太网 Ethernet 的 Frame 中,传到对端后,各个层解析自己的协议,然后把数据交给更高层的协议处理。 6 | 7 | ## TCP头格式 8 | TCP 的头格式为: 9 | * TCP的包是没有IP地址的,那是IP层上的事。但是有源端口和目标端口。 10 | * 一个TCP连接需要四个元组来表示是同一个连接(src_ip, src_port, dst_ip, dst_port)准确说是五元组,还有一个是协议。但因为这里只是说TCP协议,所以只说四元组。 11 | 12 | ![TCP头格式](https://github.com/AngryHacker/Rookie-Note/blob/master/creative/image/tcp_header.png) 13 | 14 | 注意上图中的四个非常重要的东西: 15 | 1. Sequence Number是包的序号,用来解决网络包乱序(reordering)问题。 16 | 2. Acknowledgement Number 就是ACK——用于确认收到,用来解决不丢包的问题。 17 | 3. Window 又叫 Advertised-Window,也就是著名的滑动窗口(Sliding Window),用于解决流控的。 18 | 4. TCP Flag ,也就是包的类型,主要是用于操控 TCP 的状态机的。 19 | 20 | 相关字段详细信息如下: 21 | 22 | ![TCP 头字段](https://github.com/AngryHacker/Rookie-Note/blob/master/creative/image/tcp_flag.png) 23 | 24 | ## TCP状态机 25 | 26 | 其实,网络上的传输是没有连接的,包括TCP也是一样的。而TCP所谓的“连接”,其实只不过是在通讯的双方维护一个“连接状态”,让它看上去好像有连接一样。所以,TCP的状态变换是非常重要的。 27 | 28 | 下面是:“TCP协议的状态机”和 “TCP建链接”、“TCP断链接”、“传数据” 的对照图,我把两个图并排放在一起,这样方便在你对照着看。 29 | 30 | ![TCP状态机](https://github.com/AngryHacker/Rookie-Note/blob/master/creative/image/tcp_status.png) 31 | 32 | ![TCP握手过程](https://github.com/AngryHacker/Rookie-Note/blob/master/creative/image/tcp_hand.png) 33 | 34 | 很多人会问,为什么建链接要3次握手,断链接需要4次挥手? 35 | * 对于建链接的3次握手,主要是要初始化Sequence Number 的初始值。通信的双方要互相通知对方自己的初始化的Sequence Number(缩写为ISN:Inital Sequence Number)——所以叫SYN,全称Synchronize Sequence Numbers。也就上图中的 x 和 y。这个号要作为以后的数据通信的序号,以保证应用层接收到的数据不会因为网络上的传输的问题而乱序(TCP会用这个序号来拼接数据)。 36 | * 对于4次挥手,其实你仔细看是2次,因为TCP是全双工的,所以,发送方和接收方都需要Fin和Ack。只不过,有一方是被动的,所以看上去就成了所谓的4次挥手。如果两边同时断连接,那就会就进入到CLOSING状态,然后到达TIME_WAIT状态。下图是双方同时断连接的示意图(你同样可以对照着TCP状态机看): 37 | ![TCP四次挥手](https://github.com/AngryHacker/Rookie-Note/blob/master/creative/image/tcp_finish.png) 38 | 39 | 其他几个问题: 40 | * 关于建连接时SYN超时。试想一下,如果 server 端接到了 clien 发的 SYN 后回了 SYN-ACK 后client掉线了,server 端没有收到client回来的 ACK,那么,这个连接处于一个中间状态,即没成功,也没失败。于是,server 端如果在一定时间内没有收到的 TCP 会重发 SYN-ACK。在 Linux 下,默认重试次数为 5 次,重试的间隔时间从 1s 开始每次都翻倍,5次的重试时间间隔为 1s, 2s, 4s, 8s, 16s,总共 31s,第5次发出后还要等 32s 都知道第5次也超时了,所以,总共需要 1s + 2s + 4s+ 8s+ 16s + 32s = 2^6 -1 = 63s,TCP才会把断开这个连接。 41 | 42 | * 关于 SYN Flood 攻击。一些恶意的人就为此制造了SYN Flood攻击——给服务器发了一个 SYN 后,就下线了,于是服务器需要默认等 63s 才会断开连接,这样,攻击者就可以把服务器的 SYN 连接的队列耗尽,让正常的连接请求不能处理。于是,Linux 下给了一个叫 tcp_syncookies 的参数来应对这个事——当 SYN 队列满了后,TCP 会通过源地址端口、目标地址端口和时间戳打造出一个特别的 Sequence Number 发回去(又叫cookie),如果是攻击者则不会有响应,如果是正常连接,则会把这个 SYN Cookie 发回来,然后服务端可以通过 cookie 建连接(即使你不在SYN队列中)。请注意,请先千万别用 tcp_syncookies 来处理正常的大负载的连接的情况。因为,synccookies 是妥协版的TCP协议,并不严谨。对于正常的请求,你应该调整三个 TCP 参数可供你选择,第一个是:tcp_synack_retries 可以用他来减少重试次数;第二个是:tcp_max_syn_backlog,可以增大SYN连接数;第三个是:tcp_abort_on_overflow 处理不过来干脆就直接拒绝连接了。 43 | 44 | * 关于 ISN 的初始化。ISN是不能 hard code 的,不然会出问题的——比如:如果连接建好后始终用1来做ISN,如果client发了30个segment过去,但是网络断了,于是 client重连,又用了1做ISN,但是之前连接的那些包到了,于是就被当成了新连接的包,此时,client 的 Sequence Number 可能是3,而Server端认为 client 端的这个号是30了。全乱了。RFC793 中说,ISN 会和一个假的时钟绑在一起,这个时钟会在每4微秒对 ISN 做加一操作,直到超过 2^32,又从 0 开始。这样,一个 ISN 的周期大约是 4.55 个小时。因为,我们假设我们的 TCP Segment 在网络上的存活时间不会超过 Maximum Segment Lifetime(缩写为MSL – Wikipedia语条),所以,只要MSL的值小于4.55小时,那么,我们就不会重用到ISN。 45 | 46 | * 关于 MSL 和 TIME_WAIT。通过上面的 ISN 的描述,相信你也知道 MSL 是怎么来的了。我们注意到,在TCP的状态图中,从 TIME_WAIT 状态到 CLOSED 状态,有一个超时设置,这个超时设置是 2*MSL(RFC793 定义了MSL 为2分钟,Linux设置成了 30s)为什么要这有 TIME_WAIT?为什么不直接给转成 CLOSED 状态呢?主要有两个原因: 47 | * TIME_WAIT 确保有足够的时间让对端收到了ACK,如果被动关闭的那方没有收到 Ack,就会触发被动端重发 Fin,一来一去正好 2 个 MSL, 48 | * 有足够的时间让这个连接不会跟后面的连接混在一起(你要知道,有些自做主张的路由器会缓存IP数据包,如果连接被重用了,那么这些延迟收到的包就有可能会跟新连接混在一起)。你可以看看这篇文章《TIME_WAIT and its design implications for protocols and scalable client server systems》 49 | 50 | * 关于 TIME_WAIT 数量太多。从上面的描述我们可以知道,TIME_WAIT 是个很重要的状态,但是如果在大并发的短链接下,TIME_WAIT 就会太多,这也会消耗很多系统资源。只要搜一下,你就会发现,十有八九的处理方式都是教你设置两个参数,一个叫 tcp_tw_reuse,另一个叫 tcp_tw_recycle 的参数,这两个参数默认值都是被关闭的,后者 recyle 比前者 resue 更为激进,resue 要温柔一些。另外,如果使用 tcp_tw_reuse,必需设置tcp_timestamps = 1,否则无效。这里,你一定要注意,打开这两个参数会有比较大的坑——可能会让 TCP 连接出一些诡异的问题(因为如上述一样,如果不等待超时重用连接的话,新的连接可能会建不上。正如官方文档上说的一样“It should not be changed without advice/request of technical experts”)。 51 | * 关于 tcp_tw_reuse。官方文档上说 tcp_tw_reuse 加上 tcp_timestamps(又叫 PAWS, for Protection Against Wrapped Sequence Numbers)可以保证协议的角度上的安全,但是你需要 tcp_timestamps 在两边都被打开(你可以读一下 tcp_twsk_unique 的源码 )。我个人估计还是有一些场景会有问题。 52 | * 关于 tcp_tw_recycle。如果是 tcp_tw_recycle 被打开了话,会假设对端开启了 tcp_timestamps,然后会去比较时间戳,如果时间戳变大了,就可以重用。但是,如果对端是一个 NAT 网络的话(如:一个公司只用一个IP出公网)或是对端的IP被另一台重用了,这个事就复杂了。建链接的 SYN 可能就被直接丢掉了(你可能会看到 connection time out 的错误)(如果你想观摩一下Linux的内核代码,请参看源码 tcp_timewait_state_process)。 53 | * 关于 tcp_max_tw_buckets。这个是控制并发的 TIME_WAIT 的数量,默认值是 180000,如果超限,那么,系统会把多的给 destory 掉,然后在日志里打一个警告(如:time wait bucket table overflow),官网文档说这个参数是用来对抗 DDoS 攻击的。也说的默认值 180000 并不小。这个还是需要根据实际情况考虑。 54 | Again,使用tcp_tw_reuse和tcp_tw_recycle来解决TIME_WAIT的问题是非常非常危险的,因为这两个参数违反了TCP协议(RFC 1122) 55 | 56 | 其实,TIME_WAIT表示的是你主动断连接,所以,这就是所谓的“不作死不会死”。试想,如果让对端断连接,那么这个破问题就是对方的了,呵呵。另外,如果你的服务器是于 HTTP 服务器,那么设置一个 HTTP 的 KeepAlive 有多重要(浏览器会重用一个TCP连接来处理多个HTTP请求),然后让客户端去断链接(你要小心,浏览器可能会非常贪婪,他们不到万不得已不会主动断连接)。 57 | 58 | ## 数据传输中的Sequence Number 59 | 下图是我从 Wireshark 中截了个我在访问 coolshell.cn 时的有数据传输的图给你看一下,SeqNum是怎么变的。(使用 Wireshark 菜单中的 Statistics ->Flow Graph… ) 60 | ![tcp网络包](https://github.com/AngryHacker/Rookie-Note/blob/master/creative/image/tcp_seq.png) 61 | 62 | 你可以看到,SeqNum的增加是和传输的字节数相关的。上图中,三次握手后,来了两个 Len:1440 的包,而第二个包的 SeqNum 就成了 1441。然后第一个 ACK 回的是1441,表示第一个 1440 收到了。 63 | 64 | 注意:如果你用 Wireshark 抓包程序看3次握手,你会发现 SeqNum 总是为 0,不是这样的,Wireshark 为了显示更友好,使用了 Relative SeqNum——相对序号,你只要在右键菜单中的 protocol preference 中取消掉就可以看到 “Absolute SeqNum” 了 65 | 66 | ## Refer 67 | 整理自: 68 | * [TCP 的那些事儿(上)](https://coolshell.cn/articles/11564.html) 69 | * [TCP 的那些事儿(下)](https://coolshell.cn/articles/11609.html) -------------------------------------------------------------------------------- /Network/tcp-2.md: -------------------------------------------------------------------------------- 1 | # TCP 那些事(二)重传机制和RTT算法 2 | ## TCP重传机制 3 | TCP要保证所有的数据包都可以到达,所以,必需要有重传机制。 4 | 5 | 注意,接收端给发送端的Ack确认只会确认最后一个连续的包,比如,发送端发了1,2,3,4,5一共五份数据,接收端收到了1,2,于是回ack 3,然后收到了4(注意此时3没收到),此时的TCP会怎么办?我们要知道,因为正如前面所说的,SeqNum和Ack是以字节数为单位,所以ack的时候,不能跳着确认,只能确认最大的连续收到的包,不然,发送端就以为之前的都收到了。 6 | 7 | ### 超时重传机制 8 | 一种是不回 ack,死等3,当发送方发现收不到3的 ack 超时后,会重传3。一旦接收方收到3后,回 ack 回 4——意味着3和4都收到了。 9 | 10 | 但是,这种方式会有比较严重的问题,那就是因为要死等 3,所以会导致 4 和 5 即便已经收到了,而发送方也完全不知道发生了什么事,因为没有收到 Ack,所以,发送方可能会悲观地认为也丢了,所以有可能也会导致4和5的重传。 11 | 12 | 对此有两种选择: 13 | 14 | * 一种是仅重传 timeout 的包。也就是第3份数据。 15 | * 另一种是重传 timeout 后所有的数据,也就是第3,4,5这三份数据。 16 | 17 | 这两种方式有好也有不好。第一种会节省带宽,但是慢,第二种会快一点,但是会浪费带宽,也可能会有无用功。但总体来说都不好。因为都在等 timeout,timeout 可能会很长(在下篇会说TCP是怎么动态地计算出timeout的) 18 | 19 | ### 快速重传机制 20 | 于是,TCP引入了一种叫 Fast Retransmit 的算法,不以时间驱动,而以数据驱动重传。也就是说,如果,包没有连续到达,就 ack 最后那个可能被丢了的包,如果发送方连续收到 3 次相同的 ack,就重传。Fast Retransmit 的好处是不用等 timeout 了再重传。 21 | 22 | 比如:如果发送方发出了1,2,3,4,5份数据,第一份先到送了,于是就ack回2,结果2因为某些原因没收到,3到达了,于是还是ack回2,后面的4和5都到了,但是还是ack回2,因为2还是没有收到,于是发送端收到了三个ack=2 的确认,知道了2还没有到,于是就马上重转2。然后,接收端收到了2,此时因为3,4,5都收到了,于是 ack 回6。示意图如下: 23 | 24 | ![TCP快速重传](https://github.com/AngryHacker/Rookie-Note/blob/master/creative/image/tcp_resend.png) 25 | 26 | Fast Retransmit只解决了一个问题,就是timeout的问题,它依然面临一个艰难的选择,就是重传之前的一个还是重传所有的问题。对于上面的示例来说,是重传#2呢还是重传#2,#3,#4,#5呢?因为发送端并不清楚这连续的3个ack(2)是谁传回来的?也许发送端发了20份数据,是#6,#10,#20传来的呢。这样,发送端很有可能要重传从2到20的这堆数据(这就是某些TCP的实际的实现)。可见,这是一把双刃剑。 27 | 28 | ### SACK 29 | 另外一种更好的方式叫:Selective Acknowledgment (SACK)(参看RFC 2018),这种方式需要在TCP头里加一个SACK的东西,ACK还是Fast Retransmit的ACK,SACK 则是汇报收到的数据碎版。参看下图: 30 | 31 | ![TCP SACK](https://github.com/AngryHacker/Rookie-Note/blob/master/creative/image/tcp_sack.png) 32 | 33 | 34 | 这样,在发送端就可以根据回传的SACK来知道哪些数据到了,哪些没有到。于是就优化了Fast Retransmit 的算法。当然,这个协议需要两边都支持。在 Linux下,可以通过 tcp_sack 参数打开这个功能(Linux 2.4后默认打开)。 35 | 36 | 这里还需要注意一个问题——接收方 Reneging,所谓 Reneging 的意思就是接收方有权把已经报给发送端 SACK 里的数据给丢了。这样干是不被鼓励的,因为这个事会把问题复杂化了,但是,接收方这么做可能会有些极端情况,比如要把内存给别的更重要的东西。所以,发送方也不能完全依赖SACK,还是要依赖 ACK,并维护 Time-Out,如果后续的 ACK 没有增长,那么还是要把 SACK 的东西重传,另外,接收端这边永远不能把 SACK 的包标记为 Ack。 37 | 38 | 注意:SACK 会消费发送方的资源,试想,如果一个攻击者给数据发送方发一堆 SACK 的选项,这会导致发送方开始要重传甚至遍历已经发出的数据,这会消耗很多发送端的资源。详细的东西请参看《TCP SACK的性能权衡》 39 | 40 | ### Duplicate SACK – 重复收到数据的问题 41 | Duplicate SACK 又称 D-SACK,其主要使用了 SACK 来告诉发送方有哪些数据被重复接收了。RFC-2833 里有详细描述和示例。下面举几个例子(来源于RFC-2833) 42 | 43 | D-SACK 使用了 SACK 的第一个段来做标志, 44 | * 如果 SACK 的第一个段的范围被 ACK 所覆盖,那么就是 D-SACK 45 | * 如果 SACK 的第一个段的范围被 SACK 的第二个段覆盖,那么就是 D-SACK 46 | 47 | #### 示例一:ACK丢包 48 | 下面的示例中,丢了两个 ACK,所以,发送端重传了第一个数据包(3000-3499),于是接收端发现重复收到,于是回了一个 SACK=3000-3500,因为ACK都到了 4000 意味着收到了 4000 之前的所有数据,所以这个 SACK 就是 D-SACK ——旨在告诉发送端我收到了重复的数据,而且我们的发送端还知道,数据包没有丢,丢的是ACK包。 49 | 50 | ``` 51 | Transmitted Received ACK Sent 52 | Segment Segment (Including SACK Blocks) 53 | 54 | 3000-3499 3000-3499 3500 (ACK dropped) 55 | 3500-3999 3500-3999 4000 (ACK dropped) 56 | 3000-3499 3000-3499 4000, SACK=3000-3500 57 | ``` 58 | 59 | #### 示例二,网络延误 60 | 下面的示例中,网络包(1000-1499)被网络给延误了,导致发送方没有收到 ACK,而后面到达的三个包触发了“Fast Retransmit算法”,所以重传,但重传时,被延误的包又到了,所以,回了一个 SACK=1000-1500,因为ACK已到了3000,所以,这个 SACK 是 D-SACK——标识收到了重复的包。 61 | 62 | 这个案例下,发送端知道之前因为“Fast Retransmit算法”触发的重传不是因为发出去的包丢了,也不是因为回应的 ACK 包丢了,而是因为网络延时了。 63 | 64 | ``` 65 | Transmitted Received ACK Sent 66 | Segment Segment (Including SACK Blocks) 67 | 68 | 500-999 500-999 1000 69 | 1000-1499 (delayed) 70 | 1500-1999 1500-1999 1000, SACK=1500-2000 71 | 2000-2499 2000-2499 1000, SACK=1500-2500 72 | 2500-2999 2500-2999 1000, SACK=1500-3000 73 | 1000-1499 1000-1499 3000 74 | 1000-1499 3000, SACK=1000-1500 75 | --------- 76 | ``` 77 | 78 | 可见,引入了 D-SACK,有这么几个好处: 79 | 1. 可以让发送方知道,是发出去的包丢了,还是回来的ACK包丢了。 80 | 2. 是不是自己的timeout太小了,导致重传 81 | 3. 网络上出现了先发的包后到的情况(又称reordering) 82 | 4. 网络上是不是把我的数据包给复制了。 83 | 知道这些东西可以很好得帮助 TCP 了解网络情况,从而可以更好的做网络上的流控。 84 | Linux下的 tcp_dsack 参数用于开启这个功能(Linux 2.4后默认打开) 85 | 86 | ## TCP 的 RTT 算法 87 | 从前面的TCP重传机制我们知道Timeout的设置对于重传非常重要。 88 | * 设长了,重发就慢,丢了老半天才重发,没有效率,性能差; 89 | * 设短了,会导致可能并没有丢就重发。于是重发的就快,会增加网络拥塞,导致更多的超时,更多的超时导致更多的重发。 90 | 而且,这个超时时间在不同的网络的情况下,根本没有办法设置一个死的值。只能动态地设置。 为了动态地设置,TCP 引入了 RTT——Round Trip Time,也就是一个数据包从发出去到回来的时间。这样发送端就大约知道需要多少的时间,从而可以方便地设置 Timeout——RTO(Retransmission TimeOut),以让我们的重传机制更高效。 听起来似乎很简单,好像就是在发送端发包时记下t0,然后接收端再把这个ack回来时再记一个t1,于是 RTT = t1 – t0。没那么简单,这只是一个采样,不能代表普遍情况。 91 | 92 | ### 经典算法 93 | RFC793 中定义的经典算法是这样的: 94 | 1. 首先,先采样 RTT,记下最近好几次的 RTT 值。 95 | 2. 然后做平滑计算 SRTT( Smoothed RTT)。公式为:(其中的 α 取值在0.8 到 0.9之间,这个算法英文叫 Exponential weighted moving average,中文叫:加权移动平均) SRTT = ( α * SRTT ) + ((1- α) * RTT) 96 | 3. 开始计算 RTO。公式如下: RTO = min [ UBOUND, max [ LBOUND, (β * SRTT) ] ] 97 | 其中: 98 | * UBOUND 是最大的 timeou t时间,上限值 99 | * LBOUND 是最小的 timeout 时间,下限值 100 | * β 值一般在1.3到2.0之间。 101 | 102 | ### Karn / Partridge 算法 103 | 但是上面的这个算法在重传的时候会出有一个终极问题——你是用第一次发数据的时间和 ack 回来的时间做 RTT 样本值,还是用重传的时间和 ACK 回来的时间做RTT样本值? 104 | 105 | 这个问题无论你选那头都是按下葫芦起了瓢。 如下图所示: 106 | * 情况(a)是 ack 没回来,所以重传。如果你计算第一次发送和ACK的时间,那么,明显算大了。 107 | * 情况(b)是 ack 回来慢了,但是导致了重传,但刚重传不一会儿,之前ACK就回来了。如果你是算重传的时间和ACK回来的时间的差,就会算短了。 108 | ![Karn / Partridge](https://github.com/AngryHacker/Rookie-Note/blob/master/creative/image/tcp_kp.png) 109 | 所以1987年的时候,搞了一个叫 Karn / Partridge Algorithm,这个算法的最大特点是——忽略重传,不把重传的RTT做采样(你看,你不需要去解决不存在的问题) 110 | 111 | 但是,这样一来,又会引发一个大 BUG—— 如果在某一时间,网络闪动,突然变慢了,产生了比较大的延时,这个延时导致要重传所有的包(因为之前的RTO很小),于是,因为重传的不算,所以,RTO 就不会被更新,这是一个灾难。 于是 Karn 算法用了一个取巧的方式——只要一发生重传,就对现有的 RTO 值翻倍(这就是所谓的 Exponential backoff),很明显,这种死规矩对于一个需要估计比较准确的RTT也不靠谱。 112 | 113 | ### Jacobson / Karels 算法 114 | 前面两种算法用的都是“加权移动平均”,这种方法最大的毛病就是如果RTT有一个大的波动的话,很难被发现,因为被平滑掉了。所以,1988年,又有人推出来了一个新的算法,这个算法叫 Jacobson / Karels Algorithm(参看RFC6289)。这个算法引入了最新的RTT的采样和平滑过的SRTT的差距做因子来计算。 公式如下:(其中的DevRTT是Deviation RTT的意思) 115 | 116 | SRTT = SRTT + α (RTT – SRTT) —— 计算平滑RTT 117 | 118 | DevRTT = (1-β)*DevRTT + β*(|RTT-SRTT|) ——计算平滑RTT和真实的差距(加权移动平均) 119 | 120 | RTO= µ * SRTT + ∂ *DevRTT —— 神一样的公式 121 | 122 | (其中:在Linux下,α = 0.125,β = 0.25, μ = 1,∂ = 4 ——这就是算法中的“调得一手好参数”,nobody knows why, it just works…) 最后的这个算法在被用在今天的TCP协议中(Linux的源代码在:tcp_rtt_estimator)。 123 | 124 | ## Refer 125 | 整理自: 126 | * [TCP 的那些事儿(上)](https://coolshell.cn/articles/11564.html) 127 | * [TCP 的那些事儿(下)](https://coolshell.cn/articles/11609.html) -------------------------------------------------------------------------------- /Network/tcp-3.md: -------------------------------------------------------------------------------- 1 | # TCP 那些事(三)滑动窗口 2 | ## 滑动窗口 3 | 需要说明一下,如果你不了解 TCP 的滑动窗口这个事,你等于不了解 TCP 协议。我们都知道,TCP 必需要解决的可靠传输以及包乱序(reordering)的问题,所以,TCP必需要知道网络实际的数据处理带宽或是数据处理速度,这样才不会引起网络拥塞,导致丢包。 4 | 5 | 所以,TCP引入了一些技术和设计来做网络流控,Sliding Window 是其中一个技术。 前面我们说过,TCP 头里有一个字段叫 Window,又叫 Advertised-Window,这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。 为了说明滑动窗口,我们需要先看一下TCP缓冲区的一些数据结构: 6 | 7 | ![TCP缓冲区](tcp_slide.png) 8 | 9 | 上图中,我们可以看到: 10 | 11 | * 接收端 LastByteRead 指向了 TCP 缓冲区中读到的位置,NextByteExpected 指向的地方是收到的连续包的最后一个位置,LastByteRcved 指向的是收到的包的最后一个位置,我们可以看到中间有些数据还没有到达,所以有数据空白区。 12 | * 发送端的 LastByteAcked 指向了被接收端Ack过的位置(表示成功发送确认),LastByteSent 表示发出去了,但还没有收到成功确认的 Ack,LastByteWritten 指向的是上层应用正在写的地方。 13 | 14 | 于是: 15 | * 接收端在给发送端回 ACK 中会汇报自己的 AdvertisedWindow = MaxRcvBuffer – LastByteRcvd – 1; 16 | * 而发送方会根据这个窗口来控制发送数据的大小,以保证接收方可以处理。 17 | 18 | 下面我们来看一下发送方的滑动窗口示意图: 19 | ![滑动窗口示意](tcp-windows.png) 20 | 21 | 上图中分成了四个部分,分别是:(其中那个黑模型就是滑动窗口) 22 | 23 | * #1 已收到ack确认的数据。 24 | * #2 发还没收到ack的。 25 | * #3 在窗口中还没有发出的(接收方还有空间)。 26 | * #4 窗口以外的数据(接收方没空间) 27 | 28 | 下面是个滑动后的示意图(收到 36 的 ack,并发出了46-51的字节): 29 | ![滑动窗口示意](tcp_send.png) 30 | 31 | 下面我们来看一个接收端控制发送端的图示: 32 | ![接收发送示意](tcp_client_window.png) 33 | 34 | ### Zero Window 35 | 上图,我们可以看到一个处理缓慢的 Server(接收端)是怎么把 Client(发送端)的 TCP Sliding Window 给降成0的。此时,你一定会问,如果 Window 变成0了,TCP会怎么样?是不是发送端就不发数据了?是的,发送端就不发数据了,你可以想像成 “Window Closed”,那你一定还会问,如果发送端不发数据了,接收方一会儿 Window size 可用了,怎么通知发送端呢? 36 | 37 | 解决这个问题,TCP使用了 Zero Window Probe 技术,缩写为 ZWP,也就是说,发送端在窗口变成 0 后,会发 ZWP 的包给接收方,让接收方来 ack 他的 Window 尺寸,一般这个值会设置成 3 次,第次大约 30-60 秒(不同的实现可能会不一样)。如果3次过后还是 0 的话,有的 TCP 实现就会发 RST 把链接断了。 38 | 39 | 注意:只要有等待的地方都可能出现 DDoS 攻击,Zero Window 也不例外,一些攻击者会在和 HTTP 建好链发完 GET 请求后,就把Window设置为0,然后服务端就只能等待进行ZWP,于是攻击者会并发大量的这样的请求,把服务器端的资源耗尽。(关于这方面的攻击,大家可以移步看一下 Wikipedia 的 SockStress 词条) 40 | 41 | 另外,Wireshark 中,你可以使用 tcp.analysis.zero_window 来过滤包,然后使用右键菜单里的 follow TCP stream,你可以看到 ZeroWindowProbe 及 ZeroWindowProbeAck 的包。 42 | 43 | ### Silly Window Syndrome 44 | Silly Window Syndrome 翻译成中文就是“糊涂窗口综合症”。正如你上面看到的一样,如果我们的接收方太忙了,来不及取走 Receive Windows 里的数据,那么,就会导致发送方越来越小。到最后,如果接收方腾出几个字节并告诉发送方现在有几个字节的 window,而我们的发送方会义无反顾地发送这几个字节。 45 | 46 | 要知道,我们的 TCP+IP 头有40个字节,为了几个字节,要达上这么大的开销,这太不经济了。 47 | 48 | 另外,你需要知道网络上有个 MTU,对于以太网来说,MTU是1500字节,除去 TCP+IP 头的 40 个字节,真正的数据传输可以有 1460,这就是所谓的 MSS(Max Segment Size)注意,TCP的 RFC 定义这个MSS的默认值是536,这是因为 RFC 791里说了任何一个 IP 设备都得最少接收 576 尺寸的大小(实际上来说 576 是拨号的网络的MTU,而 576 减去 IP 头的 20 个字节就是 536)。 49 | 50 | 如果你的网络包可以塞满 MTU,那么你可以用满整个带宽,如果不能,那么你就会浪费带宽。(大于 MTU 的包有两种结局,一种是直接被丢了,另一种是会被重新分块打包发送) 你可以想像成一个MTU就相当于一个飞机的最多可以装的人,如果这飞机里满载的话,带宽最高,如果一个飞机只运一个人的话,无疑成本增加了,也而相当二。 51 | 52 | 所以,Silly Windows Syndrome 这个现像就像是你本来可以坐 200 人的飞机里只做了一两个人。 要解决这个问题也不难,就是避免对小的 window size 做出响应,直到有足够大的 window size 再响应,这个思路可以同时实现在 sender 和 receiver 两端。 53 | 54 | * 如果这个问题是由 Receiver 端引起的,那么就会使用 David D Clark’s 方案。在 receiver 端,如果收到的数据导致 window size 小于某个值,可以直接 ack(0) 回 sender,这样就把 window 给关闭了,也阻止了 sender 再发数据过来,等到 receiver 端处理了一些数据后 windows size 大于等于了 MSS,或者,receiver buffer 有一半为空,就可以把 window 打开让 send 发送数据过来。 55 | * 如果这个问题是由 Sender 端引起的,那么就会使用著名的 Nagle’s algorithm。这个算法的思路也是延时处理,他有两个主要的条件(更多的条件可以看一下tcp_nagle_check函数):1)要等到 Window Size>=MSS 或是 Data Size >=MSS,2)等待时间或是超时200ms,这两个条件有一个满足,他才会发数据,否则就是在攒数据。 56 | 57 | 另外,Nagle算法默认是打开的,所以,对于一些需要小包场景的程序——比如像 telnet 或 ssh 这样的交互性比较强的程序,你需要关闭这个算法。你可以在 Socket 设置 TCP_NODELAY 选项来关闭这个算法(关闭 Nagle 算法没有全局参数,需要根据每个应用自己的特点来关闭) 58 | 59 | ``` 60 | setsockopt(sock_fd, IPPROTO_TCP, TCP_NODELAY, (char *)&value,sizeof(int)); 61 | ``` 62 | 63 | 另外,网上有些文章说 TCP_CORK 的 socket option 是也关闭 Nagle 算法,这个还不够准确。TCP_CORK 是禁止小包发送,而Nagle算法没有禁止小包发送,只是禁止了大量的小包发送。最好不要两个选项都设置。 老实说,我觉得 Nagle 算法其实只加了个延时,没有别的什么,我觉得最好还是把他关闭,然后由自己的应用层来控制数据,我个人觉得不应该什么事都去依赖内核算法。 64 | 65 | ## Refer 66 | 整理自: 67 | * [TCP 的那些事儿(上)](https://coolshell.cn/articles/11564.html) 68 | * [TCP 的那些事儿(下)](https://coolshell.cn/articles/11609.html) -------------------------------------------------------------------------------- /Network/tcp-4.md: -------------------------------------------------------------------------------- 1 | # TCP 那些事(四)拥塞处理 2 | ## 拥塞处理 3 | 上面我们知道了,TCP通过 Sliding Window 来做流控(Flow Control),但是 TCP 觉得这还不够,因为 Sliding Window 需要依赖于连接的发送端和接收端,其并不知道网络中间发生了什么。TCP 的设计者觉得,一个伟大而牛逼的协议仅仅做到流控并不够,因为流控只是网络模型 4 层以上的事,TCP的还应该更聪明地知道整个网络上的事。 4 | 5 | 具体一点,我们知道TCP通过一个 timer 采样了 RTT 并计算 RTO,但是,如果网络上的延时突然增加,那么,TCP 对这个事做出的应对只有重传数据,但是,重传会导致网络的负担更重,于是会导致更大的延迟以及更多的丢包,于是,这个情况就会进入恶性循环被不断地放大。试想一下,如果一个网络内有成千上万的 TCP 连接都这么行事,那么马上就会形成“网络风暴”,TCP 这个协议就会拖垮整个网络。这是一个灾难。 6 | 7 | 所以,TCP 不能忽略网络上发生的事情,而无脑地一个劲地重发数据,对网络造成更大的伤害。对此 TCP 的设计理念是:TCP 不是一个自私的协议,当拥塞发生的时候,要做自我牺牲。就像交通阻塞一样,每个车都应该把路让出来,而不要再去抢路了。 8 | 9 | 关于拥塞控制的论文请参看《Congestion Avoidance and Control》(PDF) 10 | 11 | 拥塞控制主要是四个算法:1)慢启动,2)拥塞避免,3)拥塞发生,4)快速恢复。这四个算法不是一天都搞出来的,这个四算法的发展经历了很多时间,到今天都还在优化中。 备注: 12 | * 1988年,TCP-Tahoe 提出了1)慢启动,2)拥塞避免,3)拥塞发生时的快速重传 13 | * 1990年,TCP Reno 在Tahoe的基础上增加了4)快速恢复 14 | 15 | ### 慢热启动算法 – Slow Start 16 | 首先,我们来看一下 TCP 的慢热启动。慢启动的意思是,刚刚加入网络的连接,一点一点地提速,不要一上来就像那些特权车一样霸道地把路占满。新同学上高速还是要慢一点,不要把已经在高速上的秩序给搞乱了。 17 | 18 | 慢启动的算法如下(cwnd 全称 Congestion Window): 19 | 1. 连接建好的开始先初始化 cwnd = 1,表明可以传一个 MSS 大小的数据。 20 | 2. 每当收到一个 ACK,cwnd++; 呈线性上升 21 | 3. 每当过了一个 RTT,cwnd = cwnd*2; 呈指数上升 22 | 4. 还有一个 ssthresh(slow start threshold),是一个上限,当 cwnd >= ssthresh 时,就会进入“拥塞避免算法”(后面会说这个算法) 23 | 24 | 所以,我们可以看到,如果网速很快的话,ACK也会返回得快,RTT也会短,那么,这个慢启动就一点也不慢。下图说明了这个过程。 25 | 26 | ![慢启动](tcp_slow_start.png) 27 | 28 | 这里,我需要提一下的是一篇 Google 的论文《An Argument for Increasing TCP’s Initial Congestion Window》。Linux 3.0 后采用了这篇论文的建议——把 cwnd 初始化成了 10个MSS。 而Linux 3.0以前,比如 2.6,Linux 采用了RFC3390,cwnd 是跟 MSS 的值来变的,如果 MSS< 1095,则 cwnd = 4;如果 MSS>2190,则 cwnd=2;其它情况下,则是3。 29 | 30 | ### 拥塞避免算法 – Congestion Avoidance 31 | 前面说过,还有一个 ssthresh(slow start threshold),是一个上限,当 cwnd >= ssthresh 时,就会进入“拥塞避免算法”。一般来说 ssthresh 的值是65535,单位是字节,当 cwnd 达到这个值时后,算法如下: 32 | 1. 收到一个 ACK 时,cwnd = cwnd + 1/cwnd 33 | 2. 当每过一个 RTT 时,cwnd = cwnd + 1 34 | 这样就可以避免增长过快导致网络拥塞,慢慢的增加调整到网络的最佳值。很明显,是一个线性上升的算法。 35 | 36 | ### 拥塞状态时的算法 37 | 前面我们说过,当丢包的时候,会有两种情况: 38 | * 等到 RTO 超时,重传数据包。TCP认为这种情况太糟糕,反应也很强烈。 39 | * sshthresh = cwnd /2 40 | * cwnd 重置为 1 41 | * 进入慢启动过程 42 | * Fast Retransmit 算法,也就是在收到 3 个 duplicate ACK 时就开启重传,而不用等到 RTO 超时。 43 | * TCP Tahoe 的实现和 RTO 超时一样。 44 | * TCP Reno 的实现是: 45 | * cwnd = cwnd /2 46 | * sshthresh = cwnd 47 | * 进入快速恢复算法——Fast Recovery 48 | 上面我们可以看到 RTO 超时后,sshthresh 会变成 cwnd 的一半,这意味着,如果 cwnd <= sshthresh 时出现的丢包,那么 TCP 的 sshthresh 就会减了一半,然后等 cwnd 又很快地以指数级增涨爬到这个地方时,就会成慢慢的线性增涨。我们可以看到,TCP 是怎么通过这种强烈地震荡快速而小心得找到网站流量的平衡点的。 49 | 50 | ### 快速恢复算法 – Fast Recovery 51 | 52 | #### TCP Reno 53 | 这个算法定义在 RFC5681。快速重传和快速恢复算法一般同时使用。快速恢复算法是认为,你还有 3 个 Duplicated Acks 说明网络也不那么糟糕,所以没有必要像 RTO 超时那么强烈。 注意,正如前面所说,进入 Fast Recovery 之前,cwnd 和 sshthresh已被更新: 54 | * cwnd = cwnd /2 55 | * sshthresh = cwnd 56 | 57 | 然后,真正的 Fast Recovery 算法如下: 58 | * cwnd = sshthresh + 3 * MSS (3的意思是确认有3个数据包被收到了) 59 | * 重传 Duplicated ACKs 指定的数据包 60 | * 如果再收到 duplicated Acks,那么 cwnd = cwnd +1 61 | * 如果收到了新的 Ack,那么,cwnd = sshthresh ,然后就进入了拥塞避免的算法了。 62 | 63 | 如果你仔细思考一下上面的这个算法,你就会知道,上面这个算法也有问题,那就是——它依赖于3个重复的Acks。注意,3个重复的Acks并不代表只丢了一个数据包,很有可能是丢了好多包。但这个算法只会重传一个,而剩下的那些包只能等到 RTO 超时,于是,进入了恶梦模式——超时一个窗口就减半一下,多个超时会超成 TCP 的传输速度呈级数下降,而且也不会触发 Fast Recovery 算法了。 64 | 65 | 通常来说,正如我们前面所说的,SACK 或 D-SACK 的方法可以让 Fast Recovery 或 Sender 在做决定时更聪明一些,但是并不是所有的 TCP 的实现都支持 SACK(SACK 需要两端都支持),所以,需要一个没有 SACK 的解决方案。而通过 SACK 进行拥塞控制的算法是 FACK(后面会讲) 66 | 67 | #### TCP New Reno 68 | 于是,1995年,TCP New Reno(参见 RFC 6582 )算法提出来,主要就是在没有 SACK 的支持下改进 Fast Recovery 算法的 69 | * 当 sender 这边收到了 3 个 Duplicated Acks,进入 Fast Retransimit 模式,开发重传重复 Acks 指示的那个包。如果只有这一个包丢了,那么,重传这个包后回来的Ack会把整个已经被 sender 传输出去的数据 ack 回来。如果没有的话,说明有多个包丢了。我们叫这个 ACK 为 Partial ACK。 70 | * 一旦 Sender 这边发现了 Partial ACK 出现,那么,sender 就可以推理出来有多个包被丢了,于是乎继续重传 sliding window 里未被ack的第一个包。直到再也收不到了 Partial Ack,才真正结束 Fast Recovery 这个过程 71 | 72 | 我们可以看到,这个“Fast Recovery的变更”是一个非常激进的玩法,他同时延长了Fast Retransmit和Fast Recovery的过程。 73 | 74 | ### 算法示意图 75 | 下面我们来看一个简单的图示以同时看一下上面的各种算法的样子: 76 | ![拥塞处理](tcp_new_reno.png) 77 | 78 | ### FACK算法 79 | 80 | FACK 全称 Forward Acknowledgment 算法,论文地址在这里(PDF)Forward Acknowledgement: Refining TCP Congestion Control 这个算法是基于 SACK 的,前面我们说过 SACK 是使用了 TCP 扩展字段 Ack 了有哪些数据收到,哪些数据没有收到,他比 Fast Retransmit 的 3 个 duplicated acks 好处在于,前者只知道有包丢了,不知道是一个还是多个,而 SACK 可以准确的知道有哪些包丢了。 所以,SACK 可以让发送端这边在重传过程中,把那些丢掉的包重传,而不是一个一个的传,但这样的一来,如果重传的包数据比较多的话,又会导致本来就很忙的网络就更忙了。所以,FACK 用来做重传过程中的拥塞流控。 81 | 82 | * 这个算法会把 SACK 中最大的 Sequence Number 保存在 snd.fack 这个变量中,snd.fack 的更新由 ack 带动,如果网络一切安好则和 snd.una 一样(snd.una 就是还没有收到 ack 的地方,也就是前面 sliding window 里的category #2 的第一个地方) 83 | * 然后定义一个 awnd = snd.nxt – snd.fack(snd.nxt 指向发送端 sliding window 中正在要被发送的地方——前面 sliding windows 图示的 category#3 第一个位置),这样 awnd 的意思就是在网络上的数据。(所谓 awnd 意为:actual quantity of data outstanding in the network) 84 | * 如果需要重传数据,那么,awnd = snd.nxt – snd.fack + retran_data,也就是说,awnd 是传出去的数据 + 重传的数据。 85 | * 然后触发 Fast Recovery 的条件是: ( ( snd.fack – snd.una ) > (3*MSS) ) || (dupacks == 3) ) 。这样一来,就不需要等到 3 个 duplicated acks 才重传,而是只要 sack 中的最大的一个数据和 ack 的数据比较长了(3个MSS),那就触发重传。在整个重传过程中 cwnd 不变。直到当第一次丢包的 snd.nxt<=snd.una(也就是重传的数据都被确认了),然后进来拥塞避免机制——cwnd 线性上涨。 86 | 87 | 我们可以看到如果没有 FACK 在,那么在丢包比较多的情况下,原来保守的算法会低估了需要使用的 window 的大小,而需要几个 RTT 的时间才会完成恢复,而 FACK 会比较激进地来干这事。 但是,FACK 如果在一个网络包会被 reordering 的网络里会有很大的问题。 88 | 89 | ### 其它拥塞控制算法简介 90 | 91 | #### TCP Vegas 拥塞控制算法 92 | 93 | 这个算法1994年被提出,它主要对 TCP Reno 做了些修改。这个算法通过对 RTT 的非常重的监控来计算一个基准 RTT。然后通过这个基准RTT来估计当前的网络实际带宽,如果实际带宽比我们的期望的带宽要小或是要多的活,那么就开始线性地减少或增加 cwnd 的大小。如果这个计算出来的 RTT 大于了 Timeout 后,那么,不等 ack 超时就直接重传。(Vegas 的核心思想是用 RTT 的值来影响拥塞窗口,而不是通过丢包) 这个算法的论文是《TCP Vegas: End to End Congestion Avoidance on a Global Internet》这篇论文给了 Vegas 和 New Reno 的对比: 94 | ![Vegas对比](tcp_vegas.png) 95 | 关于这个算法实现,你可以参看Linux源码:/net/ipv4/tcp_vegas.h, /net/ipv4/tcp_vegas.c 96 | 97 | #### HSTCP(High Speed TCP) 算法 98 | 99 | 这个算法来自 RFC 3649(Wikipedia词条)。其对最基础的算法进行了更改,他使得 Congestion Window 涨得快,减得慢。其中: 100 | 101 | * 拥塞避免时的窗口增长方式: cwnd = cwnd + α(cwnd) / cwnd 102 | * 丢包后窗口下降方式:cwnd = (1- β(cwnd))*cwnd 103 | 104 | 注:α(cwnd) 和 β(cwnd) 都是函数,如果你要让他们和标准的 TCP 一样,那么让 α(cwnd)=1,β(cwnd)=0.5 就可以了。 对于 α(cwnd) 和 β(cwnd) 的值是个动态的变换的东西。 关于这个算法的实现,你可以参看 Linux 源码:/net/ipv4/tcp_highspeed.c 105 | 106 | 107 | #### TCP BIC 算法 108 | 2004 年,产内出 BIC 算法。现在你还可以查得到相关的新闻《Google:美科学家研发BIC-TCP协议 速度是DSL六千倍》。 BIC全称 Binary Increase Congestion control,在 Linux 2.6.8 中是默认拥塞控制算法。BIC 的发明者发这么多的拥塞控制算法都在努力找一个合适的 cwnd – Congestion Window,而且 BIC-TCP 的提出者们看穿了事情的本质,其实这就是一个搜索的过程,所以 BIC 这个算法主要用的是 Binary Search——二分查找来干这个事。 关于这个算法实现,你可以参看 Linux 源码:/net/ipv4/tcp_bic.c 109 | 110 | 111 | #### TCP WestWood算法 112 | westwood 采用和 Reno 相同的慢启动算法、拥塞避免算法。westwood 的主要改进方面:在发送端做带宽估计,当探测到丢包时,根据带宽值来设置拥塞窗口、慢启动阈值。 那么,这个算法是怎么测量带宽的?每个 RTT 时间,会测量一次带宽,测量带宽的公式很简单,就是这段 RTT 内成功被 ack 了多少字节。因为,这个带宽和用 RTT 计算 RTO 一样,也是需要从每个样本来平滑到一个值的——也是用一个加权移平均的公式。 另外,我们知道,如果一个网络的带宽是每秒可以发送X个字节,而 RTT 是一个数据发出去后确认需要的时候,所以,X * RTT应该是我们缓冲区大小。所以,在这个算法中,ssthresh 的值就是 est_BD * min-RTT(最小的RTT值),如果丢包是 Duplicated ACKs 引起的,那么如果 cwnd > ssthresh,则 cwin = ssthresh。如果是 RTO 引起的,cwnd = 1,进入慢启动。关于这个算法实现,你可以参看 Linux 源码: /net/ipv4/tcp_westwood.c 113 | 114 | #### 其它 115 | 更多的算法,你可以从Wikipedia的 TCP Congestion Avoidance Algorithm 词条中找到相关的线索 116 | 117 | ## Refer 118 | 整理自: 119 | * [TCP 的那些事儿(上)](https://coolshell.cn/articles/11564.html) 120 | * [TCP 的那些事儿(下)](https://coolshell.cn/articles/11609.html) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Rookie Note 2 | 3 | 资料纯属个人兴趣整理。 4 | 5 | ### 编程语言 6 | [C语言编程](https://github.com/AngryHacker/ocean/blob/master/C-Program/README.md) 7 | 8 | [C++语言编程](https://github.com/AngryHacker/Rookie-Note/blob/master/CPlusPlus-Program/README.md) 9 | 10 | ### 网络基础 11 | 12 | [计算机网络](https://github.com/AngryHacker/ocean/blob/master/Network/README.md) 13 | 14 | [网络编程](https://github.com/AngryHacker/Rookie-Note/tree/master/Network-programming) 15 | 16 | ### 操作系统 17 | 18 | [多线程编程学习](https://github.com/AngryHacker/ocean/blob/master/multithreaded%20programming/README.md) 19 | 20 | [进程间通信](https://github.com/AngryHacker/ocean/blob/master/Interprocess-communication/README.md) 21 | 22 | ### 数据库 23 | 24 | [数据库](https://github.com/AngryHacker/Rookie-Note/tree/master/Database) 25 | 26 | #### 配置 27 | 如果出现图片展示问题,可以参考 https://github.com/catcat0921/github_hosts 28 | 29 | -------------------------------------------------------------------------------- /creative/image/concurrency.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AngryHacker/Rookie-Note/181a68377c15887a5ad1ce5185a71ab51d5962c2/creative/image/concurrency.jpg -------------------------------------------------------------------------------- /creative/image/network-programing/netty-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AngryHacker/Rookie-Note/181a68377c15887a5ad1ce5185a71ab51d5962c2/creative/image/network-programing/netty-1.png -------------------------------------------------------------------------------- /creative/image/network-programing/netty-2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AngryHacker/Rookie-Note/181a68377c15887a5ad1ce5185a71ab51d5962c2/creative/image/network-programing/netty-2.jpeg -------------------------------------------------------------------------------- /creative/image/network-programing/rpc.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AngryHacker/Rookie-Note/181a68377c15887a5ad1ce5185a71ab51d5962c2/creative/image/network-programing/rpc.jpeg -------------------------------------------------------------------------------- /creative/image/network-programing/rpc01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AngryHacker/Rookie-Note/181a68377c15887a5ad1ce5185a71ab51d5962c2/creative/image/network-programing/rpc01.png -------------------------------------------------------------------------------- /creative/image/network-programing/rpc02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AngryHacker/Rookie-Note/181a68377c15887a5ad1ce5185a71ab51d5962c2/creative/image/network-programing/rpc02.png -------------------------------------------------------------------------------- /creative/image/network-programing/rpc03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AngryHacker/Rookie-Note/181a68377c15887a5ad1ce5185a71ab51d5962c2/creative/image/network-programing/rpc03.png -------------------------------------------------------------------------------- /creative/image/network/tcp-windows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AngryHacker/Rookie-Note/181a68377c15887a5ad1ce5185a71ab51d5962c2/creative/image/network/tcp-windows.png -------------------------------------------------------------------------------- /creative/image/network/tcp_client_window.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AngryHacker/Rookie-Note/181a68377c15887a5ad1ce5185a71ab51d5962c2/creative/image/network/tcp_client_window.png -------------------------------------------------------------------------------- /creative/image/network/tcp_finish.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AngryHacker/Rookie-Note/181a68377c15887a5ad1ce5185a71ab51d5962c2/creative/image/network/tcp_finish.png -------------------------------------------------------------------------------- /creative/image/network/tcp_flag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AngryHacker/Rookie-Note/181a68377c15887a5ad1ce5185a71ab51d5962c2/creative/image/network/tcp_flag.png -------------------------------------------------------------------------------- /creative/image/network/tcp_hand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AngryHacker/Rookie-Note/181a68377c15887a5ad1ce5185a71ab51d5962c2/creative/image/network/tcp_hand.png -------------------------------------------------------------------------------- /creative/image/network/tcp_header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AngryHacker/Rookie-Note/181a68377c15887a5ad1ce5185a71ab51d5962c2/creative/image/network/tcp_header.png -------------------------------------------------------------------------------- /creative/image/network/tcp_kp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AngryHacker/Rookie-Note/181a68377c15887a5ad1ce5185a71ab51d5962c2/creative/image/network/tcp_kp.png -------------------------------------------------------------------------------- /creative/image/network/tcp_new_reno.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AngryHacker/Rookie-Note/181a68377c15887a5ad1ce5185a71ab51d5962c2/creative/image/network/tcp_new_reno.png -------------------------------------------------------------------------------- /creative/image/network/tcp_resend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AngryHacker/Rookie-Note/181a68377c15887a5ad1ce5185a71ab51d5962c2/creative/image/network/tcp_resend.png -------------------------------------------------------------------------------- /creative/image/network/tcp_sack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AngryHacker/Rookie-Note/181a68377c15887a5ad1ce5185a71ab51d5962c2/creative/image/network/tcp_sack.png -------------------------------------------------------------------------------- /creative/image/network/tcp_send.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AngryHacker/Rookie-Note/181a68377c15887a5ad1ce5185a71ab51d5962c2/creative/image/network/tcp_send.png -------------------------------------------------------------------------------- /creative/image/network/tcp_seq.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AngryHacker/Rookie-Note/181a68377c15887a5ad1ce5185a71ab51d5962c2/creative/image/network/tcp_seq.png -------------------------------------------------------------------------------- /creative/image/network/tcp_slide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AngryHacker/Rookie-Note/181a68377c15887a5ad1ce5185a71ab51d5962c2/creative/image/network/tcp_slide.png -------------------------------------------------------------------------------- /creative/image/network/tcp_slow_start.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AngryHacker/Rookie-Note/181a68377c15887a5ad1ce5185a71ab51d5962c2/creative/image/network/tcp_slow_start.png -------------------------------------------------------------------------------- /creative/image/network/tcp_status.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AngryHacker/Rookie-Note/181a68377c15887a5ad1ce5185a71ab51d5962c2/creative/image/network/tcp_status.png -------------------------------------------------------------------------------- /creative/image/network/tcp_vegas.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AngryHacker/Rookie-Note/181a68377c15887a5ad1ce5185a71ab51d5962c2/creative/image/network/tcp_vegas.png -------------------------------------------------------------------------------- /creative/image/network/thread_lock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AngryHacker/Rookie-Note/181a68377c15887a5ad1ce5185a71ab51d5962c2/creative/image/network/thread_lock.png -------------------------------------------------------------------------------- /creative/image/parallelism.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AngryHacker/Rookie-Note/181a68377c15887a5ad1ce5185a71ab51d5962c2/creative/image/parallelism.jpg -------------------------------------------------------------------------------- /creative/image/thread_status.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AngryHacker/Rookie-Note/181a68377c15887a5ad1ce5185a71ab51d5962c2/creative/image/thread_status.png -------------------------------------------------------------------------------- /multithreaded programming/README.md: -------------------------------------------------------------------------------- 1 | ## 多线程编程学习 2 | 3 | [POSIX 多线程基础(一)](https://github.com/AngryHacker/ocean/blob/master/multithreaded%20programming/posix_thread_basic_01.md) 4 | 5 | [POSIX 多线程基础(二)](https://github.com/AngryHacker/ocean/blob/master/multithreaded%20programming/posix_thread_basic_02.md) 6 | 7 | [POSIX 多线程基础(三)](https://github.com/AngryHacker/ocean/blob/master/multithreaded%20programming/posix_thread_basic_03.md) 8 | 9 | [POSIX 多线程 API](https://github.com/AngryHacker/ocean/blob/master/multithreaded%20programming/pthread_api.md) 10 | 11 | [POSIX 多线程互斥锁](https://github.com/AngryHacker/ocean/blob/master/multithreaded%20programming/pthread_mutex_basic.md) 12 | 13 | [POSIX 多线程条件变量](https://github.com/AngryHacker/ocean/blob/master/multithreaded%20programming/pthread_cond_basic.md) 14 | 15 | [POSIX 多线程编程实例](https://github.com/AngryHacker/ocean/blob/master/multithreaded%20programming/pthread_examples.md) 16 | 17 | ### 其他资料 18 | [POSIX Threads Programming](https://computing.llnl.gov/tutorials/pthreads/) 19 | 20 | [POSIX thread (pthread) libraries](http://www.cs.cmu.edu/afs/cs/academic/class/15492-f07/www/pthreads.html) 21 | -------------------------------------------------------------------------------- /multithreaded programming/posix_thread_basic_01.md: -------------------------------------------------------------------------------- 1 | ### 基础概念 2 | #### 线程 3 | 4 | * 进程里执行代码的部分; 5 | * 包含一系列机器指令所必须的机器状态,包括当前指令位置(一般为PC寄存器)、栈顶指针SP、通用寄存器、地址和数据寄存器等 6 | * 线程不包括进程中的其他数据,如地址空间和文件描述符 7 | 8 | #### 进程 9 | 10 | * 线程加上地址空间、文件描述符和其他数据 11 | * 一个进程中的所有线程共享文件和地址空间,包括程序段、数据段和堆栈 12 | 13 | #### 进程 vs 线程 14 | 15 | * 多个线程可以共享一个地址空间,而做不同的事情 16 | * 在多处理器系统中,一个进程中的多个线程可以同时做不同的工作 17 | * 系统在线程间切换比在进程间切换快得多 18 | * 每一个进程有独立的虚拟地址空间,但同一个进程中的线程共享相同的地址空间和其他进程数据 19 | 20 | #### 异步(asynchronous) 21 | 事情相互独立地发生,除非有强加的依赖性。任何两个彼此独立运行的操作都是异步的。 22 | 23 | * 如果没有同时执行多个活动,那么异步就没有优势 24 | * 如果开始了一个异步活动,然后什么也不做就等待他结束,则并没有从异步获得好处 25 | 26 | #### 并发(concurrency) 27 | 28 | * 实际上可能是串行发生的事情好像同时发生一样 29 | * 并发描述的是单处理器系统中线程或进程的行为 30 | * 在POSIX中,并发的定义要求“延迟调用线程的函数不应该导致其他线程的无限期延迟” 31 | * 并发操作之间可能任意交错,导致程序相互独立的运行(一个程序不必等到另一个程序结束后才开始运行),但并发并不代表操作同时进行 32 | 33 | ![并发][concurrency] 34 | 35 | #### 并发 vs 并行 36 | 并发:指并发序列同时执行。指事情在相同的方向上独立进行(没有交错)。 37 | 38 | ![并行][parallelism] 39 | 40 | * 真正的并行只能在多处理器系统中存在 41 | * 但并发可以在单处理器系统和多处理器系统中都存在 42 | * 并发能在单处理器系统中存在是因为并发实际上是并行的假象 43 | * 并行则要求程序能够同时执行多个操作 44 | * 而并发只要求程序能够假装同时执行多个操作 45 | 46 | ### 线程安全 47 | #### 什么是线程安全? 48 | 49 | * 定义:指代码能够被多个线程调用而不会产生灾难性后果 50 | * 特点:不要求代码在多个线程中高效的运行,只要求能够安全地运行 51 | 52 | #### 实现线程安全的工具 53 | pthreads 互斥量、条件变量、线程私有数据 54 | 55 | #### 如何实现线程安全? 56 | 57 | * 一般方法: 58 | * 对不需要保存永久状态的函数,通过整个函数调用的串行化实现 59 | * 比如,进入函数时加锁,退出函数时解锁 60 | * => 函数可以被多个线程调用,但一次只能有一个线程调用该函数 61 | * 更有效的方式 62 | * 将线程安全函数分为多个小的临界区 63 | * => 允许多个线程进入该函数,但不能同时进入一个临界区 64 | * 更好的方式 65 | * 将代码改造为对临界对象(数据)的保护而非对临界代码的保护 66 | * => 可使不同时访问相同临界数据的线程完全并行的执行 67 | 68 | ### 可重入 69 | #### 可重入 70 | 71 | * 有时用来表示有效的线程安全,即通过采用比将函数或库转换成一系列区域更为复杂的方式使代码成为线程安全的 72 | * 可重入的函数应该避免依赖任何静态数据,最好避免依赖线程间任何形式的同步 73 | * 互斥量和线程私有数据可以实现线程安全,但通常需要改变接口来使函数可重入 74 | 75 | #### 举例 76 | 77 | * pthreads 为了使 readdir() 函数可重入,增加 readdir_r() 函数,并在该函数内避免任何锁操作 78 | * 让调用者在搜索目录时分配一个数据结构来保存 readdir_r() 的环境 79 | 80 | #### 特点 81 | 这种方式只有调用者才知道数据如何使用。 82 | 83 | ### 并发系统基本功能 84 | #### 基本功能 85 | 86 | * 执行环境:是并发实体的状态;提供建立、删除、维护环境的方式 87 | * 调度:决定在某个给定时刻该执行哪个环境,并在不同的环境中切换 88 | * 同步:为并发执行的环境提供协调访问共享资源的机制 89 | 90 | 什么是同步?——让线程协调地完成工作的机制 91 | 92 | #### 同步的实现方式 93 | 94 | * 互斥量 95 | * 条件变量 96 | * 信号量 97 | * 事件 98 | * 消息机制:管道/Socket/Posix消息队列 99 | 100 | #### 线程、互斥量、条件变量关系 101 | 102 | * 线程是计算机中的可执行单元,是 CPU 调度单位 103 | * 互斥量和条件变量都是线程同步的手段 104 | * 互斥量阻止线程间发生不可预期的冲突 105 | * 一旦避免了冲突,条件变量让线程等待直到可以安全地执行 106 | 107 | 整理来自:[Reference](http://blog.csdn.net/livelylittlefish/article/details/7918110) 108 | 109 | [concurrency]: https://github.com/AngryHacker/ocean/blob/master/creative/image/concurrency.jpg 110 | [parallelism]: https://github.com/AngryHacker/ocean/blob/master/creative/image/parallelism.jpg 111 | -------------------------------------------------------------------------------- /multithreaded programming/posix_thread_basic_02.md: -------------------------------------------------------------------------------- 1 | ### 线程建立与使用 2 | #### 创建线程 3 | 4 | * 通过 `pthread_create()` 函数创建线程 5 | * 向该函数传递线程函数地址和线程函数参数 6 | * 线程函数只有一个 void* 参数 7 | * 该函数返回 pthread_t 类型的线程ID 8 | * 一般调用该函数创建线程,然后调用 `pthread_join()` 函数等待线程结束 9 | * 在当前线程从函数 `pthread_create()` 中返回以及新线程被调度执行之间不存在同步关系 10 | * 新线程可能在当前线程从 `pthread_create()` 返回值前就运行了 11 | * 或在当前线程从 `pthread_create()` 返回之前,新线程就可能已经运行完毕了 12 | 13 | ##### `pthread_join()` 14 | 15 | * 阻塞其调用者直到指定线程终止,然后可以选择地保存线程的返回值 16 | * 当 `pthread_join()` 调用返回时,被连接线程就已经被分离(detached),再也不能连接该线程了 17 | * 如果连接(joining)线程不关心返回值,或者它知道被连接(joined)的线程根本不返回任何值,则可向 `pthread_join()` 的 &retval 参数传递 NULL,此时,被连接线程的返回值将被忽略 18 | 19 | #### 初始线程 20 | 21 | * C 程序运行时,首先运行 main() 函数,main() 函数所在线程称为初始线程或主线程 22 | * 初始线程可调用 `pthread_self()` 获得其 ID,也可调用 `pthread_exit()` 来终止自己 23 | * 从 main() 返回将导致进程终止,也将使进程内所用线程终止 24 | * 在 main() 中调用 `pthread_exit()`,这样进程就必须等待所有线程结束后才能终止 25 | * 若初始线程将其 ID 保存在一个其他线程可以访问的空间,则其他线程就可以等待初始线程的终止或者分离初始线程 26 | 27 | #### 线程分离 28 | 29 | * 分离一个正在运行的线程不会对线程带来任何影响,仅仅是通知系统当该线程结束时,其所属资源可以被回收 30 | * 分离线程意味着通知系统不再需要此线程,允许系统将分配给它的资源回收 31 | * 一个没有被分离的线程终止时会保留其虚拟内存,包括堆栈和其他系统资源 32 | 33 | ### 线程生命周期 34 | 在任意时刻,线程处于下表的四个基本状态之一。 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 70 | 71 |
状态说明
就绪 ready 44 | 线程能够运行,但在等待可用的处理器 45 |
    46 |
  • 可能刚刚启动
  • 47 |
  • 或刚刚从阻塞中恢复
  • 48 |
  • 或被其他线程抢占
  • 49 |
50 |
运行 running线程正在运行;在多处理器系统中,可能有多个线程处于运行状态
阻塞 blocked 线程由于等待处理器外的其他条件无法运行,如条件变量的改变、加锁互斥量或IO操作结束
终止 terminated 63 | 不是被分离,也不是被连接,一旦线程被分离或者连接,它就可以被回收 64 |
    65 |
  • 线程从起始函数中返回
  • 66 |
  • 或调用pthread_exit
  • 67 |
  • 或被取消,终止自己并完成所有资源清理工作
  • 68 |
69 |
72 | 73 | 74 | 线程状态转换如下图。 75 | 76 | ![thread](https://github.com/AngryHacker/ocean/blob/master/creative/image/thread_status.png) 77 | 78 | #### 说明 79 | 80 | * 线程开始处于就绪状态 81 | * 当线程运行时,它调用特定的起始函数 82 | * 它可能被其他线程抢占,或者因等待外来事情而阻塞自己 83 | * 最终线程完成工作,或者从起始函数返回,或者调用 `pthread_exit` 函数,即进入终止状态 84 | * 如果线程已被分离,则它立刻被回收重用;否则,线程停留在终止状态直到被分离或被连接 85 | 86 | 87 | #### 就绪态 88 | 89 | * 线程刚被创建时 90 | * 线程被解除阻塞再次可以运行时 91 | * 运行线程被抢占时,如时间片到 92 | 93 | #### 被阻塞 94 | 95 | * 试图加锁一个已经被锁住的互斥量 96 | * 等待某个条件变量 97 | * 调用 singwait 等待信号 98 | * 执行无法立即完成的 IO 操作 99 | * 内存页错误之类的系统操作 100 | 101 | #### 初始线程(main()函数所在线程)与普通线程区别 102 | 103 | * 初始线程的启动函数 main() 是从程序外部调用的;如 crt0.o 文件复制初始化进程并调用 main() 函数;而普通线程的启动函数及其运行参数均由 `pthread_create()` 函数创建线程时传入,且由 CPU 调度的 104 | * main()函数的参数是 argc 和 argv;普通线程的参数是 void*,且由 `pthread_create()` 函数传入 105 | * 若普通线程从启动函数中返回,则线程终止,而其他线程依然可以运行;但初始线程从 main() 返回时,进程终止,进程内所有线程也被终止 106 | * 若希望在初始线程终止时,进程中的其他线程继续执行,则需要在初始线程调中调用 `pthread_exit()` 而非从 `main()` 返回 107 | * 大多数系统,初始线程运行在默认进程堆栈上,该堆栈可以增长到足够尺寸;而某些实现中,普通线程的堆栈空间是受限的 108 | * 如果线程堆栈溢出,则程序会出现段错误 109 | 110 | #### 线程睡眠原因 111 | 112 | * 被阻塞,需要的某个资源不可用 113 | * 被抢占,即系统将处理器分配给其他线程 114 | 115 | #### `pthread_join()` 的详细解释 116 | 117 | * 用来等待一个线程的结束; 118 | * 是一个线程阻塞函数,调用它的函数将一直等待到被等待的线程结束为止 119 | * 如,主线程调用 `pthread_join()` 等待它创建的线程运行结束,即主线程调用该函数后会被阻塞 120 | * 当函数返回时,被等待的线程的资源被回收 121 | * 若此时新线程没有运行,则它将在主线程被阻塞后从就绪态进入运行态;当新线程运行完毕并返回时,主线程才会被解除阻塞,返回就绪态;当处理器可用时,主线程或立即执行或等到创建的线程终止后重新运行直到结束 122 | 123 | #### 线程终止 124 | 125 | * 一般地,线程从启动函数返回来终止自己 126 | * 当调用 `pthread_exit()` 退出线程或者调用 `pthread_cancel()` 取消线程时,线程在调用每个清理过程后也进入终止状态 127 | * 清理过程又线程通过 `pthread_cleanup_push()` 注册,且尚未通过 `pthread_cleanup_poo()` 删除 128 | 129 | #### Linux 系统僵尸线程 130 | 131 | * 如果线程已经被分离,则会被回收;否则,线程处于终止状态,仍然可以被其他线程调用 `pthread_join()` 连接 132 | * 这种线程被称为僵尸线程,像 Uni 系统中的进程已经结束但还没有被一个 `wait/waitpid` 调用回收一样,即使已经死了但还存在 133 | * 僵尸线程可能会保留其运行时的大部分甚至所有资源,因此不应该让线程长时间处于这种状态;当创建不需要连接的线程时,应该使用 detachstate 属性建立线程使其自动分离 134 | 135 | #### 线程回收 136 | 137 | * 如果使用 detachstate 属性(即设置属性为 PTHREAD_CREATE_DETACH )建立线程,或者调用 `pthread_detach()` 分离线程,则当线程结束时将被立刻回收 138 | * 如果终止线程没有被分离,则它将一直处于终止状态直到被分离(通过 `pthread_detach` )或者被连接(通过 `pthread_join`) 139 | * 线程一旦被分离,就不能再访问它 140 | * 回收将释放所有在线程终止时未释放的系统和进程资源,包括 141 | * 保存线程返回值的内存空间、堆栈 142 | * 保存寄存器状态的内存空间 143 | * 实际上线程终止时上述资源就不能被访问了 144 | * 一旦线程被回收,线程ID就无效了,不能再连接、取消或者执行其他任何操作 145 | * 终止线程ID可能被分给新线程 146 | 147 | 整理来自:[Reference](http://blog.csdn.net/livelylittlefish/article/details/8096595) 148 | -------------------------------------------------------------------------------- /multithreaded programming/posix_thread_basic_03.md: -------------------------------------------------------------------------------- 1 | 注:本文涉及到的 glibc 版本为 2.11,若无特别说明,.表示 glibc-2.11 源代码目录,本文为 /usr/src/glibc-2.11。 2 | 3 | ### 基本概念 4 | 临界区:一个存取共享资源的代码段,而这些共享资源无法同时被多个线程访问;即影响共享数据的代码段。 5 | 6 | 线程同步方法 7 | * 确保对相同/相关数据的内存访问互斥地进行,即一次只能允许一个线程写数据,其他线程必须等待 8 | * Pthreads 使用特殊形式的 Edsger Dijkstra 信号灯——互斥量 9 | * mutex: mutual(相互),exclusion(排斥) 10 | 11 | ### 互斥量的例子 12 | 下图显示了共享互斥量的三个线程的时序图。 13 | 14 | ![lock](https://github.com/AngryHacker/ocean/blob/master/creative/image/thread_lock.png) 15 | 16 | #### 说明 17 | * 处于标圆形框之上的线段表示相关的线程没有拥有互斥量 18 | * 处于圆形框中心线之上的线段表示相关的线程等待互斥量 19 | * 处于圆形框中心线之下的线段表示相关的线程拥有互斥量 20 | 21 | #### 过程描述 22 | 23 | * 最初,互斥量没有被加锁 24 | * 当线程 1 试图加锁该互斥量时,因为没有竞争,线程1立即加锁成功,对应线段也移到中心线之下 25 | * 然后线程 2 试图加锁互斥量,由于互斥量已经被加锁,所以线程2被阻塞,对应线段在中心线之上 26 | * 接着,线程 1 解锁互斥量,于是线程 2 解除阻塞,并对互斥量加锁成功 27 | * 然后,线程 3 试图加锁互斥量,同样被阻塞 28 | * 此时,线程 1 调用函数 pthread_mutext_trylock 试图加锁互斥量,而立即返回 EBUSY 29 | * 然后,线程 2 解锁互斥量,解除线程3的阻塞,线程 3 加锁成功 30 | * 最后,线程 3 完成工作,解锁互斥量 31 | 32 | ### 互斥量定义 33 | #### 64位系统 34 | file: /usr/include/bits/pthreadtypes.h 35 | 36 | ```C++ 37 | /* Data structures for mutex handling. The structure of the attribute 38 | type is not exposed on purpose. */ 39 | typedef union 40 | { 41 | struct __pthread_mutex_s 42 | { 43 | int __lock; 44 | unsigned int __count; 45 | int __owner; 46 | #if __WORDSIZE == 64 47 | unsigned int __nusers; 48 | #endif 49 | /* KIND must stay at this position in the structure to maintain 50 | binary compatibility. */ 51 | int __kind; 52 | #if __WORDSIZE == 64 53 | int __spins; 54 | __pthread_list_t __list; 55 | # define __PTHREAD_MUTEX_HAVE_PREV 1 56 | #else 57 | unsigned int __nusers; 58 | __extension__ union 59 | { 60 | int __spins; 61 | __pthread_slist_t __list; 62 | }; 63 | #endif 64 | } __data; 65 | char __size[__SIZEOF_PTHREAD_MUTEX_T]; 66 | long int __align; 67 | } pthread_mutex_t; 68 | 69 | typedef union 70 | { 71 | char __size[__SIZEOF_PTHREAD_MUTEXATTR_T]; 72 | long int __align; 73 | } pthread_mutexattr_t; 74 | ``` 75 | 76 | 该定义来 自glibc,其在 glibc 代码中的位置为./nptl/sysdeps/unix/sysv/linux/x86_64/bits/pthreadtypes.h。64 位系统在安 装glibc 的时候会自动拷贝该文件(x86_64版本)到/usr/include/bits目录。 77 | 78 | 79 | 其中: 80 | 81 | ```C++ 82 | # define __SIZEOF_PTHREAD_MUTEX_T 40 83 | # define __SIZEOF_PTHREAD_MUTEXATTR_T 4 84 | ``` 85 | 86 | 关于 __pthread_list_t(双向链表)和 __pthread_slist_t(单向链表)的定义可参考源代码。 87 | 88 | #### 32位系统 89 | file: /usr/include/bits/pthreadtypes.h 90 | 91 | ```C++ 92 | /* Data structures for mutex handling. The structure of the attribute 93 | type is not exposed on purpose. */ 94 | typedef union 95 | { 96 | struct __pthread_mutex_s 97 | { 98 | int __lock; 99 | unsigned int __count; 100 | int __owner; 101 | /* KIND must stay at this position in the structure to maintain 102 | binary compatibility. */ 103 | int __kind; 104 | unsigned int __nusers; 105 | __extension__ union 106 | { 107 | int __spins; 108 | __pthread_slist_t __list; 109 | }; 110 | } __data; 111 | char __size[__SIZEOF_PTHREAD_MUTEX_T]; 112 | long int __align; 113 | } pthread_mutex_t; 114 | 115 | typedef union 116 | { 117 | char __size[__SIZEOF_PTHREAD_MUTEXATTR_T]; 118 | long int __align; 119 | } pthread_mutexattr_t; 120 | ``` 121 | 122 | 该定义来自 glibc,其 在glibc 代码中的位置为./nptl/sysdeps/unix/sysv/linux/i386/bits/pthreadtypes.h。32 位系统在安装 glibc 的时候会自动拷贝该文件(i386版本)到 /usr/include/bits 目录。 123 | 124 | 其中: 125 | 126 | ```C++ 127 | #define __SIZEOF_PTHREAD_MUTEX_T 24 128 | #define __SIZEOF_PTHREAD_MUTEXATTR_T 4 129 | ``` 130 | 131 | #### pthread_mutex_t 结构的内容 132 | 如下是在 64 位系统的实验结果。 133 | 134 | ```shell 135 | (gdb) p data.mutex 136 | $1 = { 137 | __data = { 138 | __lock = 0, 139 | __count = 0, 140 | __owner = 0, 141 | __nusers = 0, 142 | __kind = 0, 143 | __spins = 0, 144 | __list = { 145 | __prev = 0x0, 146 | __next = 0x0 147 | } 148 | }, 149 | __size = '\000' , 150 | __align = 0 151 | } 152 | ``` 153 | 154 | ### 互斥量初始化与销毁 155 | #### 初始化 156 | 互斥量使用原则:使用前必须初始化,而且只被初始化一次。 157 | 158 | ##### 静态初始化 159 | 使用宏 PTHREAD_MUTEX_INITIALIZER 声明具有默认属性的静态互斥量: 160 | file: /usr/include/pthread.h 161 | 162 | ```c++ 163 | /* Mutex initializers. */ 164 | #if __WORDSIZE == 64 165 | # define PTHREAD_MUTEX_INITIALIZER \ 166 | { { 0, 0, 0, 0, 0, 0, { 0, 0 } } } 167 | # ifdef __USE_GNU 168 | # define PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP \ 169 | { { 0, 0, 0, 0, PTHREAD_MUTEX_RECURSIVE_NP, 0, { 0, 0 } } } 170 | # define PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP \ 171 | { { 0, 0, 0, 0, PTHREAD_MUTEX_ERRORCHECK_NP, 0, { 0, 0 } } } 172 | # define PTHREAD_ADAPTIVE_MUTEX_INITIALIZER_NP \ 173 | { { 0, 0, 0, 0, PTHREAD_MUTEX_ADAPTIVE_NP, 0, { 0, 0 } } } 174 | # endif 175 | #else 176 | # define PTHREAD_MUTEX_INITIALIZER \ 177 | { { 0, 0, 0, 0, 0, { 0 } } } 178 | # ifdef __USE_GNU 179 | # define PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP \ 180 | { { 0, 0, 0, PTHREAD_MUTEX_RECURSIVE_NP, 0, { 0 } } } 181 | # define PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP \ 182 | { { 0, 0, 0, PTHREAD_MUTEX_ERRORCHECK_NP, 0, { 0 } } } 183 | # define PTHREAD_ADAPTIVE_MUTEX_INITIALIZER_NP \ 184 | { { 0, 0, 0, PTHREAD_MUTEX_ADAPTIVE_NP, 0, { 0 } } } 185 | # endif 186 | #endif 187 | ``` 188 | 189 | 该文件在 glibc 代码中的位置为 ./nptl/sysdeps/pthread/pthread.h。 190 | 191 | ##### 动态初始化 192 | * 通过 `pthread_mutex_init()` 调用动态初始化互斥量 193 | * 使用场合 194 | * 当使用 malloc 动态分配一个包含互斥量的数据结构时,应使用动态初始化 195 | * 若要初始化一个非缺省属性的互斥量,必须使用动态初始化 196 | * 也可动态初始化静态声明的互斥量,但必须保证每个互斥量在使用前被初始化,而且只能被初始化一次 197 | 198 | 动态初始化代码可参考./nptl/pthread_mutex_init.c 文件。其中 `__pthread_mutex_init()` 函数即对mutex的各个feild进行初始化。 199 | 200 | #### 销毁互斥量 201 | 使用 `pthread_mutex_destroy()` 释放互斥量。 202 | 203 | 注意 204 | * 当确信没有线程在互斥量上阻塞,且互斥量没有被锁住时,可以立即释放 205 | * 不需要销毁一个使用 PTHREAD_MUTEX_INITIALIZER 宏静态初始化的互斥量 206 | 207 | 销毁互斥量代码可参考 ./nptl/pthread_mutex_destroy.c 文件。其中` __pthread_mutex_destroy()` 函数设置 mutex 的相应字段使其不可用。代码如下: 208 | 209 | ```c++ 210 | int 211 | __pthread_mutex_destroy (mutex) 212 | pthread_mutex_t *mutex; 213 | { 214 | if ((mutex->__data.__kind & PTHREAD_MUTEX_ROBUST_NORMAL_NP) == 0 215 | && mutex->__data.__nusers != 0) 216 | return EBUSY; 217 | 218 | /* Set to an invalid value. */ 219 | mutex->__data.__kind = -1; 220 | 221 | return 0; 222 | } 223 | ``` 224 | 225 | 整理来自:[Reference](http://blog.csdn.net/livelylittlefish/article/details/8096595) 226 | -------------------------------------------------------------------------------- /multithreaded programming/pthread_api.md: -------------------------------------------------------------------------------- 1 | ### 简介 2 | POSIX thread 简称为 pthread,Posix 线程是一个 POSIX 标准线程。该标准定义内部 API 创建和操纵线程。 3 | 4 | ### 作用 5 | 线程库实行了 POSIX 线程标准通常称为 pthreads. pthreads 是最常用的 POSIX 系统如 Linux 和 Unix,而微软 Windowsimplementations 同时存在。举例来说,pthreads-w32 可支持 MIDP 的 pthread。 6 | 7 | Pthreads 定义了一套 C 程序语言类型、函数与常量,它以 pthread.h 头文件和一个线程库实现。 8 | 9 | ### 数据结构与函数 10 | #### 数据结构 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
pthread_t线程句柄
pthread_attr_t线程属性
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 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 |
pthread_create创建一个线程
pthread_exit终止当前线程
pthread_cancel中断另外一个线程的运行
pthread_join阻塞当前的线程,直到另外一个线程运行结束
pthread_attr_init初始化线程的属性
pthread_attr_setdetachstate设置脱离状态的属性(决定这个线程在终止时是否可以被结合)
pthread_attr_getdetachstate获取脱离状态的属性
pthread_attr_destroy删除线程的属性
pthread_kill向线程发送一个终止信号
63 | 64 | #### 同步函数 65 | 用于 mutex 和条件变量 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 |
pthread_mutex_init初始化互斥锁
pthread_mutex_destroy删除互斥锁
pthread_mutex_lock占有互斥锁(阻塞操作)
pthread_mutex_trylock试图占有互斥锁(不阻塞操作)。当互斥锁空闲时将占有该锁;否则立即返回 
pthread_mutex_unlock释放互斥锁
pthread_cond_init初始化条件变量
pthread_cond_destroy销毁条件变量
pthread_cond_wait等待条件变量的特殊条件发生
pthread_cond_signal唤醒第一个调用pthread_cond_wait()而进入睡眠的线程
105 | 106 | #### Thread-local storage(线程特有数据) 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 |
pthread_key_create分配用于标识进程中线程特定数据的键
pthread_setspecific为指定线程特定数据键设置线程特定绑定
pthread_getspecific获取调用线程的键绑定,并将该绑定存储在 value 指向的位置中
pthread_key_delete销毁现有线程特定数据键
125 | 126 | #### 工具函数 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 |
pthread_equal对两个线程的线程标识号进行比较
pthread_detac分离线程
pthread_self查询线程自身线程标识号
141 | -------------------------------------------------------------------------------- /multithreaded programming/pthread_cond_basic.md: -------------------------------------------------------------------------------- 1 | ### 初始化条件变量 `pthread_cond_init` 2 | 3 | ```c 4 | #include 5 | int pthread_cond_init(pthread_cond_t *cv, const pthread_condattr_t *cattr); 6 | ``` 7 | 8 | 返回值:函数成功返回 0;任何其他返回值都表示错误 9 | 10 | 初始化一个条件变量。当参数 cattr 为空指针时,函数创建的是一个缺省的条件变量。否则条件变量的属性将由 cattr 中的属性值来决定。调用 `pthread_cond_init` 函数时,参数 cattr 为空指针等价于 cattr 中的属性为缺省属性,只是前者不需要 cattr 所占用的内存开销。这个函数返回时,条件变量被存放在参数 cv 指向的内存中。 11 | 12 | 可以用宏 `PTHREAD_COND_INITIALIZER` 来初始化静态定义的条件变量,使其具有缺省属性。这和用 `pthread_cond_init` 函数动态分配的效果是一样的。初始化时不进行错误检查。如: 13 | 14 | ```c 15 | pthread_cond_t cv = PTHREAD_COND_INITIALIZER; 16 | ``` 17 | 不能由多个线程同时初始化一个条件变量。当需要重新初始化或释放一个条件变量时,应用程序必须保证这个条件变量未被使用。 18 | 19 | ### 阻塞在条件变量上 `pthread_cond_wait` 20 | 21 | ```c 22 | #include 23 | int pthread_cond_wait(pthread_cond_t *cv, pthread_mutex_t *mutex); 24 | ``` 25 | 26 | 返回值:函数成功返回 0;任何其他返回值都表示错误 27 | 28 | 函数将解锁 mutex 参数指向的互斥锁,并使当前线程阻塞在 cv 参数指向的条件变量上。 29 | 30 | 被阻塞的线程可以被 `pthread_cond_signal` 函数、`pthread_cond_broadcast` 函数唤醒,也可能在被信号中断后被唤醒。 31 | 32 | `pthread_cond_wait` 函数的返回并不意味着条件的值一定发生了变化,必须重新检查条件的值。 33 | 34 | `pthread_cond_wait` 函数返回时,相应的互斥锁将被当前线程锁定,即使是函数出错返回。 35 | 36 | 一般一个条件表达式都是在一个互斥锁的保护下被检查。当条件表达式未被满足时,线程将仍然阻塞在这个条件变量上。当另一个线程改变了条件的值并向条件变量发出信号时,等待在这个条件变量上的一个线程或所有线程被唤醒,接着都试图再次占有相应的互斥锁。 37 | 38 | 阻塞在条件变量上的线程被唤醒以后,直到 `pthread_cond_wait()` 函数返回之前条件的值都有可能发生变化。所以函数返回以后,在锁定相应的互斥锁之前,必须重新测试条件值。最好的测试方法是循环调用 `pthread_cond_wait` 函数,并把满足条件的表达式置为循环的终止条件。如: 39 | 40 | ```c 41 | pthread_mutex_lock(); 42 | while (condition_is_false) 43 | pthread_cond_wait(); 44 | pthread_mutex_unlock(); 45 | ``` 46 | 47 | 阻塞在同一个条件变量上的不同线程被释放的次序是不一定的。 48 | 49 | 注意:`pthread_cond_wait()` 函数是退出点,如果在调用这个函数时,已有一个挂起的退出请求,且线程允许退出,这个线程将被终止并开始执行善后处理函数,而这时和条件变量相关的互斥锁仍将处在锁定状态。 50 | 51 | ### 解除在条件变量上的阻塞 `pthread_cond_signal` 52 | 53 | ```c 54 | #include 55 | int pthread_cond_signal(pthread_cond_t *cv); 56 | ``` 57 | 58 | 返回值:函数成功返回 0;任何其他返回值都表示错误 59 | 60 | 函数被用来释放被阻塞在指定条件变量上的一个线程。 61 | 62 | 必须在互斥锁的保护下使用相应的条件变量。否则对条件变量的解锁有可能发生在锁定条件变量之前,从而造成死锁。 63 | 64 | 唤醒阻塞在条件变量上的所有线程的顺序由调度策略决定,如果线程的调度策略是 SCHED_OTHER 类型的,系统将根据线程的优先级唤醒线程。 65 | 66 | 如果没有线程被阻塞在条件变量上,那么调用 `pthread_cond_signal()` 将没有作用。 67 | 68 | ### 阻塞直到指定时间 `pthread_cond_timedwait` 69 | 70 | ```c 71 | #include 72 | #include 73 | int pthread_cond_timedwait(pthread_cond_t *cv, pthread_mutex_t *mp, const structtimespec * abstime); 74 | ``` 75 | 76 | 返回值:函数成功返回 0;任何其他返回值都表示错误 77 | 78 | 函数到了一定的时间,即使条件未发生也会解除阻塞。这个时间由参数 abstime 指定。函数返回时,相应的互斥锁往往是锁定的,即使是函数出错返回。 79 | 80 | 注意:`pthread_cond_timedwait` 函数也是退出点。 81 | 82 | 超时时间参数是指一天中的某个时刻。使用举例: 83 | 84 | ```c 85 | pthread_timestruc_t to; 86 | to.tv_sec = time(NULL) + TIMEOUT; 87 | to.tv_nsec = 0; 88 | ``` 89 | 超时返回的错误码是 `ETIMEDOUT`。 90 | 91 | ### 释放阻塞的所有线程 `pthread_cond_broadcast` 92 | 93 | ```c 94 | #include 95 | int pthread_cond_broadcast(pthread_cond_t *cv); 96 | ``` 97 | 98 | 返回值:函数成功返回 0;任何其他返回值都表示错误 99 | 100 | 函数唤醒所有被 `pthread_cond_wait` 函数阻塞在某个条件变量上的线程,参数 cv 被用来指定这个条件变量。当没有线程阻塞在这个条件变量上时,`pthread_cond_broadcast` 函数无效。 101 | 102 | 由于 `pthread_cond_broadcast` 函数唤醒所有阻塞在某个条件变量上的线程,这些线程被唤醒后将再次竞争相应的互斥锁,所以必须小心使用 `pthread_cond_broadcast` 函数。 103 | 104 | ### 释放条件变量 `pthread_cond_destroy` 105 | 106 | ```c 107 | #include 108 | int pthread_cond_destroy(pthread_cond_t *cv); 109 | ``` 110 | 111 | 返回值:函数成功返回 0;任何其他返回值都表示错误 112 | 113 | 释放条件变量。 114 | 115 | 注意:条件变量占用的空间并未被释放。 116 | 117 | ### 唤醒丢失问题 118 | 在线程未获得相应的互斥锁时调用 `pthread_cond_signal` 或 `pthread_cond_broadcast` 函数可能会引起唤醒丢失问题。 119 | 120 | 唤醒丢失往往会在下面的情况下发生: 121 | 122 | * 一个线程调用 `pthread_cond_signal` 或 `pthread_cond_broadcast` 函数; 123 | * 另一个线程正处在测试条件变量和调用 `pthread_cond_wait` 函数之间; 124 | * 没有线程正在处在阻塞等待的状态下。 125 | 126 | ### 例子 127 | 128 | #### 示例 1 129 | 130 | ```c 131 | #include 132 | #include 133 | 134 | #define TCOUNT 10 135 | #define WATCH_COUNT 12 136 | 137 | int count = 0; 138 | pthread_mutex_t count_mutex = PTHREAD_MUTEX_INITIALIZER; 139 | pthread_cond_t count_threshold_cv = PTHREAD_COND_INITIALIZER; 140 | int thread_ids[3] = {0,1,2}; 141 | void* inc_count(void*); 142 | void* watch_count(void*); 143 | 144 | extern int 145 | main(void) 146 | { 147 | int i; 148 | pthread_t threads[3]; 149 | pthread_create(&threads[0],NULL,inc_count, &thread_ids[0]); 150 | pthread_create(&threads[1],NULL,inc_count, &thread_ids[1]); 151 | pthread_create(&threads[2],NULL,watch_count, &thread_ids[2]); 152 | for (i = 0; i < 3; i++) { 153 | pthread_join(threads[i], NULL); 154 | } 155 | return 0; 156 | } 157 | 158 | void* watch_count(void *idp) { 159 | pthread_mutex_lock(&count_mutex); 160 | while (count < WATCH_COUNT) { 161 | pthread_cond_wait(&count_threshold_cv, 162 | &count_mutex); 163 | printf("watch_count(): Thread %d,Count is %d\n", 164 | *(int*)idp, count); 165 | } 166 | pthread_mutex_unlock(&count_mutex); 167 | return 0; 168 | } 169 | 170 | void* inc_count(void *idp) { 171 | int i; 172 | for (i =0; i < TCOUNT; i++) { 173 | pthread_mutex_lock(&count_mutex); 174 | count++; 175 | printf("inc_count(): Thread %d, old count %d, new count %d\n", 176 | *(int*)idp, count - 1, count ); 177 | if (count == WATCH_COUNT) 178 | pthread_cond_signal(&count_threshold_cv); 179 | pthread_mutex_unlock(&count_mutex); 180 | } 181 | return 0; 182 | } 183 | ``` 184 | 185 | ##### 运行结果 186 | 187 | ``` 188 | ➜ pthread gcc -Wall -pthread -o pthread_cond1 pthread_cond1.c 189 | ➜ pthread ./pthread_cond1 190 | inc_count(): Thread 1, old count 0, new count 1 191 | inc_count(): Thread 1, old count 1, new count 2 192 | inc_count(): Thread 1, old count 2, new count 3 193 | inc_count(): Thread 1, old count 3, new count 4 194 | inc_count(): Thread 1, old count 4, new count 5 195 | inc_count(): Thread 1, old count 5, new count 6 196 | inc_count(): Thread 1, old count 6, new count 7 197 | inc_count(): Thread 1, old count 7, new count 8 198 | inc_count(): Thread 1, old count 8, new count 9 199 | inc_count(): Thread 1, old count 9, new count 10 200 | inc_count(): Thread 0, old count 10, new count 11 201 | inc_count(): Thread 0, old count 11, new count 12 202 | watch_count(): Thread 2,Count is 12 203 | inc_count(): Thread 0, old count 12, new count 13 204 | inc_count(): Thread 0, old count 13, new count 14 205 | inc_count(): Thread 0, old count 14, new count 15 206 | inc_count(): Thread 0, old count 15, new count 16 207 | inc_count(): Thread 0, old count 16, new count 17 208 | inc_count(): Thread 0, old count 17, new count 18 209 | inc_count(): Thread 0, old count 18, new count 19 210 | inc_count(): Thread 0, old count 19, new count 20 211 | ``` 212 | ##### 示例 2 213 | 214 | ```c 215 | /* 这个例子是为了保证 COUNT_HALT1 到 COUNT_HALT2 的加法由 functionCount2 来完成,但我加了两个 sleep 就全变了...*/ 216 | #include 217 | #include 218 | #include 219 | #include 220 | 221 | pthread_mutex_t count_mutex = PTHREAD_MUTEX_INITIALIZER; 222 | pthread_mutex_t condition_mutex = PTHREAD_MUTEX_INITIALIZER; 223 | pthread_cond_t condition_cond = PTHREAD_COND_INITIALIZER; 224 | 225 | void *functionCount1(); 226 | void *functionCount2(); 227 | int count = 0; 228 | #define COUNT_DONE 10 229 | #define COUNT_HALT1 3 230 | #define COUNT_HALT2 6 231 | 232 | int main() 233 | { 234 | pthread_t thread1, thread2; 235 | 236 | pthread_create( &thread1, NULL, &functionCount1, NULL); 237 | pthread_create( &thread2, NULL, &functionCount2, NULL); 238 | pthread_join( thread1, NULL); 239 | pthread_join( thread2, NULL); 240 | 241 | exit(0); 242 | } 243 | 244 | void *functionCount1() 245 | { 246 | for(;;) 247 | { 248 | pthread_mutex_lock( &condition_mutex ); 249 | while( count >= COUNT_HALT1 && count <= COUNT_HALT2 ) 250 | { 251 | pthread_cond_wait( &condition_cond, &condition_mutex ); 252 | } 253 | pthread_mutex_unlock( &condition_mutex ); 254 | 255 | //sleep(1); 256 | 257 | pthread_mutex_lock( &count_mutex ); 258 | count++; 259 | printf("Counter value functionCount1: %d\n",count); 260 | pthread_mutex_unlock( &count_mutex ); 261 | 262 | if(count >= COUNT_DONE) return(NULL); 263 | } 264 | } 265 | 266 | void *functionCount2() 267 | { 268 | for(;;) 269 | { 270 | pthread_mutex_lock( &condition_mutex ); 271 | if( count < COUNT_HALT1 || count > COUNT_HALT2 ) 272 | { 273 | pthread_cond_signal( &condition_cond ); 274 | } 275 | pthread_mutex_unlock( &condition_mutex ); 276 | 277 | //sleep(1); 278 | 279 | pthread_mutex_lock( &count_mutex ); 280 | count++; 281 | printf("Counter value functionCount2: %d\n",count); 282 | pthread_mutex_unlock( &count_mutex ); 283 | 284 | if(count >= COUNT_DONE) return(NULL); 285 | } 286 | 287 | } 288 | ``` 289 | 290 | ##### 运行结果 291 | 292 | ``` 293 | ➜ pthread gcc -Wall -pthread -o pthread_cond2 pthread_cond2.c 294 | ➜ pthread ./pthread_cond2 295 | Counter value functionCount2: 1 296 | Counter value functionCount2: 2 297 | Counter value functionCount2: 3 298 | Counter value functionCount2: 4 299 | Counter value functionCount2: 5 300 | Counter value functionCount2: 6 301 | Counter value functionCount2: 7 302 | Counter value functionCount2: 8 303 | Counter value functionCount2: 9 304 | Counter value functionCount2: 10 305 | Counter value functionCount1: 11 306 | ``` 307 | 308 | 整理来自:[Reference1](http://blog.csdn.net/icechenbing/article/details/7662026) 309 | [Reference2](http://maxim.int.ru/bookshelf/PthreadsProgram/htm/r_28.html) 310 | [Reference3](http://www.cs.cmu.edu/afs/cs/academic/class/15492-f07/www/pthreads.html#SYNCHRONIZATION) 311 | -------------------------------------------------------------------------------- /multithreaded programming/pthread_examples.md: -------------------------------------------------------------------------------- 1 | ### 多线程创建 2 | #### 参考源码 3 | ```c 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | pthread_t child_tid; 11 | 12 | void print_ids(const char *str) { 13 | pid_t pid; /* 进程 id */ 14 | pthread_t tid; /* 线程 id */ 15 | pid = getpid(); /* 获取进程 id */ 16 | tid = pthread_self(); /* 获取线程 id */ 17 | printf("%s pid: %u, tid: %u (0x%x)\n", 18 | str, 19 | (unsigned int)pid, 20 | (unsigned int)tid, 21 | (unsigned int)tid); 22 | } 23 | 24 | void *func(void *arg) { 25 | print_ids("new thread:"); 26 | return ((void *)0); 27 | } 28 | 29 | int main(int argc, char **argv) { 30 | int err; 31 | err = pthread_create(&child_tid, NULL, func, NULL); /* 创建线程 */ 32 | if (err != 0) { 33 | printf("create thread error: %s\n", strerror(err)); 34 | return 1; 35 | } else { 36 | printf("create thread success! new thread id: %u\n", (unsigned int)child_tid); 37 | } 38 | print_ids("main thread:"); 39 | sleep(1); 40 | return 0; 41 | } 42 | ``` 43 | 44 | #### 运行结果 45 | 46 | ```shell 47 | ➜ pthread gcc -Wall -o pthread_create pthread_create.c -pthread 48 | ➜ pthread ./pthread_create 49 | create thread success! new thread id: 2954086144 50 | main thread: pid: 3214, tid: 2962421504 (0xb092f700) 51 | new thread: pid: 3214, tid: 2954086144 (0xb013c700) 52 | ``` 53 | 54 | ### 多线程条件变量 55 | #### 参考源码 56 | 57 | ```c 58 | #include 59 | #include 60 | #include 61 | 62 | pthread_mutex_t counter_lock; /* 互斥锁 */ 63 | pthread_cond_t counter_nonzero; /* 条件变量 */ 64 | int counter = 0; 65 | int estatus = -1; 66 | 67 | void *decrement_counter(void *argv); 68 | void *increment_counter(void *argv); 69 | 70 | int main(int argc, char **argv) { 71 | printf("counter: %d\n", counter); 72 | pthread_t thd1, thd2; 73 | int ret; 74 | 75 | /* 初始化 */ 76 | pthread_mutex_init(&counter_lock, NULL); 77 | pthread_cond_init(&counter_nonzero, NULL); 78 | 79 | ret = pthread_create(&thd1, NULL, decrement_counter, NULL); /* 创建线程1 */ 80 | if (ret) { 81 | perror("del:\n"); 82 | return 1; 83 | } 84 | 85 | sleep(1); /* 注:相比原文添加这个使得运行结果对条件变量更好理解 */ 86 | 87 | ret = pthread_create(&thd2, NULL, increment_counter, NULL); /* 创建线程2 */ 88 | if (ret) { 89 | perror("inc: \n"); 90 | return 1; 91 | } 92 | 93 | int counter = 0; 94 | while (counter != 10) { 95 | printf("counter(main): %d\n", counter); /* 主线程 */ 96 | sleep(1); 97 | counter++; 98 | } 99 | 100 | pthread_exit(0); 101 | 102 | return 0; 103 | } 104 | 105 | void *decrement_counter(void *argv) { 106 | printf("counter(decrement): %d\n", counter); 107 | pthread_mutex_lock(&counter_lock); 108 | while (counter == 0) 109 | pthread_cond_wait(&counter_nonzero, &counter_lock); /* 进入阻塞(wait),等待激活(signal) */ 110 | 111 | printf("counter--(before): %d\n", counter); 112 | counter--; /* 等待signal激活后再执行 */ 113 | printf("counter--(after): %d\n", counter); 114 | pthread_mutex_unlock(&counter_lock); 115 | 116 | return &estatus; 117 | } 118 | 119 | void *increment_counter(void *argv) { 120 | printf("counter(increment): %d\n", counter); 121 | pthread_mutex_lock(&counter_lock); 122 | if (counter == 0) { 123 | /* 激活(signal)阻塞(wait)的线程(先执行完signal线程,然后再执行wait线程) */ 124 | pthread_cond_signal(&counter_nonzero); 125 | } 126 | 127 | printf("counter++(before): %d\n", counter); 128 | counter++; 129 | printf("counter++(after): %d\n", counter); 130 | pthread_mutex_unlock(&counter_lock); 131 | 132 | return &estatus; 133 | } 134 | ``` 135 | 136 | #### 运行结果 137 | 138 | ```shell 139 | ➜ pthread gcc -pthread -Wall -o pthread_cond pthread_cond.c 140 | ➜ pthread ./pthread_cond 141 | counter: 0 142 | counter(decrement): 0 143 | counter(main): 0 144 | counter(increment): 0 145 | counter++(before): 0 146 | counter++(after): 1 147 | counter--(before): 1 148 | counter--(after): 0 149 | counter(main): 1 150 | counter(main): 2 151 | counter(main): 3 152 | counter(main): 4 153 | counter(main): 5 154 | counter(main): 6 155 | counter(main): 7 156 | counter(main): 8 157 | counter(main): 9 158 | ``` 159 | ### 线程私有数据(TSD) 160 | #### 参考源码 161 | 162 | ```c 163 | /** 164 | * 线程中特有的线程存储, Thread Specific Data 。在多线程程序中,所有线程共享程序中的全局变量。 165 | * 现在有一全局变量,所有线程都可以使用它,改变它的值。而如果每个线程希望能单独拥有它, 166 | * 那么就需要使用线程存储了。表面上看起来这是一个全局变量,所有线程都可以使用它, 167 | * 而它的值在每一个线程中又是单独存储的。这就是线程存储的意义。 168 | * 169 | * 下面说一下线程存储的具体用法。 170 | * 171 | * 创建一个类型为 pthread_key_t 类型的变量。 172 | * 173 | * 调用 pthread_key_create() 来创建该变量。该函数有两个参数, 174 | * 第一个参数就是上面声明的 pthread_key_t 变量,第二个参数是一个清理函数,用来在线程释放该线程存储的时候被调用。 175 | * 该函数指针可以设成 NULL ,这样系统将调用默认的清理函数。 176 | * 177 | * 当线程中需要存储特殊值的时候,可以调用 pthread_setspcific() 。该函数有两个参数, 178 | * 第一个为前面声明的 pthread_key_t 变量,第二个为 void* 变量,这样你可以存储任何类型的值。 179 | * 180 | * 如果需要取出所存储的值,调用 pthread_getspecific() 。 181 | * 该函数的参数为前面提到的 pthread_key_t 变量,该函数返回 void * 类型的值。 182 | * 183 | * 下面是前面提到的函数的原型: 184 | * 185 | * int pthread_setspecific(pthread_key_t key, const void *value); 186 | * 187 | * void *pthread_getspecific(pthread_key_t key); 188 | * 189 | * int pthread_key_create(pthread_key_t *key, void (*destructor)(void*)); 190 | */ 191 | 192 | #include 193 | #include 194 | #include 195 | 196 | pthread_key_t key; /* 声明参数key */ 197 | 198 | void echomsg(void *arg) /* 析构处理函数 */ 199 | { 200 | printf("destruct executed in thread = %u, arg = %p\n", 201 | (unsigned int)pthread_self(), 202 | arg); 203 | } 204 | 205 | void *child_1(void *arg) 206 | { 207 | pthread_t tid; 208 | 209 | tid = pthread_self(); 210 | printf("%s: thread %u enter\n", (char *)arg, (unsigned int)tid); 211 | 212 | pthread_setspecific(key, (void *)tid); /* 与key值绑定的value(tid) */ 213 | printf("%s: thread %u returns %p\n", /* %p 表示输出指针格式 */ 214 | (char *)arg, 215 | (unsigned int)tid, 216 | pthread_getspecific(key)); /* 获取key值的value */ 217 | sleep(1); 218 | return NULL; 219 | } 220 | 221 | void *child_2(void *arg) 222 | { 223 | pthread_t tid; 224 | 225 | tid = pthread_self(); 226 | printf("%s: thread %u enter\n", (char *)arg, (unsigned int)tid); 227 | 228 | pthread_setspecific(key, (void *)tid); 229 | printf("%s: thread %u returns %p\n", 230 | (char *)arg, 231 | (unsigned int)tid, 232 | pthread_getspecific(key)); 233 | sleep(1); 234 | return NULL; 235 | } 236 | 237 | int main(void) 238 | { 239 | pthread_t tid1, tid2; 240 | 241 | printf("hello main\n"); 242 | 243 | pthread_key_create(&key, echomsg); /* 创建key */ 244 | 245 | pthread_create(&tid1, NULL, child_1, (void *)"child_1"); /* 创建带参数的线程,需要强制转换 */ 246 | pthread_create(&tid2, NULL, child_2, (void *)"child_2"); 247 | 248 | sleep(3); 249 | pthread_key_delete(key); /* 清除key */ 250 | printf("bye main\n"); 251 | 252 | pthread_exit(0); 253 | return 0; 254 | } 255 | ``` 256 | 257 | #### 运行结果 258 | 259 | ``` 260 | ➜ pthread gcc -Wall -pthread -o pthread_setspecific pthread_setspecific.c 261 | ➜ pthread ./pthread_setspecific 262 | hello main 263 | child_2: thread 4238526208 enter 264 | child_2: thread 4238526208 returns 0x7fdafca2c700 265 | child_1: thread 4246918912 enter 266 | child_1: thread 4246918912 returns 0x7fdafd22d700 267 | destruct executed in thread = 4238526208, arg = 0x7fdafca2c700 268 | destruct executed in thread = 4246918912, arg = 0x7fdafd22d700 269 | bye main 270 | ``` 271 | 272 | ### `pthread_once` 273 | #### 参考源码 274 | 275 | ```c 276 | #include 277 | #include 278 | #include 279 | 280 | pthread_once_t once = PTHREAD_ONCE_INIT; /* 声明变量。控制变量必须使用 PTHREAD_ONCE_INIT 宏静态地初始化 */ 281 | 282 | /** 283 | * once_run()函数仅执行一次,且究竟在哪个线程中执行是不定的 284 | * 尽管pthread_once(&once,once_run)出现在两个线程中 285 | * 函数原型:int pthread_once(pthread_once_t *once_control, void (*init_routine)(void)) 286 | */ 287 | void once_run(void) 288 | { 289 | printf("Func: %s in thread: %u\n", 290 | __func__, 291 | (unsigned int)pthread_self()); 292 | } 293 | 294 | void *child_1(void *arg) 295 | { 296 | pthread_t tid; 297 | 298 | tid = pthread_self(); 299 | pthread_once(&once, once_run); /* 调用once_run */ 300 | printf("%s: thread %d returns\n", (char *)arg, (unsigned int)tid); 301 | 302 | return NULL; 303 | } 304 | 305 | void *child_2(void *arg) 306 | { 307 | pthread_t tid; 308 | 309 | tid = pthread_self(); 310 | pthread_once(&once, once_run); /* 调用once_run */ 311 | printf("%s: thread %d returns\n", (char *)arg, (unsigned int)tid); 312 | 313 | return NULL; 314 | } 315 | 316 | int main(void) 317 | { 318 | pthread_t tid1, tid2; 319 | 320 | printf("hello main\n"); 321 | pthread_create(&tid1, NULL, child_1, (void *)"child_1"); 322 | pthread_create(&tid2, NULL, child_2, (void *)"child_2"); 323 | 324 | pthread_join(tid1, NULL); /* main主线程等待线程 tid1 返回 */ 325 | pthread_join(tid2, NULL); /* main主线程等待线程 tid2 返回 */ 326 | printf("bye main\n"); 327 | 328 | return 0; 329 | } 330 | ``` 331 | 332 | #### 运行结果 333 | 334 | ``` 335 | ➜ pthread gcc -Wall -pthread -o pthread_once pthread_once.c 336 | ➜ pthread ./pthread_once 337 | hello main 338 | Func: once_run in thread: 861472512 339 | child_2: thread 861472512 returns 340 | child_1: thread 869865216 returns 341 | bye main 342 | ``` 343 | 344 | 整理来自:[Reference1](http://blog.csdn.net/sunboy_2050/article/details/6063067) [Reference2](http://www.cnblogs.com/yuxingfirst/archive/2012/07/25/2608612.html) 345 | (注:整理过程不同程度修改了代码,全部已在 gcc 5.2.1 编译通过) 346 | -------------------------------------------------------------------------------- /multithreaded programming/pthread_mutex_basic.md: -------------------------------------------------------------------------------- 1 | ### 互斥锁创建 2 | 有两种方法创建互斥锁,静态方式和动态方式。 3 | 4 | #### 静态 5 | POSIX 定义了一个宏 PTHREAD_MUTEX_INITIALIZER 来静态初始化互斥锁,方法如下: 6 | 7 | ```c 8 | pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; 9 | ``` 10 | 11 | 在 LinuxThreads 实现中,pthread_mutex_t是一个结构,而 PTHREAD_MUTEX_INITIALIZER 则是一个结构常量。 12 | 13 | #### 动态 14 | 动态方式是采用pthread_mutex_init()函数来初始化互斥锁,API定义如下: 15 | 16 | ```c 17 | int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr) 18 | ``` 19 | 20 | 其 中mutexattr 用于指定互斥锁属性(见下),如果为 NULL 则使用缺省属性。 21 | 22 | `pthread_mutex_destroy()` 用于注销一个互斥锁,API 定义如下: 23 | 24 | ```c 25 | int pthread_mutex_destroy(pthread_mutex_t *mutex) 26 | ``` 27 | 销毁一个互斥锁即意味着释放它所占用的资源,且要求锁当前处于开放状态。由于在 Linux 中,互斥锁并不占用任何资源,因此 LinuxThreads 中的 `pthread_mutex_destroy()` 除了检查锁状态以外(锁定状态则返回 EBUSY)没有其他动作。 28 | 29 | ### 互斥锁属性 30 | 互斥锁的属性在创建锁的时候指定,在 LinuxThreads 实现中仅有一个锁类型属性,不同的锁类型在试图对一个已经被锁定的互斥锁加锁时表现不同。当前(glibc2.2.3,linuxthreads0.9)有四个值可供选择: 31 | * `PTHREAD_MUTEX_TIMED_NP`,这是缺省值,也就是普通锁。当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后按优先级获得锁。这种锁策略保证了资源分配的公平性。 32 | * `PTHREAD_MUTEX_RECURSIVE_NP`,嵌套锁,允许同一个线程对同一个锁成功获得多次,并通过多次 unlock解锁。如果是不同线程请求,则在加锁线程解锁时重新竞争。 33 | * `PTHREAD_MUTEX_ERRORCHECK_NP`,检错锁,如果同一个线程请求同一个锁,则返回 EDEADLK,否则与 PTHREAD_MUTEX_TIMED_NP 类型动作相同。这样就保证当不允许多次加锁时不会出现最简单情况下的死锁。 34 | * `PTHREAD_MUTEX_ADAPTIVE_NP`,适应锁,动作最简单的锁类型,仅等待解锁后重新竞争。 35 | 36 | ### 互斥锁操作 37 | 38 | 锁操作主要包括加锁 `pthread_mutex_lock()`、解锁 `pthread_mutex_unlock()` 和测试加锁 `pthread_mutex_trylock()` 三个,不论哪种类型的锁,都不可能被两个不同的线程同时得到,而必须等待解锁。对于普通锁和适应锁类型,解锁者可以是同进程内任何线程;而检错锁则必须由加锁者解锁才有效,否则返回 EPERM;对于嵌套锁,文档和实现要求必须由加锁者解锁,但实验结果表明并没有这种限制,这个不同目前还没有得到解释。在同一进程中的线程,如果加锁后没有解锁,则任何其他线程都无法再获得锁。 39 | 40 | ```c 41 | int pthread_mutex_lock(pthread_mutex_t *mutex) 42 | int pthread_mutex_unlock(pthread_mutex_t *mutex) 43 | int pthread_mutex_trylock(pthread_mutex_t *mutex) 44 | ``` 45 | 46 | `pthread_mutex_trylock()` 语义与 `pthread_mutex_lock()` 类似,不同的是在锁已经被占据时返回 EBUSY 而不是挂起等待。 47 | 48 | ### 例子 49 | #### Example 1 50 | 51 | ```c 52 | #include 53 | #include 54 | #include 55 | #include 56 | 57 | void *function(void *arg); 58 | pthread_mutex_t mutex; 59 | int counter = 0; 60 | 61 | int main(int argc, char *argv[]) { 62 | int rc1, rc2; 63 | 64 | char *str1 = "weloveocean"; 65 | char *str2 = "daydaystudy"; 66 | pthread_t thread1, thread2; 67 | 68 | pthread_mutex_init(&mutex, NULL); 69 | if ((rc1 = pthread_create(&thread1, NULL, function, str1)) != 0) { 70 | fprintf(stdout, "thread 1 create failed: %d\n", rc1); 71 | } 72 | 73 | if ((rc2 = pthread_create(&thread2, NULL, function, str2)) != 0) { 74 | fprintf(stdout, "thread 2 create failed: %d\n", rc2); 75 | } 76 | 77 | pthread_join(thread1, NULL); 78 | pthread_join(thread2, NULL); 79 | return 0; 80 | } 81 | 82 | void *function(void *arg) 83 | { 84 | char *m; 85 | m = (char *)arg; 86 | pthread_mutex_lock(&mutex); 87 | while (*m != '\0') { 88 | printf("%c",*m); 89 | fflush(stdout); 90 | m++; 91 | sleep(1); 92 | } 93 | printf("\n"); 94 | pthread_mutex_unlock(&mutex); 95 | return (void*)0; 96 | } 97 | ``` 98 | 99 | ##### 运行结果 100 | 101 | ``` 102 | ➜ pthread gcc -Wall -pthread -o pthread_mutex1 pthread_mutex1.c 103 | ➜ pthread ./pthread_mutex1 104 | daydaystudy 105 | weloveocean 106 | ``` 107 | 108 | #### Example2 109 | 110 | ```c 111 | #include 112 | #include 113 | #include 114 | #include 115 | 116 | typedef struct ct_sum { 117 | int sum; 118 | pthread_mutex_t lock; 119 | }ct_sum; 120 | 121 | void * add1(void * cnt) 122 | { 123 | pthread_mutex_lock(&(((ct_sum*)cnt)->lock)); 124 | int i; 125 | for (i = 0;i < 50;i++) { 126 | (*(ct_sum*)cnt).sum += i; 127 | } 128 | pthread_mutex_unlock(&(((ct_sum*)cnt)->lock)); 129 | pthread_exit(NULL); 130 | return 0; 131 | } 132 | 133 | void * add2(void *cnt) { 134 | int i; 135 | cnt = (ct_sum*)cnt; 136 | pthread_mutex_lock(&(((ct_sum*)cnt)->lock)); 137 | for (i = 50;i < 101;i++) { 138 | (*(ct_sum*)cnt).sum += i; 139 | } 140 | pthread_mutex_unlock(&(((ct_sum*)cnt)->lock)); 141 | pthread_exit(NULL); 142 | return 0; 143 | } 144 | 145 | int main(void) { 146 | pthread_t ptid1, ptid2; 147 | ct_sum cnt; 148 | pthread_mutex_init(&(cnt.lock), NULL); 149 | cnt.sum = 0; 150 | pthread_create(&ptid1, NULL, add1, &cnt); 151 | pthread_create(&ptid2, NULL, add2, &cnt); 152 | 153 | pthread_join(ptid1, NULL); 154 | pthread_join(ptid2, NULL); 155 | 156 | printf("sum %d\n", cnt.sum); 157 | pthread_mutex_destroy(&(cnt.lock)); 158 | return 0; 159 | } 160 | ``` 161 | 162 | ##### 运行结果 163 | 164 | ``` 165 | ➜ pthread gcc -Wall -pthread -o pthread_mutex2 pthread_mutex2.c 166 | ➜ pthread ./pthread_mutex2 167 | sum 5050 168 | ``` 169 | 170 | 整理来自:[Reference1](http://blog.csdn.net/yusiguyuan/article/details/14148311) [Reference2](http://blog.csdn.net/wypblog/article/details/7264315) 171 | --------------------------------------------------------------------------------