1.epoll除了边沿触发还有什么?与select区别.
epoll 是Linux平台下的一种特有的多路复用IO实现方式,与传统的 select 相比,epoll 在性能上有很大的提升。
epoll是一种当文件描述符的内核缓冲区非空的时候,发出可读信号进行通知,当写缓冲区不满的时候,发出可写信号通知的机制.
epoll的工作方式:水平触发 LT 和 边沿触发 ET(默认)
(1)水平触发(level-trggered):
只要文件描述符关联的读内核缓冲区非空,有数据可以读取,就一直发出可读信号进行通知,
当文件描述符关联的内核写缓冲区不满,有空间可以写入,就一直发出可写信号进行通知
(2)边缘触发(edge-triggered):
当文件描述符关联的读内核缓冲区由空转化为非空的时候,则发出可读信号进行通知.
当文件描述符关联的内核写缓冲区由满转化为不满的时候,则发出可写信号进行通知.
(边缘触发需要一次性的把缓冲区的数据读完为止,也就是一直读,直到读到EGAIN为止,EGAIN说明缓冲区已经空了,因为这一点,边缘触发需要设置文件句柄为非阻塞)
二者的区别:水平触发是只要读缓冲区有数据,就会一直触发可读信号,而边缘触发仅仅在空变为非空的时候通知一次。
1. 用户态将文件描述符传入内核的方式
select:创建3个文件描述符集并拷贝到内核中,分别监听读、写、异常动作。这里受到单个进程可以打开的fd数量限制,默认是1024。
poll:将传入的struct pollfd结构体数组拷贝到内核中进行监听。
epoll:执行epoll_create会在内核的高速cache区中建立一颗红黑树以及就绪链表(该链表存储已经就绪的文件描述符)。接着用户执行的epoll_ctl函数添加文件描述符会在红黑树上增加相应的结点。
【select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把current往设备等待队列中挂一次,而epoll只要一次拷贝,而且把current往等待队列上挂也只挂一次(在epoll_wait的开始,注意这里的等待队列并不是设备等待队列,只是一个epoll内部定义的等待队列)。这也能节省不少的开销。】
2. 内核态检测文件描述符读写状态的方式
select:采用轮询方式,遍历所有fd,最后返回一个描述符读写操作是否就绪的mask掩码,根据这个掩码给fd_set赋值。
poll:同样采用轮询方式,查询每个fd的状态,如果就绪则在等待队列中加入一项并继续遍历。
epoll:采用回调机制。在执行epoll_ctl的add操作时,不仅将文件描述符放到红黑树上,而且也注册了回调函数,内核在检测到某文件描述符可读/可写时会调用回调函数,该回调函数将文件描述符放在就绪链表中。
【select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll其实也需要调用epoll_wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在epoll_wait中进入睡眠的进程。虽然都要睡眠和交替,但是select和poll在“醒着”的时候要遍历整个fd集合,而epoll在“醒着”的时候只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。这就是回调机制带来的性能提升。】
3. 找到就绪的文件描述符并传递给用户态的方式
select:将之前传入的fd_set拷贝传出到用户态并返回就绪的文件描述符总数。用户态并不知道是哪些文件描述符处于就绪态,需要遍历来判断。
poll:同select方式。
epoll:epoll_wait只用观察就绪链表中有无数据即可,最后将链表的数据返回给数组并返回就绪的数量。内核将就绪的文件描述符放在传入的数组中,所以只用遍历依次处理即可。这里返回的文件描述符是通过mmap让内核和用户空间共享同一块内存实现传递的,减少了不必要的拷贝。
epoll原理:
1.通过调用epoll_create,在epoll文件系统建立了个file节点,并开辟epoll自己的内核高速cache区,建立红黑树,分配好想要的size的内存对象,建立一个list链表,用于存储准备就绪的事件。
2.通过调用epoll_ctl,把要监听的socket放到对应的红黑树上,给内核中断处理程序注册一个回调函数,通知内核,如果这个句柄的数据到了,就把它放到就绪列表。
3.通过调用 epoll_wait,观察就绪列表里面有没有数据,并进行提取和清空就绪列表,非常高效。
一般来说以下场合需要使用 I/O 多路复用:
A.当客户处理多个描述字时(一般是交互式输入和网络套接口);
B.如果一个服务器既要处理 TCP,又要处理 UDP,一般要使用 I/O 复用;
C.如果一个 TCP 服务器既要处理监听套接口,又要处理已连接套接口.
2.linux pwd cat.touch区别
(1)pwd命令 – 显示当前工作目录的路径
pwd是Linux中一个非常有用而又十分简单的命令,pwd是词组print working directory的首字母缩写,即打印工作目录;工作目录就是你当前所处于的那个目录。
pwd始终以绝对路径的方式打印工作目录,即从根目录(/)开始到当前目录的完整路径。在实际工作中,我们常常记不起当前目录的完整路径,此时pwd命令就派上用场了。
(2)touch和cat命令的区别
touch命令和cat命令的共同点就是都能创建文件,那么区别就只能从这里说起。但他们的功能不仅如此。
如果文件不存在:touch命令仅创建文件,cat>命令创建文件并输入;
如果文件存在:touch命令更新文件的日期时间,cat>命令清空文件并输入。
3.内联函数
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数压栈的开销,内联函数提升程序运行的效率。
4.C++11特性
(1)统一的列表初始化 {}适用于各种STL容器
(2)类型推导 auto 和 decltype的出现
A.auto 关键字的作用在编译阶段对于=右边的对象进行自动的类型推导。C++11中已经去除了C++98中auto声明自动类型变量的功能,只可以用来进行变量类型推导。所以就要求我们必须进行显示初始化,让编译器将定义对象的类型设置为初始化值的类型。并且auto仅仅只是占位符,编译阶段编译器根据初始化表达式推演出实际类型之后会替换auto。
B.decltype的出现是为了补齐auto 不支持对于表达式的类型推导的缺陷的, 经常适用于后置返回类型的推导. decltype是C++11新增的一个类型说明符,它可以用来获取一个表达式的类型。
C.考虑到NULL底层被定义成字面量0,这样就可能回带来一些问题,因为0既能指针常量,又能表示整形常量。所以出于清晰和安全的角度考虑,C++11中新增了nullptr,用于表示空指针。
(3)右值引用移动语义 (特别重要的新特性)
顾名思义,对左值的引用就是左值引用, 对于右值的引用就是右值引用。
定义左值和右值的区别:可否进行取地址, 可以取地址的就是左值, 不可以取地址的就是右值
定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址, 所以本质还是左值.
结论:const 左值引用既可以引用左值也可以引用右值,右值引用 就只能引用右值不可以引用左值。
std::move() 方法可以将左值转换为右值。
(4)万能引用 + 完美转发:在过程中保持住右值属性不退化
万能引用: 就是 既可以引用左值 也可以引用右值
模板中的&& 万能引用
(5)可变参数模板 (参数包)
C++11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板,相比 C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进。然而由于可变模版参数比较抽象,使用起来需要一定的技巧,所以这块还是比较晦涩的。
简单的理解可以理解为一个参数包, 可以支持传入数量不固定的参数, 而且还是模板, 使用起来更加的灵活。
(6)emplace_back 的出现和对比分析 push_back接口 emplace_back 是结合这可变模板参数出现的。
(7)Lambda表达式
其实 lambda表达式的底层处理就是完全处理成了仿函数的.
(8)包装器 (适配器) (function包装器)
function 是 C++中的类模板, 也是一个包装器.
说到包装器, 首先就要思考函数指针, 仿函数, Lambda表达式
上章就提到了三者底层可能差不大多, 使用的情景也是各有雷同, 包装器其实就可以算是将上述三者进行一个统一, 适配成一个东西 如下: function 包装器可以实现对三者的统一包装。
包装器的好处:统一了可调用对象的类型, 并且指定了参数和返回值类型。
1. 简化了函数指针这样的复杂指针的使用, 函数指针复杂难以理解
2. 方便了作为参数时候的传入
3. 仿函数是一个类名没有指定参数和返回值需要知道就需要去看这个operator () 重载获取
4. lambda 在语法层, 看不到类型, 底层存在类型, 但是也是lambda_uuid, 也很难看
function出现的最最重要的原因就是有了一个确切的类型, 使用简单方便,
解决函数指针使用复杂的问题, 解决仿函数不能指定参数类型的问题, 要知道参数类型还要跑去看哪个operator() 以及解决 lambda没有具体类型的问题.
(9)线程库
(10)原子操作
多线程最主要的问题是共享数据带来的问题(即线程安全)。如果共享数据都是只读的,那么没问题,因为只读操作不会影响到数据,更不会涉及对数据的修改,所以所有线程都会获得同样的数据。但是,当一个或多个线程要修改共享数据时,就会产生很多潜在的麻烦。
//多线程对于共享数据的写操作带来的问题…
解决上述问题的方式1: 在 C++98 中采取的是加锁的方式实现避免函数的重入问题,
lock();
操作临界资源 (写入操作)
unlock();
加锁确实是可以解决上述的问题, 但是不停的解锁解锁, 效率会变得特别低,时间消耗也会大大增加,不停的加锁解锁,虽然也解决了问题,保护了临界资源,但是程序运行时延性大大增加,而且对于锁控制不好还会死锁,于是C++11 搞出来一个原子操作。
所谓原子操作:即不可被中断的一个或一系列操作,C++11引入 的原子操作类型,使得线程间数据的同步变得非常高效。
有了原子操作数据, 确实针对这些数据的操作不再需要加锁保护了, 但是如果是一段代码段的原子操作, 就还是不得不使用锁来实现, 但是只要设计到锁就可能发生死锁, C++11为了预防死锁,C++11采用RAII的方式对锁进行了封装,即lock_guard和unique_lock。
5.map底层
std map是STL的一个关联容器,map中的元素是关键字----值的对(key–value):关键字起到索引的作用,值则表示与索引相关联的数据。每个关键字只能在 map中出现一次。STL的map底层是用红黑树实现的,查找时间复杂度是log(n)。
6.指针与引用的区别
指针:指针就是内存地址,指针变量是用来存放内存地址的变量.不同类型的指针变量所占用的存储单元长度是相同的,而存放数据的变量因数据的类型不同,所占用的存储空间长度也不同。
引用:引用不是新定义一个变量,而是给已存在变量取一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间.
类型& 引用变量名= 引用实体; 且引用类型必须和引用实体是同种类型的.
引用的主要用途是:修饰函数的形参和返回值.
指针与引用的区别:
(1)初始化:引用在定义时必须初始化,指针则没有要求(尽量初始化,防止野指针)。
(2)引用在初始化引用一个实体后,就不能再引用其它实体,而指针可以在任意时候指向一个同类型实体。
(3)没有NULL引用,但是有nullptr指针.
(4)在sizeof中含义不同: 引用结果为引用类型的大小,但指针始终是地址空间,所占字节个数(32位平台占4个字节)。
(5)引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小。
(6)有多级指针,但没有多级引用。
(7)访问实体的方式不同,指针需要显式解引用,引用编译器自己处理。
(8)引用比指针使用起来相对安全。
7.锁相关的。
(1)读写锁
A.多个读者可以同时进行读;
B.写者必须互斥(只允许一个写者写,也不能读者写者同时进行);
C.写者优先于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者).
(2)互斥锁
一次只能一个线程拥有互斥锁,其他线程只有等待。
可以避免多个线程在某一时刻同时操作一个共享资源,标准C++库提供了std::unique_lock类模板,实现了互斥锁的RAII惯用语法:
std::unique_lockstd::mutex lk(mtx_sync_);
(3)条件锁
条件锁就是所谓的条件变量,某一个线程因为某个条件未满足时可以使用条件变量使该程序处于阻塞状态。一旦条件满足了,即可唤醒该线程(常和互斥锁配合使用),唤醒后,需要检查变量,避免虚假唤醒。
互斥锁一个明显的缺点是他只有两种状态:锁定和非锁定。而条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足,他常和互斥锁一起使用,以免出现竞态条件。当条件不满足时,线程往往解开相应的互斥锁并阻塞线程然后等待条件发生变化。一旦其他的某个线程改变了条件变量,他将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程。总的来说互斥锁是线程间互斥的机制,条件变量则是同步机制。
(4)自旋锁(不推荐使用)
如果进线程无法取得锁,进线程不会立刻放弃CPU时间片,而是一直循环尝试获取锁,直到获取为止。如果别的线程长时期占有锁,那么自旋就是在浪费CPU做无用功,但是自旋锁一般应用于加锁时间很短的场景,这个时候效率比较高。
自旋锁是一种基础的同步原语,用于保障对共享数据的互斥访问。与互斥锁的相比,在获取锁失败的时候不会使得线程阻塞而是一直自旋尝试获取锁。当线程等待自旋锁的时候,CPU不能做其他事情,而是一直处于轮询忙等的状态。自旋锁主要适用于被持有时间短,线程不希望在重新调度上花过多时间的情况。实际上许多其他类型的锁在底层使用了自旋锁实现,例如多数互斥锁在试图获取锁的时候会先自旋一小段时间,然后才会休眠。如果在持锁时间很长的场景下使用自旋锁,则会导致CPU在这个线程的时间片用尽之前一直消耗在无意义的忙等上,造成计算资源的浪费。
8.线程和进程的区别
进程是一个具有一定独立功能的程序在一个数据集合上依次动态执行的过程。进程是一个正在执行的程序的实例,包括程序计数器、寄存器和程序变量的当前值。
线程是操作系统能够进行运算调度的最小单元。它被包含在进程中,是进程中实际运行的单位。一个进程中可以并发多个线程,每个线程执行不同的任务。
二者区别:
(1)本质区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位。
(2)包含关系:一个进程至少有一个线程,线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。
(3)资源开销:每个进程都有独立的地址空间,进程之间的切换会有较大的开销;线程可以看做轻量级的进程,同一个进程内的线程共享进程的地址空间,每个线程都有自己独立的运行栈和程序计数器,线程之间切换的开销小。
(4)影响关系:一个进程崩溃后,在保护模式下其他进程不会被影响,但是一个线程崩溃可能导致整个进程被操作系统杀掉,所以多进程要比多线程健壮。