目录

1.虚函数的问题

2.多种构造函数问题

3.全局变量和局部变量重名的问题:

4.有符号数溢出问题

5.自己实现c的字符串库函数

6.栈破坏问题

7.右值引用

移动构造和移动赋值

8.字符串作为hash的key

9.size_type和size_t

10.同步异步、阻塞非阻塞

11.stl::sort的实现

12.线程数应该定为多少比较合适

13.如何判断一个socket当前的连接状态

14.高并发下如何限流

15.内存对齐问题

16.delete[]与new[]配对问题

17.i++和++i是线程安全的吗

18.vector 的特殊之处

19.type_id与type_info

20.如何测试进程的上下文切换时间

21.查看进程运行时的段分布

22.纯tcp通信的包识别

23. 线程局部存储的实现

24 vector中放的是指向自定义类对象的指针,扩容时会调用析构函数吗


1.虚函数的问题

今天遇到了这样一个题目:

#include <iostream>
using namespace std;class B0
{
public:virtual void display() { cout << "B0" << endl; }
};
class B1 : public B0
{
public:void display() { cout << "B1" << endl; }
};
class D1 : public B1
{
public:void display() { cout << "D1" << endl; }
};
void fun(***************)
{ptr.display();
}
int main()
{D1 d1;fun(d1);system("pause");return 0;
}

问********处应该填入啥,使得程序输出为B0。

答案是 B0 ptr。(是不是因为函数形参强制类型转换啊?)

同理, 输入 B1 ptr 就会输出B1.

若是输入B0 &ptr 就是正确调用虚函数 ,输出D1。

2.多种构造函数问题

构造函数与赋值运算符的区别是,构造函数在创建或初始化对象的时候调用,而赋值运算符在更新一个对象的值时调用

#include <iostream>
using namespace std;//构造函数与赋值运算符的区别是,构造函数在创建或初始化对象的时候调用,而赋值运算符在更新一个对象的值时调用。class A {
public:A(int a):x(a) { cout << "调用了构造函数" << endl; }A& operator=(const A& a) { cout << "调用了赋值构造函数" << endl; this->x = a.x; return *this; }A(const A& a):x(a.x){ cout << "调用了拷贝构造函数" << endl; }A(const A&& a):x(a.x){ cout << "调用了移动构造函数" << endl; }A& operator=(const A&& a) { cout << "调用了移动赋值构造函数" << endl; this->x = a.x;  return *this; }~A(){ cout << "调用了析构函数" << endl; }int x;
};A func() {A a(1);return a;
}A GetA()
{return A(1);
}A&& MoveA()
{return A(1);
}int main() {A qq = func();cout << "get func return" << endl << endl;cout << "-------------------------1-------------------------" << endl;A a(1);cout << "-------------------------2-------------------------" << endl;A b = a;cout << "-------------------------3-------------------------" << endl;A c(a);cout << "-------------------------4-------------------------" << endl;b = a;cout << "-------------------------5-------------------------" << endl;A d = A(1);cout << "-------------------------6-------------------------" << endl;A e = std::move(a);cout << "-------------------------7-------------------------" << endl;A f = GetA();cout << "-------------------------8-------------------------" << endl;A&& g = MoveA();cout << "-------------------------9-------------------------" << endl;d = A(1);system("pause");return 0;
}

运行结果:

调用了构造函数
调用了移动构造函数
调用了析构函数
get func return-------------------------1-------------------------
调用了构造函数
-------------------------2-------------------------
调用了拷贝构造函数
-------------------------3-------------------------
调用了拷贝构造函数
-------------------------4-------------------------
调用了赋值构造函数
-------------------------5-------------------------
调用了构造函数
-------------------------6-------------------------
调用了移动构造函数
-------------------------7-------------------------
调用了构造函数
-------------------------8-------------------------
调用了构造函数
调用了析构函数
-------------------------9-------------------------
调用了构造函数
调用了移动赋值构造函数
调用了析构函数

首先看前三行,即调用func的那个变量qq

先在func中构造一个临时变量,然后return出来要初始化变量qq,但是此临时变量是右值,所以用了移动构造函数

在vs下面不知道怎么把编译器优化关闭,感觉有地方被优化了

这天被问到拷贝构造和赋值构造函数的区别,先看代码:

 // 拷贝构造函数MyString(const MyString& str) {CCtor ++;m_data = new char[ strlen(str.m_data) + 1 ];strcpy(m_data, str.m_data);}// 拷贝赋值函数 =号重载MyString& operator=(const MyString& str){CAsgn ++;if (this == &str) // 避免自我赋值!!return *this;delete[] m_data;m_data = new char[ strlen(str.m_data) + 1 ];strcpy(m_data, str.m_data);return *this;}

区别在于:

1.拷贝构造没有返回值,赋值构造用 类的引用& 作为返回值

2.赋值构造需要判断是不是自我赋值的情况而拷贝构造不需要。(因为赋值构造是用别的对象给已存在的对象赋值)

3.赋值构造要先释放原来的成员分配的内存(若有new)释放掉。

对象不存在,且没用别的对象来初始化,就是调用了构造函数;

对象不存在,且用别的对象来初始化,就是拷贝构造函数

对象存在,用别的对象来给它赋值,就是赋值函数。

3.全局变量和局部变量重名的问题:

在全局区定义了全局变量a后,还能在main函数中定义同名的局部变量a吗?如何区别访问呢?

这是一个作用域的问题。一个声明将一个名字引进一个作用域;局部变量(通常在函数中定义)的作用域是从声明的那一点开始,直到这个声明所在的块结束为止(一个块就是由{}围起来的一段代码)。全局变量(在所有函数、类、命名空间之外定义)的作用域从声明的那一点开始,直到这个声明所在的文件的结束。与全局变量重名的局部变量可以屏蔽全局变量,如果想在块内使用全局变量需要通过作用域解析运算符::引用。

#include <iostream>using namespace std;static int a = 1;int main()
{int a = 0;    //屏蔽全局变量cout << "局部变量a: "<< a << endl;cout << "全局变量a: " << ::a << endl;system("pause");return 0;
}

4.有符号数溢出问题

若两个有符号的int相加导致超过了2^31-1,有什么办法能快速得出正确的值?

考虑比较简单的场景:两个有符号8位数相加:

96+64 = 0110 0000 + 0100 0000 = 1010 0000 = -96(溢出了)

只需要赋值给一个8位无符号数就能得到正确的值:

1010 0000 = 160 即正确的结果。

同理,32位的int也类似。

5.自己实现c的字符串库函数

#include<iostream>
#include <string>using namespace std;char* mystrcat(char* dest, const char* src) {if (dest == NULL || src == NULL)return NULL;char *p = dest;while (*p !='\0') {p++;}while (*src !='\0') {*p++ = *src++;}*p = '\0';return dest;
}char* mystrcpy(char* dest, const char* src) {  //这种方案在大多数情况下是可以的,但是在内存重叠的情况下就会有问题if (dest == NULL || src == NULL)   //所谓内存重叠,是指dest在src字符串的中间位置。return NULL;         //这样在执行while ((*p++ = *src++) != '\0')时,会将src的\0擦去,导致while死循环if (dest == src)return dest;char* p = dest;while ((*p++ = *src++) != '\0');return dest;
}char* mystrcpy1(char* dest, const char* src) {  //考虑内存重叠的情况,需要倒过来拷贝if (dest == NULL || src == NULL)return NULL;if (dest == src)return dest;int len = strlen(src);char* d = dest + len;const char* s = src + len;int nloop = len + 1;          //把'\0'那次也算上去while (nloop--) {*d-- = *s--;}return dest;              //返回原来的
}int main() {char a[25] = "hello";char b[10] = "myfrend";//char* c = mystrcat(a, b);//char* c = mystrcpy(a,b);char* c = mystrcpy1(b+1, b);cout << c << endl;system("pause");return 0;
}

memcopy也类似

void *memcpy(void *dest, const void *src, size_t count)
{char *d;const char *s;if (dest > (src+count)) || (dest < src)){d = dest;s = src;while (count--)*d++ = *s++;        }else /* overlap */     有重叠的情况{d = (char *)(dest + count - 1); /* offset of pointer is from 0 */s = (char *)(src + count -1);while (count --)*d-- = *s--;}return dest;
}

6.栈破坏问题

有这样一段代码:

void corrupt(char a[]) {a[10] = 'q';return;
}int main()
{char a[10];corrupt(a);system("pause");return 0;
}

会出现什么问题?

在main函数return的时候才会:

仔细一看,报的错误是   “变量a周围的栈损坏”,在函数corrupt中我们对a数组的第11个元素(下标10)赋值了,但是a的大小只有10,甚至说我们在程序结束之前执行 “cout << a[10] << endl;”也没有问题,正常返回“q”。  但是问题出在corrupt函数中对a[10]赋值的操作会破坏了别的函数的栈帧,比如main函数的栈帧,这就导致了main函数在返回的时候出错了。

说到字节数组,想到这样一个程序:

void Size1(char a[]) {cout << "Size1: "<< sizeof(a) << endl;return;
}void Size2(char a[20]) {cout << "Size2: "<< sizeof(a) << endl;return;
}int main()
{char a[10];Size1(a);Size2(a);cout << "sizeof a: " << sizeof(a) << endl;cout << "sizeof &a: " << sizeof(&a) << endl;system("pause");return 0;
}

运行结果:

传进Size1和Size2的是指针a,相当于是一个char *p了,他的大小就是4字节(32位机器下)

还有,普通指针和数组指针有啥区别?

普通指针就是一个数据的地址
数组指针呢就是一组数据的地址

 int a = 0;int * pa = &a;int b[] = { 1,2,3,4,5 };int (*pb)[5] = &b;cout << "*pa: " << pa << endl;cout << "*pb: " << pb << endl;pa++;pb++;cout << "abter++" << endl;cout << "*pa: " << pa << endl;cout << "*pb: " << pb << endl;

7.右值引用

简书上这个博主说的很棒:https://www.jianshu.com/p/d19fc8447eaa

左值、右值

C++中所有的值都必然属于左值、右值二者之一。左值是指表达式结束后依然存在的持久化对象,右值是指表达式结束时就不再存在的临时对象。所有的具名变量或者对象都是左值,而右值不具名。很难得到左值和右值的真正定义,但是有一个可以区分左值和右值的便捷方法:看能不能对表达式取地址,如果能,则为左值,否则为右值

看见书上又将右值分为将亡值和纯右值。纯右值就是c++98标准中右值的概念,如非引用返回的函数返回的临时变量值;一些运算表达式,如1+2产生的临时变量;不跟对象关联的字面量值,如2,'c',true,"hello";这些值都不能够被取地址。

而将亡值则是c++11新增的和右值引用相关的表达式,这样的表达式通常时将要移动的对象、T&&函数返回值、std::move()函数的返回值等

不懂将亡值和纯右值的区别其实没关系,统一看作右值即可,不影响使用。

左值引用、右值引用

