1、new是操作符,malloc是函数
2、new使用时先分配内存,再调用构造函数,释放时调用析构函数
3、new只能分配实例所占类型的整数倍,malloc可以随意分配。
4、new失败返回异常,malloc返回NULL
1、静态区分配:编译时分配好,主要储存全局变量,static变量等。
2、栈分配:执行函数时,函数内部的局部变量,函数结束时,这些储存单元自动被释放
3、堆分配:也叫动态分配,通过malloc以及NEW来分配,程序员自己负责free或者delete自己申请的内存
1、class可以继承,类,接口,struct只能继承接口
2、class有默认的无参构造函数,struct没有,而且struct没有析构函数
3、class有继承级别,protected等等
4、class用垃圾回收机制保证内存的回收,struct使用完之后自动接触内存分配。
1、前者在编译期起作用,后者在预处理和编译期起作用
2、前者没有数据类型,只是简单的替换,后者有数据类型,可以进行判断,避免基础的错误。
3、前者只有一个备份,后者替换多少次就有多少个备份。
1、vector 内存空间连续,底层是数组,list内存空间不连续,是双向链表,都是在堆中分配
2、vector随机访问效率高,在非尾部插入困难,list相反
3、迭代器支持不同,vector支持的“+”等等,list不支持
1、vector 为数组,支持快速随机访问
2、list底层为 双向链表,支持快速增删
3、deque是中央控制器和多个缓冲区
4、stack底层一般用list和deque实现
5、queue一般使用list和deque实现,封闭头部
6、priority_queue使用vector为底层容器,使用heap来管理规则
7、set底层为红黑树,有序不重复 multiset可重复
8、map底层为红黑树,有序不重复 multimap可重复
9、hash_set底层为hash表,无序不重复
10、hash_map底层为hash表 无序不重复
1、动态绑定就是继承虚函数
2、静态绑定就是函数重载
条件:有继承、有虚函数(virtual)重写、有父类指针(引用)指向子类对象。
实现原理:当类中声明虚函数时,编译器会在类中生成一个 虚函数表;虚函数表是一个储存 类成员函数指针的数据结构;virtual成员函数会被编译器放入虚函数表中。存在虚函数时,在创建的每个对象中都有一个指向虚函数表的指针(vptr指针)函数在运行的时候会重写这个虚函数。
由于类的多态性,基类指针可以指向派生类的对象。如果删除该基类的指针,就会调用该指针指向的派生类的析构函数,而派生类的析构函数又会自动调用基类的析构函数,这样整个派生类的对象被完全释放。
虚函数相应一个指向vtable虚函数表的指针,但是这个指向vtable的指针事实上是存储在对象的内存空间的。假设构造函数是虚的,就须要通过 vtable来调用,但是对象还没有实例化,也就是内存空间还没有,怎么找vtable呢?所以构造函数不能是虚函数。
重载是参数类型或者个数不同,覆盖是子类重写父类函数。
1、规则:栈先入后出,队列先入先出
2、插入删除定义不同:栈只能在一端插入和删除,队列只能在一端插入另一端删除
(1)复制的内容不同。strcpy只能复制字符串,而memcpy可以复制任意内容,例如字符数组、整型、结构体、类等。
(2)复制的方法不同。strcpy不需要指定长度,它遇到被复制字符的串结束符"\0"才结束,所以容易溢出。memcpy则是根据其第3个参数决定复制的长度。
(3)用途不同。通常在复制字符串时用strcpy,而需要复制其他类型数据时则一般用memcpy。
主要4个,auto_ptr(C++ 98)、unique_ptr、shared_ptr 和weak_ptr(C++ 11)
auto_ptr :现在用的少,老版C++用,能自动释放。
unique_ptr: 两个unique_ptr 不能指向一个对象,即 unique_ptr 不共享它所管理的对象,并可以放在容器中。
shared_ptr:shared_ptr 是一个标准的共享所有权的智能指针,允许多个指针指向同一个对象,shared_ptr 利用引用计数的方式实现了对所管理的对象的所有权的分享,即允许多个 shared_ptr 共同管理同一个对象。比较麻烦,需要自己写辅助类。
weak_ptr: weak_ptr 是为了配合 shared_ptr 而引入的一种智能指针,它更像是 shared_ptr 的一个助手而不是智能指针,因为它不具有普通指针的行为,没有重载 operator* 和 operator-> ,因此取名为 weak,表明其是功能较弱的智能指针。
模板:capture mutable ->return-type{statement}
1.[capture]:捕捉列表。捕捉列表总是出现在Lambda函数的开始处。实际上,[]是Lambda引出符。编译器根据该引出符判断接下来的代码是否是Lambda函数。捕捉列表能够捕捉上下文中的变量以供Lambda函数使用;
2.(parameters):参数列表。与普通函数的参数列表一致。如果不需要参数传递,则可以连同括号“()”一起省略;
3.mutable:mutable修饰符。默认情况下,Lambda函数总是一个const函数,mutable可以取消其常量性。在使用该修饰符时,参数列表不可省略(即使参数为空);
4.->return-type:返回类型。用追踪返回类型形式声明函数的返回类型。我们可以在不需要返回值的时候也可以连同符号”->”一起省略。此外,在返回类型明确的情况下,也可以省略该部分,让编译器对返回类型进行推导;
5.{statement}:函数体。内容与普通函数一样,不过除了可以使用参数之外,还可以使用所有捕获的变量。
与普通函数最大的区别是,除了可以使用参数以外,Lambda函数还可以通过捕获列表访问一些上下文中的数据。具体地,捕捉列表描述了上下文中哪些数据可以被Lambda使用,以及使用方式(以值传递的方式或引用传递的方式)。语法上,在“[]”包括起来的是捕捉列表,捕捉列表由多个捕捉项组成,并以逗号分隔。捕捉列表有以下几种形式:
1.[var]表示值传递方式捕捉变量var;
2.[=]表示值传递方式捕捉所有父作用域的变量(包括this);
3.[&var]表示引用传递捕捉变量var;
4.[&]表示引用传递方式捕捉所有父作用域的变量(包括this);
5.[this]表示值传递方式捕捉当前的this指针。
上面提到了一个父作用域,也就是包含Lambda函数的语句块,说通俗点就是包含Lambda的“{}”代码块。上面的捕捉列表还可以进行组合,例如:
1.[=,&a,&b]表示以引用传递的方式捕捉变量a和b,以值传递方式捕捉其它所有变量;
2.[&,a,this]表示以值传递的方式捕捉变量a和this,引用传递方式捕捉其它所有变量。
不过值得注意的是,捕捉列表不允许变量重复传递。下面一些例子就是典型的重复,会导致编译时期的错误。例如:
3.[=,a]这里已经以值传递方式捕捉了所有变量,但是重复捕捉a了,会报错的;
4.[&,&this]这里&已经以引用传递方式捕捉了所有变量,再捕捉this也是一种重复。
全局变量为0,局部变量栈上分配为随机数。
局部变量为随机数的主要原因是因为局部变量是栈上分配的,栈内存是反复使用的,如果不进行初始化就是上一次写入的值。
使用友元类时注意:
(1) 友元关系不能被继承。
(2) 友元关系是单向的,不具有交换性。若类B是类A的友元,类A不一定是类B的友元,要看在类中是否有相应的声明。
(3) 友元关系不具有传递性。若类B是类A的友元,类C是B的友元,类C不一定是类A的友元,同样要看类中是否有相应的申明
1、auto变量类型自动推导
2、基于范围的for循环 for(int a:vec)
3、智能指针
4、stl 新增unordered 哈希表map
5、lambda函数:无名函数,用在sort中比较多
6、线程库
先判断一波
if(p)
{
free(p);
}
或者每次free之后把指针指向NULL;
实现见 socket IO复用
select:
优点:所有平台都支持,良好的跨平台支持也是一个优点
缺点:
1、数量限制,Linux平台一般是1024个
2、线性扫描,采用轮询的方法,效率较低
3、select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理,因此需要维护一个用来存放大量fd的数据结构(fd_set)fd_set简单地理解为一个长度是1024的比特位,每个比特位表示一个需要处理的FD,如果是1,那么表示这个FD有需要处理的I/O事件,否则没有,其是连续存储的。每次select查询都要遍历整个事件列表。
poll:
主要操作的数据结构:
struct pollfd {
int fd; // 需要监视的文件描述符
short events; // 需要内核监视的事件
short revents; // 实际发生的事件
};
优点:
1、在使用该结构的时候,不用进行比特位的操作,而是对事件本身进行操作就行。同时还可以自定义事件的类型。这样的好处是在内存中存放就不需要连续的内存地址,很像是list队列结构,读或者写事件数量(文件描述符数量)理论上是无限的,取决于内存的大小。
2、它没有最大连接数的限制,原因是它是基于链表来存储的。
缺点:
epoll:
面向对象的事件驱动epoll是事件驱动(每个事件关联fd)的
触发回调函数机制,没有并发限制,效率提升。Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关。
fopen为C语言标准库函数,open是Linux系统函数,所以open是底层函数,fopen为应用函数,所以fopen只能打开常规文件,但是open可以打开描述符。
堆分配的空间在逻辑地址上是连续的,但在物理地址上是不连续的
静态链接:链接器在链接阶段将各种库文件和相关文件集成到可执行文件中,在windows下静态链接库以.lib结尾,linux下以.a结尾
静态链接的优点:
1)装载速度很快,运行速度比动态链接快;
2)只需要开发人员在开发机上有完整的.lib文件,不需要在用户机器上有完整的.lib文件,自完备
静态链接的缺点:
1)可执行文件很大,并且相同代码很多,资源浪费
动态链接:动态链接是把链接过程在运行时进行,动态链接在可执行文件装载或运行的时候,由操作系统的装载程序加载库文件,windows下以.dll结尾,也有.lib的,但是这个是叫做导入库,和静态链接的不一样,linux下以.so结尾
动态链接的优点:
1)可执行文件很小;
2)适合大规模软件开发,开发过程耦合度小、独立,便于不同开发人员和开发组织开发;
3)不同编程语言按照约定可以使用同一套.dll库;
4)dll文件与exe文件独立,如果输出接口相同,更换dll文件不会对exe文件产生影响,可拓展性和可维护性好
动态链接的缺点:
1)速度没有静态链接快;
2)不具有自完备,如果用户机器中没有.dll文件,程序将无法运行并且报错
内存溢出:程序在申请内存时,系统没有足够的内存空间供其使用。
内存泄露:向系统申请内存使用完后没有归还,导致有效内存被占用。内存泄露最终会导致内存溢出。例如:new一块内存使用,使用完后没有delete。
内存越界:向系统申请一块内存后,使用时超出了内存申请范围。例如:通过下标取数组元素时,下标过大导致越界。
创建 msgget(key_t key, int flag)
控制 msgctl(int msgqid, int cmd, struct msqid_ds *buf)(删除)
添加 msgsnd(int msgqid, const void *msgp, size_t size, int flag)
接收 msgrcv(int msgqid, void *msgp, size_t size, long msgtype, int flag)
1、互相排斥
2、不可剥夺
3、循环等待
4、保持资源
解决方案:
(1)加锁顺序(线程按照一定的顺序加锁)
(2)加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
(3)死锁检测
多进程优点:
• 每个进程互相独立,不影响主程序的稳定性,子进程崩溃没关系;
• 通过增加CPU,就可以容易扩充性能;
• 可以尽量减少线程加锁/解锁的影响,极大提高性能,就算是线程运行的模块算法效率低也没关系;
• 每个子进程都有2GB地址空间和相关资源,总体能够达到的性能上限非常大
缺点:
• 逻辑控制复杂,需要和主程序交互;
• 需要跨进程边界,如果有大数据量传送,就不太好,适合小数据量传送、密集运算
• 多进程调度开销比较大;
多线程优点:
• 无需跨进程边界;
• 程序逻辑和控制方式简单;
• 所有线程可以直接共享内存和变量等;
• 线程方式消耗的总资源比进程方式好;
多线程缺点:
• 每个线程与主程序共用地址空间,受限于2GB地址空间;
• 线程之间的同步和加锁控制比较麻烦;
• 一个线程的崩溃可能影响到整个程序的稳定性;
• 到达一定的线程数程度后,即使再增加CPU也无法提高性能,例如Windows Server2003,大约是1500个左右的线程数就快到极限了(线程堆栈设定为1M),如果设定线程堆栈为2M,还达不到1500个线程总数;
• 线程能够提高的总性能有限,而且线程多了之后,线程本身的调度也是一个麻烦事儿,需要消耗较多的CPU。
1、需要频繁销毁、切换的使用线程,进程开销大
2、需要稳定安全的时候选择进程,进程失败不影响其他进程,线程会影响
1、创建
2、就绪
3、运行
4、阻塞
5、终止
父进程会继承子进程的一些信息,包括:各种ID、根目录、环境、以及共享资源。
区别在于:fork返回值,进程ID,每个进程都有自己独立的资源空间。
1、进程上文:指进程从用户态切到内核态需要保存的用户态在CPU寄存器中的值,以便再执行的时候能恢复切换时的状态。
2、进程下文:切换到内核态中执行的程序,进程运行在内核态中的部分。
3、中断上文:硬件通过中断触发信号,导致内核调用中断处理程序,进入内核空间,这个过程中,中断也会需要硬件传入的参数或者变量,所以上文可以看做是,保护这些参数和变量
4、中断下文:执行在内核空间的中断服务函数
总而言之:保护用户状态的是上文,内核态执行的程序是下文
用户态一般是运行在低特权级别,大部分用户直接面对的程序都是运行在用户态;反之,当程序运行在0级特权级上时,就可以称之为运行在内核态。
运行在用户态下的程序不能直接访问操作系统内核数据结构和程序
当我们在系统中执行一个程序时,大部分时间是运行在用户态下的,在其需要操作系统帮助完成某些它没有权力和能力完成的工作时就会切换到内核态。
切换到内核态的三种方法:
1、系统调用:用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作,比如前例中fork()实际上就是执行了一个创建新进程的系统调用。
2、异常:当CPU在执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如缺页异常。
3、中断:当外围设备完成用户请求的操作后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到内核态的切换。
一个进程能使用的空间约是2G,一个线程栈空间大概是1M,所以理论上可以创建2048个线程。
1、同步:指的是线程之间所有的一种制约关系,一个线程的执行依赖于另一个线程的消息,没有得到另一个线程的消息时需要等到,直到另一个线程发送。
2、互斥:一般指使用进程的共享资源时,同一个资源只能被一个线程访问,指单个线程访问的排他性。
1、并发:指同一时间有多个线程在同一个处理器上运行,两种并发关系分别是同步与互斥.
2、互斥:进程之间排斥使用临界资源的现象
3、同步:线程之间相互依赖,前一个的输入作为后一个的输出。
4、异步:与同步相对,任务彼此独立,在等待某事件的过程中,继续做自己的事情,不需要等待这一事情完成后继续做自己的事。线程是实现异步的一个方式,主线程不需要等待任务完成。
5、阻塞:指调用函数结果返回前,当前线程会被挂起,函数只有得到结果之后才会返回。
6、非阻塞:如果函数不能立即得到结果,该函数不会阻塞当前线程,会立即返回。
孤儿进程:父进程退出,子进程还在,被init进程收养,完成后序状态采集工作。
僵尸进程:fork创建子进程,子进程退出后,父进程没有wait或者waitpid,子进程的进程描述符任然在。 处理:子进程退出时发送SIGCHILD信号,父进程处理SIGCHILD信号,信号处理函数中用wait处理僵尸进程。
守护进程:在后台运行不与任何终端关联的进程,一般在进程启动时就会运行,他们以root用户运行。
一个磁盘按层次分为 磁盘组合 -> 单个磁盘 -> 某一盘面 -> 某一磁道 -> 某一扇区
通俗的来讲,在Windows下如NTFS等文件系统中叫做簇;在Linux下如Ext4等文件系统中叫做块(block)。每个簇或者块可以包括2、4、8、16、32、64…2的n次方个扇区。
读写基本单位是扇区。磁盘的原理,物理实现,磁盘控制器是按照扇区这个单位读取等操作数据的。
页:操作系统经常与内存和硬盘这两种存储设备进行通信,类似于“块”的概念,都需要一种虚拟的基本单位。所以,与内存操作,是虚拟一个页的概念来作为最小单位。与硬盘打交道,就是以块为最小单位。
我们将cache放置在CPU和主存之间,作为主存数据的缓存。 当CPU试图从主存中load/store数据的时候, CPU会首先从cache中查找对应地址的数据是否缓存在cache 中。如果其数据缓存在cache中,直接从cache中拿到数据并返回给CPU。
MMU:现代操作系统普遍采用虚拟内存管理机制,这需要MMU(Memory Management Unit,内存管理单元)的支持。有些嵌入式处理器没有MMU,则不能运行依赖于虚拟内存管理的操作系统。MMU的作用就是负责虚拟地址(virtual address)转化成物理地址(physical address)。
(1)互斥锁:mutex,保证在任何时刻,都只有一个线程访问该资源,当获取锁操作失败时,线程进入阻塞,等待锁释放。
(2)读写锁:rwlock,分为读锁和写锁,处于读操作时,可以运行多个线程同时读。但写时同一时刻只能有一个线程获得写锁。
互斥锁和读写锁的区别:
(a)读写锁区分读锁和写锁,而互斥锁不区分
(b)互斥锁同一时间只允许一个线程访问,无论读写;读写锁同一时间只允许一个线程写,但可以多个线程同时读。
(3)自旋锁:spinlock,在任何时刻只能有一个线程访问资源。但获取锁操作失败时,不会进入睡眠,而是原地自旋,直到锁被释放。这样节省了线程从睡眠到被唤醒的时间消耗,提高效率。
(4)条件锁:就是所谓的条件变量,某一个线程因为某个条件未满足时可以使用条件变量使该程序处于阻塞状态。一旦条件满足了,即可唤醒该线程(常和互斥锁配合使用)
(5)信号量:允许多个线程访问资源,但是线程数由信号量决定。
实时操作系统是在外界时间和数据产生时能快速反应处理,按照任务的优先级,尽快的完成操作。
分时操作系统是使一台计算机被多个用户使用,将系统处理器时间和内存按照一定的时间间隔,轮流的切换给不同终端用户的程序使用。由于时间间隔很短,就好像每个用户独占计算机一样。
\1. 平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址 处取某些特定类型的数据,否则抛出硬件异常。
\2. 性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
总体来说: 结构体的内存对齐是拿空间来换取时间的做法。
(1)正文段( text段/代码段)
这是由 CPU 执行的机器指令的部分。通常,正文段是可共享的,所以即使是频繁执行的程序(如文本编辑器,C 编译器和 shell 等)在存储器中也只需有一个副本,另外正文段常常是只读的,以防止程序由于意外而修改其指令。
正文段是用来存放可执行文件的操作指令,也就是说它是可执行程序在内存中的镜像。
(2)初始化数据段(数据段)
数据段用来存放可执行文件中已经初始化的全局变量,换句话说就是存放程序静态分配的变量和全局变量。
例如,C 程序中任何函数之外的声明:
int num = 10;
使此变量以其初值存放在初始化数据段中。
(3)未初始化数据段(bbs段)
bbs段包含了程序中未初始化的全局变量,在程序开始执行之前,内核将此段中的数据初始化为 0 或空指针。
例如:函数外的声明:
long sum[100];
使此变量存放在非初始化数据段中。
(4)堆
堆是用于存放进程进行中被动态分配的内存段,它大小并不固定,可动态扩张或缩减。当进程调用 malloc 等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用 free 等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)。 由于历史上形成的惯例, 堆位于未初始化数据段和栈之间 。
(5)栈
栈是用户存放程序临时创建的局部变量,也就是说我们函数括弧“{}”中定义的变量(但不包括static 声明的变量,static 意味着在数据段中存放变量)。除此之外在函数调用结束后,函数的返回值也会被存放回栈中。由于栈的后进先出(LIFO)特点,所以栈特别方便用来保存/恢复调用现场。从这个意义上讲我们可以把堆栈看成一个临时数据寄存,交换的内存区。
(6)参数和环境区
命令行参数和环境变量。
1、利用信号灯实现共享内存的同步,reader和writer通过信号通信必须获取对方的进程号,可利用共享内存保存双方的进程号。reader和writer运行的顺序不确定,可约定先运行的进程创建共享内存并初始化。利用pause, kill, signal等函数可以实现该程序(流程和前边类似)。
2、互斥锁同步:需要在访问共享内存中数据的时候,查询互斥锁的状态来进行同步。
int pthread_create(pthread_t *id , pthread_attr_t attr, void(fun)(void*), void *arg);
实际上,无论是创建进程的fork,还是创建线程的pthread_create,底层实现都是调用同一个内核函数 clone。
所以线程在内核看来是轻量级进程
tcp优点:可靠,首先有三次握手来建立连接,有包括确认,窗口,重传,拥塞控制机制,传输完后还会断开连接来节约系统资源。
缺点:慢,效率低,占用系统资源多。三次握手慢,且TCP容易被攻击,其次各种保护机制占用大量资源。
udp优点:快,没有握手,是一个无状态传输协议,因此传输速度非常快。
缺点:不可靠不稳定,网络不好容易丢包。
1、前者明文传输,后者需要加密。
2、前者要申请CA证书
3、端口号不一样,前者是443,后者是80;
前者是一个概念,后者是具体的网址,后者属于前者
1、第一次握手:客户端发送syn包(若为j),客户端进入SYN_SEND状态,待服务器确认 syn:同步序列编号
2、第二次握手:服务器收到syn包,首先确认客户端的syn(j+1),自己也发一个syn包(k),进入SYN_RECV状态。
3、第三次握手:客户端收到应答包(j+1),向服务器发送确认包(k+1),此包发送完毕后进入established阶段,完成三次握手。
1、防止失效的报文从新到了服务器,而产生错误。
如果是两次握手还会出现一个问题,客户端的第一次SYN请求在网络中阻塞时,客户端重新发送第二次SYN请求,服务器收到第二次SYN请求后,成功与客户端两次握手,双方建立连接,在数据传输结束后,双方断开链接,这时,第一次的SYN请求在服务端到来,服务端会认为客户端想要重新建立链接,给客户端发出确认建立连接,会一直等待客户端发送数据,而客户端已经完成了自己的数据传输任务,不会再给服务端发信息,于是服务端就一直等待,造成了资源的浪费。
1、第一次挥手:客户端将FIN置为1,发送seq给服务端,进入FIN WAIT 1状态
2、第二次挥手:服务端收到FIN后,回复数据加一,进入CLOSE_WAIT状态,此时客户端已经没有要发送的数据了,等到客户端发送完数据,客户端进入FIN_WAIT2状态。
3、第三次挥手:服务端将FIN置为1,发送序列号给客户端,进入LAST_ACK状态;
4、第四次挥手:客户端收到FIN后,进入TIME_WAIT状态,ACK置为1,发送序列号加一,服务器收到确认后变成closed状态,不再向客户端发送数据,客户端等待2*MSL后也进入CLOSED状态,完成四次挥手。
1、保证客户端的最后一个报文能到达服务器,
2、可以使所有报文消失,不会产生握手时候的旧报文连接请求。
IO多路复用是指内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程。IO多路复用适用如下场合:
(1)当客户处理多个描述字时(一般是交互式输入和网络套接口),必须使用I/O复用。
(2)当一个客户同时处理多个套接口时,而这种情况是可能的,但很少出现。
(3)如果一个TCP服务器既要处理监听套接口,又要处理已连接套接口,一般也要用到I/O复用。
(4)如果一个服务器即要处理TCP,又要处理UDP,一般要使用I/O复用。
(5)如果一个服务器要处理多个服务或多个协议,一般要使用I/O复用。
与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。
select函数实现:
int select(int maxfdp,fd_set *readfds,fd_set *writefds,fd_set *errorfds,struct timeval *timeout);
maxfdp——传入参数,集合中所有文件描述符的范围,即最大文件描述符值+1
readfds——传入传出参数,select调用时传入要监听的可读文件描述符集合,select返回时传出发生可读事件的文件描述符集合
writefds——传入传出参数,select调用时传入要监听的可写文件描述符集合,select返回时传出发生可写事件的文件描述符集合
errorfds——传出参数,select返回时传出发生事件(包括可读和可写)中异常事件的文件描述符集合
timeout——传入参数,设置select阻塞的时间。若设置为NULL,则select一直阻塞直到有事件发生;
若设置为0,则select为非阻塞模式,执行后立即返回;
若设置为一个大于0的数,即select的阻塞时间,若阻塞时间内有事件发生就返回,否则时间到了立即返回
select工作原理:传入要监听的文件描述符集合(可读、可写或异常)开始监听,select处于阻塞状态,当有事件发生或设置的等待时间timeout到了就会返回,返回之前自动去除集合中无事件发生的文件描述符,返回时传出有事件发生的文件描述符集合。但select传出的集合并没有告诉用户集合中包括哪几个就绪的文件描述符,需要用户后续进行遍历操作。
epoll函数实现
针对select的缺点做了如下改进:
内核中保存一份文件描述符集合(红黑树),无需用户每次都重新传入,只需告诉内核修改的部分即可。
内核不再通过轮询的方式找到就绪的文件描述符,而是通过异步 IO 事件唤醒。
内核仅会将有 IO 事件的文件描述符返回给用户,用户也无需遍历整个文件描述符集合。
打印信息、崩溃日志
一般用assert 断言的方法,或者打印信息,GDB跟一下等等方法。
段错误一般看看内存溢出,指针越界等等。
gdb可以看到段错误的地方,
1、冒泡排序
两轮循环第一层0,n 第二层 0,n-i-1 条件反转
void maopao(vector<int>&arr){ int n = arr.size(); for(int i=0;i<n;i++) {• for(int j=0;j<n-i-1;j++)• {• if(arr[j]>arr[j+1])• {• swap(arr[j],arr[j+1]);• }• } }}
2、插入排序
//直接插入 一层循环,条件while注意j的大小
void dinsert_sort(vector<int>&action) { for(int i=1;i<=action.size()-1;i++) {• if(action[i]<action[i-1])• {• int temp= action[i];• int j = i;• while(j>=1&&action[j-1]>temp)• {• action[j] = action[j-1];• j--;• }• action[j] = temp;• } }}
3、希尔排序 三轮循环,第一轮while(gap) gap/=2,第二轮分组,在子序中进行插入,第三轮while查找插入点
void shell_sort(vector<int>&a){ int len = a.size(); unsigned int gap = len/2; while(gap) {• for(int i=gap;i<len;i++)• {• int temp = a[i];• int j = i;• while(j>=gap&&a[j-gap]>temp)• {• a[j] = a[j-gap];• j-=gap;• }• a[j] = temp;• }• gap /= 2; }}
4、快速排序
快排就是三个函数,第一个左右边,第二个找到中点之后分左右边递归,第三个找区分点,并返回。
int f3(vector<int>&action,int left,int right){ int leftindex = left; int index = action[left]; while(left<right) { while(left<right&&action[right]>=index)right--; while(left<right&&action[left]<=index)left++; swap(action[left],action[right]); } swap(action[leftindex],action[left]); return left;}void f2(vector<int>&action,int left,int right){ if(left<right) {• int mid = f3(action,left,right);• f2(action,left,mid-1);• f2(action,mid+1,right); } } void f1(vector<int>&action){ int n = action.size()-1; f2(action,0,n);}
5、选择排序 两轮循环,第一轮0,n,第二轮i+1到n 比较ij然后交换。n2的时间复杂度
void choosesort(vector<int>&a){ int n = a.size(); for(int i=0;i<n;i++) {• for(int j=i+1;j<n;j++)• {• if(a[i]>a[j])• {• swap(a[i],a[j]);• }• } }}
①睡眠模式:内核停止,外设比如NVIC,系统时钟Systick仍然运行
②停止模式:所有时钟停止,1.8V内核电源工作
③待机模式:1.8V内核电源关闭;只有备份寄存器和待机电路维持供电,寄存器和SRAM内容丢失,功耗最低
运行模式下降低功耗:降低系统时钟,关闭APB,AHB总线上的未使用外设时钟
(1)使能IO口时钟,配置IO口模式为输入
(2)开启AFIO时钟,设置IO口与中断线的映射关系(将GPIO端口映射到EXTI中断线上)
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);
void GPIO_EXTILineConfig(uint8_t GPIO_PortSource,uint8_t GPIO_PinSource);
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource);
(3)配置中断分组(NVIC),配置中断源、设置抢占优先级和子优先级
(4)初始化EXTI,选择触发方式
void EXTI_Init(EXTI_InitTypeDef * EXTI_InitStruct);
typedef struct
{
uint32_t EXTI_Line; //中断/事件线
EXTIMode_TypeDef EXTI_Mode;//EXTI模式
EXTITrigger_TypeDef EXTI_Trigger;//EXTI出发方式
FunctionalState EXTI_LineCmd;//中断线使能或失能
}
(5)编写EXTI中断服务函数
EXTI0_IRQHandler
EXTI1_IRQHandler
EXTI2_IRQHandler
EXTI3_IRQHandler
EXTI4_IRQHandler
EXTI9_5_IRQHandler(中断线5-9的写法)
EXTI15_10_IRQHandler(中断线10-15的写法
4、编写外部中断控制程序
要实现外部中断方式控制LED,程序框架如下:
(1)初始化对应端口的EXTI
(2)编写EXTI中断函数
(3)编写主函数
波特率是每秒钟可以传送的二进制位数,其单位为bps(bits per second)也写作bits/s。它是衡量串行数据速度快慢的重要指标。这里波特率就是有效性的一个指标,提高波特率就是提高信息的传输速率,也就是说信息的传输更快(波特率定义为单位时间内传输的码元个数)。误码率为可靠性指标。
1、上拉输入:将一个不确定的信号,通过一个电阻与电源VCC相连,固定在高电平。在IO口为输入模式且为上拉电阻时,IO口的常态为高电平。
2、下拉输入:将一个不确定的信号,通过一个电阻与地GND相连,固定在低电平。在IO口为输入模式且为下拉电阻时,IO口的常态为低电平。
3、推挽输出:可以输出高、低电平,连接数字器件。推挽结果一般是指两个三极管分别受两互补信号的控制,总是在一个三极管导通时令一个三极管截止。(推挽输出的最大特点是可以真正的输出高电平和低电平,且两种电平下都有驱动能力)。IO输出0-接GND, IO输出1 -接VCC 0~3.3V
4、开漏输出:输出端相当于三极管的集电极,要得到高电平状态需要加上拉电阻才行。适合做电流型的驱动,其吸收电流的能力比较强(20mA左右)(开漏输出最主要的特性就是高电平没有驱动能力,需要借助外部上拉电阻才能真正输出高电平)。开漏只能输出低电平,高电平的时候实际上是个高阻态,需要外接电阻来拉高的。一般用于需要输出5V的高电平。
异步通信:是按字符传输的。每传输一个字符就用起始位来进来收、发双方的同步。不会因收发双方的时钟频率的小的偏差导致错误。
同步通信方式的特点:
进行数据传输时,发送和接收双方要保持完全的同步,因此,要求接收和发送设备必须使用同一时钟。
优点是可以实现高速度、大容量的数据传送;缺点是要求发生时钟和接收时钟保持严格同步,同时硬件复杂。
简单说:异步传输,以字符为单位,同步以数据块(帧)为单位
异步传输,不需要时钟频率相同,随时可以发送,但是字符与字符之间要由间隔标识符,同步传输要求严格时钟相等
异步传输,字符与字符之间不需要同步,字符里面的位与位之间需要同步,同步传输:字符与字符之间也要同步
1、中断请求,然后关闭中断
2、保存CPU寄存器通知内核进入ISR
3、ISR给任务发信号
4、退出ISR,恢复CPU寄存器
5、中断返回,无高优先级任务则继续运行原任务。
简介:低优先级任务先运行,抢占了高优先级的资源,导致了高优先级任务阻塞,这时候优先级处于两者之间的任务就能抢占低优先级任务,高优先级任务还是不能运行,导致优先级反转。
解决方案:
1、使用互斥信号量,可以优先级继承,高优先级的任务会把低优先级的任务的优先级先提高到和自己相同的优先级,保证低优先级的任务能够继续运行至结束这样极大减少了因为高优先级获取不到信号量被阻塞过长时间的问题。
1、抢占式调度
CPU总是运行多个任务中优先级别最高的那个任务,即使CPU正在运行某个低级别的任务,当有更高优先级别的任务准备就绪时,更高优先级别的任务就会剥夺正在运行任务的CPU使用权,从而使自己获得CPU的使用权。
2、时间片调度
在FreeRTOS操作系统中只有同优先级任务才会使用时间片调度。最常用的的时间片调度算法就是Round-robin调度算法(时间片轮转),freertos就是用的该算法。一个时间片等于freertos中滴答定时器的时间间隔。
Linux内核主要由进程调度(SCHED)、进程间通信(IPC)、内存管理(MMU)、虚拟文件系统(VFS)、网络接口(NET)和5个子系统组成
1、进程调度
程调度控制系统中的多个进程对CPU的访问,使得多个进程能在CPU中“微观串行,宏观并行”地执行。
2、进程间通讯
Linux支持进程间的多种通信机制,包含信号量、共享内存、管道等,这些机制可协助多个进程、多资源的互斥访问、进程间的同步和消息传递。
3、内存管理
内存管理的主要作用是控制多个进程安全地共享主内存区域。当CPU提供内存管理单元(MMU)时,Linux内存管理完成为每个进程进行虚拟内存到物理内存的转换。
4、网络接口
网络接口提供了对各种网络标准的存取和各种网络硬件的支持。在Linux中网络接口可分为网络协议和网络驱动程序,网络协议部分负责实现每一种可能的网络传输协议,网络设备驱动程序负责与硬件设备通信
5、虚拟文件系统
Linux虚拟文件系统(VFS)隐藏各种了硬件的具体细节,为所有的设备提供了统一的接口。而且,它独立于各个具体的文件系统,是对各种文件系统的一个抽象,它使用超级块super block存放文件系统相关信息,使用索引节点inode存放文件的物理信息,使用目录项dentry存放文件的逻辑信息。
1、系统调用:提供特定的内核空间与系统空间的信息传递
2、信号:内核空间出现异常会发送信号给进程
3、/proc 可以读取内核的信息,并修改部分信息
4、 文件:可以通过指定文件的读写操作来实现通信,流程不够实时,需要循环检测来实践
用户空间read()--内核空间sys_read()--scullfop.read--scull_read()
当内核执行用户自己的代码时称为用户态此时处理器在特权级最低的用户代码中运行,使用到内核程序的时候在内核态运行,特权级最高ring0,一般中断、异常、系统调用,都能将cpu从用户态切换到内核态。
1、作用,启动内核前的一段程序,可以初始化硬件的相关设备,将设备的软硬件环境带到一种合适的状态
1、汇编实现,完成依赖于CPU体系架构的设置,调用阶段二的代码。
硬件设备初始化: 配置时钟相关参数,比如分频系数等等(内核时钟,总线时钟,IO接口时钟)
关闭看门狗:看门狗用于防止程序跑飞,但是在 uboot启动阶段,还没有加载 操作系统,
关闭MMU:MMU是用于虚拟地址向物理地址进行映射的一个结构。在 uboot阶段操作的就直接是 物理地址,所以不需要转换。
关闭中断:uboot引导linux起到的过程中本身就是一个完成的过程,不需要中断机制
为第二段代码准备RAM空间
设置pc指针指向start_armboot函数 跳转到第二段代码的C语言入口
2、C语言实现
初始化硬件设备:最少需要一个串口与用户进行交互。
将内核和根文件系统从FLASH中读到RAM中
为内核设置启动参数:重要参数bootcmd以及booargs
调用内核
1、原子操作
2、互斥锁 资源被占用时,程序只能进入休眠状态,
3、信号量
4、自旋锁 自旋锁保护的临界区不能进入休眠,所以效率高。但是一直占用CPU资源
自旋锁禁止处理器抢占,信号量不限制处理抢占,一旦自旋锁支持睡眠,cpu无法运行。
top可以实时查看内存的使用情况。
free-m可以查看内存使用情况
cat /proc/meminfo 查看更详细情况
大端:数据低位在高地址,高位在低地址
小端:低位在低地址,高位在高地址
小端序:先存最低有效位,intel AMD一般用这个方式
大端序: 先存最高有效位,ARM,Motorola采用这个方法
网络中 要按照网络字节序,一般为大端。
预处理:头文件包含,替换宏定义,识别条件编译
编译: .c 变成 .s 生成汇编文件
汇编 .s 变成 .o 汇编成二进制文件
链接 .o 变成 可执行文件
作用:硬连接的作用是允许一个文件拥有多个有效路径名,这样用户就可以建立硬连接到重要文件,以防止“误删”的功能。
软链接又称之为符号连接(Symbolic Link)。软链接文件类似于Windows的快捷方式。它实际上是一个特殊的文件。
区别:A是B的硬链接代表inode号相同,删除了一个对另外一个没有影响,软连接inode不同,a指向的是另一个数据块,这个数据块代表着B的位置,把B删了之后,A的数据块里面的内容将是无效链接。
中断是一种使CPU停止正在执行的任务,转而去处理特殊事件的操作,引起中断的事件被称为中断源。
中断的处理过程一般由三部分组成:
1、准备部分,保护现场,保存之前程序运行的上下文
2、处理部分,真正执行中断处理函数
3、结束部分,关闭中断,恢复现场,打开中断
1、挂起:pm-suspend
2、休眠:pm-hibernate
3、重启 reboot
4、关机 shutdown -h now
当运行数据超出物理内存容纳限度的时候,部分数据就会自行“溢出”,这时系统就会将硬盘上的部分空间模拟成内存——虚拟内存,并将暂时不运行的程序或不使用的数据存放到这部分空间之中,等待需要的时候方便及时调用。
RAM:(Random Access Memory,RAM)随机存储器,也叫内存,支持随机读写,电源关闭时RAM不能保存数据
ROM:(Read Only Memory,ROM)只读存储器:只能读出,断电后依然能够保存数据,但不等于硬盘
1、kmalloc和vmalloc是分配的是内核的内存,malloc分配的是用户的内存
2、kmalloc保证分配的内存在物理上是连续的,内存只有在要被DMA访问的时候才需要物理上连续,malloc和vmalloc保证的是在虚拟地址空间上的连续
3、kmalloc能分配的大小有限(可分配的内存大小范围在32~131027(128k)字节),vmalloc和malloc能分配的大小相对较大
4、vmalloc比kmalloc要慢。 尽管在某些情况下才需要物理上连续的内存块,但是很多内核代码都用kmalloc来获得内存,而不是vmalloc。这主要是出于性能的考虑。vmalloc函数为了把物理内存上不连续的页转换为虚拟地址空间上连续的页,必须专门建立页表项。糟糕的是,通过vmalloc获得的页必须一个个地进行映射,因为它们物理上是不连续的,这就会导致比直接内存映射大得多的TLB抖动,vmalloc仅在不得已时才会用–典型的就是为了获得大块内存时。
37个arm寄存器
未分组寄存器R0~R7
分组寄存器R8~R14
寄存器R13在ARM指令中常用作堆栈指针SP
R14称为子程序链接寄存器LR(Link Register)
程序计数器PC(R15)
R16用作CPSR(CurrentProgram Status Register,当前程序状态寄存器)
bin …基本命令的可执行文件
boot …内核映像已经启动时需要用到的一些文件
dev …设备文件
etc …系统配置文件,包括启动文件
home …用户目录
lib …基本库,例如C库和内核模块
lost+found …在文件系统修复时恢复的文件
mnt …临时文件系统的挂载点
nfsroot …nfs文件夹,一般不使用
opt …添加的软件包
proc …内核以及进程信息的虚拟文件系统
root …root用户目录
sbin …用于系统管理的可执行程序
share …共享文件目录
sys …系统设备和文件层次结构,向用户提供详细的内核数据信息
tmp …临时文件
usr …该目录的二级目录包含许多对用户很有用的应用程序和文档
var …存放系统日志或一些服务程序的临时文件
probe函数是总线型驱动中涉及到的函数,是当驱动与设备匹配之后执行的函数,主要完成设备初始化,其实是普通字符驱动中init函数所完成的功能,包括注册设备号,初始化cdev,添加cdev,创建类等等。
与probe相对的就是remove函数,其实作用是解除映射以及销毁设备号和cdev。
总述:总线是处理器与一个或多个设备之间的通道,在设备模型中,所有的设备都通过总线相连。在最底层,Linux系统中的每一个设备都用device结构的一个实例来表示。而驱动则是使总线上的设备能够完成它应该完成的功能。
设备:设备代表真实的、具体的物理器件,在软件上用器件的独特的参数属性来代表该器件。
驱动:代表着操作设备的方式和流程。对于应用来说,应用程序open打开设备后,接着就read访问这个设备,驱动就是如何实现这个访问的具体的过程。驱动主要包括两部分:
总线:代表着同类设备需要共同遵守的工作时序,不同的总线对于物理电平的要求是不一样的,对于每个比特的电平维持宽度也是不一样,而总线上传递的命令也会有自己的格式约束。在软件层面主要是用来管理设备与驱动。
设备与驱动想让系统知道自己首先要注册自己。另外总线还相当于红娘,给设备和驱动牵线,最简单的匹配方式就是直接对名字匹配。这个匹配方式一般是match函数
总线在匹配设备和驱动之后驱动要考虑一个这样的问题,设备对应的软件数据结构代表着静态的信息,真实的物理设备此时是否正常还不一定,因此驱动需要探测这个设备是否正常。我们称这个行为为probe,至于如何探测,那是驱动才知道干的事情,总线只管吩咐得了。所以我们可以猜测在总线的管理代码中会有这样的逻辑:
if(match(device, driver) == OK) {
driver->probe(); }
1、开机自检:主要对各种硬件进行检测,包括CPU 内存、主板、硬盘等等。
2、uboot 上下两段,引导内核
3、就开始运行第一个程序 /sbin/init
4、根据运行级别运行进程
5、启动 /etc/init.d 中的开机脚本
反问环节很重要,没有肯定是不行的,如果问得好,能稍微拉高一点面试官对你的好感。
1、新人培训
2、部门以及常用技术栈
3、典型一天或者一周的任务分配是怎样的。会议频繁吗?
4、