1、内存深拷贝(代码)
用工程的写法写一个String类,实现内存深拷贝,用char指针和内存分配。
-
内存深拷贝(Deep Copy)是指在进行对象拷贝时,不仅复制对象本身,还对对象所引用的动态分配的内存进行拷贝,使得拷贝后的对象和原对象完全独立,互不影响。
-
当一个对象包含指向动态分配内存的指针或引用时,进行浅拷贝(Shallow Copy)只是简单地复制指针或引用的值,指向相同的内存块。
-
在拷贝构造函数和赋值运算符重载中,同样进行了内存的深拷贝,避免了浅拷贝导致的潜在问题。在析构函数中,使用delete[]操作符释放了分配的内存空间。
#include<cstdio>
#include<cstring>
class String{
private:
char* data; //数组地址
int length; //长度
public:
//默认构造函数
String():data(NULL),length(0){}
//内存深拷贝构造函数1
String(const char* str){
length = strlen(str);
data = new char[length+1];
strcpy(data, str);
}
//内存深拷贝构造函数2
String(const String & others){
length = others.length;
data = new char[length+1];
strcpy(data, others.data);
}
//析构函数
~String(){
delete[] data;
}
//其他函数
int getLength() const {
return length;
}
const char* getData() const {
return data;
}
//内存深拷贝赋值
String& operator=(const String& other) {
delete[] data;
length = other.length;
data = new char[length+1];
strcpy(data, other.data);
return *this;
}
};
int main(){
String a("abcd");
String b("defghijk");
printf("%d\n%s\n", a.getLength(), a.getData());
printf("%d\n%s\n", b.getLength(), b.getData());
b = a;
printf("%d\n%s\n", b.getLength(), b.getData());
return 0;
}
2、C++基础知识
一、基础知识
1、基本语言
2、容器和算法
3、类和数据抽象
4、面向对象与泛型编程
5、编译与底层
6、C++11
虚函数了解吗
- virtual标记虚函数,派生类继承基类的虚函数之后可以重写这个成员函数,以实现类的多态。
- 五种函数,普通函数(非成员函数);静态成员函数;内联成员函数;构造函数;友元函数 不能声明为虚函数。
说一下static 关键字的作用
-
静态变量,全局,局部都可。
功能是存到 静态存储区,在整个程序运行期间一直存在,初始化自动赋值0。
作用域保持不变,静态变量在声明他的文件之外是不可见的。
-
静态函数
只可在本cpp 内使用,不会同其他cpp 中的同名函数引起冲突;
-
静态类成员:
在类中,静态成员可以实现多个对象之间的数据共享,并且使用静态数据成员还不会破坏隐藏的原则,即保证了安全性。
因此,静态成员是类的所有对象中共享的成员,而不是某个对象的成员。对多个对象来说,静态数据成员只存储一处,供所有对象共用
-
静态成员函数
和静态数据成员一样,它们都属于类的静态成员,它们都不是对象成员。因此,对静态成员的引用不需要用对象名。
在静态成员函数的实现中不能直接引用类中说明的非静态成员,可以引用类中说明的静态成员(这点非常重要)。
如果静态成员函数中要引用非静态成员时,可通过对象来引用。从中可看出,调用静态成员函数使用如下格式:<类名>::<静态成员函数名>(<参数表>);
说一下C++和C 的区别
- 设计思想上:C++是面向对象的语言,而C 是面向过程的结构化编程语言
- 语法上:
C++具有重载、继承和多态三种特性
C++相比C,增加多许多类型安全的功能,比如强制类型转换、
C++支持范式编程,比如模板类、函数模板等
c++中四种强制(cast)转换
-
1、const_cast
用于将const 变量转为非const
-
2、static_cast
用于各种隐式转换,比如非const 转const,void*转指针等, static_cast 能用于多态向上转化,如果向下转能成功但是不安全,结果未知;
-
3、dynamic_cast
用于动态类型转换。只能用于含有虚函数的类,用于类层次间的向上和向下转化。只能转指针或引用。向下转化时,如果是非法的对于指针返回NULL,对于引用抛异常。要深入了解内部转换的原理。
向上转换:指的是子类向基类的转换
向下转换:指的是基类向子类的转换
它通过判断在执行到该语句的时候变量的运行时类型和要转换的类型是否相同来判断是否能够进行向下转换。
-
4、reinterpret_cast
几乎什么都可以转,比如将int 转指针,可能会出问题,尽量少用;
-
5、为什么不使用C 的强制转换?
C 的强制转换表面上看起来功能强大什么都能转,但是转化不够明确,不能进行错误检查,容易出错。
请说一下C/C++ 中指针和引用的区别?
- 1.指针有自己的一块空间,而引用只是一个别名;
- 2.使用sizeof 看一个指针的大小是4,而引用则是被引用对象的大小;
- 3.指针可以被初始化为NULL,而引用必须被初始化且必须是一个已有对象的引用;
- 4.作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引用的修改都会改变引用所指向的对象;
- 5.可以有const 指针,但是没有const 引用;
- 6.指针在使用中可以指向其它对象,但是引用只能是一个对象的引用,不能被改变;
- 7.指针可以有多级指针(**p),而引用至于一级;
- 8.指针和引用使用++运算符的意义不一样;
- 9.如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄露。
请你说一下你理解的c++中的smart pointer 四个智能指针:
-
C++里面的四个智能指针: auto_ptr, shared_ptr, weak_ptr, unique_ptr
其中后三个是c++11 支持,并且第一个已经被11 弃用。
-
智能指针的作用是管理一个指针,因为存在以下这种情况:
申请的空间在函数结束时忘记释放,造成内存泄漏。
使用智能指针可以很大程度上的避免这个问题,因为智能指针就是一个类,当超出了类的作用域是,类会自动调用析构函数,析构函数会自动释放资源。
所以智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放内存空间。
给定三角形ABC 和一点P(x,y,z),判断点P 是否在ABC 内,给出思路并手写代码
- 根据面积法,如果P 在三角形ABC 内,那么三角形ABP 的面积+三角形BCP 的面积+三角形
ACP 的面积应该等于三角形ABC 的面积。算法如下:
怎么判断一个数是二的倍数,怎么求一个数中有几个1
- 判断一个数是不是二的倍数,即判断该数二进制末位是不是0:
- 求一个数中1 的位数,可以直接逐位除十取余判断:
请回答一下数组和指针的区别
请你回答一下野指针是什么?
- 野指针就是指向一个已删除的对象
或者未申请访问受限内存区域的指针
请你介绍一下C++中的智能指针
-
智能指针主要用于管理在堆上分配的内存,它将普通的指针封装为一个栈对象。当栈对象的生存周期结束后,会在析构函数中释放掉申请的内存,从而防止内存泄漏。
-
C++ 11 中最常用的智能指针类型为shared_ptr,它采用引用计数的方法,记录当前内存资源被多少个智能指针引用。该引用计数的内存在堆上分配。当新增一个时引用计数加1,当过期时引用计数减一。只有引用计数为0 时,智能指针才会自动释放引用的内存资源。
-
对shared_ptr 进行初始化时不能将一个普通指针直接赋值给智能指针,因为一个是指针,一个是类。可以通过make_shared 函数或者通过构造函数传入普通指针。并可以通过get 函数获得普通指针。
请你回答一下智能指针有没有内存泄露的情况
- 当两个对象相互使用一个shared_ptr 成员变量指向对方,会造成循环引用,使引用计数失效,从而导致内存泄漏。
请你来说一下智能指针的内存泄漏如何解决
- 为了解决循环引用导致的内存泄漏,引入了weak_ptr 弱指针,weak_ptr 的构造函数不会修改引用计数的值,从而不会对对象的内存进行管理,其类似一个普通指针,但不指向引用计数的共享内存,但是其可以检测到所管理的对象是否已经被释放,从而避免非法访问。
请你回答一下为什么析构函数必须是虚函数?为什么C++默认的析构函数不是虚函数
- 将可能会被继承的父类的析构函数设置为虚函数,可以保证当我们new 一个子类,然后使用基类指针指向该子类对象,释放基类指针时可以释放掉子类的空间,防止内存泄漏。
- C++默认的析构函数不是虚函数是因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。
而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。因此C++默认的析构函数不是虚函数,而是只有当需要当作父类时,设置为虚函数。
请你来说一下函数指针
请你来说一下fork 函数
-
Fork:创建一个和当前进程映像一样的进程可以通过fork( )系统调用:
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
-
成功调用fork( )会创建一个新的进程,它几乎与调用fork( )的进程一模一样,这两个进程都会继续运行。在子进程中,成功的fork( )调用会返回0。 在父进程中fork( )返回子进程的pid。如果出现错误,fork( )返回一个负值。
-
最常见的fork( )用法是创建一个新的进程,然后使用exec( )载入二进制映像,替换当前进程的映像。这种情况下,派生(fork)了新的进程,而这个子进程会执行一个新的二进制可执行文件的映像。这种“派生加执行”的方式是很常见的。
请你来说一下C++中析构函数的作用
-
析构函数与构造函数对应,当对象结束其生命周期,如对象所在的函数已调用完毕时,系统会自动执行析构函数。
-
析构函数名也应与类名相同,只是在函数名前面加一个位取反符 ~,例如~stud( ),以区别于构造函数。它不能带任何参数,也没有返回值(包括void 类型)。只能有一个析构函数,不能重载。
-
如果用户没有编写析构函数,编译系统会自动生成一个缺省的析构函数(即使自定义了析构函数,编译器也总是会为我们合成一个析构函数,并且如果自定义了析构函数,编译器在执行时
会先调用自定义的析构函数再调用合成的析构函数),它也不进行任何操作。所以许多简单的类
中没有用显式的析构函数。
-
如果一个类中有指针,且在使用的过程中动态的申请了内存,那么最好显示构造析构函数在
销毁类之前,释放掉申请的内存空间,避免内存泄漏。
-
类析构顺序:
1)派生类本身的析构函数;2)对象成员析构函数;3)基类析构函数。
请你来说一下静态函数和虚函数的区别
-
静态函数在编译的时候就已经确定运行时机,虚函数在运行的时候动态绑定。
- 虚函数因为用了虚函数表机制,调用的时候会增加一次内存开销
请你来说一说重载和覆盖
- 重载:两个函数名相同,但是参数列表不同(个数,类型),返回值类型没有要求,在同一作用
域中
-
重写:子类继承了父类,父类中的函数是虚函数,在子类中重新定义了这个虚函数,这种情况是重写
请你来说一说static 关键字
- 1.加了static 关键字的全局变量只能在本文件中使用。例如在a.c 中定义了static int a=10;
那么在b.c 中用extern int a 是拿不到a 的值得,a 的作用域只在a.c 中。
- 2.static 定义的静态局部变量分配在数据段上,普通的局部变量分配在栈上,会因为函数栈帧
的释放而被释放掉。
-
- 对一个类中成员变量和成员函数来说,加了static 关键字,则此变量/函数就没有了this
指针了,必须通过类名才能访问
请你说一说strcpy 和strlen
- 从src 逐字节拷贝到dest,直到遇到’\0’结束,因为没有指定长度,可能会导致拷贝越界,造成缓冲区溢出漏洞, 安全版本是strncpy 函数。
- strlen 函数是计算字符串长度的函数,返回从开始到’\0’之间的字符个数。
请你说一说你理解的虚函数和多态
- 多态的实现主要分为静态多态和动态多态,
-
静态多态主要是重载,在编译的时候就已经确定;
-
动态多态是用虚函数机制实现的,在运行期间动态绑定。
- 举个例子:一个父类类型的指针指向一个子类对象时候,使用父类的指针去调用子类中重写了的父类中的虚函数的时候,会调用子类重写过后的函数,在父类中声明为加了virtual 关键字的函数,在子类中重写时候不需要加virtual也是虚函数。
- 虚函数的实现:
在有虚函数的类中,类的最开始部分是一个虚函数表的指针,这个指针指向一个虚函数表,表中放了虚函数的地址,实际的虚函数在代码段(.text)中。
当子类继承了父类的时候也会继承其虚函数表,当子类重写父类中虚函数时候,会将其继承到的虚函数表中的地址替换为重新写的函数地址。使用了虚函数,会增加访问内存开销,降低效率。
请你来写个函数在main 函数执行前先运行
__attribute((constructor))void before()
{
printf("before main\n");
}
以下四行代码的区别是什么?(数组指针)
const char * arr = "123";
//字符串123 保存在常量区,const 本来是修饰arr 指向的值不能通过arr 去修改,但是字符串
“123”在常量区,本来就不能改变,所以加不加const 效果都一样
char * brr = "123";
//字符串123 保存在常量区,这个arr 指针指向的是同一个位置,同样不能通过brr 去修改"123"
的值
const char crr[] = "123";
//这里123 本来是在栈上的,但是编译器可能会做某些优化,将其放到常量区
char drr[] = "123";
//字符串123 保存在栈区,可以通过drr 去修改
请你来说一下C++里是怎么定义常量的?常量存放在内存的哪个位置?
- 常量在C++里的定义就是一个top-level const 加上对象类型,常量定义必须初始化。
- 对于局部对象,常量存放在栈区,
- 对于全局对象,常量存放在全局/静态存储区。对于字面值常量,常量存放在常量存储区。
请你来回答一下const 修饰成员函数的目的是什么?
const 修饰的成员函数表明函数调用不会对对象做出任何更改,事实上,如果确认不会对对象做更改,就应该为函数加上const 限定,这样无论const 对象还是普通对象都可以调用该函数。
如果同时定义了两个函数,一个带const,一个不带,会有问题吗?
不会,这相当于函数的重载。
请你来说一说隐式类型转换
- 首先,对于内置类型,低精度的变量给高精度变量赋值会发生隐式类型转换,
- 其次,对于只存在单个参数的构造函数的对象构造来说,函数调用可以直接使用该参数传入,编译器会自动调用其构造函数生成临时对象。
请你来说一说extern “C”
-
C++调用C 函数需要extern C,因为C 语言没有函数重载。
请你回答一下new/delete 与malloc/free 的区别是什么
- 首先,new/delete 是C++的关键字,而malloc/free 是C 语言的库函数,
- 后者使用必须指明申请内存空间的大小,
- 对于类类型的对象,后者不会调用构造函数和析构函数
请你说说虚函数表具体是怎样实现运行时多态的?
-
子类若重写父类虚函数,虚函数表中,该函数的地址会被替换,对于存在虚函数的类的对象,在VS 中,对象的对象模型的头部存放指向虚函数表的指针,通过该机制实现多态。
-
请你说说C 语言是怎么进行函数调用的? & 请你说说C 语言参数压栈顺序?
- 每一个函数调用都会分配函数栈,在栈内进行函数执行过程。调用前,先把返回地址压栈,然后
把当前函数的esp 指针压栈。
- 从右到左
请你回答一下C++中拷贝赋值函数的形参能否进行值传递?
不能。如果是这种情况下,调用拷贝构造函数的时候,首先要将实参传递给形参,这个传递的时
候又要调用拷贝构造函数。。如此循环,无法完成拷贝,栈也会满。
请你说说fork,wait,exec 函数
- 父进程产生子进程使用fork 拷贝出来一个父进程的副本,此时只拷贝了父进程的页表,两个进
程都读同一块内存,当有进程写的时候使用写实拷贝机制分配内存,
- exec 函数可以加载一个elf文件去替换父进程,从此父进程和子进程就可以运行不同的程序了。
fork 从父进程返回子进程的pid,从子进程返回0.
- 调用了wait 的父进程将会发生阻塞,直到有子进程状态改变,执行成功返回0,错误返回-1。exec 执行成功则子进程从新的程序开始运行,无返回值,执行失败返回-1
请你回答一下静态函数和虚函数的区别
静态函数在编译的时候就已经确定运行时机,虚函数在运行的时候动态绑定。虚函数因为用了虚
函数表机制,调用的时候会增加一次内存开销
请你来说一下C++中类成员的访问权限
- C++通过public、protected、private 三个关键字来控制成员变量和成员函数的访问权限,
它们分别表示公有的、受保护的、私有的,被称为成员访问限定符。
- 在类的内部(定义类的代码内部),无论成员被声明为public、protected 还是private,都是可以互相访问的,没有访问权限的限制。
-
在类的外部(定义类的代码之外),只能通过对象访问成员,并且通过对象只能访问public 属性的成员,不能访问private、protected 属性的成员
请你来说一下C++中struct 和class 的区别
-
在C++中,可以用struct 和class 定义类,都可以继承。
-
区别在于:
structural 的默认继承权限和默认访问权限是public,
而class 的默认继承权限和默认访问权限是private。
-
另外,class 还可以定义模板类形参,比如template <class T, int i>。
请你回答一下什么是右值引用,跟左值又有什么区别?
- 左值:能对表达式取地址、或具名对象/变量。一般指表达式结束后依然存在的持久对象。
右值:不能对表达式取地址,或匿名对象。一般指表达式结束就不再存在的临时对象。
- 左值可以寻址,而右值不可以。
- 左值可以被赋值,右值不可以被赋值,可以用来给左值赋值。
- 左值可变,右值不可
请你来说一下一个C++源文件从文本到可执行文件经历的过程?
-
对于C++源文件,从文本到可执行文件一般需要四个过程:
-
预处理阶段:对源代码文件中文件包含关系(头文件)、预编译语句(宏定义)进行分析和
替换,生成预编译文件。
-
编译阶段:将经过预处理后的预编译文件转换成特定汇编代码,生成汇编文件
-
汇编阶段:将编译阶段生成的汇编文件转化成机器码,生成可重定位目标文件
-
链接阶段:将多个目标文件及所需要的库连接成最终的可执行目标文件
请你来回答一下include 头文件的顺序以及双引号””和尖括号<>的区别?
- 双引号和尖括号的区别:编译器预处理阶段查找头文件的路径不一样。
- 对于使用双引号包含的头文件,会先查找当前头文件目录。 尖括号直接去库里了。
请你回答一下malloc 的原理,
- Malloc 函数用于动态分配内存。为了减少内存碎片和系统调用的开销,malloc 其采用内存池的方式,先申请大块内存作为堆区,然后将堆区分为多个内存块,以块作为内存管理的基本单位。当用户申请内存时,直接从堆区分配一块合适的空闲块。Malloc 采用隐式链表结构将堆区分成连续的、大小不一的块,包含已分配块和未分配块;同时malloc 采用显示链表结构来管理所有的空闲块,使用一个双向链表将空闲块连接起来,每一个空闲块记录了一个连续的、未分
配的地址。
- 当进行内存分配时,Malloc 会通过隐式链表遍历所有的空闲块,选择满足要求的块进行分
配;当进行内存合并时,malloc 采用边界标记法,根据每个块的前后块是否已经分配来决定是
否进行块合并。
- Malloc 在申请内存时,一般会通过brk 或者mmap 系统调用进行申请。其中当申请内存小于
128K 时,会使用系统函数brk 在堆区中分配;而当申请内存大于128K 时,会使用系统函数mmap
在映射区分配。
说一说C++的内存管理是怎样的
在C++中,虚拟内存分为代码段、数据段、BSS 段、堆区、文件映射区以及栈区六部分。
代码段: 包括只读存储区和文本区,其中只读存储区存储字符串常量,文本区存储程序的机器代码。
数据段:存储程序中已初始化的全局变量和静态变量
bss 段:存储未初始化的全局变量和静态变量(局部+全局),以及所有被初始化为0 的全局变量和静态变量。
堆区: 调用new/malloc 函数时在堆区动态分配内存,同时需要调用delete/free 来手动释放申请的内存。
映射区:存储动态链接库以及调用mmap 函数进行的文件映
如何判断内存泄漏?
- 内存泄漏通常是由于调用了malloc/new 等内存申请的操作,但是缺少了对应的free/delete。
- 为了判断内存是否泄露,我们一方面可以使用linux 环境下的内存泄漏检查工具Valgrind,
- 另一方面我们在写代码时可以添加内存申请和释放的统计功能,统计当前申请和释放的内存是否一致,以此来判断内存是否泄露。
请你来说一下什么时候会发生段错误
段错误通常发生在访问非法内存地址的时候,具体来说分为以下几种情况:
请自己设计一下如何采用单线程的方式处理高并发
- 在单线程模型中,可以采用I/O 复用来提高单线程处理多个请求的能力,然后再采用事件驱动模型,基于异步回调来处理事件来
请问C++11 有哪些新特性?
-
auto 关键字:编译器可以根据初始值自动推导出类型。但是不能用于函数传参以及数组类型的推导
-
nullptr 关键字:nullptr 是一种特殊类型的字面值,它可以被转换成任意其它的指针类型;而NULL 一般被宏定义为0,在遇到重载时可能会出现问题。
-
智能指针:C++11 新增了std::shared_ptr、std::weak_ptr 等类型的智能指针,用于解决内存管理的问题。
-
初始化列表:使用初始化列表来对类进行初始化
-
右值引用:基于右值引用可以实现移动语义和完美转发,消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率
-
atomic 原子操作用于多线程资源互斥操作
-
新增STL 容器array 以及tuple
STL 容器
2.1、std::array
2.2、std::forward_list
2.3、std::unordered_map
2.4、std::unordered_set
请你详细介绍一下C++11 中的可变参数模板、右值引用和lambda 这几个新特性。
C++11 的可变参数模板,对参数进行了高度泛化,可以表示任意数目、任意类型的参数,其
语法为:在class 或typename 后面带上省略号”。
Template<class ... T>
void func(T ... args)
{
cout<<”num is”<<sizeof ...(args)<<endl;
}
func();//args 不含任何参数
func(1);//args 包含一个int 类型的实参
func(1,2.0)//args 包含一个int 一个double 类型的实参
其中T 叫做模板参数包,args 叫做函数参数包
省略号作用如下:
1)声明一个包含0 到任意个模板参数的参数包
2)在模板定义得右边,可以将参数包展成一个个独立的参数
C++11 可以使用递归函数的方式展开参数包,获得可变参数的每个值。通过递归函数展开参
数包,需要提供一个参数包展开的函数和一个递归终止函数。例如:
3、操作系统
操作系统主要就是考内存管理和进程管理,其他不怎么考。
熟悉多线程编程技术,对异步、并发技术有深入理解。有大规模高并发系开发和设计经验者优先;
请你说一下进程与线程的概念,以及为什么要有进程线程,其中有什么区别
-
进程是对运行时程序的封装,是系统进行资源调度和分配的的基本单位,实现了操作系统的
并发;
-
线程是进程的子任务,是CPU 调度和分派的基本单位, 用于保证程序的实时性,实现进程内部的并发;
-
线程是操作系统可识别的最小执行和调度单位。每个线程都独自占用一个虚拟处理器:独自的寄存器组,指令计数器和处理器状态。 每个线程完成不同的任务,但是共享同一地址空间(也就是同样的动态内存,映射文件,目标代码等等),打开的文件队列和其他内核资源。
-
-
一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。 线程依赖
于进程而存在。
-
2.进程在执行过程中拥有独立的内存单元,而多个线程共享进程的内存
-
3.进程是资源分配的最小单位,线程是CPU 调度的最小单位;
-
进程编程调试简单可靠性高,但是创建销毁开销大;
线程正相反,开销小,切换速度快,但是编程调试相对复杂。
进程间通信的方式:
-
进程间通信主要包括管道、系统IPC(包括消息队列、信号量、信号、共享内存等)、以及套接字socket。
-
1、管道主要包括无名管道和命名管道:
管道可用于具有亲缘关系的父子进程间的通信,有名管道除了具有管道所具有的功能外,它还允许无亲缘关系进程间的通信
-
2、消息队列
消息队列,是消息的链接表,存放在内核中。一个消息队列由一个标识符(即队列ID)来标记。(消息队列克服了信号传递信息少,管道只能承载无格式字节流以及缓冲区大小受限等特点)
具有写权限得进程可以按照一定得规则向消息队列中添加新信息;对消息队列有读权限得进程则可以从消息队列中读取信息
1)消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级。
2)消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除。
3)消息队列可以实**现消息的随机查询,**消息不一定要以先进先出的次序读取,也可以按消息的类型读取。
-
2.2 信号量semaphore
信号量(semaphore)与已经介绍过的IPC 结构不同,它是一个计数器,可以用来控制多个进程对共享资源的访问。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。
1)信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。
2)信号量基于操作系统的PV 操作,程序对信号量的操作都是原子操作。
3)每次对**信号量的PV 操作不仅限于对信号量值加1 或减1,**而且可以加减任意正整数。
4)支持信号量组。
-
共享内存(Shared Memory)
它使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据得更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等
1)共享内存是最快的一种IPC,因为进程是直接对内存进行存取
2)因为多个进程可以同时操作,所以需要进行同步
3)信号量+共享内存通常结合在一起使用,信号量用来同步对共享内存的访问
-
套接字SOCKET:
socket 也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同主机之间的进程通信。
线程间通信的方式:
- 临界区:通过多线程的串行化来访问公共资源或一段代码, 速度快,适合控制数据访问;
- 互斥量Synchronized/Lock:采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问
- 信号量Semphare:为控制具有有限数量的用户资源而设计的,它允许多个线程在同一时刻去访问同一个资源,但一般需要限制同一时刻访问此资源的最大线程数目。
- 事件(信号),Wait/Notify:通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作
请你说一说Linux 虚拟地址空间
-
为了防止不同进程同一时刻在物理内存中运行而对物理内存的争夺和践踏,采用了虚拟内存。
- 虚拟内存技术使得不同进程在运行过程中,它所看到的是自己独自占有了当前系统的4G 内
存。
虚拟内存的优点:
1.扩大地址空间;
2.内存保护:每个进程运行在各自的虚拟内存地址空间,互相不能干扰对方。虚存还对特定
的内存地址提供写保护,可以防止代码或数据被恶意篡改。
3.公平内存分配。采用了虚存之后,每个进程都相当于有同样大小的虚存空间。
4.当进程通信时,可采用虚存共享的方式实现。
5.当不同的进程使用同样的代码时,比如库文件中的代码,物理内存中可以只存储一份这样
的代码,不同的进程只需要把自己的虚拟内存映射过去就可以了,节省内存
6.虚拟内存很适合在多道程序设计系统中使用,许多程序的片段同时保存在内存中。当一个
程序等待它的一部分读入内存时,可以把CPU 交给另一个进程使用。在内存中可以保留多个进程,
系统并发度提高
虚拟内存的代价:
1.虚存的管理需要建立很多数据结构,这些数据结构要占用额外的内存
2.虚拟地址到物理地址的转换,增加了指令的执行时间。
3.页面的换入换出需要磁盘I/O,这是很耗时的
4.如果一页中只有一部分数据,会浪费内存
操作系统中的程序的内存结构
一个程序本质上都是由BSS 段、data 段、text 段三个组成的。
可以看到一个可执行程序在存储(没有调入内存)时分为代码段、数据区和未初始化数据区三部分。
- 可执行程序在运行时又多出两个区域:栈区和堆区。
- 栈区:由编译器自动释放,存放函数的参数值、局部变量等。
每当一个函数被调用时,该函数的返回类型和一些调用的信息被存放到栈中。然后这个被调用的函数再为他的自动变量和临时变量在栈上分配空间。每调用一个函数一个新的栈就会被使用。栈区是从高地址位向低地址位增长的,是一块连续的内存区域,最大容量是由系统预先定义好的,申请的栈空间超过这个界限时会提示溢出,用户能从栈中获取的空间较小。
- 堆区:用于动态分配内存,位于BSS 和栈中间的地址区域。
- 由程序员申请分配和释放。 堆是从低地址位向高地址位增长,采用链式存储结构。频繁malloc/free 造成内存空间的不连续,产生碎片。当申请堆空间时库函数是按照一定的算法搜索可用的足够大的空间。因此堆的效率比栈要低的多。
请你说一说操作系统中的缺页中断
-
malloc()和mmap()等内存分配函数,在分配时只是建立了进程虚拟地址空间,并没有分配虚拟内存对应的物理内存。当进程访问这些没有建立映射关系的虚拟内存时,处理器自动触发一个缺页异常。
-
缺页中断:
**在请求分页系统中,可以通过查询页表中的状态位来确定所要访问的页面是否存在于内存中。**每当所要访问的页面不在内存是,会产生一次缺页中断,此时操作系统会根据页表中的外存地址在外存中找到所缺的一页,将其调入内存。缺页本身是一种中断,与一般的中断一样,需要经过4 个处理步骤:
1、保护CPU 现场
2、分析中断原因
3、转入缺页中断处理程序进行处理
4、恢复CPU 现场,继续执行
-
但是缺页中断是由于所要访问的页面不存在于内存时,由硬件所产生的一种特殊的中断,因
此,与一般的中断存在区别:
1、在指令执行期间产生和处理缺页中断信号
2、一条指令在执行期间,可能产生多次缺页中断
3、缺页中断返回是,执行产生中断的一条指令,而一般的中断返回是,执行下一条指令。
请你回答一下fork 和vfork 的区别
- fork( )的子进程拷贝父进程的数据段和代码段;vfork( )的子进程与父进程共享数据段
- fork( )的父子进程的执行次序不确定;vfork( )保证子进程先运行,在调用exec 或exit
之前与父进程数据是共享的,在它调用exec 或exit 之后父进程才可能被调度运行。
- vfork( )保证子进程先运行,在它调用exec 或exit 之后父进程才可能被调度运行。如果在调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁。
- 当需要改变共享数据段中变量的值,则拷贝父进程。
请问如何修改文件最大句柄数?
- linux 默认最大文件句柄数是1024 个,在linux 服务器文件并发量比较大的情况下,系统会报"too many open files"的错误。故在linux 服务器高并发调优时,往往需要预先调优Linux参数,修改Linux 最大文件句柄数。
-
ulimit -n <可以同时打开的文件数>,将当前进程的最大句柄数修改为指定的参数
- 对所有进程都有效的方法,修改Linux 系统参数
vi /etc/security/limits.conf 添加
请你说一说并发(concurrency)和并行(parallelism)
- 并发(concurrency):指宏观上看起来两个程序在同时运行,比如说在单核cpu 上的多任务。但是从微观上看两个程序的指令是交织着运行的,你的指令之间穿插着我的指令,我的指令之间穿插着你的,在单个周期内只运行了一个指令。这种并发并不能提高计算机的性能,只能提高效率。
- 并行(parallelism):指严格物理意义上的同时运行,比如多核cpu,
两个程序分别运行在两个核上,两者之间互不影响,单个周期内每个程序都运行了自己的指令,也就是运行了两条指令。这样说来并行的确提高了计算机的效率。所以现在的cpu 都是往多核方面发展。
请问MySQL 的端口号是多少,如何修改这个端口号
- 使用命令show global variables like ‘port’;查看端口号,mysql 的默认端口是3306。
- (补充:sqlserver 默认端口号为:1433;oracle 默认端口号为:1521;DB2 默认端口号为:5000;PostgreSQL 默认端口号为:5432)
说一说操作系统中的页表寻址
页式内存管理,内存分成固定长度的一个个页片。
- 操作系统为每一个进程维护了一个从虚拟地址到物理地址的映射关系的数据结构,叫页表,页表的内容就是该进程的虚拟地址到物理地址的一个映射。
- 页表中的每一项都记录了这个页的基地址。
- 通过页表,由逻辑地址的高位部分先找到逻辑地址对应的页基地址,再由页基地址偏移一定长度就得到最后的物理地址,偏移的长度由逻辑地址的低位部分决定。一般情况下,这个过程都可以由硬件完成,所以效率还是比较高的。
- 页式内存管理的优点就是比较灵活,内存管理以较小的页为单位,方便内存换入换出和扩充地址空间。
有了进程,为什么还要有线程?
进程可以使多个程序能并发执行,以提高资源的利用率和系统的吞吐量;但是其具有一些缺点:
- 进程在同一时间只能干一件事
- 进程在执行的过程中如果阻塞,整个进程就会挂起,即使进程中有些工作不依赖于等待的资源,仍然不会执行。
因此,操作系统引入了比进程粒度更小的线程,作为并发执行的基本单位,从而减少程序在并发执行时所付出的时空开销,提高并发性。和进程相比,线程的优势如下:
-
从资源上来讲,线程是一种非常"节俭"的多任务操作方式。在linux 系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种"昂贵"的多任务工作方式。
-
从切换效率上来讲,运行于一个进程中的多个线程,它们之间使用相同的地址空间,而且线程间彼此切换所需时间也远远小于进程间切换所需要的时间。据统计,一个进程的开销大约是一个线程开销的30 倍左右
-
使多CPU 系统更加有效。操作系统会保证当线程数不大于CPU 数目时,不同的线程运行于不同的CPU 上。
单核机器上写多线程程序,是否需要考虑加锁,为什么?
- 在单核机器上写多线程程序,仍然需要线程锁。因为线程锁通常用来实现线程的同步和通信。
- 在单核机器上的多线程程序,仍然存在线程同步的问题。因为在抢占式操作系统中,通常为每个线程分配一个时间片,当某个线程时间片耗尽时,操作系统会将其挂起,然后运行另一个线程。
-
如果这两个线程共享某些数据,不使用线程锁的前提下,可能会导致共享数据修改引起冲突。
你说一说线程间的同步方式,最好说出具体的系统调用
-
信号量
信号量是一种特殊的变量,可用于线程同步。它只取自然数值,并且只支持两种操作:
P(SV):如果信号量SV 大于0,将它减一;如果SV 值为0,则挂起该线程。
V(SV):如果有其他进程因为等待SV 而挂起,则唤醒,然后将SV+1;否则直接将SV+1。
其系统调用为:
sem_wait(sem_t *sem):以原子操作的方式将信号量减1,如果信号量值为0,则sem_wait将被阻塞,直到这个信号量具有非0 值。
sem_post(sem_t *sem):以原子操作将信号量值+1。当信号量大于0 时,其他正在调用
sem_wait 等待信号量的线程将被唤醒。
-
互斥量
互斥量又称互斥锁,主要用于线程互斥,不能保证按序访问,可以和条件锁一起实现同步。当进入临界区时,需要获得互斥锁并且加锁;当离开临界区时,需要对互斥锁解锁,以唤醒其他等待该互斥锁的线程。其主要的系统调用如下:
pthread_mutex_init:初始化互斥锁
pthread_mutex_destroy:销毁互斥锁
pthread_mutex_lock:以原子操作的方式给一个互斥锁加锁,如果目标互斥锁已经被上锁,
pthread_mutex_lock 调用将阻塞,直到该互斥锁的占有者将其解锁。
pthread_mutex_unlock:以一个原子操作的方式给一个互斥锁解锁。
请你说一下多线程和多进程的不同
进程是资源分配的最小单位,而线程时CPU 调度的最小单位。
-
多线程之间共享同一个进程的地址空间,线程间通信简单,同步复杂,线程创建、销毁和切换简单,速度快,占用内存少,适用于多核分布式系统,但是线程间会相互影响,一个线程意外终止会导致同一个进程的其他线程也终止,程序可靠性弱。
- 而**多进程间拥有各自独立的运行地址空间,进程间不会相互影响,程序可靠性强,**但是进程创建、销毁和切换复杂,速度慢,占用内存多,进程间通信复杂,但是同步简单,适用于多核、多机分布。
OS 缺页置换算法
- 当访问一个内存中不存在的页,并且内存已满,则需要从内存中调出一个页或将数据送至磁盘对换区,替换一个页,这种现象叫做缺页置换。
- 当前操作系统最常采用的缺页置换算法如下:
- 先进先出(FIFO)算法:置换最先调入内存的页面,即置换在内存中驻留时间最久的页面。按照进入内存的先后次序排列成队列,从队尾进入,从队首删除。
- 最近最少使用(LRU)算法: 置换最近一段时间以来最长时间未访问过的页面。根据程序局部性原理,刚被访问的页面,可能马上又要被访问;而较长时间内没有被访问的页面,可能最近不会被访问。
- 当前最常采用的就是LRU 算法。
请你说一说死锁发生的条件以及如何解决死锁
-
死锁是指两个或两个以上进程在执行过程中,因争夺资源而造成的下相互等待的现象。死锁发生的四个必要条件如下:
互斥条件:进程对所分配到的资源不允许其他进程访问,若其他进程访问该资源,只能等待,直至占有该资源的进程使用完成后释放该资源;
请求和保持条件:进程获得一定的资源后,又对其他资源发出请求,但是该资源可能被其他进程占有,此时请求阻塞,但该进程不会释放自己已经占有的资源
不可剥夺条件:进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用后自己释放
环路等待条件:进程发生死锁后,必然存在一个进程-资源之间的环形链
-
解决死锁的方法即破坏上述四个条件之一,主要方法如下:
资源一次性分配,从而剥夺请求和保持条件
可剥夺资源:即当进程新的资源未得到满足时,释放已占有的资源,从而破坏不可剥夺的条件
资源有序分配法:系统给每类资源赋予一个序号,每个进程按编号递增的请求资源,释放则相反,从而破坏环路等待的条件
虚拟内存置换的方式
比较常见的内存替换算法有:FIFO,LRU,LFU,LRU-K,2Q。
1、FIFO(先进先出淘汰算法)
思想:最近刚访问的,将来访问的可能性比较大。
实现:使用一个队列,新加入的页面放入队尾,每次淘汰队首的页面,即最先进入的数据,最先被淘汰。
弊端:无法体现页面冷热信息
2、LFU(最不经常访问淘汰算法)
思想:如果数据过去被访问多次,那么将来被访问的频率也更高。
实现:每个数据块一个引用计数,所有数据块按照引用计数排序,具有相同引用计数的数据块则按照时间排序。每次淘汰队尾数据块。
开销:排序开销。
3、LRU(最近最少使用替换算法)
思想:如果数据最近被访问过,那么将来被访问的几率也更高。
实现:使用一个栈,新页面或者命中的页面则将该页面移动到栈底,每次替换栈顶的缓存页面。优点:LRU 算法对热点数据命中率是很高的。
缺陷:
1)缓存颠簸,当缓存(1,2,3)满了,之后数据访问(0,3,2,1,0,3,2,1。。。)。
2)缓存污染,突然大量偶发性的数据访问,会让内存中存放大量冷数据。
多线程,线程同步(通信)的几种方式
1、临界区:
通过多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问;
2、互斥量Synchronized/Lock:
采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问
3、信号量Semphare:
为控制具有有限数量的用户资源而设计的,它允许多个线程在同一时刻去访问同一个资源,但一般需要限制同一时刻访问此资源的最大线程数目。
4、事件(信号),Wait/Notify:
通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作
请你回答一下软链接和硬链接区别
- 为了解决文件共享问题,Linux 引入了软链接和硬链接。
- 除了为Linux 解决文件共享使用,还带来了隐藏文件路径、增加权限安全及节省存储等好处。若1 个inode 号对应多个文件名,则为硬链接,即硬链接就是同一个文件使用了不同的别名, 使用ln 创建。
- 若文件用户数据块中存放的内容是另一个文件的路径名指向,则该文件是软连接。软连接是一个普通文件,有自己独立的inode,但是其数据块内容比较特殊
请问什么是大端小端以及如何判断大端小端
大端是指低字节存储在高地址;
小端存储是指低字节存储在低地址。
我们可以根据联合体来判断该系统是大端还是小端。
因为联合体变量总是从低地址存储。
说一说用户态和内核态区别
- 用户态和内核态是操作系统的两种运行级别,两者最大的区别就是特权级不同。
- 用户态拥有最低的特权级,内核态拥有较高的特权级。
- 运行在用户态的程序不能直接访问操作系统内核数据结构和程序。
内核态和用户态之间的转换方式主要包括:系统调用,异常和中断。
请问如何设计server,使得能够接收多个客户端的请求
多线程,线程池,io 复用
死循环+来连接时新建线程的方法效率有点低,怎么改进?
- 提前创建好一个线程池,用生产者消费者模型,创建一个任务队列,队列作为临界资源,
- 有了新连接,就挂在到任务队列上,队列为空所有线程睡眠。
- 改进死循环:使用select epoll 这样的技术
请问怎样确定当前线程是繁忙还是阻塞?
使用ps 命令查看
两个进程访问临界区资源,会不会出现都获得自旋锁的情况?
单核cpu,并且开了抢占可以造成这种情况。
请你说一说死锁产生的必要条件?
1.互斥条件:一个资源每次只能被一个进程使用。
2.请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
3.不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
4.循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系
请你来说一说协程
- 协程,又称微线程,纤程,英文名Coroutine。协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。
- 那和多线程比,协程最大的优势就是协程极高的执行效率。 因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明
- 第二大优势就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。
说一下微内核与宏内核
- 宏内核:除了最基本的进程、线程管理、内存管理外,将文件系统,驱动,网络协议等等都集成在内核里面,例如linux 内核。
优点:效率高。
缺点:稳定性差,开发过程中的bug 经常会导致整个系统挂掉。
- 微内核:内核中只有最基本的调度、内存管理。驱动、文件系统等都是用户态的守护进程去实现的。
优点:稳定,驱动等的错误只会导致相应进程死掉,不会导致整个系统都崩溃
缺点:效率低。典型代表QNX,QNX 的文件系统是跑在用户态的进程,称为resmgr 的东西,是订阅发布机制,文件系统的错误只会导致这个守护进程挂掉。不过数据吞吐量就比较不乐观了。
说一下僵尸进程
-
1)正常进程
正常情况下,子进程是通过父进程创建的,子进程再创建新的进程。子进程的结束和父进程的运行是一个异步过程,即父进程永远无法预测子进程到底什么时候结束。当一个进程完成它的工作终止之后,它的父进程需要调用wait()或者waitpid()系统调用取得子进程的终止状态。
unix 提供了一种机制可以保证只要父进程想知道子进程结束时的状态信息, 就可以得到:在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。但是仍然为其保一定的信息,直到父进程通过wait / waitpid 来取时才释放。保存信息包括:
1 进程号the process ID
2 退出状态the termination status of the process
3 运行时间the amount of CPU time taken by the process 等
-
2)孤儿进程
一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init 进程(进程号为1)所收养,并由init 进程对它们完成状态收集工作。
-
3)僵尸进程
-
一个进程使用fork 创建子进程,如果子进程退出,而父进程并没有调用wait 或waitpid 获取子进程的状态信息,**那么子进程的进程描述符仍然保存在系统中。**这种进程称之为僵尸进程。
-
僵尸进程是一个进程必然会经过的过程:这是每个子进程在结束时都要经过的阶段。
-
如果进程不调用wait / waitpid 的话, 那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程。
-
解决方法:fork 两次,原理是将子进程成为孤儿进程,从而其的父进程变为init 进程
-
解决方法:kill掉
请你来介绍一下5 种IO 模型
1.阻塞IO:调用者调用了某个函数,等待这个函数返回,期间什么也不做,不停的去检查这个函数有没有返回,必须等这个函数返回才能进行下一步动作
2.非阻塞IO:非阻塞等待,每隔一段时间就去检测IO 事件是否就绪。没有就绪就可以做其他事。
3.信号驱动IO:信号驱动IO:linux 用套接口进行信号驱动IO,安装一个信号处理函数,进程继续运行并不阻塞,当IO 时间就绪,进程收到SIGIO 信号。然后处理IO 事件。
4.IO 复用/多路转接IO:linux 用select/poll 函数实现IO 复用模型,这两个函数也会使进程阻塞,但是和阻塞IO 所不同的是这两个函数可以同时阻塞多个IO 操作。而且可以同时对多个读操作、写操作的IO 函数进行检测。知道有数据可读或可写时,才真正调用IO 操作函数
5.异步IO:linux 中,可以调用aio_read 函数告诉内核描述字缓冲区指针和缓冲区的大小、文件偏移及通知的方式,然后立即返回,当内核将数据拷贝到缓冲区后,再通知应用程序。
异步编程的事件循环
- 事件循环就是不停循环等待时间的发生,然后将这个事件的所有处理器,以及他们订阅这个事件的时间顺序依次依次执行。
- 当这个事件的所有处理器都被执行完毕之后,事件循环就会开始继续等待下一个事件的触发,不断往复。
- 当同时并发地处理多个请求时,以上的概念也是正确的,可以这样理解:在单个的线程中,事件理器是一个一个按顺序执行的。即如果某个事件绑定了两个处理器,那么第二个处理器会在第一个处理器执行完毕后,才开始执行。在这个事件的所有处理器都执行完毕之前,事件循环不会去检查是否有新的事件触发。在单个线程中,一切都是有顺序地一个一个地执行的!
操作系统为什么要分内核态和用户态
为了安全性。
在cpu 的一些指令中,有的指令如果用错,将会导致整个系统崩溃。分了内核态和用户态后,当用户需要操作这些指令时候,内核为其提供了API,可以通过系统调用陷入内核,让内核去执行这些操作。
server 端监听端口,但还没有客户端连接进来,此时进程处于什么状态?
这个需要看服务端的编程模型,如果如上一个问题的回答描述的这样,则处于阻塞状态,如果使用了epoll,select 等这样的io 复用情况下,处于运行状态
请问怎么实现线程池
1.设置一个生产者消费者队列,作为临界资源
2.初始化n 个线程,并让其运行起来,加锁去队列取任务运行
3.当任务队列为空的时候,所有线程阻塞
4.当生产者队列来了一个任务后,先对队列加锁,把任务挂在到队列上,然后使用条件变量去通知阻塞中的一个线程
参考资料:牛客校招面试题库
4、设计模式 & 面向对象
熟练掌握C/C++语言,熟悉面向对象、设计模式,熟悉LINUX开发环境。
请问你用过哪些设计模式,介绍一下单例模式的多线程安全问题
- 单例模式:主要解决一个全局使用的类频繁的创建和销毁的问题。
单例模式下可以确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。
C++的实现有两种,一种通过局部静态变量,利用其只初始化一次的特点,返回对象。
另外一种,则是定义全局的指针,getInstance 判断该指针是否为空,为空时才实例化对象
-
在单例模式的实现中,如果不采取任何措施,在多线程下是不安全的,可能会同时创建多个实例。
-
观察者模式:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依
赖于它的对象都得到通知并被自动更新。
-
工厂模式:工厂模式主要解决接口选择的问题。该模式下定义一个创建对象的接口,让其子
类自己决定实例化哪一个工厂类,使其创建过程延迟到子类进行。
解耦,代码复用,更改功能容易。
请问你了解哪些设计模式?
装饰器模式:
对已经存在的某些类进行装饰,以此来扩展一些功能,从而动态的为一个对象增加新的功能。
装饰器模式是一种用于代替继承的技术,无需通过继承增加子类就能扩展对象的新功能。使用对象的关联关系代替继承关系,更加灵活,同时避免类型体系的快速膨胀。
请问如何保证单例模式只有唯一实例?你知道的都有哪些方法?
- 1)饿汉式:基于class loader 机制避免多线程的同步问题,不过,instance 在类装载时就实例化,可能会产生垃圾对象。
- 2)懒汉式:通过双重锁机制实现线程安全。使
用锁机制,防止多次访问,可以这样,第一次判断为空不加锁,若为空,再进行加锁判断是否为空,若为空则生成对象。
OOP 的设计模式的五项原则
1、单一职责原则
2、接口隔离原则
3、开放-封闭原则
4、替换原则
5、依赖倒置原则
5、系统设计 & 集群与分布式
分布式系统设计与开发、负载均衡技术,系统容灾设计,高可用系统等知识; 对云原生相关技术有所有了解。
负载均衡技术
-
轮询(Round Robin)
请求到达后,将客户端发送到负载均衡器的请求依次轮流地转发给服务集群的某个节点。
-
随机(Random)
随机选取集群中的某个节点来处理该请求,由概率论的知识可知,随着请求量的变大,随机算法会逐渐演变为轮询算法,即集群各个节点会处理差不多数量的请求。
-
加权
加权算法主要是根据集群的节点对应机器的性能的差异,给每个节点设置一个权重值,其中性能好的机器节点设置一个较大的权重值,而性能差的机器节点则设置一个较小的权重值。权重大的节点能够被更多的选中。它是和随机、轮训一起使用的。
-
最小连接数
主要是根据集群的每个节点的当前连接数来决定将请求转发给哪个节点,即每次都将请求转发给当前存在最少并发连接的节点。
-
hash
将对请求的IP地址或者URL计算一个哈希值,然后与集群节点的数量进行取模来决定将请求分发给哪个集群节点。它不是真正意义上的负载均衡,在某些意义上也是一个单点服务。
负载均衡分类
-
- 硬件负载均衡:
-
- 软件负载均衡
目前使用最广泛的三种负载均衡软件Nginx/LVS/HAProxy,他们都是开源免费的负载均衡软件,这些都是通过软件级别来实现,所以费用较低。足以见得,搞软件的是多么不值钱!
- 成熟的架构
负载均衡业界早已有成熟的架构,比较常用的有LVS+Keepalived、Nginx+Keepalived、HAProxy+Keepalived
分布式锁 & 分布式事务
分布式锁
- 在单机场景下,可以使用语言的内置锁来实现进程同步。但是在分布式场景下,需要同步的进程可能位于不同的节点上,那么就需要使用分布式锁。
分布式事务
- 指事务的操作位于不同的节点上,需要保证事务的 ACID 特性。
- 例如在下单场景下,库存和订单如果不在同一个节点上,就涉及分布式事务。
分布式锁和分布式事务区别:
-
锁问题的关键在于进程操作的互斥关系,例如多个进程同时修改账户的余额,如果没有互斥关系则会导致该账户的余额不正确。
- 而事务问题的关键则在于事务涉及的一系列操作需要满足 ACID 特性,例如要满足原子性操作则需要这些操作要么都执行,要么都不执行。
系统容灾设计 & 性能优化
-
集群
将多台服务器组成集群,使用负载均衡将请求转发到集群中,避免单一服务器的负载压力过大导致性能降低。
-
缓存
缓存能够提高性能的原因如下:
缓存数据通常位于内存等介质中,这种介质对于读操作特别快;
缓存数据可以位于靠近用户的地理位置上;
可以将计算结果进行缓存,从而避免重复计算。
-
异步
某些流程可以将操作转换为消息,将消息发送到消息队列之后立即返回,之后这个操作会被异步处理。
高可用系统等知识
-
冗余
保证高可用的主要手段是使用冗余,当某个服务器故障时就请求其它服务器。
应用服务器的冗余比较容易实现,只要保证应用服务器不具有状态,那么某个应用服务器故障时,负载均衡器将该应用服务器原先的用户请求转发到另一个应用服务器上,不会对用户有任何影响。
存储服务器的冗余需要使用主从复制来实现,当主服务器故障时,需要提升从服务器为主服务器, 这个过程称为切换。
-
监控
对 CPU、内存、磁盘、网络等系统负载信息进行监控,当某个信息达到一定阈值时通知运维人员,从而在系统发生故障之前及时发现问题。
-
服务降级
服务降级是系统为了应对大量的请求,主动关闭部分功能,从而保证核心功能可用。
攻击技术3个
-
session cookie 区别 什么时候都要使用
(cookie不是很安全,别人可以分析存放在本地的cookie并进行cookie欺骗,考虑到安全应当使用session。session会在一定时间内保存在服务器上。 当访问增多,会比较占用你服务器的性能,考虑到减轻服务器性能方面,应当使用cookie。)
-
泛洪攻击
一种拒绝服务(DDoS) 攻击,旨在耗尽可用服务器资源,致使服务器无法传输合法流量。通过重复发送初始连接请求(SYN) 数据包,攻击者将可击垮目标服务器计算机上的所有可用端口,导致目标设备在响应合法流量时表现迟钝乃至全无响应。
-
sql注入
SQL注入式攻击,把SQL命令插入到Web表单的输入域或页面请求的查询字符串,欺骗服务器执行恶意的
-
xss 跨站脚本攻击,
利用XSS利用的是用户对网站的信任,指攻击者在网页中嵌入客户端脚本(例如JavaScript), 当用户浏览此网页时,脚本就会在用户的浏览器上执行,从而达到攻击者的目的. 比如获取用户的Cookie,导航到恶意网站,携带木马
-
crsf, 跨站请求伪造,
CSRF利用的是网站对用户网页浏览器的信任,欺骗用户的浏览器去访问一个自己曾经认证过的网站并运行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行。