c++98中的引用很常见了,就是给变量取了个别名,在c++11中,因为增加了右值引用(rvalue reference)的概念,所以c++98中的引用都称为左值引用(lvalue reference)

int a = 10;
int& refA = a; // refA是a的别名, 修改refA就是修改a, a是左值,左移是左值引用int& b = 1; //编译错误! 1是右值,不能够使用左值引用

c++11中的右值引用使用的符号是&&,如

int&& a = 1; //实质上就是将不具名(匿名)变量取了个别名
int b = 1;
int && c = b; //编译错误! 不能将一个左值复制给一个右值引用
class A {public:int a;
};
A getTemp()
{return A();
}
A && a = getTemp();   //getTemp()的返回值是右值(临时变量)

getTemp()返回的右值本来在表达式语句结束后,其生命也就该终结了(因为是临时变量),而通过右值引用,该右值又重获新生,其生命期将与右值引用类型变量a的生命期一样,只要a还活着,该右值临时变量将会一直存活下去。实际上就是给那个临时变量取了个名字。

注意:这里a类型是右值引用类型(int &&),但是如果从左值和右值的角度区分它,它实际上是个左值因为可以对它取地址,而且它还有名字,是一个已经命名的右值。

移动构造和移动赋值

回顾一下如何用c++实现一个字符串类MyStringMyString内部管理一个C语言的char *数组,这个时候一般都需要实现拷贝构造函数和拷贝赋值函数,因为默认的拷贝是浅拷贝,而指针这种资源不能共享,不然一个析构了,另一个也就完蛋了。

我们看看普通拷贝构造、移动拷贝构造、普通赋值构造、移动赋值构造的区别:

 // 拷贝构造函数MyString(const MyString& str) {CCtor ++;m_data = new char[ strlen(str.m_data) + 1 ];strcpy(m_data, str.m_data);}// 移动构造函数MyString(MyString&& str) noexcept:m_data(str.m_data) {MCtor ++;str.m_data = nullptr; //不再指向之前的资源了}// 拷贝赋值函数 =号重载MyString& operator=(const MyString& str){CAsgn ++;if (this == &str) // 避免自我赋值!!return *this;delete[] m_data;m_data = new char[ strlen(str.m_data) + 1 ];strcpy(m_data, str.m_data);return *this;}// 移动赋值函数 =号重载MyString& operator=(MyString&& str) noexcept{MAsgn ++;if (this == &str) // 避免自我赋值!!return *this;delete[] m_data;m_data = str.m_data;str.m_data = nullptr; //不再指向之前的资源了return *this;}

可以看到,移动构造函数与拷贝构造函数的区别是,拷贝构造的参数是const MyString& str,是常量左值引用,而移动构造的参数是MyString&& str,是右值引用,而MyString("hello")是个临时对象,是个右值,优先进入移动构造函数而不是拷贝构造函数。而移动构造函数与拷贝构造不同,它并不是重新分配一块新的空间,将要拷贝的对象复制过来,而是""了过来,将自己的指针指向别人的资源,然后将别人的指针修改为nullptr,这一步很重要,如果不将别人的指针修改为空,那么临时对象析构的时候就会释放掉这个资源,"偷"也白偷了。下面这张图可以解释copy和move的区别。

不用奇怪为什么可以抢别人的资源,临时对象的资源不好好利用也是浪费,因为生命周期本来就是很短,在你执行完这个表达式之后,它就毁灭了,充分利用资源,才能很高效。

对于一个左值,肯定是调用拷贝构造函数了,但是有些左值是局部变量,生命周期也很短,能不能也移动而不是拷贝呢?C++11为了解决这个问题,提供了std::move()方法来将左值转换为右值,从而方便应用移动语义。我觉得它其实就是告诉编译器,虽然我是一个左值,但是不要对我用拷贝构造函数,而是用移动构造函数吧。。。

MyString str1("hello"); //调用构造函数
MyString str2("world"); //调用构造函数
MyString str3(str1); //调用拷贝构造函数
MyString str4(std::move(str1)); // 调用移动构造函数
//    cout << str1.get_c_str() << endl; // 此时str1的内部指针已经失效了!不要使用
//注意:虽然str1中的m_dat已经称为了空,但是str1这个对象还活着,直到出了它的作用域才会析构!而不是move完了立刻析构
MyString str5;
str5 = str2; //调用拷贝赋值函数
MyString str6;
str6 = std::move(str2); // str2的内容也失效了,不要再使用

需要注意一下几点:

  1. str6 = std::move(str2),虽然将str2的资源给了str6,但是str2并没有立刻析构,只有在str2离开了自己的作用域的时候才会析构,所以,如果继续使用str2m_data变量,可能会发生意想不到的错误。
  2. 如果我们没有提供移动构造函数,只提供了拷贝构造函数,std::move()会失效但是不会发生错误,因为编译器找不到移动构造函数就去寻找拷贝构造函数,也这是拷贝构造函数的参数是const T&常量左值引用的原因!
  3. c++11中的所有容器都实现了move语义,move只是转移了资源的控制权,本质上是将左值强制转化为右值使用,以用于移动拷贝或赋值,避免对含有资源的对象发生无谓的拷贝。move对于拥有如内存、文件句柄等资源的成员的对象有效,如果是一些基本类型,如int和char[10]数组等,如果使用move,仍会发生拷贝(因为没有对应的移动构造函数),所以说move对含有资源的对象说更有意义。

8.字符串作为hash的key

字符串作为key,设计一个hash函数使得映射出来的索引冲突较少。

方案1:字符串每位的ascii值累加后取模

优点:简单

缺点:冲突较多,如abc,bca,acb映射后的索引都是一样的

方案2:

hash = (hash>>5) + *str

效果见下:

                 

可以见到,前面三个分布都还算均匀,最后一次冲突较大,有4+3个相同的索引

代码:

#include<iostream>
#include <string>
#include <vector>
using namespace std;int main() {string s;vector<string> a;for (int i = 0; i < 5; i++) {cin >> s;a.emplace_back(std::move(s));}uint32_t hash = 0;uint32_t mod = 10;cout << "mod: " << mod << endl;for (int i = 0; i < a.size(); i++) {const char* str = a[i].c_str();cout << "字符串: " << str << endl;while (*str != '\0') {hash = (hash << 5) + *str++;}cout << "索引: " << hash % mod << endl;hash = 0;}system("pause");return 0;
}

其思想是求解多项式的霍纳法则:

long int
horner(int coefficient[], int n, int x) //coefficient[]为待求多项式的系数数组,n为数组大小,x为多项式中未知数x的具体值
{                                       //注意:coefficient[0]存放系数a0,coefficient[1]存放系数a1,以此类推…int i;long int result;result = coefficient[n-1];for(i = 1; i <= n-1; i++){result = result * x + coefficient[n-1-i];}return result;
}

实际上就是把str的每一位作为coefficient,乘上32

其实这个方案有点类似DJB:这个算法是Daniel J.Bernstein 教授发明的,是目前公布的最有效的哈希函数。

public long DJBHash(String str)  {  long hash = 5381;  for(int i = 0; i < str.length(); i++)  {  hash = ((hash << 5) + hash) + str.charAt(i);  }  return hash;  }  

方案3:其他

已有的hash算法见常见hash算法

评估hash函数优劣的基准主要有以下两个指标:

(1) 散列分布性

即桶的使用率backet_usage = (已使用桶数) / (总的桶数),这个比例越高,说明分布性良好,是好的hash设计。

(2) 平均桶长

即avg_backet_len,所有已使用桶的平均长度。理想状态下这个值应该=1,越小说明冲突发生地越少,是好的hash设计。

hash函数计算一般都非常简洁,因此在耗费计算时间复杂性方面判别甚微,这里不作对比。

评估方案设计是这样的:

(1) 以200M的视频文件作为输入源,以4KB的块为大小计算MD5值,并以此作为hash关键字;

(2) 分别应用上面提到的各种字符串hash函数,进行hash散列模拟;

(3) 统计结果,用散列分布性和平均桶长两个指标进行评估分析。

结果如下:

9.size_type和size_t

size_type由string类类型和vector类类型定义的类型,用以保存任意string对象或vector对象的长度,标准库类型将size_type定义为unsigned类型。

vector::size()返回值类型是size_type的无符号整数。

同时string::find()函数在查找字符串中的某一个字符时的返回值也是 size_type类型的,用size_type类型无论string对象多大,也不用担心越界的问题,同时 如果 在字符串中没有找到目标字符,则 find()会返回string::npos,它是与size_type类型相匹配的
    而且,如果使用int类型变量来保存string对象的长度,那么可能会出现问题:有些机器上的int变量的表示范围太小,甚至无法存储实际并不长的string对象。如在有16位int型的机器上,int类型变量最大只能表示32767个字符的string对象。而能容纳一个文件内容的string对象轻易就能超过这个数字,因此,为了避免溢出,保存一个string对象的size的最安全的方法就是使用标准库类型string::size_type().

size_t 类型定义在cstddef头文件中,该文件是C标准库的头文件stddef.h的C++版。是为了方便系统之间的移植而定义的,例如:在32位系统中size_t是4字节的,而在64位系统中,size_t是8字节的,这样利用该类型可以增强程序的可移植性。

10.同步异步、阻塞非阻塞

1.同步机制 发送方发送请求之后,需要等接收方发回响应后才接着发

2.异步机制 发送方发送一个请求之后不等待接收方响应这个请求,就继续发送下个请求(或者做别的事情)。

3.阻塞调用 调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回,该线程在此过程中不能进行其他处理

4.非阻塞调用 调用结果不能马上返回,当前线程也不会被挂起,而是立即返回执行下一个调用。(网络通信中主要指的是网络套接字Socket的阻塞和非阻塞方式,而soket 的实质也就是IO操作)

换一种说法:

同步:

所谓同步,就是发出一个功能调用时,在没有得到结果之前,该调用就不返回或继续执行后续操作。

简单来说,同步就是必须一件一件事做,等前一件做完了才能做下一件事。

例如:B/S模式中的表单提交,具体过程是:客户端提交请求->等待服务器处理->处理完毕返回,在这个过程中客户端(浏览器)不能做其他事。

异步:

异步与同步相对,当一个异步过程调用发出后,调用者在没有得到结果之前,就可以继续执行后续操作。当这个调用完成后,一般通过状态、通知和回调来通知调用者。对于异步调用,调用的返回并不受调用者控制。

对于通知调用者的三种方式,具体如下:

状态

即监听被调用者的状态(轮询),调用者需要每隔一定时间检查一次,效率会很低。

通知

当被调用者执行完成后,发出通知告知调用者,无需消耗太多性能。

回调

与通知类似,当被调用者执行完成后,会调用调用者提供的回调函数。

例如:B/S模式中的ajax请求,具体过程是:客户端发出ajax请求->服务端处理->处理完毕执行客户端回调,在客户端(浏览器)发出请求后,仍然可以做其他的事。

同步和异步的区别:

总结来说,同步和异步的区别:请求发出后,是否需要等待结果,才能继续执行其他操作。

