概念:
进程:是并发执行的程序在执行过程中分配和管理资源的基本单位。
线程:处理器任务的基本单位。线程也被称为轻量级进程。
协程:是一种比线程更加轻量级的存在。C++ 20的协程是一个特殊函数。只是这个函数具有挂起和恢复的能力,可以被挂起(挂起后调用代码继续向后执行),而后可以继续恢复其执行。
区别:
地址空间:进程之间是独立的地址空间,线程共享本进程的地址空间。
健壮性:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。
执行过程或者切换时:进程执行开销大。线程执行开销小。
最小单位: 进程是管理资源的基本单位。线程是处理器调度的基本单位
进程与线程的选择取决以下几点:
多进程、多线程同步(通讯)的方法
进程间通讯:
线程通讯:
父子不同处: 1.进程ID 2.fork返回值 3.父进程ID 4.进程运行时间 5.闹钟(定时器) 6.未决信号集
父子进程间遵循读时共享写时复制的原则。
处理器总处于以下三种状态之一:
1、内核态,运行于进程上下文,内核代表进程运行于内核空间;
2、内核态,运行于中断上下文,内核代表硬件运行于内核空间;
3、用户态,运行于用户空间。
什么是中断上下文?
当执行一个中断处理函数时,内核处于中断上下文。
所谓的“ 中断上下文”,其实也可以看作就是硬件传递过来的这些参数和内核需要保存的一些其他环境(主要是当前被打断执行的进程环境)。
中断上下文:
(1)中断上文:硬件通过中断触发信号,导致内核调用中断处理程序,进入内核空间。这个过程中,硬件的一些变量和参数也要传递给内核,内核通过这些参数进行中断处理。中断上文可以看作就是硬件传递过来的这些参数和内核需要保存的一些其他环境(主要是当前被中断的进程环境。
(2)中断下文:执行在内核空间的中断服务程序。
什么是进程上下文?
用户空间的应用程序,通过系统调用,进入内核空间。这个时候用户空间的进程要传递 很多变量、参数的值给内核,内核态运行的时候也要保存用户进程的一些寄存 器值、变量等。所谓的“进程上下文”,可以看作是用户进程传递给内核的这些参数以及内核要保存的那一整套的变量和寄存器值和当时的环境等。
进程上下文:
(1)进程上文:其是指进程由用户态切换到内核态是需要保存用户态时cpu寄存器中的值,进程状态以及堆栈上的内容,即保存当前进程的进程上下文,以便再次执行该进程时,能够恢复切换时的状态,继续执行。
(2)进程下文:其是指切换到内核态后执行的程序,即进程运行在内核空间的部分。
理论上,一个进程可用虚拟空间是2G,默认情况下,线程的栈的大小是1MB,所以理论上最多只能创建2048个线程。如果要创建多于2048的话,必须修改编译器的设置。
一个进程可以创建的线程数由可用虚拟空间和线程的栈的大小共同决定,只要虚拟空间足够,那么新线程的建立就会成功。如果需要创建超过2K以上的线程,减小你线程栈的大小就可以实现了。
线程同步:每个线程之间按预定的先后次序进行运行,协同、协助、互相配合。可以理解成“你说完,我再做”。有了线程同步,每个线程才不是自己做自己的事情,而是协同完成某件大事。
线程互斥:当有若干个线程访问同一块资源时,规定同一时间只有一个线程可以得到访问权,其它线程需要等占用资源者释放该资源才可以申请访问。线程互斥可以看成是一种特殊的线程同步。
1、常量区
2、全局变量区
3、静态变量区
4、代码区
两个动态区域,这就是
堆和栈。
同步是个过程,阻塞是线程的一种状态。多个线程操作共享变量时可能会出现竞争。这时需要同步来防止两个以上的线程同时进入临界区,在这个过程中,后进入临界区的线程将阻塞,等待先进入的线程走出临界区。
线程同步不一定发生阻塞,线程同步的时候,需要协调推进速度,互相等待和互相唤醒会发生阻塞。
并发(concurrency):在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行。其中两种并发关系分别是同步和互斥。
1.互斥:是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
2.同步:是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源。
异步(asynchronous):异步和同步是相对的,同步就是顺序执行,执行完一个再执行下一个,需要等待、协调运行。异步就是彼此独立,在等待某事件的过程中继续做自己的事,不需要等待这一事件完成后再工作。线程就是实现异步的一个方式。异步是让调用方法的主线程不需要同步等待另一线程的完成,从而可以让主线程干其它的事情。
阻塞和非阻塞是针对于进程在访问数据的时候,根据IO操作的就绪状态来采取的不同方式,说白了是一种读取或者写入操作函数的实现方式。阻塞方式下读取或者写入函数将一直等待,而非阻塞方式下,读取或者写入函数会立即返回一个状态值。
一般来说IO模型可以分为:同步阻塞,同步非阻塞,异步阻塞,异步非阻塞。
同步阻塞IO :用户进程在发起一个IO操作以后,必须等待IO操作的完成,只有当真正完成了IO操作以后,用户进程才能运行。
同步非阻塞IO :用户进程发起一个IO操作以后可返回做其它事情, 但是用户进程需要时不时的询问 IO操作是否就绪,这就要求用户进程不停的去询问,从而引入不必要的CPU资源浪费。应用发起一个IO操作以后,使用阻塞select系统调用 来等待 I/O可用的通知。 select 调用非常有趣的是它可以用来为多个IO描述符提供通知,而不仅仅为一个描述符提供通知。
异步阻塞IO :应用发起一个IO操作以后,不等待内核IO操作的完成, 等内核完成IO操作以后会通知应用程序,这其实就是同步和异步最关键的区别同步必须等待或者主动的去询问IO是否完成。
异步非阻塞IO :用户进程只需要发起一个IO操作然后立即返回, 等IO操作真正的完成以后,应用程序会得到IO操作完成的通知, 此时用户进程只需要对数据进行处理就好了,不需要进行实际的IO读写操作因为真正的IO读取或者写入操作已经由内核完成了。
同步与异步是对应的,它们是线程之间的关系,两个线程之间要么是同步的,要么是异步的。
阻塞与非阻塞是对同一个线程来说的,在某个时刻,线程要么处于阻塞,要么处于非阻塞。
阻塞是使用同步机制的结果,非阻塞则是使用异步机制的结果。
一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程所收养,并由init进程对它们完成状态收集工作。
一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵尸进程。
Linux Daemon(守护进程)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。
1.子进程退出时向父进程发送SIGCHILD信号,父进程处理SIGCHILD信号。在信号处理函数中调用wait进行处理僵尸进程。
2.把父进程杀掉。父进程死后,僵尸进程成为"孤儿进程",过继给进程init,init始终会负责清理僵尸进程。它产生的所有僵尸进程也跟着消失。
fork两次,父进程fork一个子进程,然后继续工作,子进程fork一 个孙进程后退出,那么孙进程被init接管,孙进程结束后,init会回收。不过子进程的回收 还要自己做。
两次fork的原理是将子进程成为孤儿进程,从而其的父进程变为init进程,通过init进程可以处理僵尸进程。
1、fork()创建子进程,父进程exit()退出;
2、在子进程调用setsid()创建新会话;
3、再次 fork() 一个子进程,父进程exit退出;
4、在子进程中调用chdir()让根目录“/”成为子进程的工作目录;
5、在子进程中调用umask()重设文件权限掩码为0;
6、在子进程中close()不需要的文件描述符;
7、守护进程退出处理
malloc和free是库函数,而new和delete是C++操作符;
new自己计算需要的空间大小,比如’int * a = new,malloc需要指定大小,例如’int * a = malloc(sizeof(int))’;
new在动态分配内存的时候可以初始化对象,调用其构造函数,delete在释放内存时调用对象的析构函数。而malloc只分配一段给定大小的内存,并返回该内存首地址指针,如果失败,返回NULL。
new是C++操作符,是关键字,而operate new是C++库函数
opeartor new /operator delete可以重载,而malloc不行
new可以调用malloc来实现,但是malloc不能调用new来实现
对于数据C++定义new[]专门进行动态数组分配,用delete[]进行销毁。new[]会一次分配内存,然后多次调用构造函数;delete[]会先多次调用析构函数,然后一次性释放。
分配数组不同之处
int char* pa = new char[100];
int char* pb = malloc(sizeof(char) * 100);
malloc能够直观地重新分配内存
使用malloc分配的内存后,如果在使用过程中发现内存不足,可以使用realloc函数进行内存重新分配实现内存的扩充。realloc先判断当前的指针所指内存是否有足够的连续空间,如果有,原地扩大可分配的内存地址,并且返回原来的地址指针;如果空间不够,先按照新指定的大小分配空间,将原有数据从头到尾拷贝到新分配的内存区域,而后释放原来的内存区域。
new没有这样直观的配套设施来扩充内存。
malloc()工作机制
malloc函数的实质体现在,它有一个将可用的内存块连接为一个长长的列表的所谓空闲链表。调用malloc函数时,它沿连接表寻找一个大到足以满足用户请求所需要的内存块。然后,将该内存块一分为二(一块的大小与用户请求的大小相等,另一块的大小就是剩下的字节)。接下来,将分配给用户的那块内存传给用户,并将剩下的那块(如果有的话)返回到连接表上。调用free函数时,它将用户释放的内存块连接到空闲链上。到最后,空闲链会被切成很多的小内存片段,如果这时用户申请一个大的内存片段,那么空闲链上可能没有可以满足用户要求的片段了。于是,malloc函数请求延时,并开始在空闲链上翻箱倒柜地检查各内存片段,对它们进行整理,将相邻的小空闲块合并成较大的内存块。
是有可能申请1.2G的内存的。
回答这个问题前需要知道malloc的作用和原理,应用程序通过malloc函数可以向程序的虚拟空间申请一块虚拟地址空间,与物理内存没有直接关系,得到的是在虚拟地址空间中的地址,之后程序运行所提供的物理内存是由操作系统完成的。
内存分配方式:
程序的内存空间:
extern “C”的作用是告诉C++编译器用C规则编译指定的代码(除函数重载外,extern “C”不影响C++其他特性)。
__stdcall和__cdecl都是函数调用约定关键字。
__stdcall:参数由右向左压入堆栈;堆栈由函数本身清理。
__cdecl:参数也是由右向左压入堆栈;但堆栈由调用者清理
1.数组就是一片地址连续且空间大小一致的存储空间 (但是每个空间存的还是其他数据的地址)
2.数组存在于堆内存中,但凡在堆中存储的数据都称之为 对象(但凡在堆内存中创建的对象都会有默认初始值)
1.1、char转int:
char a = '1';
int b = a - '0'; // b = 1;
1.2、int转char:
int a = 1;
char b = a + '0'; // b = '1';
首先volatile修饰的变量,作用在编译阶段,影响编译出的结果,其修饰的变量是随时可能被修改的,volatile告诉编译器,这个变量是重要人物,不要偷懒的去走捷径,每次认认真真的去从内存拿值。
一般说来,volatile用在如下的几个地方:
http://t.csdn.cn/wBwuS
若对指针进行dynamic_cast,失败返回nullptr,成功返回正常cast后的对象指针
结构体大小等于最后一个成员的偏移量加上最后一个成员的大小。
偏移量指的是结构体变量中成员的地址和结构体变量地址的差。
1、结构体变量中成员的偏移量必须是成员大小的整数倍(0被认为是任何数的整数倍)
2、结构体大小必须是所有成员大小的整数倍。
虚继承的目的是让某个类做出声明,承诺愿意共享它的基类。其中,这个被共享的基类就称为虚基类(Virtual Base Class),本例中的 A 就是一个虚基类。在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含一份虚基类的成员。
https://blog.csdn.net/yuupengsun/article/details/104136210
和类中的数据有关,和函数无关,若是有虚函数,内存加上虚指针的大小
1.继承,2.虚函数重写,3.父类指针或引用指向子类对象
拷贝构造函数一般用于以下三种情况:
1.当用类的一个对象去初始化该类的另外一个对象时。
2.如果函数的形参是类的对象,调用函数时,是值传递。(引用传递并不调用拷贝构造函数)
3.如果函数的返回值是类的对象,函数执行完成时会返回调用者时。
如果是浅拷贝,那么只会拷贝指针b=a,它们都指向D内存区域。如果是深拷贝,不仅拷贝指针b=a,还会申请一块新的内存空间E,原来的a指向D,新的b指向E。
当类存在指针类成员变量时,默认拷贝构造函数是浅拷贝,会导致二次析构问题,所以要自定义拷贝构造函数。
1)如果析构函数抛出异常,则异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会执行,会造成诸如资源泄漏的问题。
2)通常异常发生时,c++的机制会调用已经构造对象的析构函数来释放资源,此时若析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常,会造成程序崩溃的问题。
1.当用类的一个对象去初始化该类的另外一个对象时。
2.如果函数的形参是类的对象,调用函数时,是值传递。(引用传递并不调用拷贝构造函数)
3.如果函数的返回值是类的对象,函数执行完成时会返回调用者时。
在派生类中申请的资源就不会得到释放,就会造成内存泄漏
存储空间角度:虚函数对应一个vtable,vtable存储于对象的内存空间
若构造函数是虚的,则需要通过 vtable来调用,若对象还未实例化,即内存空间还没有,无法找到vtable
使用角度:虚函数主要用于在信息不全的情况下,能使重载的函数得到对应的调用。
构造函数本身就是要初始化实例,那使用虚函数就没有实际意义
从实际含义上看,在调用构造函数时还不能确定对象的真实类型(因为子类会调父类的构造函数);而且构造函数的作用是提供初始化,在对象生命期只执行一次,不是对象的动态行为,也没有太大的必要成为虚函数
纯虚函数只有函数的名字而不具备函数的功能,不能被调用。在虚函数后面添加 =0 ,虚函数就成为纯虚函数
静态类型:对象在声明时采用的类型,在编译期既已确定;
动态类型:通常是指一个指针或引用目前所指对象的类型,是在运行期决定的;
静态绑定:绑定的是静态类型,所对应的函数或属性依赖于对象的静态类型,发生在编译期;
动态绑定:绑定的是动态类型,所对应的函数或属性依赖于对象的动态类型,发生在运行期;
C++中的构造函数可以分为4类:
(1)默认构造函数。以Student类为例,默认构造函数的原型为
Student();//没有参数
(2)初始化构造函数
Student(int num,int age);//有参数
(3)复制(拷贝)构造函数
Student(Student&);//形参是本类对象的引用
(4)转换构造函数
Student(int r) ;//形参时其他类型变量,且只有一个形参
Overload(重载):重载是指不同的函数使用相同的函数名,但是函数的参数个数或类型(参数列表不同)。调用的时候根据函数的参数来区别不同的函数。有以下特征:
(1)相同的范围(在同一个类中);
(2)函数名字相同;
(3)参数不同;
(4)virtual关键字可有可无;
Override(覆盖):是指派生类函数覆盖基类函数,有以下特征:
(1)不同的范围(分别位于派生类与基类);
(2)函数名字相同;
(3)参数相同;
(4)基类函数必须有virtual关键字
Overwrite(重写):是值在派生类中重新对基类中的虚函数重新实现。即函数名和参数都一样,只是函数的实现体不一样
覆盖就是重写
如果有些成员是类,那么在进入构造函数之前,会先调用一次默认构造函数,进入构造函数后所做的事其实是一次赋值操作(对象已存在),所以如果是在构造函数体内进行赋值的话,等于是一次默认构造加一次赋值,而初始化列表只做一次赋值操作。
将构造函数声明为explicit,来防止隐式类型转换。
explicit关键字只能用于类内部的构造函数声明上,而不能用在类外部的函数定义上。
UDP 常用于以下几个方面:
1.包总量较少的通信(DNS、SNMP等);
2.视频、音频等多媒体通信(即时通信);
3.限定于 LAN 等特定网络中的应用通信;
4.广播通信(广播、多播)
网络模型一般是指OSI七层参考模型和TCP/IP四层参考模型。
OSI分层(7层):物理层、数据链路层、网络层、传输层、会话层、表示层、应用层。
TCP/IP分层(4层):网络接口层、网际层、运输层、应用层。
网络层:IP协议、ICMP协议、ARP协议、RARP协议。
传输层:UDP协议、TCP协议。
应用层:FTP(文件传送协议)、Telnet(远程登录协议)、DNS(域名解析协议)、SMTP(邮件传送协议),POP3协议(邮局协议),HTTP协议。
三次握手的目的是建立可靠的通信通道,说到通信,简单来说就是数据的发生与接收,而三次握手最主要的目的就是双方确认自己与对方的发送与接收是否正常
第一次握手:Client什么都不能确认,Server确认了对方发送正常,自己接收正常
第二次握手:Client确认了:自己发送、接收正常,对方发送正常、接收正常;Server确认了:对方发送正常,自己接收正常
第三次握手:Client确认了:自己发送、接收正常,对方发送正常接收正常;Server 确认了:对方发送正常,接收正常,自己发送正常,接收正常。
下面解释明明两次就可以建立连接的为什么还要加第三次的确认。
如果发送两次就可以建立连接话,那么只要客户端发送一个连接请求,服务端接收到并发送了确认,就会建立一个连接。
可能出现的问题:如果一个连接请求在网络中跑的慢,超时了,这时客户端会从发请求,但是这个跑的慢的请求最后还是跑到了,然后服务端就接收了两个连接请求,然后全部回应就会创建两个连接,浪费资源!
如果加了第三次客户端确认,客户端在接受到一个服务端连接确认请求后,后面再接收到的连接确认请求就可以抛弃不管了。
服务端的资源分配是在二次握手时分配的,而客户端的资源是在三次握手时分配的。
SYN攻击,即客户端在短时间内伪造大量不存在的IP地址,并向SERVER端不断的发送SYN包,SERVER收到请求即回复确认,并等待客户端的确认,由于源地址不存在,因此SERVER需要不断的重发直到超时。
这些伪造的SYN包长时间占用未连接队列,导致正常的SYN请求因为队列满而丢弃,因为网络拥塞。
一般服务端会重试5次。
分类: 嵌入式100题
第一次挥手: Client端发起挥手请求,向Server端发送标志位是FIN报文段,设置序列号seq,此时,Client端进入FIN_WAIT_1状态,这表示Client端没有数据要发送给Server端了。
第二次分手:Server端收到了Client端发送的FIN报文段,向Client端返回一个标志位是ACK的报文段,ack设为seq加1,Client端进入FIN_WAIT_2状态,Server端告诉Client端,我确认并同意你的关闭请求。
第三次分手: Server端向Client端发送标志位是FIN的报文段,请求关闭连接,同时Client端进入LAST_ACK状态。
第四次分手 : Client端收到Server端发送的FIN报文段,向Server端发送标志位是ACK的报文段,然后Client端进入TIME_WAIT状态。Server端收到Client端的ACK报文段以后,就关闭连接。此时,Client端等待2MSL的时间后依然没有收到回复,则证明Server端已正常关闭,那好,Client端也可以关闭连接了
TIME_WAIT状态的时长设置为2MSL的主要原因:
确保被动关闭TCP连接的一端能收到第四次挥手的ACK
避免上一次TCP连接的数据包影响到下一次的TCP连接。
建立连接时因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。所以建立连接只需要三次握手。
由于TCP协议是一种面向连接的、可靠的、基于字节流的运输层通信协议,TCP是全双工模式。这就意味着,关闭连接时,当Client端发出FIN报文段时,只是表示Client端告诉Server端数据已经发送完毕了。当Server端收到FIN报文并返回ACK报文段,表示它已经知道Client端没有数据发送了,但是Server端还是可以发送数据到Client端的,所以Server很可能并不会立即关闭SOCKET,直到Server端把数据也发送完毕。当Server端也发送了FIN报文段时,这个时候就表示Server端也没有数据要发送了,就会告诉Client端,我也没有数据要发送了,之后彼此就 会愉快的中断这次TCP连接。
https://www.runoob.com/w3cnote/bubble-sort.html
void Merge(vector<int> &Array, int front, int mid, int end) {
// preconditions:
// Array[front...mid] is sorted
// Array[mid+1 ... end] is sorted
// Copy Array[front ... mid] to LeftSubArray
// Copy Array[mid+1 ... end] to RightSubArray
vector<int> LeftSubArray(Array.begin() + front, Array.begin() + mid + 1);
vector<int> RightSubArray(Array.begin() + mid + 1, Array.begin() + end + 1);
int idxLeft = 0, idxRight = 0;
LeftSubArray.insert(LeftSubArray.end(), numeric_limits<int>::max());
RightSubArray.insert(RightSubArray.end(), numeric_limits<int>::max());
// Pick min of LeftSubArray[idxLeft] and RightSubArray[idxRight], and put into Array[i]
for (int i = front; i <= end; i++) {
if (LeftSubArray[idxLeft] < RightSubArray[idxRight]) {
Array[i] = LeftSubArray[idxLeft];
idxLeft++;
} else {
Array[i] = RightSubArray[idxRight];
idxRight++;
}
}
}
void MergeSort(vector<int> &Array, int front, int end) {
if (front >= end)
return;
int mid = (front + end) / 2;
MergeSort(Array, front, mid);
MergeSort(Array, mid + 1, end);
Merge(Array, front, mid, end);
}
template <typename T>
void quick_sort_recursive(T arr[], int start, int end) {
if (start >= end)
return;
T mid = arr[end];
int left = start, right = end - 1;
while (left < right) { //在整个范围内搜寻比枢纽元值小或大的元素,然后将左侧元素与右侧元素交换
while (arr[left] < mid && left < right) //试图在左侧找到一个比枢纽元更大的元素
left++;
while (arr[right] >= mid && left < right) //试图在右侧找到一个比枢纽元更小的元素
right--;
std::swap(arr[left], arr[right]); //交换元素
}
if (arr[left] >= arr[end])
std::swap(arr[left], arr[end]);
else
left++;
quick_sort_recursive(arr, start, left - 1);
quick_sort_recursive(arr, left + 1, end);
}
template <typename T> //整數或浮點數皆可使用,若要使用物件(class)時必須設定"小於"(<)、"大於"(>)、"不小於"(>=)的運算子功能
void quick_sort(T arr[], int len) {
quick_sort_recursive(arr, 0, len - 1);
}
vector是按照2倍方式扩容的,扩容需要经过以下步骤:
两端都能快速安插和删除元素,这些操作可以在分期摊还的常数时间(amortized constant time)内完成。
元素的存取和迭代器的动作比vector稍慢。
迭代器需要在不同区块间跳转,所以它非一般指针。
因为deque使用不止一块内存(而vector必须使用一块连续内存),所以deque的max_size()可能更大。
不支持对容量和内存重新分配时机的控制。不过deque的内存重分配优于vector,因为其内部结构显示,deque不必在内存重分配时复制所有元素。
除了头尾两端,在任何地方安插或删除元素,都将导致指向deque元素的所有pointers、references、iterators失效。
deque的内存区块不再被使用时,会自动被释放。deque的内存大小是可自动缩减的。
deque与vector组织内存的方式不一样。在底层,deque按“页”(page)或“块”(chunk)来分配存储器,每页包含固定数目的元素。而vector只分配一块连续的内存。例如,一个10M字节的vector使用的是一整块10M字节的内存,而deque可以使用一串更小的内存块,比如10块1M的内存。所以不能将deque的地址(如&deque[0])传递给传统的C API,因为deque内部所使用的内存不一定会连续。
deque的下述特性与vector差不多:
在中部安插、删除元素的速度较慢。
迭代器属于random access iterator(随机存取迭代器)。
虽然STL中提供了标准的sort函数,但是它只适用于随机存取的容器,而显然list并不是这样的容器,因此list提供了专用的链表排序函数,虽然同样名为sort,但是这个sort函数是list类的成员函数。
C++ STL 的实现:
1.vector 底层数据结构为数组 ,支持快速随机访问
2.list 底层数据结构为双向链表,支持快速增删
3.deque 底层数据结构为一个中央控制器和多个缓冲区,详细见STL源码剖析P146,支持首尾(中间不能)快速增删,也支持随机访问
deque是一个双端队列(double-ended queue),也是在堆中保存内容的.它的保存形式如下:
[堆1] --> [堆2] -->[堆3] --> …
每个堆保存好几个元素,然后堆和堆之间有指针指向,看起来像是list和vector的结合品.
4.stack 底层一般用list或deque实现,封闭头部即可,不用vector的原因应该是容量大小有限制,扩容耗时
5.queue 底层一般用list或deque实现,封闭头部即可,不用vector的原因应该是容量大小有限制,扩容耗时
(stack和queue其实是适配器,而不叫容器,因为是对容器的再封装)
6.priority_queue 的底层数据结构一般为vector为底层容器,堆heap为处理规则来管理底层容器实现
7.set 底层数据结构为红黑树,有序,不重复
8.multiset 底层数据结构为红黑树,有序,可重复
9.map 底层数据结构为红黑树,有序,不重复
10.multimap 底层数据结构为红黑树,有序,可重复
11.hash_set 底层数据结构为hash表,无序,不重复
12.hash_multiset 底层数据结构为hash表,无序,可重复
13.hash_map 底层数据结构为hash表,无序,不重复
14.hash_multimap 底层数据结构为hash表,无序,可重复
(1)对于关联容器(如map,set,multimap,multiset),删除当前的iterator,仅仅会使当前的iterator失效,只要在erase时,递增当前的iterator即可。这是因为map之类的容器,使用了红黑树来实现,插入,删除一个结点不会对其他结点造成影响。
(2)对于序列式容器(如vector,deque,list等),删除当前的iterator会使后面所有元素的iterator都失效。这是因为vector,deque使用了连续分配的内存,删除一个元素导致后面所有的元素会向前移动一个位置。不过erase方法可以返回下一个有效的iterator。
C++ STL中的map用红黑树实现,搜索效率是O(lgN)
Linux内核主要由五个子系统组成:
进程调度,内存管理,虚拟文件系统,网络接口,进程间通信。
系统调用,提供特定的用户空间与内核空间的信息传递。
信号,内核空间出现一些异常时候会发送信号给进程,如SIGSEGV、SIGILL、SIGPIPE等。
/proc,proc可以读取内核空间的信息并且设置部分属性的值,需要循环检测。
文件,可以通过指定文件的读写操作来实现通信,但是流程不够实时,需要循环检测来实现。
netlink,类似socket通信方式,可以读写大量的数据,实现稍微复杂。
ioctl,可以实现数据量比较少时候的通信。
用户空间read()–>内核空间sys_read()–>scull_fops.read–>scull_read();
该过程分为两个部分:用户空间的处理和核心空间的处理。在用户空间中通过 0x80 中断的方式将控制权交给内核处理,内核接管后,经过6个层次的处理最后将请求交给磁盘,由磁盘完成最终的数据拷贝操作。在这个过程中,调用了一系列的内核函数。
(1) 系统调用可以为用户空间提供访问硬件资源的统一接口,以至于应用程序不必去关 注具体的硬件访问操作。
比如,读写文件时,应用程序不用去管磁盘类型,甚至于不用关心是哪种文件系统。
(2) 系统调用可以对系统进行保护,保证系统的稳定和安全。系统调用的存在规定了用 户进程进入内核的具体方式,
换句话说,用户访问内核的路径是事先规定好的,只能从规定位置进入内核,而不准许肆意跳入内核。有了这样的进
入内核的统一访问路径限制才能保证 内核的安全。
用户空间就是用户进程所在的内存区域,相对的,系统空间就是操作系统占据的内存区域。用户进程和系统进程的所有数据都在内存中。
开机加电,系统启动后,就对物理内存进行了划分。
当一个任务(进程)执行系统调用而陷入内核代码中执行时,我们就称进程处于内核运行态(或简称为内核态)。 此时处理器处于特权级最高的(0级)内核代码中执行。 当进程处于内核态时,执行的内核代码会使用当前进程的内核栈。 每个进程都有自己的内核栈。 当进程在执行用户自己的代码时,则称其处于用户运行态(用户态)。
linux的内核的确是由bootloader装载到内存中的。 linux的bootloader有2个部分组成:bootstrap和uboot。
原子操作
信号量(semaphore)
读写信号量(rw_semaphore)
Spinlock
Mutex
BKL(Big Kernel Lock,只包含在2.4内核中,不讲)
Rwlock
brlock(只包含在2.4内核中,不讲)
RCU(只包含在2.6内核及以后的版本中)
seqlock(只包含在2.6内核及以后的版本中)
大端序(Big-Endian)将数据的低位字节存放在内存的高位地址,高位字节存放在低位地址。这种排列方式与数据用字节表示时的书写顺序一致,符合人类的阅读习惯。
小端序(Little-Endian),将一个多位数的低位放在较小的地址处,高位放在较大的地址处,则称小端序。小端序与人类的阅读习惯相反,但更符合计算机读取内存的方式,因为CPU读取内存中的数据时,是从低地址向高地址方向进行读取的。
小端模式 :强制转换数据不需要调整字节内容,1、2、4字节的存储方式一样。
大端模式 :符号位的判定固定为第一个字节,容易判断正负。
预处理:.i文件。在预编译的过程中,主要处理源代码中的预处理指令,引入头文件,去除注释,处理所有的条件编译指令,宏的替换,添加行号,保留所有的编译器指令。当进行预编译以后的文件中将不再存在宏,所有的宏都已经被替代。
编译:.s文件。在预处理结束后,进行的是编译。编译过程所进行的是对预处理后的文件进行语法分析,词法分析,语义分析,符号汇总,然后生成汇编代码。
汇编:.o文件。汇编过程将汇编代码转成二进制文件,二进制文件就可以让机器来读取。每一条汇编语句都会产生一句机器语言。这个阶段会形成符号表,并给这些符号分配虚拟地址。
链接:链接程序的主要工作就是将有关的目标文件彼此相连接,也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够被操作系统装入执行的统一整体
堆(heap):是由malloc之类函数分配的空间所在地。地址是由低向高增长的。
栈(stack):是自动分配变量,以及函数调用的时候所使用的一些空间。地址是由高向低减少的。
内存溢出(out of memory):通俗理解就是内存不够,通常在运行大型软件或游戏时,软件或游戏所需要的内存远远超出了你主机内安装的内存所承受大小,就叫内存溢出。
内存泄漏(Memory Leak):是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
1.软链接是存放另一个文件的路径的形式存在。
2.软链接可以 跨文件系统 ,硬链接不可以。
3.软链接可以对一个不存在的文件名进行链接,硬链接必须要有源文件。
4.软链接可以对目录进行链接。
虚拟内存,借助于地址转换,操作系统可以给应用程序一种假象,独占整个计算机内存,可以使用超过实际物理大小的内存,应用程序之间互不干扰。
进程隔离,地址转换可以用来构建沙盒(SandBoxes)技术,让第三方代码在沙盒中运行,限制其对内存的访问,从而避免操作系统内核和应用程序受到病毒或者恶意代码的攻击。
进程间通信,地址转换可以将不同进程空间的地址映射到同一段物理内存,从而实现进程间通信。
共享代码段,动态加载so库可以在多个程序实例之间进行共享。
程序初始化,使用地址转换技术可以让应用程序只加载部分代码和数据就可以运行(内核在后台继续加载剩余部分),若执行到未加载部分,则发生中断挂起应用程序,待内核将剩余部分加载到内存后再恢复应用程序的执行。
缓存管理,内核通过合理安排程序所在的内存位置来提高缓存效率。
内存映射文件,将文件内容映射到应用程序地址空间,这样一来,文件的内容就可以被应用程序直接访问。
内存的两种视角
虚拟地址,进程看到的内存地址称为虚拟地址,他们不对应任何物理实体,每个进程有自己的地址空间。
物理地址,内存系统看到的地址称为物理地址,他们用实际的地址去查找和存储内容。
虚拟寻址的方法。CPU生成一个虚拟地址(VA),然后MMU(Memory Management Unit 内存管理单元) 将虚拟地址翻译成实际的物理地址,然后再进行物理寻址。
虚拟内存是计算机系统中的一种技术,它通过将物理内存和磁盘空间结合起来,为每个进程提供了一个统一的连续地址空间。在虚拟内存中,使用虚拟地址来访问内存,而不是直接使用物理地址。虚拟地址需要通过地址转换机制转换为对应的物理地址,以实现内存的访问。
对于电脑CPU来说,64位和32位就是CPU一次出来数据的能力,32位就是32bit即一次可以处理4个字节的数据;64位就是64bit即一次可以处理8个字节。
32位的系统许多支持4G的内存,而64位则可以支持上百G的内存。
中断和异常
相同点:都是CPU对系统发生的某个事情做出的一种反应。
区别:中断由外因引起,异常由CPU本身原因引起。
把中断分为外中断和内中断。
外中断——就是我们指的中断——是指由于外部设备事件所引起的中断,如通常的磁盘中断、打印机中断等;
内中断——就是异常——是指由于 CPU 内部事件所引起的中断,如程序出错(非法指令、地址越界)。内中断(trap)也被译为“捕获”或“陷入”。
异常是由于执行了现行指令所引起的。由于系统调用引起的中断属于异常。
中断则是由于系统中某事件引起的,该事件与现行指令无关。
Linux 操作系统挂起、休眠、关机相关命令
立刻关机:
shutdown -h now
shutdown -h 0
定时/延时关机:
shutdown -h 10:00
shutdown -h +30 //单位为分钟
重启:
reboot
待机/挂起:
sudo pm-suspend
sudo pm-suspend-hybrid
echo “mem” > /sys/power/state
sudo hibernate-ram
休眠:
sudo pm-hibernate
echo “disk” > /sys/power/state
sudo hibernate-disk
(1)管理方式不同。栈由操作系统自动分配释放,无需我们手动控制;堆的申请和释放工作由程序员控制,容易产生内存泄漏;
(2)空间大小不同。每个进程拥有的栈的大小要远远小于堆的大小。理论上,程序员可申请的堆大小为虚拟内存的大小,进程栈的大小 64bits 的 Windows 默认 1MB,64bits 的 Linux 默认 10MB;
(3)生长方向不同。堆的生长方向向上,内存地址由低到高;栈的生长方向向下,内存地址由高到低。
(4)分配方式不同。堆都是动态分配的。栈有2种分配方式:静态分配和动态分配。静态分配是由操作系统完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由操作系统进行释放,无需我们手工实现。
(5)分配效率不同。栈由操作系统自动分配,会在硬件层级对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是由C/C++提供的库函数或运算符来完成申请与管理,实现机制较为复杂,频繁的内存申请容易产生内存碎片。显然,堆的效率比栈要低得多。
(6)存放内容不同。栈存放的内容,函数返回地址、相关参数、局部变量和寄存器内容等。当主函数调用另外一个函数的时候,要对当前函数执行断点进行保存,需要使用栈来实现,首先入栈的是主函数下一条语句的地址,即扩展指针寄存器的内容(EIP),然后是当前栈帧的底部地址,即扩展基址指针寄存器内容(EBP),再然后是被调函数的实参等,一般情况下是按照从右向左的顺序入栈,之后是被调函数的局部变量,注意静态变量是存放在数据段或者BSS段,是不入栈的。出栈的顺序正好相反,最终栈顶指向主函数下一条语句的地址,主程序又从该地址开始执行。堆,一般情况堆顶使用一个字节的空间来存放堆的大小,而堆中具体存放内容是由程序员来填充的。
从以上可以看到,堆和栈相比,由于大量malloc()/free()或new/delete的使用,容易造成大量的内存碎片,并且可能引发用户态和核心态的切换,效率较低。栈相比于堆,在程序中应用较为广泛,最常见的是函数的调用过程由栈来实现,函数返回地址、EBP、实参和局部变量都采用栈的方式存放。虽然栈有众多的好处,但是由于和堆相比不是那么灵活,有时候分配大量的内存空间,主要还是用堆。
无论是堆还是栈,在内存使用时都要防止非法越界,越界导致的非法内存访问可能会摧毁程序的堆、栈数据,轻则导致程序运行处于不确定状态,获取不到预期结果,重则导致程序异常崩溃,这些都是我们编程时与内存打交道时应该注意的问题。
产生死锁的原因(两个):
由竞争资源引起死锁:多个进程,共享资源,资源不足,竞争资源。
进程推进顺序不当引起死锁:进程运行过程中,请求和释放资源的顺序不当,而导致进程死锁。
产生死锁的四个必要条件:
① 互斥条件——进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占有。
② 请求和保持条件——当进程因请求资源而阻塞时,对已获得的资源保持不放。
③ 不剥夺条件——进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
④ 环路等待条件——在发生死锁时必然存在一个“进程—资源”的环形链。
解决死锁的基本方法(四种):
① 预防死锁——通过设置某些限制条件,以破坏产生死锁的四个必要条件中的一个或几个,来防止发生死锁。
② 避免死锁——在资源的动态分配过程中,使用某种方法去防止系统进入不安全状态,从而避免了死锁的发生。
③ 检测死锁——检测死锁方法允许系统运行过程中发生死锁。但通过系统所设置的检测机构,可以及时检测出死锁的发生,并精确地确定与死锁有关的进程和资源,然后采取适当措施,从系统中消除所发生的死锁。
④ 解除死锁——解除死锁是与检测死锁相配套的一种设施,用于将进程从死锁状态下解脱出来。常用的方法是撤销或者挂起一些进程,以便于释放出一些资源,再将它分配给已经处于阻塞的进程,使其转换为就绪状态可以继续运行。解决死锁的基本方法(四种):
① 预防死锁——通过设置某些限制条件,以破坏产生死锁的四个必要条件中的一个或几个,来防止发生死锁。
② 避免死锁——在资源的动态分配过程中,使用某种方法去防止系统进入不安全状态,从而避免了死锁的发生。
③ 检测死锁——检测死锁方法允许系统运行过程中发生死锁。但通过系统所设置的检测机构,可以及时检测出死锁的发生,并精确地确定与死锁有关的进程和资源,然后采取适当措施,从系统中消除所发生的死锁。
④ 解除死锁——解除死锁是与检测死锁相配套的一种设施,用于将进程从死锁状态下解脱出来。常用的方法是撤销或者挂起一些进程,以便于释放出一些资源,再将它分配给已经处于阻塞的进程,使其转换为就绪状态可以继续运行。
CPU从内存或缓存中取出指令,放入指令寄存器,并对指令译码分解成 系统指令的执行。
内存(即物理 内存,是相对于硬盘这个“外存”而言)作为硬盘和CPU的“中转站”,对电脑运行速度有较大影响。
当运行数据超出物理内存容纳限度的时候,部分数据就会自行“溢出”,虚拟内存运行的程序或 不使用的数据存放到这部分空间之中,等待需要的时候方便及时调用。
由于内存是带电存储的(一旦断电数据就会消失),而且容量有限,有了硬盘
波特率是指数据传送时,每秒传送数据二进制代码的位数,它的单位是位/秒(b/s)。1波特就是一位每秒。
ARM具有比较强的事务管理功能,可以用来跑界面以及应用程序等,其优势主要体现在控制方面,它的速度和数据处理能力一般,但是外围接口比较丰富,标准化和通用性做的很好,而且在功耗等方面做得也比较好,所以适合用在一些消费电子品方面;
而DSP主要是用来计算的,比如进行加密解密、调制解调等,优势是强大的数据处理能力和较高的运行速度。由于其在控制算法等方面很擅长,所以适合用在对控制要求比较高的场合,比如军用导航、电机伺服驱动等方面。
如果只是着眼于嵌入式应用的话,嵌入式CPU和DSP的区别应该只在于一个偏重控制一个偏重运算了。
DSP的优势主要是速度,它可以在一个指令周期中同时完成一次乘法和一次加法,这非常适合快速傅立叶变换的需求。DSP有专门的指令集,主要是专门针对通讯和多媒体处理的;而ARM使用的是RISC指令集(当然ARM的E系列也支持DSP指令集)是通用处理用的。
浮空输入 :当IO口没有接输入的时候,此时的电平会是一个不确定的值,也就是我们所说的浮空。电平会处于一个跳变的状态,一会高,一会低。只有输入了一个高/低电平才会确定下来。
上拉输入 :在没有信号输入的时候,此时的电平就是VDD的电平,此时读取到的电平就是高电平。如果输入了一个高电平,VDD和O点(最上面的图中的O点)之间就几乎没有电势差,此时O点的电平就仍然是高电平,读取到的电平就是高电平。当输入信号是一个低电平的时候,此时O点的电平的电平就会变成低电平,那么VDD和O点之间形成了电势差,但是因为上拉电阻的存在,所以不会出现一个大电流。此时单片机读取到的一个电平就是一个低电平。上拉输入的好处就是输入的电平不会上下浮动而导致输入信号不稳定,在没有信号输入的情况下可以稳定在高电平。
下拉输入 : 在没有信号输入的时候,根据电路知识,电平就是VSS的电平,此时读取到的电平就是低电平。此时输入的电平如果是一个低电平,就没有办法和之前的情况进行区分。但如果输入的是一个高电平,O点和VSS之间同样形成了电势差,O点的电平会变成外部的高电平,那么单片机得到的就是一个高电平信号。
下拉输入的好处就是输入的电平不会上下浮动而导致输入信号不稳定,在没有信号输入的情况下可以稳定在低电平。
模拟输入 : 模拟输入需要走的路径如图所示。首先得知道,模拟输出走的这一条路径,是我们需要对一个模拟信号进行读取。
在我们使用单片机的时候,我们有时候需要用AD采集到IO口上面的真实电压。这就有了我们所需要的模拟输入。为了让外部的电压真实的读取到单片机的AD模块,我们既不能闭合上拉和下拉的开关,也不能让信号经过施密特触发器。
优势:可以让AD读取电压。还可以在低功耗模式下运行,实现省电的作用。
开漏输出
我们可以把这一个MOS管当成一个三极管,对于图中所示的这种三极管我们可以简单的理解成一个水龙头,左侧就是一个水龙头开关,当给一个高电平的时候, O点和GND就会导通。(O点的输出就是一种反向器的输出,也就是O点的电平会和左侧MOS的栅极(三极管的基极)相反)
所以说,开漏输出就很好理解了。当我们给一个低电平的时候,MOS管关闭,此时输出的电压就是一个浮空,即不确定的电压。如果给一个高电平,那么MOS管导通,相当于IO口与VSS相连,此处就输出了一个低电平电压。
优势①
虽然我们可以看到开漏输出是没有办法在内部输出一个高电平,但是这一个看似是缺点。其实实际上是一种优点。我们可以得到,当给一个低电平的时候,MOS管没有导通,此时电压不确定导致无法输出高电平,但是一旦我们在外部增加一个上拉,那么这一个缺点就会被有效避免。并且,因为是我们自己设计一个上拉,这个上拉的电压是由我们自己确定,这样我们就可以根据外部电路需要多少V的高电平来给这一个上拉的电压,可以更好的适应更多情况。如下图,我们可以给定任意的VDD电压,来适应我们实际所需要的情况。
优势②
开漏输出的实质其实就是一个OD门(OD:漏极输出(Open Drain))。而在数电中,OD门有一个非常重要的特性就是可以实现线与的功能,简单来说,就是在像IIC这样的总线协议中,只要有一个给低电平,那么总线都会被拉低。
推挽输出 :推挽输出就是可以需要利用两个不同的MOS管来实现输出。P-MOS和N-MOS是不同的控制方式,当给一个高电平的时候,N-MOS不导通,P-MOS导通,此时IO口接通在VDD,此时输出的是高电平。当给一个低电平的时候,N-MOS导通,P-MOS不导通,此时IO口接通在VSS 电源上面,此时输出的是低电平。
一定要把这个MOS管理解成开关控制的水龙头!!!
优势
带载能力强。
扇区是磁盘中最小的物理存储单位。通常情况下每个扇区的大小是512字节。
块是操作系统中最小的逻辑存储单位。操作系统与磁盘打交道的最小单位是磁盘块。
与内存操作,是虚拟一个页的概念来作为最小单位。与硬盘打交道,就是以块为最小单位。吧
UART:通用异步串行口,速率不快,可全双工,结构上一般由波特率产生器、UART发送器、UART接收器组成,硬件上两线,一收一发。
I2C:双向、两线、串行、多主控接口标准。速率不快,半双工,同步接口,具有总线仲裁机制,非常适合器件间近距离经常性数据通信,可实现设备组网。
SPI:高速同步串行口,高速,可全双工,收发独立,同步接口,可实现多个SPI设备互联,硬件3~4线。
USB:通用串行总线,高速,半双工,由主机、hub、设备组成。设备可以与下级hub相连构成星型结构。
串行通信进行数据传送时是将要传送的数据按二进制位,依据一定的顺序逐位发送到接收方。其有两种通信方式:异步通信和同步通信。
异步通信,是指数据传送以字符为单位,字符与字符间的传送是完全异步的,位与位之间的传送基本上是同步的。异步通信采用固定的通信格式,数据以相同的帧格式传送。每一帧由起始位、数据位、奇偶校验位和停止位组成。异步串行通信的特点可以概括为:
以字符为单位传送信息。
相邻两字符间的间隔是任意长。
因为一个字符中的比特位长度有限,所以需要的接收时钟和发送时钟只要相近就可以。
异步方式特点简单的说就是:字符间异步,字符内部各位同步。
异步位系统是面向字符来传输信息的,也就是我们一般情况下的一个字符,8位,1bit,当然了传输的时候还要加上起始位和结束位,没有这两位接收方就不知道什么时候开始接收数据什么时候结束了。
同步通信,是指数据传送是以数据块(一组字符)为单位,字符与字符之间、字符内部的位与位之间都同步。同步通信时,通信双方共用一个时钟,这是同步通信区分于异步通信的最显著的特点。同步串行通信的特点可以概括为:
以数据块为单位传送信息。
在一个数据块(信息帧)内,字符与字符间无间隔。
因为一次传输的数据块中包含的数据较多,所以接收时钟与发送进钟严格同步,通常要有同步时钟。