一、关键字
定义一年有多少秒
#define SECOND_YEAR (365*24*60*60)UL
volatile
一种数据类型,提醒编译器变量是随时可变,因此在编译的过程中不要对其进行优化,
而是直接访问变量的内存地址获得获得数据。一般用于访问特殊的寄存器地址,
或者用于多线程多进程之间的通信。
static
1.定义在函数中的变量为局部变量,加上static关键字就是静态局部变量,它拥有同全局变量一样的生存期,
但是没办法被函数外访问,作用域没有改变
2.静态全局变量,只能在当前文件中使用,其他文件可以定义重名的变量,一般在不需要与其他文件共享
该变量时可以使用
面向对象:
1.静态成员:
类内申明,类外初始化
所用对象公用一个静态成员变量
静态成员也有访问权限的问题(public,private)
2.静态成员函数
所有对象共用一个静态成员函数
静态成员函数只能调用静态成员变量,不能调用非静态成员变量
也有访问权限的问题
extern
1.引用同一个文件的变量
在使用之前引用,也就是可以在程序的任何位置进行申明而不报错
2.引用其他文件变量
相当于是全局变量,可以供该文件使用
3.引用另外一个文件函数
可以不用包含该头文件而使用该函数
4.extern"C"
c++中可以调用c代码
new/delete malloc/free
1.new/delete是操作符 而malloc/free是标准c库函数
2.new/free可以调用构造和析构函数,malloc/free用于分配内存
struct和union
1.struct可以有多种不同类型数据类型成员,且拥有独立的内存空间
联合体也有多种数据组合,但是他们共用同一片内存空间,修改一个成员变量的值,其他成员也会被重写
联合体变量的长度等于最长成员的长度
const
修饰变量为常量
修饰参数为常量
修饰返回值为常量
只读属性
常量指针和指针常量
sizeof strlen
1.sizeof计算该参数所占内存空间大小,输入参数可以是数据类型、函数
2.strlen计算只能计算字符串的大小,输入参数只能为char*,且遇到‘\0’就停止
typedef define
1.typedef给相当于给数据类型起一个别名
2.define只是简单的做一个字符串的替换,不做正确性检查,只有带入之后才会发现
const define谁定义常量更好
define只是简单的做文本的替换,存在于编译期,不分配内存空间,不做正确性检查
const 修饰数据变量,保护数据不被修改
所以一般使用const定义常量
c++和c中结构体不同点
1.c的结构体默认是public,而c++中struct是有权限的
2.c中结构体不可以对成员初始化,而c++可以
3.c中使用结构体必须使用struct或者typedef来定义变量,但是c++中定义之后可以直接使用
4.c的空struct大小是0,而c++大小为1
5.c的struct不可以继承,而c++可以继承
6.c中struct不可以有函数,而c++可以
class和struct区别
1.struct默认是public,而classs有权限设置
2.struct不可以继承吗,而class可以继承
3.class可以定义模版,而struct不可以
union用来判断大小端的问题
大端字节序:高字节存放低位地址,低字节存放高位地址
小端字节序:低位字节存放低位地址,高位字节存放高位地址
int main(){
union Test{
int a[2];
short t;
}test;
test.t=0x1234;
if(a[0]==0x12 && a[1]==0x34){
printf("大端字节序!\n");
}
}
大小端转换
移位之后与0x00FF相与
enum
枚举的成员的值默认为前一个成员值加1
register
寄存器变量,存放在寄存器中,该值不能取地址,并且是整数,不能是浮点数
auto
自动变量,一般没有特别说明的局部变量都是auto类型,存放在栈中
左值和右值
左值一般支出现在等号左边的变量,一般来说他的值是可以改变的
右值一般指出现在等号右边的变量或者表达式,通常左值可以当做右值,右值不能做左值
什么是短路求值
++a和a++
局部变量能否和全局变量重名
能,局部变量会屏蔽全局变量
全局变量能否定义在多个不同.c文件的头文件中
可以,在不同的.c文件中使用static来申明同名的全局变量,前提是在一个.c文件中对该变量进行赋值
gets和scanf函数的区别
1.gets可以接受空格,scanf遇到空格就会结束
2.gets仅用于读入字符串,scanf为标准的格式化输出函数,可以读入任意的c语言基础类型数据变量
3.gets返回值是char* ,读入成功时会返回字符串指针地址,scanf返回值是int型,返回实际成功赋值变量的个数,遇到文件结尾标识符发返回EOF
c语言编译过程中,volatile和extern关键字分别在哪个阶段起作用
volative是在预处理阶段,代码优化是在汇编,extern在链接阶段
printf函数返回值
返回输出的字符串的个数
char* str1="hello"和char str2[]=“hello”
str2是地址常量,也就是常量指针,不可以修改地址
str1相当于是指针常量,他不可以修改元素的内容
全局变量和局部变量同名,如何使用全局变量
1.使用函数,把全局变量包在里面
2,使用{extern}
二、内存
c语言内存分配方式
1.栈上分配
执行函数之前,定义局部变量都会在栈上创建,函数执行完毕之后就会自动释放
2.静态全局变量区
全局变量和静态变量
3.堆上分配
程序员使用malloc,free,new,delete等分配内存
c++内存内存是如何管理的
分为代码区,数据区,BSS区,堆区,栈区,文件映射区
代码区:存放代码
数据区:存储及初始化全局变量和静态变量
BSS区:存储未初始化的全局变量和静态变量
堆区:手动分配的内存(malloc/free)
栈区:局部变量 参数 返回值
文件映射区:动态链接库,mmap函数文件映射
堆和栈的区别
1.申请方式:栈是操作系统自动分配释放,堆是手动
2.申请大小。栈空间有限,是向低地址拓展的连续内存空间,堆是向高地址拓展的不连续的空闲空间
3.申请效率。栈是由系统分配的,速度快。堆是由malloc函数分配,速度慢,可能会产生内存碎片的问题
栈的作用
1.存储临时变量和返回值
2.多线程的基础。每一个线程都需要一个栈来存储临时变量和返回值
堆栈溢出是由于什么问题导致的
1.函数的递归调用。函数调用的层次太多,系统要在栈中不断地保存函数的现场和产生的变量,调用的太深就会产生栈溢出的问题
2.数组越界访问
C语言没有提供数组下标的检测机制,如果出现数组下标超出数组范围则会出现内存访问错误
3.指针的非法访问
指针保存了一个非法的地址,通过这个指针访问的地址就会产生内存访问错误
4.动态内存申请
动态内存申请空间没有释放。C语言程序没有垃圾资源回收的机制,需要程序员主动释放动态空间。申请的动态内存空间是堆空间,没有主动释放堆空间就会造成堆空间溢出
栈在C语言中有什么作用
1.存储临时变量,临时变量包括:函数的返回参数,函数内部变量
2.多线程编译的基础,每一个线程都需要一个单独的栈空间
内存泄露
程序员手动申请了一个内存空间,但是没有手动释放他,也就是没有指针指向他,那么这一段内存就泄露了
避免内存泄露方法
1.分配内存用链表管理,使用完毕从链表删除,程序结束检查链表
2.良好的编程习惯,申请释放配对使用
3.smart pointer
字节对齐
各种数类型按照一定的规则在空间上排列,而不是按照顺序一个接一个排列,这个就是对齐!
主要是求复合类型的数据结构大小,结构体,联合体
为什么需要内存对齐?
主要是内存访问效率的问题,假设是32位cpu,那么他每次取指就是4个字节。对于一个int数据类型来说,如果每次存发都按照四字节对齐,那么他一次取指就可以将这个数据取出来。但是如果这个int数据刚好处于取数的边界,就需要进行两次取指才能把这个数据取出来。
假设一个int型数据存放在0x02~0x05的地址空间,那么系统需要先读0x01~0x04内容,再读0x05~0x08,然后将这两部分拼接得到值
C语言参数压栈的顺序是怎么样的
从右往左,栈上分配空间是由高位地址向低位地址扩展的,所以先入栈的是高位地址
c++如何处理返回值
栈空间最大值是?
windows下是2MB,linux下是8MB,使用ulimit -s命令
在1G内存的计算机中能否malloc1.2G
可以,因为malloc函数是向进程申请虚拟内存,与物理地址空间没有关系
strcat,strncat,strcmp,strcpy哪些函数会导致内存溢出?如何改进?
strcat函数将src字符串拼接到dest字符串后,需要dest空间足够大,否则机会内存溢出
strncat函数是将n个字符拼接到dest字符串后,并覆盖dest字符串的'\0',如果n大于src的长度,则会将src整个添加在dest尾部,如果n<src的长度,则会将src n个字符长度添加在dest尾部
strcmp函数是比较从左到右比较两个字符串的大小,直到比较出大小或者遇到'\0'结束返回
strcpy函数是将src字符串复制到dest的内存空间中,因此需要保证dest内存空间足够大,否则就会溢出
malloc ,calloc,realloc内存申请函数
1)malloc函数。其原型void *malloc(unsigned int num_bytes);
num_byte为要申请的空间大小,需要我们手动的去计算,如int *p = (int *)malloc(20*sizeof(int)),如果编译器默认int为4字节存储的话,那么计算结果是80Byte,一次申请一个80Byte的连续空间,并将空间基地址强制转换为int类型,赋值给指针p,此时申请的内存值是不确定的。
(2)calloc函数,其原型void *calloc(size_t n, size_t size);
其比malloc函数多一个参数,并不需要人为的计算空间的大小,比如如果他要申请20个int类型空间,会int *p = (int *)calloc(20, sizeof(int)),这样就省去了人为空间计算的麻烦。但这并不是他们之间最重要的区别,malloc申请后空间的值是随机的,并没有进行初始化,而calloc却在申请后,对空间逐一进行初始化,并设置值为0;
3)realloc函数和上面两个有本质的区别,其原型void realloc(void *ptr, size_t new_Size)
用于对动态内存进行扩容(及已申请的动态空间不够使用,需要进行空间扩容操作),ptr为指向原来空间基址的指针, new_size为接下来需要扩充容量的大小。
三、指针
什么是指针
指针其实也是变量,但是这个变量存储的是内存地址
指针本身所占的内存区
在32位系统当中,无论什么样类型的指针所占的内存空间都是4个字节
数组指针和指针数组
数组指针,本质是一个指针,这个指针指向一个数组
指针数组,本质是一个指针类型的数组,数组存储的类型是指针
函数指针和指针函数
函数指针,本质是一个指针,这个指针指向的是一个函数的地址
指针函数,本质是一个函数,不过他的返回值是一个指针
数组名和指针的区别
1.数组保存的是数据,指针保存的是地址,数组名表示第一个元素的地址
2.sizeof不同,指针为指针的大小,数组名去该数组的所有元素的大小
指针常量,常量指针,指向常量的指针
指针常量:int * const p;地址不可变,值可变
常量指针:const int *p;地址可变,值不可变
指向常量的指针:const int* cosnt p;地址和值都不可变
指针与引用的区别
1.指针是地址,引用相当于是给变量取别名
2.引用的本质是指针常量,地址不可变,值可变
3.指针需要解引用
4.sizeof不同,指针大小,一个是对象大小
野指针
1.指针创建时没有初始化或者指向的是不可用的内存地址的指针
2.指向被free/delete释放了的地址且没有置为null的指针
解决:
初始化指针要置NULL
申请内存之后判空
指针释放后置NULL
使用智能指针
智能指针
c++智能指针是一个类,用来存储指针(指向动态分配对象的指针)
四种智能指针:
auto_ptr(弃用)
shared_ptr:共享智能指针,使用引用计数,所有的shared_ptr拷贝都指向同一个内存,最后一个shared_ptr析构才会释放内存
unique_ptr:独占智能指针,不允许其他智能指针共享其内部指针,也不能赋值给其他的unique_ptr,但是可以通过move
weak_ptr:是shared_ptr指针的补充,主要是解决两个shared_ptr指针相互引用造成死锁的问题。
智能指针内存泄露问题(死锁)
引入weak_ptr来解决shared_ptr两个指针互相引用造成死锁的问题。weak_ptr不使用引用来计数!
死锁:对象 a 有指向对象 b 的共享指针,对象 b 也有指向对象 a 的共享指针,那么它们都不会被析构
数组名num/&num的区别
一维数组:
num+1偏移到下个元素,&num+1是偏移整个数组
二维数组:
num+1是偏移一个一维数组,&num+1是偏移整个数组
有了指针为什么还需要引用
使用指针会出现:
1.操作空指针
2.使用野指针
3.不知不觉改变了指针的值,但是仍然继续使用
引用就是解决上述问题
1.引用不存在空引用(防止空指针)
2.引用必须初始化(防止野指针)
3,引用始终指向初始化对象(保证指针值不变)
使用指针的好处
1.指针可以动态分配内存
2,可以操作链表节点
3,解析字符串
4.相同类型的指针可以直接复制
指针和引用的异同,如何转换
1.两者其实都是指针概念,指针保存的是内存的地址,引用是某块内存的别名,这个内存一旦初始化就不能再去指向别的内存
2.两者都会占用内存:
区别:
1.指针是实体,引用是别名
2.引用的本质是指针常量,指向指针的地址不可以变,但是指针的内容可变
3.引用必须初始化
4,引用不能为空
5.引用不需要解引用
6.sizeof引用是变量值的大小,而指针是指针的大小
7.引用自增是值自增,指针自增是地址自增
转换:
指针转引用:指针用*就可以转换成对象,可以用在引用参数当中
引用转转指针:引用用&
sizeof(数组名)和sizeof(&数组)
sizeof(数组);整个数组的大小
sizeof(&数组);指针的大小
数组下标取负值
从当前地址向前寻址
使用Free释放内存之后,指针还能使用吗?
free释放掉内存之后,只是把内存归还给系统,内存里的值会被清除掉,但是指针指向的是这个内存的地址,所以并不会为NULL
指针加减法
指针减指针,也就是地址相减。但是指针之间不能做加法
四、预处理
预处理标志#error是什么意思?
遇到\#errno就生成一个编译错误提示信息,并停止编译
#ifndef #define #endif
一般用在头文件当中,防止头文件的重复包含
#include" "和#include<>的区别
#include“”首先寻找自己定义的头文件,
#include<>首先寻找系统头文件
typedef和#define
1,#define是预处理阶段,只是简单进行文本的替换,不做类型的检查
2,typedef是关键字,用于给已经存在的数据类型起一个别名
define的一些缺点
1.无法进行类型的检查
2.无法调试
3.运算优先级的问题
头文件作用
1.通过文件调用库,对源码进行保护
2.头文件加强安全类型的检查,编译器报错
简述c++从这个代码到.exe可执行文件的过程
预处理:
对所有的#define展开宏定义
处理所有的条件编译指令,#if #ifdef
处理#include预编译指令
编译:
对代码语法进行分析检错,做代码优化,生成汇编文件
汇编:将汇编代码转换成机器可以执行的指令
链接:将源文件产生的目标文件进行链接,形成可执行文件
头文件是否可以添加静态变量
不可以,静态变量是有记忆的,不会随着函数的结束而结束,定义在头文件当中则可以被多个文件使用,被多个文件开辟出空间,造成资源的浪费
枚举和define
枚举是在编译阶段,给一组常量赋值,代码更加的简洁,并且会做错误检查,
define是在预处理阶段,只是简单的字符串替换,不做错误检查
const和define的区别
1.两者都可以定义常量
2.const是在编译阶段,define是在预处理阶段
3.const会做错误的检查
内联函数和宏函数
利用宏定义的形式实现一个功能
内联函数:在函数面前加inline
五、函数
宏函数和内联函数
1.宏函数其实是利用宏定义实现一个简单功能,其实不是函数,而是宏定义,只是简单的字符串替换
2.内联函数是一个函数,但是内联函数只能用于函数体简单的语句,不能使用while,for等流程控制语句
3.内联函数和宏函数如何提高效率
内联函数在编译的时候将代码进行插入,每次调用内联函数的就是将内联函数内容拓展,节省了函数调用的时间
4,宏函数只是简单的做字符串替换,不进行类型错误检查;内联函数会做类型错误检查
普通函数和内联函数的区别
内联函数代码简单,不能使用复杂的控制语句 while for等,并且自己不能调用自身
简单一句话:普通函数调用的话会进入到函数的入口,有返回值,参数等,而内联函数只是将代码进行替换,有n个地方调用内联函数就有n个地方复制的那串代码
为什么析构函数必须是虚函数
1.c++允许基类的指针指向派生类的对象,当new了一个子类时,然后delete时,如果析构函数不是虚函数,
那么这个子类只会调用基类的析构函数,而派生类的析构函数没有调用,造成内存的泄露,
因此基类的析构函数必须是虚析构函数,这样在delete时会调用派生类的析构函数!
为什么c++默认的析构函数不是虚函数
虚函数拥有虚函数表和虚指针表,占用额外的空间,当对于那些不会被继承的类就不需要虚函数,节省空间
静态函数和虚函数的区别
1.静态函数编译时确定运行的时机
2.虚函数运行是动态绑定的,且使用虚函数表,内存开销增加
重载与覆盖
1.覆盖:就是子类继承父类的方法,并对父类的方法重新进行改写(函数名,返回值,输入参数都不能变)
2.重载:重载就是可以存在同名函数,但是函数的返回值或者输入参数其中之一必须要改变
虚函数实现多态原理
虚函数表是一个类地址表,子类创建时,按照函数的声明将虚函数放入地址表。当子类重写父类的虚函数时,父类虚函数在表中的地址就会被子类覆盖
C语言函数调用方法
1.使用栈来支持函数的调用,栈被用来传递返回值,参数,局部变量等
2.函数调用主要是调用栈帧结构
select函数
功能:监听指定的fd_set集合中的文件描述符
将fd_set从用户空间拷贝到内核空间,在内核空间中遍历所有的socke文件描述符,如果没有socket套接字满足条件,则会阻塞或者等待时间结束;当有socket套接字可读或者可写,则会继续执行下一步
函数原型:
int select(int maxfd,fd_set *readset,fd_set *writeset,fd_set *exceptset,
, const struct timeval *timeout)
FD_SET();
FD_ZERO();
FD_ISSET();
FD_CLR();
fork wait exec(进程与线程)
文件IO
数组的下标可以为负数吗?
可以,数组的下标指地址偏移量,根据偏移量可以得到目标地址,但一般建议不使用这种方式
inline函数和宏定义的区别
宏和函数的优缺点
宏由编译计算,增加编译的时间,函数由运行时计算,增加运行时间;
函数的返回值 输入参数有数据类型,而宏定义只是简单的字符替换
ASSERT()作用
ASSERT()是一个测试程序时经常使用到的宏,运行时计算括号内的表达式,如果为FALSE程序报错,
并且终止执行;如果为TRUE,怎继续执行之后的语句
strcpy()和memcpy()区别
1.复制内容不同。strcpy只能复制字符串,而memcpy能够复制多种不同数据类型的内容:数组,字符,结构体
2.复制方法不同。strcpy复制字符串只有遇到'\0'才停止,所以可能会造成内存溢出,memcpy则是靠第三个参数指定复制的数据长度。
六、数据结构与算法
map与set区别及底层实现
1.底层实现都是红黑树
2.map存储的是键值对,key-value,根据key可以找到value
3.set是统一类型数据的集合,值是唯一的,且会自动进行排序
4.map可以根据key找到value,修改value的值,而set中的值不允许改变
5.map可以使用key作为下表,而set只能使用find查找
vector与deque
vector称为单端数组,但是不像数组那样是静态的内存空间,而是动态可以拓展的。
动态拓展:
当插入一个数据后发现原有的内存空间不够了,这时vector向内存申请一块新的更大的内存空间,将原有空间的数据拷贝进新的内存空间,然后释放原有的内存空间
deque:双端数组
工作原理:
deque存在一个中控器,存储各个缓冲区的地址,缓冲区中存放真实的数据。
与vector不同之处:
1.头插法,vector效率要差一点
2.访问速度deque相较于vector要慢(deque访问完一个缓冲区的数据后需要通过中控器寻找下一个缓冲区的地址)
STL中allocator有什么作用
1.分配内存由alloc::allocator负责,释放内存由alloc::deallocate()负责,
构造对象由::construct负责,对象析构由::destory()负责
2.提高内存管理的效率,STL采用两级配置器,当分配的内存空间超过128B时,使用一级配置器来管理内存,
也就是使用后malloc,free,new,delete来申请释放内存空间;
当分配的内存空间不足128B时,则使用第二级空间配置器来管理,采用内存池技术,通过空闲链表来管理内存
STL迭代器如何删除元素
序列容器 vector dequeue,使用erase(iterator)后,后面的每个元素的迭代器都会失效,
但是后面的元素会往前移动一个位置,erase会返回下一个有效的迭代器
关联性容器map set,使用迭代器erase后,因为底层是红黑树,因此不会影响后面元素的迭代器
list使用不连续的分配内存空间,他的erase方法会返回下一个有效的迭代器地址,上述方法都可以使用
STL中map与unordered_map有什么区别
1.map底层实现是红黑树,unordered_map底层是hash表
2.map中序遍历是有序的,unordered_map是无序的
vector和list区别
1.vector是数组实现的,list是双向链表
2.vector是顺序存储的,list是随机的
3.vector是一次性分配内存,不够才进行二次扩容,而list是一个个分配
4.vector随机访问性能好,插入和删除操作操作满,list则反之
迭代器与指针
1.迭代器也叫做游标模式,提供一种顺序访问对象元素的方式,但是又不暴露对象内部表示
2.迭代器是类模版,表现的像指针,重载了指针的一些操作,指针的++只是增加地址,但是不能对list生效,
而迭代器可以
3.迭代器有着更好的用法begin,end不用担心越界
STL中resize和reserve区别是什么
resize改变容器中元素的数量,会新增元素0,而reserve只是增加容器空间,不会新增元素
链表和数组的区别
1.数组是按照顺序在连续内存空间(栈上)进行存储的,而链表是在堆上是随机存储的
2.链表不支持随机访问,数组支持下标操作
3.数据删除或者插入一个元素需要移动很多元素,因此他的插入和删除效率较差!链表查找某个元素需要从头开始遍历,所以他的查找效率较差!
判断链表是否有环
哈希表:最简单的,我们每到一个节点,判断他是否访问过即可。具体可以使用哈希表数据结构
遍历所有链表节点,每次到达一个新的节点就将其放入到哈希表中,如果出现重复,就可以知道链表是有环的。
如何知道环的长度
利用哈希表(set)
算法思想:
顺序表
1.顺序表 底层实现原理是数组
//结构原型
struct List{
int* arry;//底层数组,使用指针方便扩容
int capacity;//数组容量
int size;//元素个数
};
typedef struct List* ArryList;
初始化顺序表:申请内存,结构体赋值
指定位置插入元素:检查插入位置合法性,检查是否需要扩容(使用reallo函数,扩容赋值),将插入位置之后的所有元素后移一位
删除指定位置元素:判断删除位置合法性,删除位置之后的所有元素往前移动1位
链表
结构原型
struct ListNode{
int element;
struct ListNode* next;
};
typedef struct ListNode* Node;
初始化链表:头结点,next=NULL
链表指定位置插入:从头结点开始遍历,找到前驱节点,使用malloc申请内存,赋值,将新节点的next==前驱节点的next,将前驱节点的next==新节点
删除节点:先找到前驱节点,创建变量保存前驱节点next,保存要删除节点的指针,将前驱节点next==前驱节点next->next,然后free要删除节点
双向链表和循环链表
struct ListNode{
int element;
struct Listnode* prev;
struct ListNode* next;
};
typedef struct ListNode* Node;
指定位置插入:
一般情况:找到前驱节点,malloc申请新节点,新节点next=前驱节点next,前驱节点->next->prev=新节点,新节点prev=前驱节点,前驱节点next=新节点
特殊情况:最后位置插入,前驱节点的next=新节点,新节点prev=前驱节点,其余为NULL
删除指定位置元素:
一般情况:找到前驱节点,保存前驱节点next指针,将前驱next==删除节点的next,删除节点的next->prev=前驱节点。free删除节点
栈
一种先入后出的数据结构,只能从栈顶存取数据,出栈和入栈操作
底层是数组实现栈
struct Stack{
int* element;
int top;//栈顶,表示当前栈中元素个数,
int capycity;
};
typedef struct Stack* stack;
出栈:
入栈:
底层是指针实现栈
struct Stack{
int element;
struct Stack* next;
};
typedef struct Stack* stack;
直接将头结点连接到栈顶节点,栈顶节点连接后续栈内节点,和单链表一样
入栈:使用头插法(单链表使用尾插法),找到头结点,malloc创建新节点,新节点next->头结点next,头结点next=新节点,
出栈:找到头结点,新建变量存储头结点next,将头结点->next=头结点->next->next;free临时节点
队列
队列是一种先入先出的数据结构,相当于队列头取数据,队列尾插入数据,底层使用数据实现,采用循环队列的方式,通过判断队首队尾指针是否重合来判断队列是否已满
struct Queue{
int* element;
int capycity;
int rear,front;//队首,队尾指针
};
typedef struct Queue* queue;
还有一种就是使用链表来实现队列
二叉树
二叉树是一种特殊的树,他的度只能为2,因此有左右子树之分,
struct TreeNode{
int element;
struct TreeNode* left;
struct TreeNode* right;
};
typedef struct TreeNode* Node;
二叉树遍历:前序遍历 中序遍历 后序遍历
前序遍历:
1.打印节点
2.前序遍历左子树
3.前序遍历右子树
void preOrder(Node root){
if(root == NULL) return; //如果走到NULL了,那就表示已经到头了,直接返回
printf("%c", root->element);
preOrder(root->left);
preOrder(root->right);
}
中序遍历:
1.中序遍历左子树
2.打印节点
3.中序遍历右子树
void inOrder(Node root){
if(root == NULL) return;
inOrder(root->left); //先完成全部左子树的遍历
printf("%c", root->element); //等待左子树遍历完成之后再打印
inOrder(root->right); //然后就是对右子树进行遍历
}
后序遍历:
1.后序遍历左子树
2.后序遍历右子树
3.打印节点
void postOrder(Node root){
if(root == NULL) return;
postOrder(root->left);
postOrder(root->right);
printf("%c", root->element); //时机延迟到最后
}
七、类和数据抽象
c++类成员访问权限
1.类对象通过public protect private三种 关键字来管理成员变量和函数的访问权限
2.类内随便访问,类外只能访问public成员
引用和指针的区别
1.引用必须初始化,指针不需要
2.引用初始化后不能被改变,指针可以改变
3.不存在指向空值的引用,但是存在指向空值的指针
struct与class的区别
两者都可以定义类,但是struct没有访问权限的区别,默认都是public
面向对象和泛型编程
1.面对对象是一种程序设计思想,把对象作为程序的基本单元,包括数据类型和操作数据的函数
2.泛型编程让类型参数化,程序从逻辑上抽象,使用对象作为传递的参数。
右值引用和左值引用的区别
左值:既可以出现在等号左边也可以出现在等号右边(变量)
右值:只能出现在等号右边(常量)
int value1=10;
int &value2=value1;int &value3=value2;//左值引用
int &&value4=10;// int &&value4=value1;错误,右值引用只能引用右值变量
1.左值可以寻址,右值不可以寻址
2.左值可以被赋值,右值不可以被赋值,但可以给左值赋值
3.左值可变,右值不可变
析构函数可以为virtual,构造函数不能,为什么
1、虚函数的作用主要是用于多态,若构造函数使用,派生类必须在初始化列表给基类参数初始化!
2 构造函数运行时对象的动态类型还不完整,所以无法动态绑定
c++类和c中struct区别
c++中的类有权限设置:public protect private
struct中没有权限设置,默认权限为public
c++空类默认有哪些函数
1.构造函数
2.析构函数
3.拷贝构造
4.赋值运算符重载
5.取指运算符重载
6.const取指运算符重载
静态成员函数与非静态成员函数
静态成员函数:类内申明,类外初始化,只能访问静态成员变量,且所有对象共享同一个静态函数
八、 面向对象
面向对象与面向过程有什么区别
面向过程是将问题分解成一个个步骤,然后使用函数将这些步骤一步步解决!面向过程是将问题分解成为一个个对象,建立对象不是为了完成一个具体步骤,而是描述某个事物在解决问题中的行为
1.面向对象以对象对中心,面向过程以步骤为中心
2.面向对象将代码封装成一个整体,其他对象无法访问其内部数据。面对过程将代码封装成一个个函数,各个函数存在控制与被控制的关系
3.面对对象是将问题分解成为不同的对象,给与每个对象不同的属性和行为;面对过程将问题分解成为不同的步骤,按照步骤完成编程
面对对象的基本特点
1.封装:将数据与过程分装起来,只有定义接口才能调用
2.继承:子类继承父类的功能,公有继承、私有继承、保护继承,他们的权限是不同的
3.多态:不同的对象继承父类的同一个功能有不同的实现
重写:动态多态。
重写需要满足的条件:
1.基类中必须要有虚函数
2.通过基类的指针或者引用调用这个虚函数
重载:静态多态
深拷贝和浅拷贝
浅拷贝:只是单纯的文本复制,默认构造函数就是浅拷贝
深拷贝:在堆区创建内存空间,再把数据进行复制
总结:浅拷贝只是对指针的拷贝,拷贝后两个指针指向同一片内存区;深拷贝不仅仅只是对指针的拷贝,而且还是对指针内容的拷贝,拷贝指针指向两个不同的内存空间
友元
关键字:friend
1.友元函数:友元函数可以访问友元类中private和protect的成员
2.友元类:友元A类可以访问友元B类中的保护和私有成员
基类的构造函数和析构函数能不能被继承
不能
哪些函数不能是虚函数
普通函数(非成员函数),静态成员函数,构造函数,内联函数,友元函数
普通函数:只能被overload,不能被override,编译时才绑定函数
静态:所有对象共享同一段代码
构造:虚函数是在不同对象产生执行不同的动作,但是现在对象都还没有产生
友元:友元函数无法继承,因此没有虚函数说法
vector底层实现
vector和list区别
vector底层是数组,list底层实现是双链表
vector支持随机访问,list只能遍历
vector是顺序内存
vector随机访问性能好,插入删除差,list反之
vector一次性分配内存,内存不够时才进行二倍扩容,list则是每插入一个节点就申请内存
vector扩容发生了什么
vector内存已满的时候会重新申请新的内存空间,将原有的内容复制到新的内存空间中国并销毁原来容器
存储空间的重新分配会导致迭代器的失效
删除元素(当前元素迭代器)会导致迭代器发生什么变化
1.关联式容器(map,set):删除当前元素的iterator,仅仅只会使当前的iterator失效。因为关联式容器的底层实现是红黑树,删除当前的节点并不影响其他的节点
2.序列式容器(vector):删除当前元素的iterator会导致之后的的迭代器都失效,因为vector是使用连续的内存,删除元素会使得所有元素往前移动一个位置。但是使用easer方法可以返回下一个有效的iterator
3.list容器:他是用不连续的内存,上述两种方法都可以是使用
如何理解迭代器
迭代器主要支持两种访问:随机访问和双向访问
对于序列式容器:vector,deque使用的是随机访问
对于关联式容器:map,unorder_map使用双向访问
容器的迭代器由什么组成
假设给你你个class,让你实现一个智能指针,你会怎么做?
STL中迭代器有什么用?为什么有指针还需要迭代器?
迭代器提供一种方法顺序访问聚合对象当中的元素,而又不暴露对象内部结构。
迭代器不是指针,而是类模版,表现的像指针,模拟了指针的一些功能,重载了指针的一些操作符
迭代器返回的值是引用而不是对象的值
迭代器产生的原因:
迭代器的访问方式就是将不同集合类的访问逻辑抽象出来,在不暴露对象内部构造的情况下达到访问的效果
STL迭代器是如何删除元素的
对于序列式容器,vector,dequeue使用easer后,后面元素的迭代器都会失效,每个元素都会向前移动一个位置,easer返回下一个迭代器的有效值
对于关联式容器,map,set,使用easer方法后仅仅只是当前元素的迭代器失效,后面元素的迭代器没有影响,因此在easer之前记录下一个元素迭代器即可
对于list,使用随机内存空间存储,上述两种方法都可以使用
map和set两者区别
map和set都是关联式容器,底层实现都是红黑树
1.map存储的键值对,set是关键字集合
2.set的迭代器是const,不允许修改元素的值
map可以修改value的值,但不允许修改key的值
STL的allocater有什么作用
STL中map是如何存放数据的
map是使用红黑树存放的,unorder_map采用的是hash表
STL中map和unorder_map的区别
map是按照红黑树来存放数据的,unorder_map是按照hash表存放数据的
map是按照operator<来判断元素的大小,找到合适的位置插入map中,因此对map遍历是有序的
unorder_map是计算元素的hash值,根据hash值判断元素是否相同,多以对unorder_map遍历是无序的
STL中的resize和reserver区别
resize改变容器中含有元素的数量,如果原来有10个元素,使用resize(15)会增加5个值为0的新元素
reserver是改变容器的最大容量,不会生成新元素,如果改变之后容器的capacity大于原先的capacity,就会分配一个新的内存空间,将之前的元素都复制到新的内存空间中
初始化列表和构造函数的区别
Example::Example():ival(0),dval(0.0){}
Example:Example(){
ival=0;
dval=0.0
}
结果是一样的!都是对类中成员进行一个赋值
但是初始化列表有以下两种情况:
1.成员类型为没有默认构造函数的类
2.const成员或者引用类型成员
类成员变量初始化顺序是什么
1.初始化成员列表方法赋值,与初始化成员列表的顺序无关,只与定义成员变量的顺序有关
2.不使用初始化列标方法,则与构造函数有关
public protect private继承的区别
1.public继承:public还是public,protect还是protect,private还是private
2.protect继承:public变为protect,其余不变
3.private继承:全都变为private
为什么基类构造函数和析构函数不能被继承
派生类的构造函数需要对自身成员初始化,也要对继承过来的基类成员初始化,基类没有默认构造函数的时候,派生类通过初始化列表来调用基类构造函数
c++如何阻止一个类被实例化
纯虚函数是什么
纯虚函数也称之为抽象函数,他只有函数名,返回类型,参数而没有函数体。
class<类名>
{
virtual 函数名(参数);
};
含有纯虚函数的类称为抽象类,纯虚函数一般不会在基类当中实现,而是留给派生类实现具体功能,因此纯虚函数主要是为了管理派生类的对象
c++哪些情况只能初始化列表,而不能赋值
1.类成员有const,引用成员时只能初始化
2.类成员中有没有默认构造函数的类时,应该使用初始化成员列表
3.派生类在构造函数中对自身成员进行初始化,还要对继承来的成员进行初始化,当基类没有默认构造函数的时候通过派生类初始化列表调用基类默认构造函数初始化
虚函数表建立时间
编译的时候建立
九、虚函数
虚函数注意内容
1.只有类中成员函数才能成为虚函数,构造函数、静态函数都不能成为虚函数,析构函数可以成为虚函数
2.虚函数只需要在申明的函数体前加上virtual关键字,定义中不需要
3.基类成员申明为虚函数后,子类成员自动为虚函数
什么函数不能申明虚函数
普通函数(非成员函数) 构造函数 静态成员函数 友元函数
十 、操作系统
进程与线程
进程是资源分配的基本单位,程序执行的一个实例,在程序执行时创建
线程是程序执行的基本单位,是进程的执行流,一个进程包含多个线程
进程,线程,协程的区别
进程和线程的区别:
1.进程是资源分配的基本单元,线程是程序执行的最小单元
2.进程拥有自己独立的地址空间,每创建一个进程,系统就会分配一个地址空间,建立数据表用来维护代码段,数据段,BSS段,堆,栈,进程的全局变量不是共用的。线程共享进程的地址空间。
3.线程之间通信更加方便,同一进程下线程共享全局变量,静态数据。而进程之间需要通过通信的ipc机制(管道,共享内存,信号,套接字)实现通信。
4.进程和进程之间是相互独立的,杀死一个进程不会影响到其他进程。而杀死一个线程可能会导致进程死掉
5.每个线程都用于自己独立的栈段和寄存器组
线程和协程:
1.一个线程可以有多个协程,一个进程可以有多个协程
2.线程和进程都是同步机制,协程是异步机制
3.协程能保存上一次调用的状态,每次程序重入的时候就相当于进入上一次调用的状态
4.线程是抢占式的,协程是非抢占式的,需要用户自己释放使用权切换到其他协程,因此同一时间其实只能有一个协程拥有运行权,相当于单线程能力
5.线程是协程的资源。协程通过interceptor来间接使用这个资源
何时使用多线程?何时使用多进程
对资源保护和管理要求高,不限制效率和内存使用多进程
要求效率高,切换频繁,使用多线程
创建进程的方式
1.系统初始化,后台进程,守护进程
2.fork()函数
进程有几种状态
进程的五种状态:创建,就绪,执行,阻塞,终止
创建状态:进程在创建时需要申请一个空白PCB,向其中填写控制和管理进程的信息,完成资源分配。如果创建工作无法完成,比如资源无法满足,就无法被调度运行,把此时进程所处状态称为创建状态
就绪状态:进程已经准备好,已分配到所需资源,只要分配到CPU就能够立即运行
执行状态:进程处于就绪状态被调度后,进程进入执行状态
阻塞状态:正在执行的进程由于某些事件(I/O请求,申请缓存区失败)而暂时无法运行,进程受到阻塞。在满足请求时进入就绪状态等待系统调用
终止状态:进程结束,或出现错误,或被系统终止,进入终止状态。无法再执行
进程间通信方式
有名管道:没有亲缘关系的进程之间通信
无名管道:具有亲缘关系的进程之间通信
共享内存:映射一段能够被其他进程访问的内存空间
信号:发送信号给进程调用其回调函数
套接字:用于不同机器之间的通信(Tcp/Ip)
消息队列:消息链表
信号量:计数器,控制多个进程对资源的访问,用于同步和互斥
进程间通信的选择
管道应用在进程间通信频率高的场景
共享内存应用于共享数据庞大,读写频繁场景
其余采用套接字
僵尸进程 孤儿进程 守护进程
僵尸进程:一个进程使用fork产生子进程,子进程退出,但是父进程没有使用wait或者waitpid回收子进程,那么这个进程就是僵尸进程
孤儿进程:父进程被异常终止,子进程由1号进程收养
守护进程:后台进程,父进程被异常终止后,子进程由1号进程收养,重新设置会话使其成为会话组长,并关闭文件标准描述符
僵尸进程的危害
僵尸进程的进程号并不会被释放,但是系统的进程号是有限的,因此出现大量的僵尸进程可能会导致系统没有可用的进程号而无法产生新的进程
如何杀死僵尸进程:
kill命令或者杀死父进程
线程间通信方式
临界区,互斥锁,读写锁,条件变量
临界区:每个线程访问临界资源的代码就是临界区,每次只允许一个线程进入临界区,进入后其他线程无法进入
互斥锁:互斥对象机制,访问临界资源时进行上锁,访问完毕解锁
读写锁:读写锁和互斥锁类似,但是一个线程只有一个写锁,多个读锁。也就是说一次允许多个线程读,但是一次只能允许一个线程写
线程、进程之间同步方式
线程:互斥锁,读写锁,条件变量
内核线程和用户线程
用户线程是由用户管理的,用户线程的创建、调度、销毁都是由库函数在用户空间完成,不需要借助内核,线程开销小
内核线程主要是用操作系统创建和销毁的
守护进程
1.创建子进程,终止父进程
2,使用setsid更改会话组组长
3,将当前文件目录改为根目录(防止被删除)
4.重设文件权限掩码(umask)
5.关闭标准的文件描述符
进程调度算法的策略
1.先来先服务
2.时间片轮询
3.高优先级优先
4.短作业优先调度
5.多级反馈队列
抢占式和非抢占式区别
非抢占式:系统将处理机分配给当前就绪队列优先权最高的进程,该进程就会一直运行下去,直到完成
抢占式:系统将处理机分配给当前就绪队列优先权最高的进程,该进程运行,如果之后遇到比该进程优先级还要高的进程,那么就会停止该进程的执行,重新将处理机分配给更高优先级的进程
并发和并行
并发:单个cpu来说,只能是一个进程在运行,线程的切换时间减少的纳秒级,多个任务不停的切换
并行:多个cpu来说,多个进程同时运行
进程间通信管道的实现原理是什么?
操作系统在内核中开辟一块缓冲区(管道)用于通信
编程:
1.父进程使用pipe开辟管道。得到两个文件描述符
2.父进程使用fork创建子进程,子进程也得到两个文件描述符
3.父进程关闭读文件描述符,子进程关闭写文件描述符,因为是管道半双工通信。如果想要实现全双工通信就需要再次创建一个管道。
什么是死锁,产生的条件,如何解决?
死锁:就是一个进程当中存在多把锁,那么多线程在执行的过程当中,访问多个临界资源造成相互等待,这时系统产生死锁
产生条件:
1.存在多个互斥锁,对于访问的临界资源不允许其他线程访问
2.在已获得锁的情况之下想要访问其他线程的临界资源造成等待
解决:
1.多把锁同时释放或者同时获得
2.只使用一把互斥锁
3.根据线程的执行时间创造锁获得的时间差
单核机械上写多线程程序,是否考虑加锁?
需要。线程锁主要是线程之间同步和通信的,如果两个线程共享某些临界资源,不使用线程锁会导致访问临界资源出现错误
互斥锁和读写锁
互斥锁:保证任何时刻只能有一个线程访问临界资源。当没有获得到锁的时候,线程会处于休眠等待的状态,直到获取到锁
互斥锁和读写锁:
1.读写锁区分读锁和写锁,互斥锁不区分
2.读写锁只能有一个写锁,但是可以有多个读锁,而互斥锁同一时间只能允许一个线程访问临界资源。
什么是信号量,有什么作用
信号量只能进行等待和发送信号,P(sv)和V(sv)
P操作:如果sv的值大于0,就给他减1;如果sv的值等于0的话就挂起等待
V操作:如果有其他进程因为等待sv而挂起,就让他恢复运行,如果没有则将sv加1
作用:
用于多进程对共享资源的访问,保护共享资源,使得共享资源在同一时刻只能有一个进程访问
进程、线程中断切换的过程是怎么样的
上下文切换:内核在cpu上对进程或者线程进行切换
进程的上下文切换:
1.保护被中断进程的处理器现场信息
2.修改被中断进程的进程控制块
3.将被中断进程的线程控制块加入到队列
4.选择下一个占用处理器的进程
5.根据被选中的进程设置操作系统需要用到的地址转换和存储器信息
6.根据选择的进程恢复处理器现场
多线程/单线程区别,多线程编程注意点,多线程加锁注意什么?
为什么创建线程池?线程池设计思路,线程池中的数量又什么决定?
为什么:
频繁的创建和销毁线程的时间大于线程执行任务的时间,使用线程池可以提高系统效率,节约资源
实现线程池步骤:
1.创建任务队列,线程池数据结构
2.线程池初始化
3.任务添加到线程池
4.实现工作线程
5.销毁线程池
多路I/O复用技术有哪些,区别是什么?
select,pool,epoll都是多路I/O复用机制,I/O多路复用就是通过一种机制监听多个文件描述符,一旦某个文件描述符就绪,能够通知应用程序进行相应读写操作
区别:
1.每次调用select都会将监听的fd集合从用户态拷贝到内核,在fd很多的时候就会造成开销很大;而epoll保证每个fd在整个过程当中只拷贝一次
2.每次调用select都需要在内核遍历所有传递进来的fd,才知道有哪些fd被设置了;而epoll只需要轮询一次fd集合,同时查看就绪链表里面有没有fd就可以了。select设置的监听集合不能被重新使用
3.select支持的文件描述符数量少,默认是1024;而epoll没有限制,他所支持的fd上限是最大可以打开文件的数目,这个数字一般远大于2048.
有名管道的通信
1,使用mkfifo函数创建管道文件
2.打开管道文件
3,读写管道文件
信号
信号作用:用于进程间通信,还可以将信号发送给自己
常见信号:
SIGINT ctrl+c
SIGKILL 终止进程,不可以被捕捉和忽略
SIGTOP 中止进程。无法处理和忽略。
SIGCONT 当被stop的进程恢复运行的时候,自动发送
SIGALRM 定时器信号
SIGCHLD 子进程终止时会发送给父进程
SIGQUIT CTRL+\ 退出
发送信号:
kill函数
进程对信号的处理方式:
缺省:按照默认方式
捕获:改变信号的处理方式
忽略:忽略发送过来的信号
创建定时器
共享内存
共享内存:映射一段能被其他进程访问的内存空间
1.创建文件描述符
2.打开文件(打开文件的权限要>=映射文件权限)
3.使用mmap函数映射共享内存地址
4.关闭文件描述符(可选)
5.读写
6.munmap释放映射文件内存
硬链接和软链接的区别
软连接:软连接又称为符号链接,包含源文件位置信息的特殊文件,相当于是创建快捷方式
硬链接:通过索引节点进行链接
区别:
1.软连接是存放源文件位置信息的特殊文件;硬链接是源文件的引用,不占实际内存空间
2.删除源文件,软连接也失效了;删除软连接,源文件不受影响
删除源文件,硬链接依然有效
3.软连接可以针对目录;硬链接不可以
4.软文件可以跨分区;硬链接不可跨文件系统
使用ln -s可以创建软连接 不用-s参数创建硬链接
静态库和动态库的制作及使用,区别是什么
静态库制作:
1.编写.c文件
2.生成.o文件
gcc -c xxxx.c -o xxx.0
3,生成静态库
ar -rsv -libxxx.a xxx.o
4.链接静态库
gcc -o 目标文件 源文件 -L. -l静态库
动态库制作:
1.编写.c源文件
2.编译生成不随目录变化的.o文件
gcc -c -fpic xxx.c -o xxx.o
3.制作动态库
gcc -shared -o -libxxx.so xxx.c
4.链接动态库
gcc -o 目标文件 源文件 -L. -lxxx
需要注意环境变量的问题
用export LD_LIBRARY_PATH=.L将动态库路径加载到环境变量中
ldd 查看使用动态库
区别:
1.静态库将库文件直接加载到源文件当中,因此生成的文件比动态库文件要大,执行的速度要快;动态库反之
2.静态库是在编译时加载,动态库是在运行时加载
GDB常见调试命令,什么是条件断点,多进程如何调试
1,编译时必须加上-g参数
gcc -g -o xxxx.c
2.调试
gdb xxx进入调试
run 直接运行
start 从main函数运行
n 下一步
b 断点
info b查看断点
q退出
多进程调试:
gdb xxx进入调试
start
set follow-fork-mode child 调试子进程 父进程会自动运行
set detach-on-fork off 关闭线程分离,这样父进程就不会运行
info inferiors查看所有进程
inferiors 1切换调试进程=
条件断点:break if 条件 以条件表达式为断点
简述linux系统态和用户态,什么时候进入系统态
内核态拥有最高权限,可以访问所有指令,用户态只能访问一部分指令
什么时候进入内核态:系统调用 异常 设备中断
为什么区分用户态和内核态:在cpu中有些指令比较危险,如果使用错误会造成系统崩溃,比如清理内存
什么是页表,为什么要有?
页表其实就是虚拟内存概念,虚拟内存到物理地址的映射表,就是页表
原因:
虚拟内存要和物理内存映射起来,就必须通过虚拟内存映射表,那么如果每个byte的虚拟内存地址都映射
到物理地址,每个条目需要8字节,那么在内存为4G的情况下,地址映射表就要32G,这张表大的连物理地址都放不下 了 ,因此操作系统引入页的概念,使用4个字节映射的对应物理地址1页->4k,这样就把全部的虚拟地址映射到页表就节省很大空间
虚拟内存高20位知道在自己在页表中的位置,低12位对应物理地址页里面的具体的内存地址
操作系统当中malloc的实现原理
malloc使用的是内存池的管理方式,减少内存碎片。malloc首先申请大块的内存作为堆区,然后将堆区分
为多个内存块。当用户申请时,直接从堆区分配一个合适空闲块。malloc采用隐式链表的方式记录所有空
闲块,每个空闲块记记录一个未分配的空闲的内存地址。
简述操作系统当中的缺页中断
malloc和mmap函数在内存分配的时只是建立了进程的虚拟地址,并没有分配虚拟地址对应的物理地址。
当进程访问这些没有建立映射关系的虚拟地址时,处理器就会产生缺页异常,引发缺页中断。
缺页异常引发缺页中断,此时操作就会根据页表中的外存地址在外存中找到所缺的一页,将他调入内存
mmap函数的原理和使用场景
mmap函数就是将文件映射到内存空间的一种方法,也就是将文件映射到进程的地址空间,
实现文件的磁盘地址和进程的虚拟内存地址一一对应的关系。实现这样的映射关系之后,就可以通过指针
来读写操作这一段内存,而不需要使用read、write函数对文件操作。
使用场景:
1.对一块区域频繁读写
2.实现用户空间和内核空间的高效交互
3.可提供进程间的共享内存和相互通信
4.可实现大规模的数据传输
为什么使用虚拟内存
1.扩大地址空间。进程可以独占4G的虚拟内存空间
2.内存保护。防止不同线程对真实物理地址的争夺和抢占
3.实现内存共享,进程通信
4.避免内存碎片,虽然物理内存地址不连续,但是虚拟内存上可以连续
用户空间和内核空间通信方式
1.内核提供的api
Copy_from_user copy_to_user
2.proc文件系统
3.mmap系统调用
中断响应执行流程?顶半部和底半部?
中断响应执行流程:
cpu接受中断->保存中断上下文执行中断函数->执行中断上半部->执行中断下半部->恢复中断上下文
顶半部:执行中断比较紧急的任务 清中断
低半部:执行不是特别紧急的任务
busybox
缩小班uinx系统常用命令工具箱
根文件系统
内核启动时挂载的第一个文件系统,内核代码的映像文件保存在根文件系统中
自旋锁 信号量 二者区别
自旋锁:不会进入睡眠
自旋锁只有两个状态,锁定和解锁,在锁定期间不允许其他进程访问。
信号量:会进入睡眠
信号量是一个计数器,用来统计资源的可用次数,如果B想要使用资源,当资源可用的时候就去通知B,而不是让B在那里等着,不会一直占用cpu,提高系统执行效率
区别:
信号量会让进程睡眠,所以信号量适合锁被长时间占有情况
自旋锁一直循环判断是否锁可用,占用cpu极高,不适合锁被长时间占用
自旋锁禁止处理器抢占,信号量允许。这也是为什么自旋锁不能睡眠的原因。如果自旋锁睡眠,那么就无法通过抢占唤醒睡眠的自旋锁
信号量不能用于中断,因为信号量会引起睡眠,自旋锁可以用于中断
为什么堆空间不是连续的
堆内存空间主要是通过链表维护和管理已用的和空闲的内存,当用户申请堆内存空间时会在里面找到一个合适的内存块,当我们释放掉大内存块时,会和堆空间中零散的小内存块合并成一个内存块。
liunx内核的组成部分
linux内核主要是由5个子系统构成:进程调度,内存管理,虚拟文件系统,网络接口,进程间通信
linux系统的组成部分
内核,shell,文件系统和应用程序
线程的同步和互斥的区别和联系
同步:按照一定的顺序执行,同步当中包含了互斥
互斥:是指一个资源只能有一个进程访问,互斥是没办法按照顺序执行的,无序的
常见命令
1,解压、压缩
tar
zip
uzip
2.查看内存
free
Cat/proc/meminfo
Df -h
top
3.查看cpu
Cat/proc/cpuinfo
4.ps -elf|grep xxx
5.修改文件权限
chmod xxx
6.查看内核
Uname -a
7.查看栈大小
ulimit -s
进程终止
正常终止:
main函数调用return
应用程序中调用exit
异常终止:
应用程序调用abort
进程接受到信号,SIGKILL
特殊进程
linux下三个特殊进程:
idle进程(pid=0),系统自动创建,运行在内核态
init进程(pid=1),由idle进程创建,运行在用户空间,父进程是idle
kthread(pid=3),内核线程,负责内核线程创建工作,父进程是idle
多线程使用较多函数
pthread_self()//获取进程号
pthread_create()创建进程
pthread_join()回收进程
pthread_detach()线程分离
pthread_mutex_init()动态创建互斥锁
pthread_cond_signal和pthread_cond_broadcast
pthread_cond_signal向指定线程发送条件变量
pthread_cond_broadcat广播发送条件变量,
肯能会产生惊群效应(多个线程争抢一个资源,造成段错误)
驱动为什么需要并发和互斥
并发是指多个单元同时执行,这样就会使共享内存数据被修改,我们需要互斥让一个时间段只能让一个单元对数据访问,自旋锁,信号量都是可用的操作
软中断和硬中断
1.软中断是中断指令产生的,硬中断是外设引发的
2.硬中断的中断信号是由中断控制器提供的,软中断是由中断指令产生的,不需要中断控制器
3.硬中断可以屏蔽,软中断不可以屏蔽‘
4.硬中断处理程序确保他能快速完成任务,属于上半部
5.软中断处理硬中断未完成的任务,属于下半部
fork和vfork区别
1.fork的子进程拷贝父进程大代码段和数据段;vfork子进程父进程共享数据段
2.fork函数父子进程执行的顺序是不确定的;vfor保证子进程先运行,在调用exec或者exit之前与父进程
共享数据,调用exec和exit函数之后父进程才可能调度运行
3.需要改变共享数据段中变量的值,则拷贝父进程
vfor中子进程修改全局变量会影响父进程的全局变量
什么是虚拟内存
虚拟内存是一种内存管理技术,主要是为了扩展内存,因为我们的代码数据是存放在硬盘的,而cpu是没办法直接
从硬盘中取数据的,必须借助内存,但是内存空间是有限的,所以内存不够就会把一部分硬盘作为虚拟内存,
这样cpu就可以读取虚拟内存的数据了
并发编程三个概念
原子性问题:即一个操作或者多个操作,要么全部执行且执行过程不会被任何因素打断要么都不执行
可见性问题:当多个线程访问一个变量时,一个线程改变了这个变量,其他线程能够立刻看到
有序性问题:程序执行的顺序按照代码的先后顺序执行
同步和异步
同步一般指阻塞等待,异步一般是不需要等待。
十一、网络编程
TCP头部结构
1、URG:当URG=1时,注解此报文应尽快传送,而不要按本来的列队次序来传送。与“紧急指针”字段共同应用,紧急指针指出在本报文段中的紧急数据的最后一个字节的序号,
使接管方可以知道紧急数据共有多长。
2、ACK:只有当ACK=1时,确认序号字段才有效;
3、PSH:当PSH=1时,接收方应该尽快将本报文段立即传送给其应用层。
4、RST:当RST=1时,表示出现连接错误,必须释放连接,然后再重建传输连接。复位比特还用来拒绝一个不法的报文段或拒绝打开一个连接;
5、SYN:SYN=1,ACK=0时表示请求建立一个连接,携带SYN标志的TCP报文段为同步报文段;
6、FIN:发端完成发送任务。
参考链接:https://zhuanlan.zhihu.com/p/471415273
socket网络编程中客户端和服务端使用到的函数
服务端:
1.socket 创建一个套接字
2.bind 绑定ip和port
3.listen 监听端口(套接字变为被动连接)
4.accept 等待客户端连接
5.read/write 接受发送数据
6.close 关闭连接
客户端:
1.socket 创建套接字
2.connnect 连接服务器
3.read/write 发送接受函数
4.close 关闭
UDP通信
通信模型图:
服务端:
1.socket 创建套接字
2.bind 绑定ip和端口
3.recvfrom 等待连接
4.sendto 回应
5.close 关闭
客户端:
1.socket 创建套接字
2.sendto 发送消息
3.recvfrom 等待回应
4.close 关闭
网络四层模型
应用层 ftp/http
传输层 tcp/UDP
网络层 ip/icmp
数据链路层
物理层
tcp如何保证可靠性
1.接受报文中有校验和字段,如果校验失败则丢弃该数据包
2.序列号 确认应答、超时重传
数据到达接收端之后,接收端会发送一个确认应答,表示收到数据包,并且确认下一个想要收到的序列号,如果发送方没有收到确认应答,就会超时重传
3.窗口控制和重发控制/快速重传
利用窗口大小来提高传输的速度,窗口就是在一个窗口大小内,接受到的报文不需要一一确认,只需要确认窗口接收到报文的最后一个报文即可,发送确认应答,表示在这之前的报文都已经接收到了。
TCP滑动窗口和重传机制
滑动窗口协议是传输层对于流量控制的一种协议,接受方通过发送自己接受窗口的大小告知发送方,以此控制发送方发送数据的速度,防止接收方因发送方发送数据过快而淹没。滑动窗口可以理解为缓冲机制,告诉对方自己还能接受多少数据
重传机制:
tcp在发送数据时会设置一个计时器,如果计时器超时了还没有接收到接收端发送的确认应答,就会重发该数据包,这个就是超时重传机制
另外一种方式是快速重传机制,tcp的累积确认没有返回ack或者返回的ack出现这个失序的报文时,快速重传推断出丢失的包,并重新发送
tcp慢启动
慢启动是tcp使用的一种阻塞控制协议。在tcp完成三次握手之后并不是一下子发送大量数据的,而是先确定一个
小尺寸大窗口发送数据,如果没有出现丢包事件,则认为网络是良好的,因此线性增大发送窗口大小(加之前窗口的大小,1->2,2->4,4->8)。如果出现丢包,认为网络拥塞,快速减少发送窗口的大小(快衰减)
tcp建立连接和断开连接的过程(三次握手和四次挥手)
建立连接的三次握手:
第一次握手:客户端确认自己能发,服务器能收
第二次握手:服务端确认自己能发,客户端能收
第三次握手:服务器确认客户端能够应答
三次握手过程:
1.clinet将SYN置为1,随机产生一个seq,发送给server,clien进入SYN_SENT状态
2.server接受到来自client的数据包,server将SYN和ACK都置为1,将数据包发送client确认连接请求,server进入SYN_RCVD状态
3.客户端检查ack是否为1,如果是将数据包发送给服务端,服务端检查ack的值,正确则连接成功,都进入established状态
四次挥手:
1.客户端发送FIN=1服务端,告诉数据发送完毕,请求关闭连接,此时客户端不能发送数据但能接受数据(FIN_wait_1)
2.服务端接受到FIN包后给客户端返回ACK表示自己收到,但没有断开socket连接,而是等待数据传输完成
(client FIN_wait_2)
3.服务端等待数据完成之后,向客户端发送FIN包,表示可以断开连接
(server last_ack)
4.客户端接受到FIN包之后,返回ACK表明收到,等待一段时间(clinet TIME_wait),确保服务端没有数据发送,关闭socket
连接三次握手
1、客户端请求,标志位SYN置为1 发送x;
2、服务端回复,标志位SYN和ACK都置为1 回复 x+1,和 y
3、客户端收到后回复 y+1,服务端检查ack是否为1,是的话就连接成功
断开
1、客户端发送 x+2 回复y+1 进入FIN_WAIT_1 状态
2、服务端回复x+3 服务器进入CLOSE_WAIT状态。客户端收到后进入FIN_WAIT_2状态
3、服务端发送完所有数据之后发送y+1 服务器进入LAST_ACK状态
4、客户端回复y+2 客户端进入TIME_WAIT状态等待2MSL(报文段最大生存时间)后关闭
为什么2次握手不行,必须要三次
tcp协议双发都必须维护一个序列号,三次握手双方都会告知序列号的起始值,并且确认对方的序列号
两次握手的话只有连接发起方的序列号能够被确认,另一方的序列号得不到确认,服务器这边无法判断客户端能否接受
关闭连接的四次挥手可以为三次挥手嘛
不可以,
第一次 客户端发送FIN给server
原本第二次和第三次合并 客户端回应ACK和FIN 但是如果这时候存在server还有数据在发送的情况,
因此第二次挥手和第三次挥手不可以合并
滑动窗口过小会怎么样
数据延迟比较大
假设窗口大小只有1的话,那么每次只能发送一个数据,并且发送方只能在接受方对该数据确认接受之后才能发送下一个数据,因此当数据较多时会造成很大的延迟
三次握手中每次握手消息对方没有接收到会怎么样
第一次握手 客户端发送SYN丢失,server不会回应ack,那么客户端超时重发
第二次握手 服务端发送ack 和seq丢失,客户端不会回应ack,服务端重发
第三次握手 客户端回应ack丢失,这是服务端处于SYN_RECV状态,超时重发,如果仍然没有接受到客户端发送的消息,关闭连接
tcp和udp的区别,头部结构是怎么样的
1.tcp是有连接,udp是无连接的
2.tcp是可靠传输,udp是最大可能传输
3.tcp拥有流量控制和拥塞避免,udp网路的堵塞不会影响他的发送速率
4.tcp是一对一的连接,udp支持一对一,多对多,一对多
4.tcp面向字节流服务,udp面向的是报文服务
域名解析过程
1.输入域名,系统首先在本机host检查是否有这个网络映射,有则调用
2.如果没有,则去查找本地DNS服务器,有则返回调用
3.如果没有,根域名服务器->顶级域名服务器->权限域名服务器
tcp粘包和拆包
TCP底层不了解上层业务数据的具体含义,他会根据TCP缓冲区的实际情况对包进行划分,在业务上认为一个完整的包可能被拆分成多个包发送。也可能将多个小数据包封装成一个大的数据包发送 这就是TCP粘包和拆包
如何解决拆包和粘包
粘包主要是由于不知道一个数据包的边界在哪,因此只要知道包的边界就可以划分有效用户信息
1.固定长度消息
2.特殊字符作为边界
3.自定义消息结构
TCP为什么比UDP可靠
1.确认和重传机制
三次握手和四次挥手
2.数据排序
3.流量控制
滑动窗口和计时机制
4.拥塞避免
慢启动 拥塞避免 快速重传 快速恢复
什么是http
http是超文本传输协议,是一个基于TCP/IP通信协议传递数据
http有什么特点
为什么客户端端四次挥手后还需要等待2MS
保证客户端ack报文能够被服务端接受,因为这个报文有可能丢失。
在服务器的角度来看他发送的断开连接的报文(FIN)客户端可能没有收到,因此他会重发这个报文,客户端能够在这个时间段内收到这个报文,并回应
什么时候应该使用TCP
当对网络通讯质量有要求(可靠传输)的时候,需要使用tcp协议:
比如将数据准确无误的传递给对方,http,https,ftp等文件传输协议,pop,smtp邮件传输协议
什么时候使用udp
网络通讯质量不高的时候,要求网络通讯速度尽量的快
IPV4和IPV6区别
1.协议地址区别
地址长度:IPV4 32位地址长度,ipv6 128位地址长度
表示方法:iPV4小数表示二进制数,ipv6十六进制表示二进制数
2.地址解析协议
iPV4协议:地址解析协议将iPV4地址映射到MAC地址
iPV6协议:地址解析协议被邻居发现协议(NDP)功能取代
3.身份验证和加密
ipv6提供身份验证和加密,ipv4不提供
4.数据包区别
包大小:IPV4协议数据包最小为576个字节;ipv6协议数据包最小为1280个字节
包头:ipv4长度为20~40字节;ipv6固定为40字节
IP地址
32位二进制,点分十进制表示
网络ID和主机ID
A B C类ip地址
子网掩码:
用来指明一个ip地址的哪些标识位是主机所在子网,那些标识位是主机位掩码
作用:划分子网,求出网络ID和主机ID
如何判断两个ip是否处于同一网段?
通过ip地址和子网掩码,相与得出网络ID,相同就是在同一个网段
字节序
大端字节序:数据的高位存放在内存的低位,低位存放在内存的高位
小端字节序:数据的高位存放内存的高位,低位存放内存的低位
十二 字符串函数
字符串长度函数实现:strlen
字符串拷贝函数实现:strcpy
字符串是否相等函数式实现:strcmp
字符串拼接函数的实现:strcat
以上注意关键点就是找到'\0'的存在
memset atoi memcpy
十三 通信协议
USART
USART\UART:通用同步/异步收发器,通用串行、同步/异步通信总线,该总线有两根数据线,全双工通信。
帧格式:起始位(空闲时 数据位为高电平到低电平)+数据位(8位)+校验位(可选)+停止位(1位/1.5位/2位)
一次只能发送一个字节,校验位可以选择(奇校验 偶校验 无校验)
波特率:每秒可以发送多少位数据 常见:115200 9600 等
为什么一次只能发送一位数据?先发高字节还是低字节
保证数据的准确性,时间累积误差。
假设需要发送1111 0000的数据,接收方如何知道发送了几个1几个0,这就需要波特率,
波特率规定了发送1个bit需要的时间,但是接受方和发送方可能存在时间误差,会导致累计时间误差,
造成数据的不准确,因此规定每次只能发送一个字节,就可以避免累计时间误差了
先发送低位
通信串口存在的问题:
1.串口只是定义了协议也就是发送帧的格式,用高电平表示1低电平表示0 ,但是在不同的处理器中这个电气
特性是不一样的,因此不同的处理器不能够直连通信
2.抗干扰能力差
3.通信的距离短
由于串口通信存在的问题,因此出现了RS232,RS485协议
RS232
RS32底层都是串口通信,但是与USART不同的是电气特性做了修改
RS232一般有9根线,常用的是TXD RXD GND
电气信号:规定逻辑1的电平为-5v~-15v,逻辑0的电压为+5v~+15v,提高了抗干扰能力
由于我们的处理器电平是TTL电平,达不到RS232电平的要求,因此一般还需要使用MAX232芯片,把TTL电平转换为RS232适合的电平
问题:RS232的电信号较高,容易烧坏芯片,并且还需要使用额外的芯片成本较高,传输速率低,因此出现了RS485
RS485和RS232的区别
传输方式:RS232采用不平衡传输方式,即单端通讯;RS485采用平衡传输方式,差分传输
传输距离:RS232适合本地设备通信,一般不超过20m;RS485传输距离为几十米到上千米
设备数量:RS232适合一对一通信;RS485在总线上允许多个设备连接
连接方式:RS232规定电平表示数据,因此需要两根线实现全双工通信;
RS485使用差分信号表示数据,因此需要4根线实现全双工通信
IIC协议
IIC协议:串行同步半双工协议,
SDA-------数据收发
SCL-------通信双方是时钟同步
IIC总线上可以挂载许多从机,那么主机是如何区分从机的呢?通过从机的地址
一主多从模式:
任何时候主机完全掌握SCL时钟线的控制。在空闲时刻,主机可以主动发起对SDA数据线的控制(从机不允许发起对SDA数据线控制),只有从机发送数据和从机应答的时候,主机才会转交SDA数据线的控制权
通信过程及时序基本单元:
起始条件:SCL高电平期间,SDA从高电平切换到低电平
终止条件:SCL高电平期间,SDA从低电平切换到高电平
主机发送一个字节:SCL低电平期间主机依次将数据放入到SDA中(高位现行),释放SCL的控制权,从机将在SCL高电平的期间读取SDA上的电平,SDA在SCL高电平期间不允许改变电平,依次重复上述过程8次,就可以发送一个完整的字节
主机接受一个字节:SCL低电平期间,从机将数据依次放入到SDA上,然后释放SCL,主机在SCL高电平期间读取SDA上的电平,SDA在SCL高电平期间不允许改变电平,依次重复上述过程8次,就可以接受一个完整的字节
接受应答:主机在接受完一个字节之后,在下一个时钟发送一位数据。0表示应答,1表示非应答
发送应答:主机在发送完一个字节之后,在下一个时候接受一位数据,判断从机是否应答,0表示应答,1表示非应答
完整的IIC时序数据帧
指定地址写:
起始条件+从机地址+读写位(0)(0表示写,1表示读)+从机应答位+从机的寄存器地址+应答+数据+结束条件
当前地址读:
起始条件+从机地址+读写位(1)+从机应答+数据+终止条件
指定地址读:
起始条件+从机地址+读写位(0)+应答+从机的寄存器地址+应答+重复起始条件+DATA+结束条件
SPI
SPI:高速 全双工 同步的串行通信总线,至少4根线
MOSI:主机发送从机接受
MISO:主机接受从机发送
SCL:时钟线用于同步
CS:片选,确定哪个设备
通信过程:
先发送高位 在发送低位,高位先行,
在SCL时钟线上升沿或者下降沿,主机的移位寄存器和从机的移位寄存器都向左移动一位,在紧接着的下一个下降沿/上升沿接受数据
notes:片选(cs)引脚线为低表示该从设备被选中
通信模式的选择(极性和相位)------>确认上升沿发送数据还是下降沿发送数据
SPI四种工作模式(主要取决于寄存器CPOL和CPHA)
1)CPOL表示空闲的状态
CPOL=0,空闲时SCLK为低电平
CPOL=1,空闲时SCLK为高电平
2)CPHA 表示采样的时刻
CPHA=0,每个周期的第一个时钟沿采样(奇采样)
CPHA=1,每个周期的第二个时钟沿采样(偶采样)
SPI和IIC串口通信的区别
相同:
都是采用同步,串行数据传输
采用的都是TTL电平
都有主机/从机之分
不同点:
IIC为半双工,SPI为全双工
IIC有应答机制,SPI没有
IIC的时钟和极性是固定的,SPI可以通过CPOL和CPHA寄存器来改变
IIC发送一个地址去寻找从机,SPI通过片选引脚线选择
SPI和IIC的具体使用
CAN总线
CAN:控制局域网总线
特征:两根信号线(CAN_H,CAN_L),没有共地,传输差分信号,抗干扰能力强
异步、半双工,可以挂在多个设备,多设备同时发送数据通过仲裁判断先后
11位/29位报文ID,用于区分消息,也决定优先级
可以配置1~8字节载荷(可以一次发送1~8个字节)
广播和请求式两种传输方式
CAN总线适用于多个主控相互通信的情况
每个主控设备通过CAN控制器的TX RX分别连接到CAN收发器的CAN_H,CAN_L,并连接到CAN总线上
为什么需要闭环CAN总线需要加电阻:
1、防止回波反射
2、空闲时,保证两根总线的默认电平是一致的
电平标准:
闭环(高速):
电压差为0V 表示逻辑1
电压差为2V 表示逻辑0
低速(开环)
电压差为-1.5V 表示逻辑1
电压差为3V 表示逻辑0
特别补充音视频处理
参考正点原子HAL开发视频
硬件设备:OV7725摄像头
硬件电路采集数据:
SCCB串行摄像头通信总线,有两根线。SIO_C(传输时钟线),SIO_D(传输数据),与IIC协议类似,
不同点:没有应答位 -------Don`t位或NA
读写时序:
写操作:三相写传输周期:start+设备ID+写位(0)+X+寄存器地址+X+数据+stop
读操作:
二相写+二相读:start+设备ID+写位(0)+X+寄存器地址+stop+start+设备ID+读位+X+数据+stop
注意:SCCB不支持连续的读写,只能读一个或者写一个
音频编解码:I2S协议 及ES8388器件
I2S引脚线:
SCK---------时钟线
LRCK-------左右声道时钟线(左右声道选择)
SDATA-----数据线
ES8388引脚线:
数据输出
MSCLK-----主时钟线(MCU提供时钟)
SCK---------时钟线
LRCK-------左右声道时钟线(左右声道选择)
ASDOUT-----输出
DSDIN-------输入
命令配置参数
CCLK-------I2C CLK
CDATA-----I2C DATA
十四 硬件外设
GPIO外设
配置流程:
1.打开RCC时钟(APB2)
2.配置GPIO结构体,引脚 模式 speed
外部中断
配置流程:
1.开启GPIO总线时钟
2.初始化GPIO结构体
3.AFIO中断引脚选择
4.配置中断参数(中断线 中断使能 中断模式 中断触发沿),使能外部中断
5.配置NVIC配置中断优先级
PWM配置
定时中断:
1.开启总线时钟。选择内部时钟源(TIM2)
2.配置时基单元(分频系数 计数模式 ARR重装载值 PSC预分频器的值 重复计数器的值)
3.清除中断标志位
4.使能中断
5.配置NVIC
6.启动TIM
PWM波输出:
1.开启总线时钟。
2.配置GPIO
3.选择内部时钟源(TIM)
4.配置时基单元(分频系数 计数模式 ARR重装载值 PSC预分频器的值 重复计数器的值)
5.配置输出比较单元
6.启动TIM
ADC+DMA配置
1.开启时钟
2.配置ADC的CLK
3.配置GPIO(模拟输入)
4.配置ADC规则组通道
5.配置ADC参数(ADC模式 数据对齐 外部触发沿 扫描 连续)并使能
6.配置DMA模式,(地址自增 外设和内存地址 数据搬运方向 循环模式 通道数)
6.ADC校验
CPU内部结构
控制单元,运算单元,存储单元,时钟
CPU、内存、虚拟内存、硬盘关系
CPU是从内存中或者缓冲中拿到指令,放入寄存器,对指令译码分解成一系列微操作,然后发出控制命令,执行伪操作
CPU并不能直接调用存储在硬盘中的系统、程序和数据,所以必须要将硬盘上的数据存储在内存中,这样才能被CPU读取,所以内存是硬盘和CPU的中转站。
内存一般是比较小的,当内存超出额度之后,就会把硬盘上的部分空间模拟成内存------>虚拟内存,将暂时不用的数据或者程序放入到虚拟内存中,等到需要的时候在调用
总结:CPU是工厂,内存是中转站,硬盘是仓库,虚拟内存是临时中转站
ARM结构处理器分类
1.嵌入式微处理器------>32位以上的处理器
2.嵌入式微控制器------>单片机,以某种微处理器内核为核心,芯片内部集成ROM,RAM等功能
3.嵌入式DSP处理器------>擅长数字信号处理
嵌入式微处理器和嵌入式DSP有什么区别
偏重方向不一样,微处理器偏向于界面操作,他的速度和计算能力一般,一般使用在消费电子方面
DSP计算能力较强。对于数据处理能力极好,所以一般用于计算,军工
存储
通用计算机采用cache(高速缓冲存储器),主存储器(RAM,内存),外部存储器组成三级存储体系。
嵌入式系统存储体系:
速度:寄存器>cache>内存(RAM、ROM)>硬盘
嵌入式存储类型:
RAM
RAM随机访问存储器,直接和CPU进行交互的内存,通常作为正在运行的程序和临时数据的存储媒介
RAM分类:
静态SRAM:速度比DRAM快,不需要时钟同步
动态DRAM:同步动态RAM,需要周期性刷新,密度比SRAM高,需要时钟刷新
片内RAM和片外RAM:
片内RAM一般存放中断处理,RTOS调度器,任务的上下文切换,内存分配释放等使用率较高的代码和中断堆栈读写频率极高的内存区,成本较高
片外RAM市面上成品,较便宜,存放全局变量,BSS,以及malloc所分配的堆空间
ROM
ROM,只读存储器,与RAM最大的区别就是RAM在断电后保存在上面的数据会消失,而ROM不会自动消失,可以长时间断电保存
ROM有两种:
一集成在芯片内部的一个只读存储区域,一般几K到十几K字节大小,用来存储系统刚上电时对CPU和一些核心外设(时钟,串口,MCU,DRAM,falsh)进行系统初始化的代码
二 flash,他和硬盘的一个显著区别就是flash中存放的代码是可以由cpu直接取指并执行的
flash和RAM对比:
DDR
cache
常见的CPU指令集
CISC复杂指令集:
指令系统的指令不等长,指令非常的多
RISC精简指令集:
采用RISC指令集的微处理器处理能力更加强,采用超标量和流水线结构,大大增强了并行处理能力
CPU流水线处理能力
CPU取指步骤:
指令取指->指令译码->指令执行->访存->写回
notes:CPU并不会等待一条指令完全执行完毕才执行下一个指令,而是而是像流水一样
嵌入式式流水线工作有什么不同
嵌入式CPU为了提高效率,采用超流水线的结构,CPU执行一条指令需要5个步骤,每个步骤可能需要1ns,但是有个长指令在指令执行需要2ns,整个时间就被限制了
为了提高效率,可以把指令执行这个步骤在拆分成两组(寄存器+组合逻辑),使得每组的执行时间为1ns,这样我们的流水线就成了6级流水线,也就是超流水线
uboot启动做了什么事
第一阶段:
初始化时钟,关闭看门狗,关中断,启动ICACHE,关闭DCACHE和TLB ,关闭MMU,初始化SDRAM,初始化NAND FLASH,重定位
第二阶段:
初始化串口,检测系统内存映射,将内核映像和根文件系统映像从FLASH读到SDRAM空间中,为内核设置启动参数,调用内核
中断和异常
中断:停止当前的工作转而去处理另一个任务,处理完毕继续执行当前任务
异常:指向内部指令引起的错误
区别:中断由外因引起,异常由CPU本身引起
DMA和中断的区别
DMA不需要CPU的参与,就可以进行数据的传输,大大提高了数据传输时CPU的效率,但是中断CPU必须要参与
中断函数的注意
1.中断不能有阻塞,不能让中断进入睡眠,如中断不能有信号量
2.中断函数的返回值,应该使用内核定义的宏,而不是自定义
3.中断函数遵循快进快出的原则,不能消耗过度的时间
为什么需要向内核传参
内核并不是对所有的开发板都是完美适配的,内核对于开发板的环境是一无所知的,所以想要启动内核,就需要通过传参告诉内核当前环境
如何传参
uboot传的是R0,R1,R2的参数,R0默认是0,R1存放的是CPU的型号,R2里面存放的是基地址,如:内存的起始地址,内存的大小,根文件系统信息
规定参数的格式:
uboot为什么关闭caches
caches是CPU的2级缓存,主要作用是将常用的指令和数据存放在CPU内部,刚上电CPU还不能管理cache,指令cache可以关闭也可以不关闭,但是数据caches必须关闭,否则可能由于刚开始代码到caches取数据,由于caches没准备好,导致数据异常
驱动申请函数
kmalloc()
vmalloc()
AD转换原理
1.逐次逼近法
AD转换步骤:采样,保持 量化 编码
简述配置ADC外设启用DMA的流程
uboot如何引导内核启动,如何向内核传参?
uboot引导内核启动主要是向内核传递三个参数:R0,R1,R2.R0一般为0,R1指明CPU的类型,R2存放内存的基地址:告诉内核映像文件在哪,板子剩余空间,这些参数都是以tag_list的方式传递的
uboot如何向内核传参:
在内核2.6以前是通过参数结构(oarameter_struct)的方式传递,现在主要是通过参数链表tag_list
RT-thread专题
内核
RT-thread操作系统内核包括:实时调度器 线程间通信 对象管理 设备管理 内存管理 时钟管理 线程管理
线程调度:基于优先级抢占多线程调度 支持256个线程优先级
时钟管理:时钟节拍为基础
线程间同步:采用互斥量 信号量 信号集实现线程间同步
线程间通信:邮箱和消息队列
内存管理:静态内存池和动态内存堆
RT-thread启动流程:
启动文件->$sub$$main函数->rtthread_startup
初始化相关硬件
初始化内核对像 定时器 调度器 信号
创建main线程,对各类模块依次初始化
创建定时器线程 空闲线程 启动调度器
线程管理
线程控制块:
struct rt_thread
rt-thread具有独立的线程栈,存放线程上下文环境和变量
线程的的五个状态:
初始状态(创建未运行) 就绪状态(线程优先级排毒) 运行状态 挂起状态(执行完或者被中断,阻塞) 关闭状态
线程优先级:
时间片:只对优先级相同的就绪线程有用
空闲线程:最低优先级线程,当系统没有其他线程运行就调用空闲线程
线程管理:
动态创建线程:rt_thread_create(线程名称,线程入口函数,线程入口函数的参数,线程栈大小,线程优先级,线程时间片大小)
删除线程:rt_thread_delete
静态初始化和脱离线程:
rt-thread_init(线程语句柄,线程名,线程入口函数,线程入口参数,线程起始地址,栈大小,优先级,时间片大小)
rt_thread_detach();
启动线程:rt_thread_startip();
获得当前线程:rt-thread_self()
线程睡眠:rt_thread_sleep() rt_thread_delay()
空闲钩子函数:执行空闲线程使,自动执行空闲钩子函数
rt_err_t rt_thread_idle_sethook(void (*hook)(void));
线程间同步
信号量:
动态创建信号量:rt_sem_create(信号量名称,信号量值,信号量标志(优先级))
rt_sem_delete();
静态创建信号量:rt_sem_init(信号量语句柄,信号量名称,信号量初始值,信号量标志(优先级))
rt_sem_detach()
获取信号量:rt+sem_take(信号语句柄,时间)
如果获取到的信号量的值为0,则根据时间参数挂起等待,知道其他线程释放信号量
释放信号量:rt_sem_release()
信号量值加1
生产消费者模型:
创建三个信号量:
lock empty(仓库的容量,是否为空) full(仓库的货物)三个信号量
相当于一个线程往仓库生产货物,一个线程往仓库中取获取
生产者线程先判断仓库有没有满(获取empty的信号量),之后给线程上锁(获取lock信号量),完成生产者线程后,
先释放lock信号量,在释放一个信号量给full信号量(放货物)
消费者线程判断仓库有没有货物(获取Ffull信号量的值),然后给自己工作线程上锁(获取lock信号量值),完成工作后释放lock信号量,消耗一个仓库位置(释放empty信号量)
互斥量:特殊的二值信号量,只有开锁和关锁两种状态
当一个线程获得互斥锁,其他线程就不能对他开锁或者持有它,只有等该线程自己完成任务后开锁,其他线程才能才能获得该锁,主要是防止多个线程访问临界资源造成错误而使用
信号量的使用可能会造成优先级翻转的问题,即有多个线程通过信号量机制访问共享资源时,低优先线程获得了信号量,而这个低优先级信号量又被许多中等的优先级线程中断,造成高优先级线程被许多低优先级的线程阻塞等待,实时性得不到保证
创建初始化:
rt_mutex_create
rt_mutex_init
删除:
rt_mutex_detach
rt_mutex_delete
获取:
rt_mutex_take
释放锁:
rt_mutex_release
事件集:
主要是用于线程间同步,可以是一对多或者多对多;任意一个事件可以唤醒线程,或者多个事件发生唤醒线程;
线程间通信
邮箱
线程间通信的一种机制,一份邮件只能存放4字节的内容,刚好是一个指针的大小,将邮件发往邮箱当中,
而一个或者多个线程就可以从邮箱当中接受邮件并处理。当一个线程向邮箱发送消息,邮箱没满,就复制,满了发送线程可以设置时间参数,是等待挂起还是直接返回错误。
当一个线程从邮箱中接受线程也是一样的,空的就根据时间参数选择等待还是直接返回错误。有的话直接复制缓存区
rt_mailbox
创建:
rt_mb_create(邮箱名称 邮箱容量 邮箱标志)
rt_mb_init
发送:
rt_mb_send
接受:rt_mb_recv
删除脱离:rt_mb_delete rt_mb_detach
紧急邮件:rt_mb_urgent
消息队列:
邮箱的拓展,用于线程间的消息交换,可以接受来自线程或者中断的不定长的消息存储在自己的内存空间中,其他的线程也能从消息队列中读取相应的消息,如果消息队列没有消息,则挂起线程等待,等到有消息才唤醒线程,是一种异步的通信方式
消息队列满足先入先出的原则,也就是将发送来的消息放到队列尾,利用链表的数据结构管理消息。
消息队列控制块:消息队列名称,消息队列大小,内存缓冲区,队列长度等。消息队列中的第一个和最后一个分别称为消息队列链表头和消息队列链表尾。
消息队列的创建:rt_mq_create(消息队列名称,消息队列一条消息的最大字节,消息队列的最大个数,消息队里标志(优先级))
创建消息队列时先从对象管理器中分配一个消息队列的对象,然后给消息队列的对象分配一块内存空间,组织成空闲消息链表,这块内存的大小=消息的大小+消息头的大小*消息队列的最大个数
rt_mq_delete
rt_mq_send
rt_mq_send_wait
rt_mq_recv
rt_mq_urget
信号:
嵌入式系统面试经典问题
微信公众号链接:深度Linux
https://mp.weixin.qq.com/s/ytncVkiQH2TOXw8bNayDZw
各种公众号搜嵌入式八股文
内存映射原理
将一块内存空间映射到不同的进程空间当中去
define和const区别
数组和链表区别
指针和引用区别
解释下QT的信号与槽
IIC为什么要上拉电阻,为什么使用开漏输出
上拉电阻:
1.当IIC处于空闲状态时,SDA和SCL应当是高电平的状态
2.开漏输出无法输出高电平,使用上拉电阻可以实现高低电平的转换
开漏输出:
实现线与的功能
推挽输出可能导致器件损坏
MQTT通信的过程
创建客户端
指定IP和端口号
进行链接
发布或者订阅主题
数据传输
断开连接
Linux线程同步
互斥锁 读写锁 条件变量
TCP和UDP应用场景
TCP:可靠连接,对于网络质量要求比较高:HTTP是协议,ftp协议
UDP:最大可能的传输,实时性要好:直播,游戏
野指针是什么,什么情况下产生野指针
互斥锁
线程间同步的一种机制,确保同一时刻只能有一个线程访问临界资源
数组和指针的区别
如何防止头文件重复包含
栈和队列的区别
栈,先入后出的一种数据结构;队列,先入先出的数据结构
栈一般用于局部变量定义 函数调用栈 函数的入参 返回值等场景
队列 则用于任务调度 消息传递等先入先出的场景
为什么中断不能传递参数
中断时异步调用,无法像函数那样主动调用,也不知道什么时候调用
串口数据帧格式
中断概念
中断时计算机系统当中一种重要的机制,用于响应某种事件或条件的发生。中断是异步事件,可以打断当前正在执行的程序或者任务,以处理紧急情况或者外部请求
static作用
中断执行过程
中断请求-中断控制器响应-中断响应-中断向量确定-中断处理程序执行-中断处理程序结果
什么是多态
c++三大特性之一,他允许以一种统一的方式处理不同类型的对象,通过相同的接口表现出不同的行为
C语言内存分配方式
静态内存分配
栈上内存分配
堆上内存分配
struct和class区别
函数和中断区别
函数不会发生上下文切换,中断会
函数可以主动调用,中断不可以
函数是同步,中断是异步的
函数有参数和返回值,中断没有
自旋锁和信号量
链表是否有环
使用多线程需要注意什么
线程安全:多个线程环境下,多个线程同时访问共享资源可能会引发竟态条件,导致数据不一致或其他异常情况,因此需要保证访问共享资源的安全,需要使用互斥锁 读写锁 条件变量等同步机制来保护共享资源的访问
线程间通信:多线程编程中,不同的线程之间需要通信和协作,因此需要合理的设计和使用线程间通信机制。队列 信号量 事件等,有效实现线程间同步和传递消息
死锁:死锁主要是指两个线程互相等待对方释放资源而无法继续执行的状态。避免死锁需要固定锁获取的顺序或者只使用一把锁
上下文切换开销:线程的切换需要保存当前线程的上下文并加载下一个线程的上下文,涉及时间和空间的开销。在设计多线程编程时,需要考虑减少线程切换的频率,提高程序性能
错误和异常处理:多线程环境下,错误和异常处理机制需要及时捕获线程中的异常,确保程序的稳定可可靠性
实现strcpy函数
char* mystrcpy(char*des,const char* src){
char* temp=des;
while(*temp++=*src++);
return des;
}
实现strcat函数
char* mystrcat(char* des,const char* src){
char* temp=des;
while(*temp!='\0')temp++;
while(*src!='\0'){
*temp=*src;
temp++;
src++;
}
*temp+='\0';
return des;
}