阻塞和非阻塞这两个概念与程序(线程)等待消息通知(无所谓同步或者异步)时的状态有关。也就是说阻塞与非阻塞主要是程序(线程)等待消息通知时的状态角度来说的。

阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.

阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。

非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。

那么同步和阻塞有啥区别?

这里转载一篇博客:

有人觉得堵塞就是同步,非堵塞就是异步,其实以前我也是这么想的,其实同步与堵塞这完全是两码事,所以写篇文章来说说为什么是两码事,也顺便说说各种组合的可以达到的效果,帮助大家了解底层的原理.

首先需要了解这些概念,OS里面有内核态和用户态两种,程序进行IO操作的时候一般是两步,第一步是IO初始化也就是准备好IO操作,第二步就是真正的IO操作.其中第一步决定同步还是异步,第二步决定堵塞还是非堵塞的,为什么这么说呢,我们以Linux为例来看看这个关键字的组合

1.     同步堵塞IO

2.     同步非堵塞IO

3.     异步堵塞IO

4.     异步非堵塞IO

同步堵塞IO:就是最最普通的write/read操作,以读操作为例, 程序调用内核接口,然后等待系统返回,内核做读写初始化操作(寻址,读取原始数据到内核等),接着将数据读出到内核态,然后把数据从内核态拷贝到用户态下并返回结果给程序,然后程序才知道完成整个读操作,在内核初始化到将数据从存储介质取出到内核态内存中的这段时间程序就是一直等待,期间CPU的状态是wait的,夸张一点,如果内核调用一次读的操作是1秒的话,那么这一秒内程序是将CPU占着不做任何事情的,是不是感觉好浪费.

同步非堵塞IO:这里再内核调用里面会有一个设置参数叫O_NONBLOCK,使用这个的时候就会变成了非堵塞的调用,(其实从这个参数差不多可以猜测出堵塞和非堵塞是针对内核调用来说的,因为这个参数直接作用的就是内核调用),这里会有一个状态通知的概念,程序不会立即完成IO操作,还是以读操作为例,程序首先向内核发出一个读操作的调用,这个时候系统会在很短的时候返回一个状态,表示读操作是否初始化完成,大多数时候是返回不能读初始化(因为有资源竞争),接着程序会继续像死循环一个的发起这个请求直到内核返回读操作已经准备好(记住死循环的时候程序实际上被读这个操作的时候实际不能做别的事情,所以是同步的),这个时候程序在才能将数据从内核态拷贝到用户态后并且返回,这个读过程就算完成了.虽然内核态是是非堵塞的,但是程序这个线程里面在完成读操作之前也没有能做别的事情,所以这里还是同步的.相对同步堵塞IO来说,这种方式可以让程序不用占着CPU时间也不给别人用的情况.,另外由于操作是循环调用查看状态的,所以再内核准备好数据到程序下一次查看状态之间存在延时,这样会导致系统的吞吐量下降.

异步堵塞IO:这里就是我们java里面用到的Select或者Clib里面的poll来实现的,这里面实际也用到上面提到的非阻塞的参数,只是采用Select这种阻塞的方式来调用非阻塞的内核调用.虽然读和写的操作并没有被堵塞住,而是由Select中获取了I/O从操作符再进行下一步操作,这里的数据真正的操作还是被堵塞住了(仍然要等read返回),但是这个的好处就是可以同时再一个线程里面对多个IO操作进行处理,这种顺序处理的效率确实不高,再有的程序里面会启动多个线程来做Select的操作提高性能.这个一个的好处就是再同一个线程内可以有多个IO操作同时进行,他不能提高单个IO的吞吐量,但是可以提高程序的IO并发能力,从而提高整体的IO吞吐量.

异步非堵塞IO:(IO多路复用+非阻塞IO)这个厉害了,这就是我们提得比较热的一个概念,叫AIO,他采用的是回调的方式实现异步操作,然后也使用非堵塞的参数,这样一来程序不需要死循环来监听IO操作符,内核准备好了就会通知程序做对应的事情,而程序再读写等待的时候就可以把剩下的CPU时间用在做别的业务处理.这个的好处就太多了,首先他吧CPU时间让出来了,可以做别的事情,其次是回调方式的,只要数据准备好就可以回调让程序把数据从内核拷贝走.坏的地方么我只能说回调对于程序员的编码要求确实有点高的.

所以总结一下这四个方式:同步堵塞IO在IO操作开始的时候就堵塞程序,这个时候还不能重复的进行别的IO操作,同步非堵塞IO解决了内核可以同时处理来自程序的多个IO请求.而异步堵塞主要是解决在一个线程里面处理多个IO的操作,他并没有对IO进行堵塞,但是对事件进行了堵塞(Select实际上实在选择事件),最后的终极大咖异步非堵塞IO,实现了内核非堵塞和回调通知的方法,让IO的操作更高效.

那是不是异步非堵塞是最好的呢,这个不会一定,我们在做大数据的时候会知道程序有CPU密集型和IO密集型两种区别,至于那种类型使用哪种方式的IO模式这个就需要看攻城狮自己的判断了

补充:

忘了说了,LInux在2.6内核里面才讲AIO作为标准标准特性.

下面举一个生活中的例子,挺形象的

老张爱喝茶,废话不说,煮开水。
出场人物:老张,水壶两把(普通水壶,简称水壶;会响的水壶,简称响水壶)。
1 老张把水壶放到火上,立等水开。(同步阻塞)
老张觉得自己有点傻
2 老张把水壶放到火上,去客厅看电视,时不时去厨房看看水开没有。(同步非阻塞)
老张还是觉得自己有点傻,于是变高端了,买了把会响笛的那种水壶。水开之后,能大声发出嘀~~~~的噪音。
3 老张把响水壶放到火上,立等水开。(异步阻塞)
老张觉得这样傻等意义不大
4 老张把响水壶放到火上,去客厅看电视,水壶响之前不再去看它了,响了再去拿壶。(异步非阻塞)
老张觉得自己聪明了。

所谓同步异步,只是对于水壶而言。
普通水壶,同步;响水壶,异步。
虽然都能干活,但响水壶可以在自己完工之后,提示老张水开了。这是普通水壶所不能及的。
同步只能让调用者去轮询自己(情况2中),造成老张效率的低下。

所谓阻塞非阻塞,仅仅对于老张而言。
立等的老张,阻塞;看电视的老张,非阻塞。
情况1和情况3中老张就是阻塞的,媳妇喊他都不知道。虽然3中响水壶是异步的,可对于立等的老张没有太大的意义。所以一般异步是配合非阻塞使用的,这样才能发挥异步的效用。

11.stl::sort的实现

大致看一下调用的:

template<class _RanIt,class _Pr> inlinevoid _Sort_unchecked(_RanIt _First, _RanIt _Last, _Iter_diff_t<_RanIt> _Ideal, _Pr _Pred){    // order [_First, _Last), using _Pred_Iter_diff_t<_RanIt> _Count;while (_ISORT_MAX < (_Count = _Last - _First) && 0 < _Ideal){ // divide and conquer by quicksortauto _Mid = _Partition_by_median_guess_unchecked(_First, _Last, _Pred);// TRANSITION, VSO#433486_Ideal = (_Ideal >> 1) + (_Ideal >> 2);    // allow 1.5 log2(N) divisions    递归深度<1.5 log2(N)时继续使用快排if (_Mid.first - _First < _Last - _Mid.second){  // loop on second half    右半边_Sort_unchecked(_First, _Mid.first, _Ideal, _Pred);_First = _Mid.second;}else{    // loop on first half    左半边_Sort_unchecked(_Mid.second, _Last, _Ideal, _Pred);_Last = _Mid.first;}}if (_ISORT_MAX < _Count){   // heap sort if too many divisions    递归太深时对这部分进行堆排_Make_heap_unchecked(_First, _Last, _Pred);_Sort_heap_unchecked(_First, _Last, _Pred);}else if (2 <= _Count)    //少量数据直接使用插入排序{_Insertion_sort_unchecked(_First, _Last, _Pred);  // small}}template<class _RanIt,class _Pr> inlinevoid sort(const _RanIt _First, const _RanIt _Last, _Pr _Pred){   // order [_First, _Last), using _Pred_Adl_verify_range(_First, _Last);const auto _UFirst = _Get_unwrapped(_First);const auto _ULast = _Get_unwrapped(_Last);_Sort_unchecked(_UFirst, _ULast, _ULast - _UFirst, _Pass_fn(_Pred));}template<class _RanIt> inlinevoid sort(const _RanIt _First, const _RanIt _Last){   // order [_First, _Last), using operator<_STD sort(_First, _Last, less<>());}

提到了三种排序方式:插入排序、快排、堆排序

总结:

STL的sort算法,数据量大时采用QuickSort快排算法,分段归并排序。一旦分段后的数据量小于某个门槛(16),为避免QuickSort快排的递归调用带来过大的额外负荷,就改用Insertion Sort插入排序。如果递归层次过深,还会改用HeapSort堆排序

说明:
1、默认情况下是对 [first,last)区间的元素采用由小到大的方式排列

2、可以自定义比较函数,也可以调用stl内提供的比较函数,less<T>() 、greater<T>()

3、排序的区间可以必须是通过迭代器遍历的(数组下标也算),迭代器的类型为随机迭代器;

stl set map 使用红黑树 avl树作为底层数据结构的实现,不支持随机迭代器,所以不能使用sort来排序;

自己定义的数据结构支持这种随机迭代器时,也可以使用sort()算法来排序

4、排序是通过多次内存的copy来实现的,所以链表不能使用stl 算法sort来对其排序(next指针修改问题);

对于链表的排序,stl list<T>中可以实现对双端链表的排序

12.线程数应该定为多少比较合适

https://blog.csdn.net/qun_y/article/details/81451223

一、需求
        Web-Server通常有个配置,最大工作线程数,后端服务一般也有个配置,工作线程池的线程数量,这个线程数的配置不同的业务架构师有不同的经验值,有些业务设置为CPU核数的2倍,有些业务设置为CPU核数的8倍,有些业务设置为CPU核数的32倍。“工作线程数”的设置依据是什么,到底设置为多少能够最大化CPU性能

二、一些共性认知
        在进一步深入讨论之前,先以提问的方式就一些共性认知达成一致。
        提问:工作线程数是不是设置的越大越好?
        回答:肯定不是的
        1)一来服务器CPU核数有限,同时并发的线程数是有限的,1核CPU设置10000个工作线程没有意义
        2)线程切换是有开销的,如果线程切换过于频繁,反而会使性能降低
 
        提问:调用sleep()函数的时候,线程是否一直占用CPU?
        回答:不占用,等待时会把CPU让出来,给其他需要CPU资源的线程使用
        不止调用sleep()函数,在进行一些阻塞调用,例如网络编程中的阻塞accept()【等待客户端连接】和阻塞recv()【等待下游回包】也不占用CPU资源
 
        提问:如果CPU是单核,设置多线程有意义么,能提高并发性能么?
        回答:即使是单核,使用多线程也是有意义的
        1)多线程编码可以让我们的服务/代码更加清晰,有些IO线程收发包,有些Worker线程进行任务处理,有些Timeout线程进行超时检测
         2)如果有一个任务一直占用CPU资源在进行计算,那么此时增加线程并不能增加并发,例如这样的一个代码
         while(1){ i++; }
         该代码一直不停的占用CPU资源进行计算,会使CPU占用率达到100%
         3)通常来说,Worker线程一般不会一直占用CPU进行计算,此时即使CPU是单核,增加Worker线程也能够提高并发,因为这个线程在休息的时候,其他的线程可以继续工作

三、常见服务线程模型
        了解常见的服务线程模型,有助于理解服务并发的原理,一般来说互联网常见的服务线程模型有如下两种
        IO线程与工作线程通过队列解耦类模型

如上图,大部分Web-Server与服务框架都是使用这样的一种“IO线程与Worker线程通过队列解耦”类线程模型:
1)有少数几个IO线程监听上游发过来的请求,并进行收发包(生产者)
2)有一个或者多个任务队列,作为IO线程与Worker线程异步解耦的数据传输通道(临界资源)
3)有多个工作线程执行正真的任务(消费者)
这个线程模型应用很广,符合大部分场景,这个线程模型的特点是,工作线程内部是同步阻塞执行任务的,因此可以通过增加Worker线程数来增加并发能力,今天要讨论的重点是“该模型Worker线程数设置为多少能达到最大的并发”。
 
纯异步线程模型
        任何地方都没有阻塞,这种线程模型只需要设置很少的线程数就能够做到很高的吞吐量,Lighttpd有一种单进程单线程模式,并发处理能力很强,就是使用的的这种模型。该模型的缺点是:
         1)如果使用单线程模式,难以利用多CPU多核的优势
         2)程序员更习惯写同步代码,callback的方式对代码的可读性有冲击,对程序员的要求也更高
        3)框架更复杂,往往需要server端收发组件,server端队列,client端收发组件,client端队列,上下文管理组件,有限状态机组件,超时管理组件的支持however,这个模型不是今天讨论的重点。

四、工作线程的工作模式
        了解工作线程的工作模式,对量化分析线程数的设置非常有帮助:

上图是一个典型的工作线程的处理过程,从开始处理start到结束处理end,该任务的处理共有7个步骤:
         1)从工作队列里拿出任务,进行一些本地初始化计算,例如http协议分析、参数解析、参数校验等
         2)访问cache拿一些数据(等待)
         3)拿到cache里的数据后,再进行一些本地计算,这些计算和业务逻辑相关
         4)通过RPC调用下游service再拿一些数据,或者让下游service去处理一些相关的任务(等待)
         5)RPC调用结束后,再进行一些本地计算,怎么计算和业务逻辑相关
         6)访问DB进行一些数据操作(等待)
        7)操作完数据库之后做一些收尾工作,同样这些收尾工作也是本地计算,和业务逻辑相关
 
分析整个处理的时间轴,会发现:
        1)其中1,3,5,7步骤中【上图中粉色时间轴】,线程进行本地业务逻辑计算时需要占用CPU
        2)而2,4,6步骤中【上图中橙色时间轴】,访问cache、service、DB过程中线程处于一个等待结果的状态,不需要占用CPU,进一步的分解,这个“等待结果”的时间共分为三部分:
        2.1)请求在网络上传输到下游的cache、service、DB
        2.2)下游cache、service、DB进行任务处理
       2.3)cache、service、DB将报文在网络上传回工作线程

五、量化分析并合理设置工作

最后一起来回答工作线程数设置为多少合理的问题。
通过上面的分析,Worker线程在执行的过程中,有一部计算时间需要占用CPU,另一部分等待时间不需要占用CPU,通过量化分析,例如打日志进行统计,可以统计出整个Worker线程执行过程中这两部分时间的比例,例如:
        1)时间轴1,3,5,7【上图中粉色时间轴】的计算执行时间是100ms
        2)时间轴2,4,6【上图中橙色时间轴】的等待时间也是100ms
得到的结果是,这个线程计算和等待的时间是1:1,即有50%的时间在计算(占用CPU),50%的时间在等待(不占用CPU):
        1)假设此时是单核,则设置为2个工作线程就可以把CPU充分利用起来,让CPU跑到100%
        2)假设此时是N核,则设置为2N个工作现场就可以把CPU充分利用起来,让CPU跑到N*100%
 
结论:
        N核服务器,通过执行业务的单线程分析出本地计算时间为x,等待时间为y,则工作线程数(线程池线程数)设置为 N*(x+y)/x,能让CPU的利用率最大化。
 
经验:
        一般来说,非CPU密集型的业务(加解密、压缩解压缩、搜索排序等业务是CPU密集型的业务),瓶颈都在后端数据库,本地CPU计算的时间很少,所以设置几十或者几百个工作线程也都是可能的。
 
六、结论
        N核服务器,通过执行业务的单线程分析出本地计算时间为x,等待时间为y,则工作线程数(线程池线程数)设置为 N*(x+y)/x,能让CPU的利用率最大化。

13.如何判断一个socket当前的连接状态

1.read返回0则表示对端已经close fd了,连接断开

2.write后收到rst,再写收到sigpipe表示对端已经断开或者对方没有程序在监听目标端口

3.getsocketopt

 使用getsockopt测试:int getsockopt(int socket, int level, int option_name,void *restrict option_value, socklen_t *restrict option_len);RETURN VALUEUpon successful completion, getsockopt() shall return 0; otherwise, -1 shall be returned and errno set to indicate the error.ERRORSThe getsockopt() function shall fail if:EBADF  The socket argument is not a valid file descriptor.EINVAL The specified option is invalid at the specified socket level.ENOPROTOOPTThe option is not supported by the protocol.ENOTSOCKThe socket argument does not refer to a socket.The getsockopt() function may fail if:EACCES The calling process does not have the appropriate privileges.EINVAL The socket has been shut down.ENOBUFSInsufficient resources are available in the system to complete the function.The following sections are informative.

在getsockopt的错误码中:
EINVAL The socket has been shut down.
有这么一个返回值,可以用于测试连接是否断开

4.使用 tcp的keepalive:

转自https://cloud.tencent.com/developer/article/1411975

在写TCP服务程序时,除了要处理SIGPIPE外,还要有客户端连接检测机制,用于及时发现崩溃的客户端连接。一般来说,有两种检测方式:1. 在应用层,由业务程序自己检测;2. 使用TCP的KeepAlive机制。

使用第一种方式,意味着要在应用层自己实现一个ping-pong逻辑和协议,并支持设置空闲时长,重试次数,重试间隔等。这无疑会增加一定的代码量,好处则是可以自己控制逻辑,同时不用学习内核的实现:)

但是如果没有特殊的需求,我更倾向于第二种方式。如非必要,不要引入额外的逻辑。更何况既然TCP本身已经支持KeepAlive,为什么还要自己实现一套,多此一举呢?代码写的越多,越可能引入Bug:D

本文将对TCP的KeepAlive的使用和原理做比较详细的分析。先看如何使用TCP KeepAlive来检测“失联”的TCP连接。完整的测试代码位于:https://github.com/gfreewind/LinuxDetails/blob/master/networks/6.tcp_keepalive_reporter/tcp_keepalive_report.c。

本文只截图几个关键的函数

其中SO_KEEPALIVE用于打开或者关闭KeepAlive功能,TCP_KEEPIDLE用于设置空闲时间——即有多久没有发送报文就进行探测,TCP_KEEPCNT用于设置KeepAlive的尝试次数,TCP_KEEPINTVL用于设置重试KeepAlive的报文间隔。这里容易搞混的是TCP_KEEPIDLE和TCP_KEEPINTVL,前者是需要进行KeepAlive探测的空闲时间,而后者是在某次KeepAlive探测失败,再次重试的间隔时间。对于上面的程序来说,当该TCP连接有5秒没有进行数据传输时,就会发送KeepAlive探测报文。当探测报文失败时,会隔2秒再次发送探测报文,3次探测失败就判断连接失败。

通过测试程序,我们可以使用tcpdump抓包,来分析KeepAlive。

前三个报文是TCP的三次握手,连接成功后,没有任何报文发送。间隔5秒后,发送KeepAlive,即第4个报文。第5个报文为KeepAlive ACK。再间隔5秒后,再次发送KeepAlive探测报文,即第6个报文。

(请忽略报文的黑颜色,因为这个测试是本机发给本机,所以TCP校验和是不正确的——没有真正通过网卡)

为了测试KeepAlive检测报文失败的情况,在连接成功之后,我使用iptables创建一条规则,丢弃发往port 34567端口的数据报文。这时抓包结果如下:

同上,前三个报文完成TCP三次握手,间隔5秒后发送KeepAlive探测报文,但由于没有收到ACK,所以每间隔2秒再次发送KeepAlive,重试3次后,判定连接失败,在11秒时(应该发送第4个KeepAlive时)发送RST给对端,中断连接。

那么当KeepAlive机制判断连接崩溃时,应用层如何得到通知呢?当连接正常关闭时,应用层可以得到可读事件通知,并且进行read操作时,返回结果为0——这也是服务端判断客户端关闭连接的方法。就此推测,KeepAlive机制判断连接崩溃时,其行为应该与正常关闭类似。在测试代码中,分别使用了select和epoll来进行io事件测试,其输出如下:

根据测试,无论是使用select还是epoll,当KeepAlive中止连接时,应用层都可以得到可读事件通知,并且read结果为0。上面的输出,还有一个sock err is 110的结果。这是另外一种判断机制:

当KeepAlive中断连接时,其会设置socket错误,应用层通过getsockopt即SO_ERR可以获得该错误。

通过测试程序,我们只是学到了KeepAlive的使用方法。接下来就要进入内核对KeepAlive一探究竟。

tcp_keepalive_timer为KeepAlive定时器的回调函数。在这个函数中

当探测超时,就会调用tcp_send_active_reset向对端发送RST报文,中止连接,然后调用tcp_write_err。

设置sk->sk_err等于ETIMEOUT,其值就是测试程序打印的110。而sk_error_report指向的是sock_def_error_report。

其会唤醒等待在当前套接字的进程,且其IO事件是POLLERR。

而在使用epoll_ctl添加监听fd时,内核会自动把EPOLLERR和EPOLLHUP加到监听事件中。这就是为什么epoll可以得到通知。

至此,我们已经从实现和使用上,完成了对TCP KeepAlive机制的探索。

14.高并发下如何限流

转自 https://segmentfault.com/a/1190000019896078

就今天来说的限流,书面意思和作用一致,就是为了限制,通过对并发访问或者请求进行限速或者一个时间窗口内的请求进行限速来保护系统。一旦达到了限制的临界点,可以用拒绝服务、排队、或者等待的方式来保护现有系统,不至于发生雪崩现象。

限流的表现形式上大部分可以分为两大类:

  1. 限制消费者数量。也可以说消费的最大能力值。比如:数据库的连接池是侧重的是总的连接数。还有线程池,本质上也是限制了消费者的最大消费能力。
  2. 可以被消费的请求数量。这里的数量可以是瞬时并发数,也可以是一段时间内的总并发数。

除此之外,限流还有别的表现形式,例如按照网络流量来限流,按照cpu使用率来限流等。按照限流的范围又可以分为分布式限流,应用限流,接口限流等。无论怎么变化,限流都可以用以下图来表示:

常用技术实现

令牌桶算法

令牌桶是一个存放固定容量令牌的桶,按照固定速率往桶里添加令牌,桶填满了就丢弃令牌,请求是否被处理要看桶中令牌是否足够,当令牌数减为零时则拒绝新的请求。令牌桶允许一定程度突发流量,只要有令牌就可以处理,支持一次拿多个令牌。令牌桶中装的是令牌。

漏桶算法

漏桶一个固定容量的漏桶,按照固定常量速率流出请求,流入请求速率任意,当流入的请求数累积到漏桶容量时,则新流入的请求被拒绝。漏桶可以看做是一个具有固定容量、固定流出速率的队列,漏桶限制的是请求的流出速率。漏桶中装的是请求。

计数器

有时我们还会使用计数器来进行限流,主要用来限制一定时间内的总并发数,比如数据库连接池、线程池、秒杀的并发数;计数器限流只要一定时间内的总请求数超过设定的阀值则进行限流,是一种简单粗暴的总数量限流,而不是平均速率限流。

redis实现简单限流

限定某个接口在指定时间内只能被访问指定次数,通过incr实现

比如在2秒内限制访问次数上限为100次

(1)string

1.记录key  :ip,如果key不存在,则创建并设置过期时间为2秒

2.如果key值<100,则计数加1,否则返回错误信息

(2)list实现

为每个ip设置列表,每次访问判断列表长度,如果合法,则插入列表,否则返回错误信息。同时为每个key设置过期时间

15.内存对齐问题

遇到一个非常nice的面试官,引导我解决了一直疑惑的问题

为什么要内存对齐?
       访问未对齐的内存,处理器要访问两次(数据先读高位,再度地位),访问对齐的内存,处理器只要访问一次,为了提高处理器读取数据的效率,我们使用内存对齐。Windows 默认对齐数为8字节,Linux 默认对齐数为4字节。
       使用内存对齐的原因还有平台的原因:不是所有的硬件平台都能访问特定的地址上的任意数据,某些平台只能访问特定的地址上的获取数据,否则会抛出异常。

上例子:(64位机器,指针为8字节)

#include <iostream>
using namespace std;class base {
public:int a;       //4double b;    //8char c;      //1virtual void func() {};      //vptr  8
};class son : public base {int d;       //4char e;      //1virtual void func() {};      //vptr  8
};int main()
{cout << "----------------------" << endl;cout << "sizeof base: " << sizeof(base) << endl;cout << "sizeof son: " << sizeof(son) << endl;return 0;
}

运行结果如下:

先说内存对齐的原则:

  • 数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如说是数组,结构体等)的整数倍开始(比如int在32位机为4字节,则要从4的整数倍地址开始存储。
  • 结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储.(struct a里存有struct b,b里有char,int ,double等元素,那b应该从8的整数倍开始存储.)
  • 收尾工作:结构体的总大小,也就是sizeof的结果,.必须是其内部最大成员的整数倍.不足的要补齐。

分析一下base类:

vptr是放在类内存的最开始位置,因此其内存大小为

8 + 4+4 + 8 + 1+7 = 32 bytes

vptr + int + double + char

其中加黑的数字是为了内存对齐而加上的字节。

son类:

8 + 4+4 + 8 + 1+3 + 4 + 1+7 = 40 bytes

vptr + int + double + char +  int  +   char

(son)  (   base类的内存   )   (son自己的)

16.delete[]与new[]配对问题

我们知道,通常new应该与delete配对,new[]应该与delete[]配对:

B* arrptr = new B[10];delete[] arrptr;

因为delete[] 会先找到arrptr前面4字节内存放的数组元素个数n,然后执行n次析构函数,接着调用void operator delete(void *pUserData),free掉arrptr指向的内存块

那么当我们用delete去释放new[]的内存时会发生什么?

若用delete去释放new[],会如下所示,只调用一次析构函数并且程序崩溃了(但是arrptr指向的内存块仍会被释放,只是每个对象在堆上new的内容不会被回收,因为没有调用他的析构函数)

这是为什么呢?

参考https://www.cnblogs.com/sura/archive/2012/07/03/2575448.html

对于普通数据存储空间的分配形式:

公式1)_CrtMemBlockHeader + <Your Data> +gap[nNoMansLandSize];这类数据用delete和delete[]都一样!

通常我们的指针都是指向<your data>的首地址!

而对于对象数组则是:

公式2)_CrtMemBlockHeader +数组元素个数+ <Your Data> +gap[nNoMansLandSize];

 确实如此,delete arrptr;在调用void operator delete(void*pUserData)时会将<yourdata>的首地址传给pUserData,

那么程序会将<your data>的前8*4Byte数据当成_CrtMemBlockHeader,也就是说(_CrtMemBlockHeader:: pBlockHeaderPrev开始到  数组元素个数)数据当成CrtMemBlockHeader的数据;

  然而delete[] arrptr;则会将 (数组元素个数+<yout data>)整个数据当成pUserData!数组元素个数的前8*4Byte当成_CrtMemBlockHeader,写入到pHead!

17.i++和++i是线程安全的吗

i++ 先赋值再+1, ++i是先+1再赋值, 例如:

int i = 0;
int j = 0;
i  = i++;
cout<<"i的最后结果" << i<<endl;//0;
j = i++;
cout<<"i的最后结果 "<< i<<endl;//1
cout<<"j的最后结果"<<j<<endl;//0

不论是++i还是i++都不是原子操作,在运行中都可能会有CPU调度产生,造成i的值被修改,造成脏读脏写。(读入eax寄存器值+1再赋值给地址 这个过程不是原子的,会存在竞争现象)

结论: 由于线程不共享栈区,共享堆区和全局区,所以当且仅当 i 位于栈上是安全的,反之不安全。(栈,方法栈,是每个线程方法自己的,因为每个线程有自己的寄存器),atomic_int 和 各种 Lock 都可以确保线程安全。atomic_int 的效率高是因为它是互斥区非常小,只有一条指令,而 Lock 的互斥区是拿锁到放锁之间的区域,至少三条指令。

volatile不能解决这个线程安全问题。因为volatile只能

  • 阻止编译器为了提高速度而将一个变量缓冲到寄存器而不写回
  • 阻止编译器调整操作volatile变量的指令顺序

而不能阻止cpu动态调度顺序

因此解决方式有:

  • 锁。
  • atomic_int。(如atomic_int64_t total = 0; //atomic_int64_t相当于int64_t,但是本身就拥有原子性),其实现原理是利用处理器的CAS操作(Compare And Swap,比较与交换,一种有名的无锁算法)来检测栈中的值是否被其他线程改变,如果被改变则CAS操作失败。这种实现方法在CPU指令级别实现了原子操作,因此,其比使用synchronized来实现同步效率更高。

(CAS操作过程都包含三个运算符:内存地址V、期望值E、新值N。当操作的时候,如果地址V上存放的值等于期望值E,则将地址V上的值赋为新值N,否则,不做任何操作,但是要返回原值是多少。这就保证比较和设置这两个动作是原子操作。系统主要利用JNI(Java Native Interface,Java本地接口)来保证这个原子操作,它利用CPU硬件支持来完成,使用硬件提供swap和test_and_set指令,但CPU下同一指令的多个指令周期不可中断,SMP(Symmetric Multi-Processing)中通过锁总线支持这两个指令的原子性。)

i++和++i的区别:

i++:
int operator++(int)
{int tmp = *this;++*this;return tmp;
}++i:
int operator++()
{++*this;return *this;
}

18.vector <bool> 的特殊之处

我们知道,一个bool 变量占用了 1 个字节的内存,当值为 false 的时候,实际上存储的是 0x00,为ture时实际上存储的是 0x01,这是因为内存要addressable,单个bit是没办法寻址的。

然而在vector<bool>中,为了节省空间,其内部是用一个bit来表示一个bool值的,这就导致了:

  • 1. operator[]不会返回一个指向bool值的引用,而是返回一个代理(proxy)

简单说说C++的容器。我们知道vector<T>(T不是bool)是一个顺序容器(Sequence container)。因此他满足C++标准中关于容器的标准。比方说下面这条:

Expression Return type
X::reference lvalue of T

简单说vector<int>::reference至少必须是一个int。而vector<bool>则不是这样的。vector<bool>::reference是一个可以和bool兼容的代理数据结构。

最简单的验证这一点的例子就是:

template <typename T>
void func(vector<T>& v)
{T& ref = v[0];
}

上面这段代码你在vc2012或者g++4.6.1上,传递一个vector<bool>对象,都不能通过编译。g++给出的编译错误就相当清晰了:

vectorbool.cpp: In function ‘void func(std::vector<T>&) [with T = bool']’:vectorbool.cpp:18:11:    instantiated from herevectorbool.cpp:9:17: error: invalid initialization of non-const reference of type ’bool&’ from an rvalue of type ‘std::vector<bool>::reference {aka std::_Bit_reference}’

operator[]返回的reference是一个右值而不是个左值(lvalue of T)。

  • 2. vector<bool>不要求底层的存储必须是连续的空间

见标准文档:There is no requirement that the data be stored as a contiguous allocation of bool values. A space-optimized representation of bits is recommended instead.

这两点差异导致了使用vector<bool>时会造成一些性能上的问题:

(例子来自https://www.cnblogs.com/wpcockroach/p/3179572.html)

int main()
{clock_t t1, t2, t3;vector<bool> vb;vector<int> vi;vector<char> vc;t1 = clock();for (int i = 0; i < 1000; ++i){for (int j = 0; j < 1000; ++j){vb.push_back(true);}}t1 = clock() - t1;t2 = clock();for (int i = 0; i < 1000; ++i){for (int j = 0; j < 1000; ++j){vi.push_back(1);}}t2 = clock() - t2;t3 = clock();for (int i = 0; i < 1000; ++i){for (int j = 0; j < 1000; ++j){vc.push_back('a');}}t3 = clock() - t3;cout << "'vector<bool>::push_back(true)' 1000000 times cost: " << t1 << endl;cout << "'vector<int>::push_back(1)' 1000000 times cost: " << t2 << endl;cout << "'vector<char>::push_back('a')' 1000000 times cost: " << t3 << endl;t1 = clock();for (int i = 0; i < 1000; ++i){for (int j = 0; j < 1000; ++j){bool b = vb[j + 1000 * i];}}t1 = clock() - t1;t2 = clock();for (int i = 0; i < 1000; ++i){for (int j = 0; j < 1000; ++j){int b = vi[j + 1000 * i];}}t2 = clock() - t2;t3 = clock();for (int i = 0; i < 1000; ++i){for (int j = 0; j < 1000; ++j){char b = vc[j + 1000 * i];}}t3 = clock() - t3;cout << "'vector<bool>::operator[]' 1000000 times cost: " << t1 << endl;cout << "'vector<int>::operator[]' 1000000 times cost: " << t2 << endl;cout << "'vector<char>::operator[]' 1000000 times cost: " << t3 << endl;return 0;
}

运行结果:

当我们须要vector<bool>时,我们有两种选择能够做:

  1. 用deque<bool>。deque差点儿提供了vector所提供的一切(能够看到的省略仅仅有reserve和capacity),但deque<bool>是一个STL容器,并且它确实存储bool。当然,deque中元素的内存不是连续的,所以你不能把deque<bool>中的数据传递给一个期望bool数组的C API,但对于vector<bool>,我们也不能这么做,由于没有一种可移植的方法可以得到vector<bool>中的数据。

  2. 选择bitset。bitset不是STL容器,但它是标准C++库的一部分。

19.type_id与type_info

引自:type_info

  • type_id的作用是返回一个变量或数据类型的“类型”。

如:

cout<<typeid(int).name()<<endl;
int a;
cout<<typeid(a).name()<<endl;

会输出:

int
int

比如有类 A,其中定义有虚函数,而类B,C,D都是从类A派生而来的且重定义了该虚函数,这时有个类A的指针p和p1,按照虚函数的原理,基类的指针可以指向任何派生类的对象,在这时就有可能需要比较两个指针是否指向同一个对象,这时就可以这样使用typeid了,typeid(*p)= =typeid(*p1);这里要注意的是typeid(*p)与typeid(p)是指的不同的对象类型,typeid(p)表示的是p的类型,在这里p是一个指针,这个指针指向的是类A的对象,所以typeid(p)的类型是A*, 而typeid(*p)则不一样, *p表示的是指针p实际所指的对象的类型, 比如这里的指针p指向派生类B,则typeid(*p)的类型为B。所以在测试两个指针的类型是否是相等时应使用*p,即typeid(*p)= =typeid(*p1)。如果是typeid(p)= =typeid(p1)的话,则无论指针p和p1指向的什么派生类对象,他们都是相等的,因为都是A *的类型。

例子:

struct Poly_Base { virtual void Member() {} };
struct Poly_Derived : Poly_Base {};Poly_Derived son;Poly_Base* father = &son;cout << "typeid(father).name(): " << typeid(father).name() << endl;cout << "typeid(*father).name(): " << typeid(*father).name() << endl;

  • type_info
函数typeid()返回值类型class type_info。
其中type_info重载了操作符 == , != , =分别用来比较是否相等、不等、赋值。
函数name()返回类型名称。
class type_info {
public:virtual ~type_info();booloperator== (consttype_info& rhs) const;booloperator!= (consttype_info& rhs) const;boolbefore (consttype_info& rhs) const;constchar* name() const;
private:type_info (consttype_info& rhs);type_info& operator= (consttype_info& rhs);
};

当类型是类和类指针时需要注意:
    普通类基类指针base*指向继承类deived,则typeid(deived) != typeid(*base) 前者类型是继承类的type_info类型,而后者是父类的type_info类型。
    虚类基类指针base*指向继承类deived,则typeid(deived) == typeid(*base) 两者都是父类的type_info类型。

代码见下:

// type_info example
#include <iostream>
#include <typeinfo>
using namespace std;struct Base {};
struct Derived : Base {};
struct Poly_Base { virtual void Member() {} };
struct Poly_Derived : Poly_Base {};int main() {Poly_Derived son;Poly_Base* father = &son;cout << "typeid(father).name(): " << typeid(father).name() << endl;cout << "typeid(*father).name(): " << typeid(*father).name() << endl;// built-in types:int i;int * pi;cout << "int is: " << typeid(int).name() << endl;cout << "  i is: " << typeid(i).name() << endl;cout << " pi is: " << typeid(pi).name() << endl;cout << "*pi is: " << typeid(*pi).name() << endl << endl;// non-polymorphic types:Derived derived;Base* pbase = &derived;cout << "derived is: " << typeid(derived).name() << endl;cout << " *pbase is: " << typeid(*pbase).name() << endl;cout << boolalpha << "same type? ";cout << (typeid(derived) == typeid(*pbase)) << endl << endl;// polymorphic types:Poly_Derived polyderived;Poly_Base* ppolybase = &polyderived;cout << "polyderived is: " << typeid(polyderived).name() << endl;cout << " *ppolybase is: " << typeid(*ppolybase).name() << endl;cout << boolalpha << "same type? ";cout << (typeid(polyderived) == typeid(*ppolybase)) << endl << endl;system("pause");return 0;
}

输出结果:

  • dynamic_cast:

dynamic_cast<>用于C++类继承多态间的转换,分为:
1.子类向基类的向上转型(Up Cast)
2.基类向子类的向下转型(Down Cast)
    其中向上转型不需要借助任何特殊的方法,只需用将子类的指针或引用赋给基类的指针或引用即可,dynamic_cast向上转型其总是肯定成功的。

而向下转换时要特别注意:dynamic_cast操作符,将基类类型的指针或引用安全的转换为派生类的指针或引用。dynamic_cast将一个基类对象指针(或引用)cast到继承类指针,dynamic_cast会根据基类指针是否真正指向继承类指针来做相应处理。如果绑定到引用或指针的对象不是目标类型的对象,则dynamic_cast失败。如果是指针类型失败,则dynamic_cast的返回结果为NULL,如果是引用类型的失败,则抛出一个bad_cast错误。(即下行转换要看本质是什么类型)
注意:dynamic_cast在将父类cast到子类时,父类必须要有虚函数。因为dynamic_cast运行时需要检查RTTI信息。只有带虚函数的类运行时才会检查RTTI。

例子如下:

现有继承关系 A <- B <- C

A *a = new B;  // 本来new了个B,向上转换为A
B* b = dynamic_cast<B*> (a); // 从A再转换为B,因为本质是B,回到自身,所以成功
C* c = dynamic_cast<C*> (b); // 将本质是B的东西,转变为子类对象,所以失败。即c=0.

再看一例:

继承关系   A <- B <- C <- D <- E

A* a = new C; // 本质
B* b = dynamic_cast<B*> (a); // 成功,因为B是C的父类
E* e = dynamic_cast<E*> (a); // 失败,因为E是C的子类

20.如何测试进程的上下文切换时间

原文:

https://www.iteye.com/blog/ocelot1985-163-com-1029949

https://www.jianshu.com/p/ebf1f832694c

我们知道,进程分配的时间片到了或者进程进行了阻塞IO操作都会触发操作系统进行进程调度动作,进程调度过程中就存在进程上下文切换。

进程上下文切换开销都有哪些

那么上下文切换的时候,CPU的开销都具体有哪些呢?开销分成两种,一种是直接开销、一种是间接开销。

直接开销就是在切换时,cpu必须做的事情,包括:

  • 1、切换页表全局目录
  • 2、切换内核态堆栈
  • 3、切换硬件上下文(进程恢复前,必须装入寄存器的数据统称为硬件上下文)
    • ip(instruction pointer):指向当前执行指令的下一条指令
    • bp(base pointer): 用于存放执行中的函数对应的栈帧的栈底地址
    • sp(stack poinger): 用于存放执行中的函数对应的栈帧的栈顶地址
    • cr3:页目录基址寄存器,保存页目录表的物理地址
    • ......
  • 4、刷新TLB
  • 5、系统调度器的代码执行

间接开销主要指的是虽然切换到一个新进程后,由于各种缓存并不热,速度运行会慢一些。如果进程始终都在一个CPU上调度还好一些,如果跨CPU的话,之前热起来的TLB、L1、L2、L3因为运行的进程已经变了,所以以局部性原理cache起来的代码、数据也都没有用了,导致新进程穿透到内存的IO会变多。

废话不多说,我们先用个实验测试一下,到底一次上下文切换需要多长的CPU时间!实验方法是创建两个进程并在它们之间传送一个令牌。其中一个进程在读取令牌时就会引起阻塞。另一个进程发送令牌后等待其返回时也处于阻塞状态。如此往返传送一定的次数,然后统计他们的平均单次切换时间开销。(即两个进程之间用管道互相读写数据,由于是双向的动作,因此会用到两个管道。)

代码:

#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <time.h>
#include <sched.h>
#include <sys/types.h>
#include <unistd.h>      //pipe()  int main()
{int x, i, fd[2], p[2];char send ='s';char receive;pipe(fd);pipe(p);struct timeval tv;struct sched_param param;param.sched_priority = 0;while ((x = fork()) == -1);if (x==0) {sched_setscheduler(getpid(), SCHED_FIFO, &param);gettimeofday(&tv, NULL);printf("Before Context Switch Time %u us\n", tv.tv_usec);for (i = 0; i < 10000; i++) {read(fd[0], &receive, 1);write(p[1], &send, 1);}exit(0);}else {sched_setscheduler(getpid(), SCHED_FIFO, &param);for (i = 0; i < 10000; i++) {write(fd[1], &send, 1);read(p[0], &receive, 1);}gettimeofday(&tv, NULL);printf("After Context SWitch Time %u us\n", tv.tv_usec);}return 0;
}

多次测量结果如下:


Before Context Switch Time 29308 us
After Context SWitch Time 78138 usBefore Context Switch Time 100984 us
After Context SWitch Time 145337 usBefore Context Switch Time 933486 us
After Context SWitch Time 973566 usBefore Context Switch Time 909323 us
After Context SWitch Time 951427 usBefore Context Switch Time 621892 us
After Context SWitch Time 666569 usBefore Context Switch Time 197603 us
After Context SWitch Time 238739 usBefore Context Switch Time 116898 us
After Context SWitch Time 158117 usBefore Context Switch Time 277832 us
After Context SWitch Time 323269 us

我们可以看到,前后差值大概在45000us左右,因此单次切换时间约:

45000/20000 = 2.25us

进程切换时间为2.25 us

注: cpu MHz    : 2394.454  (用lscpu命令查看)

21.查看进程运行时的段分布

我们知道在程序运行之前,可以用objdump readelf等命令查看其段分布的情况,那么在运行时怎么查看呢?

其实很简单,先 ps -elf | grep pname 查看其pid ,然后 cd /proc/pid

ubuntu@VM-0-12-ubuntu:~$ ps -elf | grep Web
4 S root     13568  1870  0  80   0 - 15715 -      16:56 pts/0    00:00:00 sudo ./WebServer -l /home/chenjinan/build/release/WebServer/log.log
4 S root     13570 13568  0  80   0 - 82238 -      16:56 pts/0    00:00:00 ./WebServer -l /home/chenjinan/build/release/WebServer/log.log
0 S ubuntu   13601 13579  0  80   0 -  3443 pipe_w 16:56 pts/1    00:00:00 grep --color=auto Web
ubuntu@VM-0-12-ubuntu:~$ cd /proc/13570

ll可以看到有许多文件

ubuntu@VM-0-12-ubuntu:/proc/13570$ ll
ls: cannot read symbolic link 'cwd': Permission denied
ls: cannot read symbolic link 'root': Permission denied
ls: cannot read symbolic link 'exe': Permission denied
total 0
dr-xr-xr-x   9 root root 0 Oct 15 16:56 ./
dr-xr-xr-x 104 root root 0 Oct 15 15:00 ../
dr-xr-xr-x   2 root root 0 Oct 15 16:57 attr/
-rw-r--r--   1 root root 0 Oct 15 16:57 autogroup
-r--------   1 root root 0 Oct 15 16:57 auxv
-r--r--r--   1 root root 0 Oct 15 16:57 cgroup
--w-------   1 root root 0 Oct 15 16:57 clear_refs
-r--r--r--   1 root root 0 Oct 15 16:56 cmdline
-rw-r--r--   1 root root 0 Oct 15 16:57 comm
-rw-r--r--   1 root root 0 Oct 15 16:57 coredump_filter
-r--r--r--   1 root root 0 Oct 15 16:57 cpuset
lrwxrwxrwx   1 root root 0 Oct 15 16:57 cwd
-r--------   1 root root 0 Oct 15 16:57 environ
lrwxrwxrwx   1 root root 0 Oct 15 16:57 exe
dr-x------   2 root root 0 Oct 15 16:56 fd/
dr-x------   2 root root 0 Oct 15 16:57 fdinfo/
-rw-r--r--   1 root root 0 Oct 15 16:57 gid_map
-r--------   1 root root 0 Oct 15 16:57 io
-r--r--r--   1 root root 0 Oct 15 16:57 limits
-rw-r--r--   1 root root 0 Oct 15 16:57 loginuid
dr-x------   2 root root 0 Oct 15 16:57 map_files/
-r--r--r--   1 root root 0 Oct 15 16:57 maps
-rw-------   1 root root 0 Oct 15 16:57 mem
-r--r--r--   1 root root 0 Oct 15 16:57 mountinfo
-r--r--r--   1 root root 0 Oct 15 16:57 mounts
-r--------   1 root root 0 Oct 15 16:57 mountstats
dr-xr-xr-x   5 root root 0 Oct 15 16:57 net/
dr-x--x--x   2 root root 0 Oct 15 16:57 ns/
-r--r--r--   1 root root 0 Oct 15 16:57 numa_maps
-rw-r--r--   1 root root 0 Oct 15 16:57 oom_adj
-r--r--r--   1 root root 0 Oct 15 16:57 oom_score
-rw-r--r--   1 root root 0 Oct 15 16:57 oom_score_adj
-r--------   1 root root 0 Oct 15 16:57 pagemap
-r--------   1 root root 0 Oct 15 16:57 patch_state
-r--------   1 root root 0 Oct 15 16:57 personality
-rw-r--r--   1 root root 0 Oct 15 16:57 projid_map
lrwxrwxrwx   1 root root 0 Oct 15 16:57 root
-rw-r--r--   1 root root 0 Oct 15 16:57 sched
-r--r--r--   1 root root 0 Oct 15 16:57 schedstat
-r--r--r--   1 root root 0 Oct 15 16:57 sessionid
-rw-r--r--   1 root root 0 Oct 15 16:57 setgroups
-r--r--r--   1 root root 0 Oct 15 16:57 smaps
-r--r--r--   1 root root 0 Oct 15 16:57 smaps_rollup
-r--------   1 root root 0 Oct 15 16:57 stack
-r--r--r--   1 root root 0 Oct 15 16:56 stat
-r--r--r--   1 root root 0 Oct 15 16:57 statm
-r--r--r--   1 root root 0 Oct 15 16:56 status
-r--------   1 root root 0 Oct 15 16:57 syscall
dr-xr-xr-x   7 root root 0 Oct 15 16:57 task/
-r--r--r--   1 root root 0 Oct 15 16:57 timers
-rw-rw-rw-   1 root root 0 Oct 15 16:57 timerslack_ns
-rw-r--r--   1 root root 0 Oct 15 16:57 uid_map
-r--r--r--   1 root root 0 Oct 15 16:56 wchan

查看maps文件即可,它记录了程序执行过程中的内存空间的情况:代码段、堆、共享段、栈、内核空间等

$ sudo cat maps
55903efe7000-55903f00a000 r-xp 00000000 fc:01 1049532                    /home/chenjinan/build/release/WebServer/WebServer
55903f20a000-55903f20b000 r--p 00023000 fc:01 1049532                    /home/chenjinan/build/release/WebServer/WebServer
55903f20b000-55903f20c000 rw-p 00024000 fc:01 1049532                    /home/chenjinan/build/release/WebServer/WebServer
55904097e000-55904099f000 rw-p 00000000 00:00 0                          [heap]
7f94bc000000-7f94bc021000 rw-p 00000000 00:00 0
7f94bc021000-7f94c0000000 ---p 00000000 00:00 0
7f94c4000000-7f94c4021000 rw-p 00000000 00:00 0
7f94c4021000-7f94c8000000 ---p 00000000 00:00 0
7f94c8000000-7f94c8021000 rw-p 00000000 00:00 0
7f94c8021000-7f94cc000000 ---p 00000000 00:00 0
7f94cc000000-7f94cc021000 rw-p 00000000 00:00 0
7f94cc021000-7f94d0000000 ---p 00000000 00:00 0
7f94d0919000-7f94d0c27000 rw-p 00000000 00:00 0
7f94d0c27000-7f94d0c28000 ---p 00000000 00:00 0
7f94d0c28000-7f94d1736000 rw-p 00000000 00:00 0
7f94d1736000-7f94d1737000 ---p 00000000 00:00 0
7f94d1737000-7f94d2245000 rw-p 00000000 00:00 0
7f94d2245000-7f94d2246000 ---p 00000000 00:00 0
7f94d2246000-7f94d2d54000 rw-p 00000000 00:00 0
7f94d2d54000-7f94d2d55000 ---p 00000000 00:00 0
7f94d2d55000-7f94d3863000 rw-p 00000000 00:00 0
7f94d3863000-7f94d3a00000 r-xp 00000000 fc:01 131667                     /lib/x86_64-linux-gnu/libm-2.27.so
7f94d3a00000-7f94d3bff000 ---p 0019d000 fc:01 131667                     /lib/x86_64-linux-gnu/libm-2.27.so
7f94d3bff000-7f94d3c00000 r--p 0019c000 fc:01 131667                     /lib/x86_64-linux-gnu/libm-2.27.so
7f94d3c00000-7f94d3c01000 rw-p 0019d000 fc:01 131667                     /lib/x86_64-linux-gnu/libm-2.27.so
7f94d3c01000-7f94d3de8000 r-xp 00000000 fc:01 131621                     /lib/x86_64-linux-gnu/libc-2.27.so
7f94d3de8000-7f94d3fe8000 ---p 001e7000 fc:01 131621                     /lib/x86_64-linux-gnu/libc-2.27.so
7f94d3fe8000-7f94d3fec000 r--p 001e7000 fc:01 131621                     /lib/x86_64-linux-gnu/libc-2.27.so
7f94d3fec000-7f94d3fee000 rw-p 001eb000 fc:01 131621                     /lib/x86_64-linux-gnu/libc-2.27.so
7f94d3fee000-7f94d3ff2000 rw-p 00000000 00:00 0
7f94d3ff2000-7f94d4009000 r-xp 00000000 fc:01 131164                     /lib/x86_64-linux-gnu/libgcc_s.so.1
7f94d4009000-7f94d4208000 ---p 00017000 fc:01 131164                     /lib/x86_64-linux-gnu/libgcc_s.so.1
7f94d4208000-7f94d4209000 r--p 00016000 fc:01 131164                     /lib/x86_64-linux-gnu/libgcc_s.so.1
7f94d4209000-7f94d420a000 rw-p 00017000 fc:01 131164                     /lib/x86_64-linux-gnu/libgcc_s.so.1
7f94d420a000-7f94d4383000 r-xp 00000000 fc:01 394411                     /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.25
7f94d4383000-7f94d4583000 ---p 00179000 fc:01 394411                     /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.25
7f94d4583000-7f94d458d000 r--p 00179000 fc:01 394411                     /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.25
7f94d458d000-7f94d458f000 rw-p 00183000 fc:01 394411                     /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.25
7f94d458f000-7f94d4593000 rw-p 00000000 00:00 0
7f94d4593000-7f94d459a000 r-xp 00000000 fc:01 131714                     /lib/x86_64-linux-gnu/librt-2.27.so
7f94d459a000-7f94d4799000 ---p 00007000 fc:01 131714                     /lib/x86_64-linux-gnu/librt-2.27.so
7f94d4799000-7f94d479a000 r--p 00006000 fc:01 131714                     /lib/x86_64-linux-gnu/librt-2.27.so
7f94d479a000-7f94d479b000 rw-p 00007000 fc:01 131714                     /lib/x86_64-linux-gnu/librt-2.27.so
7f94d479b000-7f94d47b5000 r-xp 00000000 fc:01 131708                     /lib/x86_64-linux-gnu/libpthread-2.27.so
7f94d47b5000-7f94d49b4000 ---p 0001a000 fc:01 131708                     /lib/x86_64-linux-gnu/libpthread-2.27.so
7f94d49b4000-7f94d49b5000 r--p 00019000 fc:01 131708                     /lib/x86_64-linux-gnu/libpthread-2.27.so
7f94d49b5000-7f94d49b6000 rw-p 0001a000 fc:01 131708                     /lib/x86_64-linux-gnu/libpthread-2.27.so
7f94d49b6000-7f94d49ba000 rw-p 00000000 00:00 0
7f94d49ba000-7f94d49e1000 r-xp 00000000 fc:01 131597                     /lib/x86_64-linux-gnu/ld-2.27.so
7f94d4bd3000-7f94d4bda000 rw-p 00000000 00:00 0
7f94d4be1000-7f94d4be2000 r--p 00027000 fc:01 131597                     /lib/x86_64-linux-gnu/ld-2.27.so
7f94d4be2000-7f94d4be3000 rw-p 00028000 fc:01 131597                     /lib/x86_64-linux-gnu/ld-2.27.so
7f94d4be3000-7f94d4be4000 rw-p 00000000 00:00 0
7ffc11ee6000-7ffc11f07000 rw-p 00000000 00:00 0                          [stack]
7ffc11f94000-7ffc11f97000 r--p 00000000 00:00 0                          [vvar]
7ffc11f97000-7ffc11f99000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]

22.纯tcp通信的包识别

啊面试被问到了没反应过来,太亏了

客户端send连发3个包,服务端一次recv如何区分3个包的

测试方案:连接成功后,服务端sleep 3s,客户端连发3包,看服务端能否正确读出

直接上代码:

服务端:

#include <iostream>
#include <string.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <unistd.h>using namespace std;
#define PORT 2333               //端口号
#define BACKLOG 5       //最大监听数int main()
{int serverfd = 0;int ret = 0;int newfd = 0;char buf[4096] = {0};struct sockaddr_in server_addr = {0};struct sockaddr_in client_addr = {0};socklen_t socklen = 0;serverfd = socket(AF_INET,SOCK_STREAM,0);if(0 > serverfd){cout<<"创建socket失败"<<endl;return 0;}server_addr.sin_family = AF_INET;server_addr.sin_port = htons(PORT);server_addr.sin_addr.s_addr = htonl(INADDR_ANY);if(0 > bind(serverfd, (struct sockaddr *)&server_addr, sizeof(server_addr))){cout<<"绑定失败"<<endl;return 0;}if(0 > listen(serverfd, BACKLOG)){cout<<"监听失败"<<endl;return 0;}newfd = accept(serverfd, (struct sockaddr *)&client_addr, &socklen);if(0 > newfd){cout<<"accept 失败"<<endl;return 0;}else{cout<<"accept 成功,服务器会发送一条消息给客户端"<<endl;send(newfd, "服务器接收连接成功!", sizeof("服务器接收连接成功!"), 0);}cout<<"server wait for 3 second, client send message 3 times"<<endl;sleep(3);ret = recv(newfd, buf, sizeof(buf), 0);if(0 >= ret){cout<<"client close or recv failed"<<endl;}else{cout<<"ret = " << ret <<endl;cout<<"recv message: "<<endl;for(int i = 0 ; i < ret ;i++)cout<<buf[i]<<' ';//cout<<"recv message: "<<buf<<endl;}close(newfd);close(serverfd);return 0;
}

客户端:

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <unistd.h>
#include <string>
using namespace std;#define PORT 2333                       //目标地址端口号
#define SERVER_IP "127.0.0.1" //目标地址IPint main(){int client_fd = 0;struct sockaddr_in server_addr = {0};char buf[4096] = {0};client_fd = socket(AF_INET, SOCK_STREAM, 0);if(0 > client_fd){cout<<"创建socket失败!"<<endl;return 0;}server_addr.sin_family = AF_INET;server_addr.sin_port = htons(PORT);server_addr.sin_addr.s_addr = inet_addr(SERVER_IP);if(0 > connect(client_fd, (struct sockaddr *)&server_addr, sizeof(server_addr))){cout<<"connect failed"<<endl;}else{cout<<"connect success!"<<endl;recv(client_fd,buf,sizeof(buf),0);cout<<"recv: "<<buf<<endl;}for(int i=1;i<=3;i++){send(client_fd,"test",sizeof("test"),0);cout<<"this time i send: "<< "test" << endl;}close(client_fd);return 0;
}

结果:

服务端

./server
accept 成功,服务器会发送一条消息给客户端
server wait for 3 second, client send message 3 times
ret = 15
recv message:
t e s t  t e s t  t e s t

客户端:

./client
connect success!
recv: 服务器接收连接成功!
this time i send: test
this time i send: test
this time i send: test

结论:可以识别的,其实想一想网络各层协议的封包拆包过程,服务端在网络层往运输层提交时会去掉ip协议首部,同样道理,运输层往应用层提交时才拆掉tcp协议首部,因此在套接字的接收缓冲区内实际上数据包还是带着tcp首部的,首部中有报文总长度和报文编号,就可以区分开包了。

注意,首部中有:

32位序号:用于标识从TCP发端向TCP收端发送的数据字节流,表示在这个报文段中的第一个数据字节。

也就是说下一个tcp报文的序号 = 本报文的序号 + 本报文的总长度 +1

23. 线程局部存储的实现

我们知道,在定义变量时,在前面加上__thread,就能把这个变量变为本线程的局部变量,其他的线程访问不到的。

那么他是如何实现的呢?

对于windows来说,正常情况下一个全局变量或静态变量会被放到.data 或.bss段中,但当我们使用__thread定义一个线程私有变量的时候,编译器会把这些变量放到PE文件的.tls段中。当系统启动一个新线程时,它会从进程的堆中分配一块足够大小的空间,然后把.tls段中的内容复制到这块空间中,于是每个线程都有自己独立的一个.tls副本。所以对于用__thread定义的同一个变量,它们在不同线程中的地址都是不同的。

不同线程创建和退出时会涉及到这些私有变量的初始化和销毁,所有线程的私有变量的地址信息都存放在PE文件中的TLS表中。每个线程都有线程环境块TEB,里面会保存此线程的栈地址、线程ID等信息,其中有一个域是一个TLS数组(一般有64个元素),此数组的第一个元素就是指向该线程的.tls副本的地址。

因此,在多线程库中,函数调用出错的情况下通常返回-1,并把错误信息errno,此时errno是存放在每个线程的TLS中的。

24 vector中放的是指向自定义类对象的指针,扩容时会调用析构函数吗

今天看到这个问题,网上查了一下,没有得到准确的答案,写一下试试:

class test {
public:test() {cout << "call construct func" << endl;}~test() {cout << "call destruct func" << endl;}
};int main() {vector<test*>a;test* t1 = new test;test* t2 = new test;test* t3 = new test;test* t4 = new test;test* t5 = new test;test* t6 = new test;test* t7 = new test;test* t8 = new test;a.emplace_back(t1);cout << "vector's capacity = " << a.capacity() << endl;cout << "vector's size = " << a.size() << endl;a.emplace_back(t2);cout << "vector's capacity = " << a.capacity() << endl;cout << "vector's size = " << a.size() << endl;a.emplace_back(t3);cout << "vector's capacity = " << a.capacity() << endl;cout << "vector's size = " << a.size() << endl;a.emplace_back(t4);cout << "vector's capacity = " << a.capacity() << endl;cout << "vector's size = " << a.size() << endl;a.emplace_back(t5);cout << "vector's capacity = " << a.capacity() << endl;cout << "vector's size = " << a.size() << endl;a.emplace_back(t6);cout << "vector's capacity = " << a.capacity() << endl;cout << "vector's size = " << a.size() << endl;a.emplace_back(t7);cout << "vector's capacity = " << a.capacity() << endl;cout << "vector's size = " << a.size() << endl;a.emplace_back(t8);cout << "vector's capacity = " << a.capacity() << endl;cout << "vector's size = " << a.size() << endl;system("pause");return 0;
}

运行结果如下

可以看到,扩容时并不会调用析构函数(若调用a.clear()也只会释放一个个指针的空间,不会调用对象的析构函数,将造成内存泄漏),若vector中存放的是对象,则会调用析构函数

 vector<test> b;test t1 = test();test t2 = test();test t3 = test();test t4 = test();test t5 = test();b.emplace_back(t1);cout << "vector's capacity = " << b.capacity() << endl;cout << "vector's size = " << b.size() << endl;b.emplace_back(t2);.........

运行结果

【学习点滴】cpp遇到的一些疑问和积累相关推荐

  1. python异常处理_Python学习点滴04 - 学会异常处理(2)

    前言 我们在开发Python程序时经常会遇到一些错误(语法错误和异常),尤其是程序代码发生异常(Exceptions)时,如果不能及时捕获异常和有效处理异常,则程序运行会被终止,有可能会造成相应的后果 ...

  2. 小白爬坑记:C语言学习点滴——我对单、双引号的理解

    小白爬坑记:C语言学习点滴--我对单.双引号的理解 一.单引号的作用: 二.双引号的作用: 三.字符或字符串容易犯的错误: 三.做个小题: 一.单引号的作用: 将单引号中间的所有符号直接转换为ASCI ...

  3. C++学习点滴(多次调用同一个类)

    在C++编程的时候,如果一个类中的多个函数都调用同一个类的函数,就应该在调用类中定义一个被调用类的全局实例,而不应该在每个函数中都定义一个被调用类的实例.简单的说,如果A类种的多个函数都调用B类的函数 ...

  4. 再读Socket编程——《UNIX网络编程(卷一)》学习点滴

    原先曾以Socket编程为入口开始自己的新的学习,毕竟未曾致用,时至今日已比较生疏.借着阅读<UNIX网络编程(卷一)>(简称UNPv1)的机会,正好复习一番,而且希望将新的感受记录下来. ...

  5. 【学习点滴】linux调试工具、cmake和网络抓包

    目录 gdb 多进程调试 多线程调试: gdb底层原理 Linux下查看服务器端的并发连接个数: Valgrind memcheck strace Linux下,绑定1024以下的端口需要root权限 ...

  6. 【学习点滴】网络相关理解与http协议

    目录 http的连接: http报文格式: http请求方法 cookie和session referer: 长连接和短连接 http状态码 https建立连接的过程: http缓存 1. 优点 2. ...

  7. python学习点滴记录-Day07

    面向对象 摘自林老师博客http://www.cnblogs.com/linhaifeng/articles/6182264.html#_label7 理论 什么是面向对象的程序设计及为什么要有它? ...

  8. Windows Phone 7编程学习点滴一——页面切换、返回键重载和工具栏

    1. 页面切换和对齐方式 2 (1)XAML实现方式 <HyperlinkButton Content="TestPage1" NavigateUri="/Test ...

  9. python学习点滴记录-Day10-线程

    多线程 协程 io模型 并发编程需要掌握的点: 1 生产者消费者模型2 进程池线程池3 回调函数4 GIL全局解释器锁 线程 理论部分 (摘自egon老师博客) 一.定义: 在传统操作系统中,每个进程 ...

最新文章

  1. 论文阅读工具ReadPaper
  2. springboot-springmvc-requestParam
  3. (转)WinForm控件使用文章收藏整理完成
  4. 最后的分的计算机公式,省考最后10天!掌握这些数学运算公式,提分!
  5. 公司数据部培训讲义:ArcMap数字化培训教程
  6. StringUtil中常用的方法
  7. linux命令 ps -ef 的含义
  8. 使用Gradle构建Java项目
  9. 啤酒与尿布,咩叔原创基于图论简单到爆的实时关联性算法
  10. 一个人如果不学习,30岁就死了,活着的只是重复和应付
  11. Javascript中常用的经典技巧
  12. excel制作跨职能流程图_你会用Excel制作流程图吗?超级强大的功能
  13. pd虚拟机镜像:懒人一键安装win10、11
  14. nodejs python性能_监控Nodejs的性能
  15. LVS-NAT基于NFS存储部署Discuz
  16. 三维分子模型软件PyMOL
  17. VJ第一周算法题(A - Mud Puddles)
  18. 学习python:练习2.投资理财计算器
  19. 【mixly】APDS9960第三方库开发
  20. Python+Yolov5人脸口罩识别

热门文章

  1. tenacity发生异常/失败/错误时重试retry机制,Python
  2. 快消巨头与“饿了么”的数字革命
  3. [生存志] 第55节 吴公子札巡访中原
  4. 为全面硬件升级做好准备 鑫谷开元T1全塔机箱评测
  5. C语言 - 计算n的阶乘(n!)
  6. 利用APPium获取自动浏览获取喵币
  7. 万云网实名认证不成功_头条发文章显示实名认证接口调用失败,无法发送,怎么办才能解决?我实名了的?...
  8. 计算机网络有哪些分类方式,计算机网络有哪些分类?
  9. Android IOS平台AE动画库
  10. html5性格测试,9种性格测试