笔试
1、计算出结构体大小(对齐规则)
struct st
{
char name;
int age;
double money;
}s;
规则(字节对齐)
1、结构体中每个成员的偏移量必须是该成员自己所占内存大小 的整数倍,否则,会用空白字节填充
char 从0开始,0是任何数字的整数倍
int 是4个字节,4开始
double 是8个字节,从8开始
0~16 = 17
2、
最后还要满足结构体总大小为最大类型的整数倍
24个字节
2、字符串
字符串总是以’\0’作为结尾,所以’\0’也被称为字符串结束符,字符数组没有/o
两种字符串
char* s = “sss ” 字符指针指向字符串的第一个字符,只读不能修改
string s = “sss”,该字符串使用的strin类,c++中是可以使用类函数修改我都
定义一个字符指针,指向hello
char *y = "hello"; 字符串常量在内存中是只读的,因此不能修改这个字符串的内容
定义一个字符数组,数组中存放字符串
charz[] = "hello" 字符数组可以修改数组中的内容
char str[] = {'h', 'e', 'l', 'l', 'o'}; // 字符数组,逐个字符初始化
定义一个字符指针数组,里面存放的是指向字符串常量的指针,但是可以直接打印出内容
char* a[] = { "BEIJING", "SHENZHEN", "SHANGHAI", "GUANGZHOU" };
printf("%s", a[0]); // 输出 "BEIJING"
计算字符长度
sizeof 计算字节大小会包含/0
strlen 计算字符个数,找到/0就结束
字符串像是“hello”,编译器会在后面名加个/o
char str[] = {'h', 'e', 'l', 'l', 'o'}; ,这种不会再后面添加/o
print 函数%s,表示以字符串打印,遇到/o就结束打印
2、数组
1、一维数组
arr 数组名表示数组的首地址
定义
int arr[] = {1, 2, 3, 4, 5}; // 省略了数组大小,根据初始化列表确定大小为5
int arr[5] = {1,2,3,4,5} 定义的时候,写了元素个数,注意arr[5] 不存在,下标从0开始
数组名作为函数的形参,arr[2] 写成ar[] 就可以,参数没有意义,实参写成arr
2、二维数组
a[2[[4] a[0] 表示第一行元素的首地址
形参需要写上列 arr[][2] 实参arr
二维数组有时候可以将一行元素作为一个对象,如下,实际上是将每行首地址赋值
int arr[2][4]=
int *p[4]={arr[0]、arr[1]、arr[2]、arr[3]}
牛客大佬总结
一、一维数组
- 静态 int array[100]; 定义了数组array,并未对数组进行初始化
- 静态 int array[100] = {1,2}; 定义并初始化了数组array
- 动态 int* array = new int[100]; delete []array; 分配了长度为100的数组array
- 动态 int* array = new int[100](1,2); delete []array; 为长度为100的数组array初始化前两个元素
二、二维数组
- 静态 int array[10][10]; 定义了数组,并未初始化
- 静态 int array[10][10] = { {1,1} , {2,2} }; 数组初始化了array[0][0,1]及array[1][0,1]
- 动态 int (*array)[n] = new int[m][n]; delete []array;
- 动态 int** array = new int*[m]; for(i) array[i] = new int[n]; for(i) delete []array[i]; delete []array; 多次析构
- 动态 int* array = new int[m][n]; delete []array; 数组按行存储
三、***数组
int* array = new int[m][3][4]; 只有第一维可以是变量,其他维数必须是常量,否则会报错
delete []array; 必须进行内存释放,否则内存将泄漏
四、数组作为函数形参传递
- 一维数组传递:
- void func(int* array);
- void func(int array[]);
- 二维数组传递:
- void func(int** array);
- void func(int (*array)[n]);
数组名作为函数形参时,在函数体内,其失去了本身的内涵,仅仅只是一个指针,而且在其失去其内涵的同时,它还失去了其常量特性,可以作自增、自减等操作,可以被修改。
五、字符数组
char类型的数组被常委字符数组,在字符数组中最后一位为转移字符'\0'(也被成为空字符),该字符表示字符串已结束。在C++中定义了string类,在Visual C++中定义了Cstring类。
字符串中每一个字符占用一个字节,再加上最后一个空字符。如:
char array[10] = "cnblogs";
虽然只有7个字节,但是字符串长度为8个字节。
也可以不用定义字符串长度,如:
char array[] = "cnblogs"
作者:Hellowriter
链接:牛客网公司真题_免费模拟题库_企业面试|笔试真题
来源:牛客网
字符数组
数组中存放字符串
注意字符串后面需要有一个空格\0
char[4] ="123"
运算优先级
逗号表达式是从左到右,以最后一个为准
等号是右边开始
.优先级大于*
常用数据结构
链表
指针是一个地址,指针指向地址代表的空间
上一个节点想要连接到下一个节点,需要将下一个节点的地址存放到上一个节点中next域
//头插法 // 把地址给那个变量,就是让那个指针变量指向自己 // 注意左边是指针,右边是地址 //传入头节点、数据域 void headInsert(Node* list, int data) { Node* newnode = (Node*)malloc(sizeof(Node));//开辟一个空间返回一个地址,新的节点指针node指向这个空间 newnode->data = data;//将数据给新开辟的空间节点 newnode->next = list->next;//让头节点下一个地址赋给newnode的next指针,也就是newnode的next指针指向第二个节点 list-> next = newnode;//将头节点的next指针指向node节点 list->data++;//代表了头节点插入了一个元素 }
栈
单端操作(入栈与出栈在同一侧)
入栈 1,2,3,4,5
出栈 5,4,3,2,1
队列
(入队与出队在不同侧)
入队1,2,3,4
出队1,2,3,4
vector
就是一个长度可变的数组,任何部分都可进行删除,插入(insert ,eserve),在尾部插入删除简单(push_back,pop_back)
vector的数据结构是一个连续线性空间,当空间不够的时候就会动态扩容,底层是一个数组
应用场景:vector
小数据,任意位置插入
list
list底层是一个双向循环链表,以结点为单位存放数据,结点的地址在内存中不一定连续,每次插入或删除一个元素,就配置或释放一个元素空间
大数据,任意位置
deque
deque是一个双向开口的连续线性空间(双端队列),在头尾两端进行元素的插入和删除操作都有,可以扩容比vector优点多
理想的时间复杂度小数组,两端插入或者删除
queue 队列
队列是一个线性的数据结构,并且这个数据结构只允许在一端进行插入,另一端进行删除,
map(映射键值对)
底层是通过红黑树来实现的
应用:典型的例如按学生的名字来查询学生信息,即可将学生名字作为关键字,将学生信息作为元素值,保存在map 中。
set(集合)
底层使用红黑树实现
通常是满足/不满足某种要求的值集合,用set最为方便。
set中不会存在重复的元素,若是保存相同的元素,将直接视为无效
map、set、multiset、multimap
map与set
std::map
:存储键值对(key-value pairs),每个元素都有一个唯一的键(key)和一个关联的值(value)。std::set
:存储唯一的值(value),不允许重复元素
底层都是红黑树,会自动排序
map : 是一个关联容器,它存储键-值对,并根据键的排序顺序自动对其进行排序。每个键在
std::map
中是唯一的,因此不允许重复键。插入和查找操作的时间复杂度为 O(log n)。它适用于需要按键进行排序和查找的情况。
multiset
(多重集合):std::multiset
与std::set
类似,但它允许存储多个相同的值。它自动对元素进行排序,但在插入和查找操作的时间复杂度上与std::set
相同(O(log n))。std::multiset
适用于需要存储多个相同值的情况
set
(集合):std::set
是一个存储唯一值的关联容器。它自动对其元素进行排序,并且每个元素在std::set
中只能出现一次。插入和查找操作的时间复杂度为 O(log n)。std::set
适用于需要存储唯一值且不关心键-值对的情况
multiset
(多重集合):std::multiset
与std::set
类似,但它允许存储多个相同的值。它自动对元素进行排序,但在插入和查找操作的时间复杂度上与std::set
相同(O(log n))。std::multiset
适用于需要存储多个相同值的情况
unordered_map、unordered_set
底层都是哈希表,不会自动排序
map与hash_map或者unordered_map的区别
底层实现:map是基于红黑树实现的有序容器、unorderded_map是基于哈希表实现的无序容器
map中不允许存在相同的key,unorderd_map中允许
哈希表是小数据中,红黑树大数据
面试
嵌入式基础
三大数据
结构体
联合体(共有体)
所有变量都共用首地址
枚举
第一个常量没有定义时,默认为0
后一个比前一个大一
static
修饰局部变量,存放到静态全局区,
修饰全局变量 ,只有在本文件有效
修饰全局函数也是,只在本文件有效·
字节对齐
1、起始地址是成员变量大小的整数倍
2、总字节大小是最大成员变量的整数倍
内存分配
代码段:存放程序执行代码的一块内存区域。只读,不允许修改,代码段的头部还会包含一些只读的常量,如字符串常量字面值(注意:const变量虽然属于常量,但是本质还是变量,不存储于代码段)。
数据段data:存放程序中已初始化的全局变量和静态变量的一块内存区域。
BSS 段:存放程序中未初始化的全局变量和静态变量的一块内存区域。
可执行程序在运行时又会多出两个区域:堆区和栈区。
堆区:动态申请内存用。堆从低地址向高地址增长。
栈区:存储局部变量、函数参数值。栈从高地址向低地址增长。是一块连续的空间。
volatile的用法,和它的作用
1、提醒编译器它后面所定义的变量随时有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据。
2、防止编译器优化
3、volatile 可以保证对特殊地址的稳定访问(变量地址)
4、用法a 中断服务程序中修改变量需要加
b 全局变量
c 多任务间共享变量
什么是开漏输出和推挽输出。iic为什么一定要使用开漏输出
推挽输出:高低电平都具有驱动能力
开漏输出:高电平没有驱动能力,需要借助上拉电阻完成对外驱动
llc从器件不具备拉高总线的能力,只能通过令SDA接地或者不接地实现输出0或者1,而无法通过接地或者接vcc来实现输出0或者1,这就要求iic总线上必须默认是上拉,否则从机无法令SDA为高
几种内存创建函数
malloc底层原理
copy to user 的底层原理
ioct底层实现原理,内核态与用户态怎么交互的
在驱动程序中实现的ioctl函数体内,实际上是有一个switch{case}结构,每一个case对应一个命令码,然后对应做出一些具体的操作
int (*ioctl) (struct inode * node, struct file *filp, unsigned int cmd, unsigned long arg);
1、文件描述符
2、cmd命令
上层应用open怎么访问到底层的open
1、使用open打开文件设备节点,进入系统调用,
2、找到文件对应的innode节点,
3、通过文件节点中的设备号,在内核中找到设备结构体,调用operator中的open函数
4、内核会创建file 结构体,底层的operator操作结构体指针指向file结构体,返回到应用层就是fd(文件描述符)
野指针
(野指针不是NULL指针,是指向被释放的或者访问受限的内存的指针)
1、指针变量没有被初始化,任何刚创建的指针不会自动成为NULL
2、指针被free或delete之后,没有置NULL
3、指针操作超越了变量的作用范围,比如要返回指向栈内存的指针或引用,因为栈内存在函数结束时会被释放
中断
裸机中断不能传参,实时操作系统可以,arm中两种中断IRQ和FIR中断
中断号,中断号是一个用于唯一标识不同类型中断的数字或代码。它在计算机系统中用来区分不同的中断事件,
中断向量表:表中每一个向量项都对应着一种中断类型,并且包含了一个可执行的中断处理函数的地址
中断向量:中断信号统一进行了编号(一共256个:0~255),称为中断向量,中断服务程序的入口地址,
外围设备中断,操作系统之前cpu执行的是用户态程序,就会从用户态切换到内核态
硬件设置触发一个中断信号,linux内核会响应这个信号暂停当前执行的任务,去执行
CPU会根据中断号在中断向量表中查找对应的中断处理程序的地址,然后跳转到该地址,开始执行中断处理程序的代
中断上下文
比如硬件触发中断传入的一些信息参数
linux驱动中的多线程(竞争)
当一个线程在访问某个外设驱动的时候,怎么才能防止其他线程访问
1、原子操作
就是绝不会在执行完毕前被任何其他任务和时间打断
2、自旋锁
自旋锁最多只能被一个内核任务持有,如果一个内核任务试图请求一个已被争用(已经被持有)的自旋锁,那么这个任务就会一直进行忙循环——旋转——等待锁重新可用
3、信号量
信号量和自旋锁的使用方法基本一样。与自旋锁相比,信号量只有当得到信号量的进程或者线程时才能够进入临界区,执行临界代码。信号量和自旋锁的最大区别在于:当一个进程试图去获得一个已经锁定的信号量时,进程不会像自旋锁一样在远处忙等待
信号量是一种睡眠锁。如果有一个任务试图获得一个已被持有的信号量时,信号量会将其推入等待队列,然后让其睡眠。这时处理器获得自由去执行其它代码。当持有信号量的进程将信号量释放后,在等待队列中的一个任务将被唤醒,从而便可以获得这个信号量。
线程与进程
线程是
一个标准的线程由线程ID、当前指令指针(PC)、寄存器集合和堆栈组成
进程是执行过程中的代码,进程是指正在运行的一个程序的实例,包括代码、数据、堆栈、打开的文件等系统资源,是操作系统中资源分配和调度的基本单位。
父进程与子进程
父进程使用fork() 函数创建子进程,将父进程的数据拷贝一次,但是两个有自己的pid > 于o 父进程,小于0子进程
父进程会在子进程结束的时候,回收子进程,要是父进程先于子进程结束,就会产生僵尸进程,这时候需要init进程,自动调用wait()函数进行收尸
fork复制的内容
程序代码段、数据、堆内存、堆内存、文件描述符、
不同的是进程pid、独立的地址空间、自己的文件描述符
clone
写时复制:允许父子进程读取相同的物理页,单只有子进程准备写的时候才会去复制新的物理页
vfork
创建子进程,共享父进程的资源。会将父进程阻塞,保证子进程先于父进程执行,直到子进程退出或执行新程序。
孤儿进程
当父进程退出后它的子进程还在运行,那么这些子进程就是孤儿进程。孤儿进程将被init进程所收养,并由init进程对它们完成状态收集工作。
僵尸进程
子进程退出后而父进程并未接收结束子进程(如调用waitpid获取子进程的状态信息),进程的进程描述符(Process Descriptor)仍然保留在系统进程表中,并占用一定的系统资源。
这个一般是父进程可以通过调用
wait()
或waitpid()
等系统调用来等待子进程的终止
线程
线程与进程的区别
1、进程有独立的地址,线程共享进程的地址空间
2、每个进程都有自己的数据栈、pc和其他状态信息,系统为每个进程分配独立的空间,线程共享进程的内存与资源,因此线程之间的切换相对快,开销比较小
进程有自己的独立地址空间,多个线程共有一个地址空间
每个线程都有自己的堆区、局部变量
多个线程共享代码区、堆区、全局数据区、打开的文件(文件描述符)都是线程共享的
线程实最小的执行单位,进程是最小的的资源分配单位
多个线程可以抢占更多的时间片
线程切换上下文比进程切换快
线程和进程一样,子线程退出的时候其内核资源主要由主线程回收,线程库中提供的线程回收函叫做 pthread_join(),这个函数是一个阻塞函数,如果还有子线程在运行,调用该函数就会阻塞,子线程退出函数解除阻塞进行资源的回收,函数被调用一次,只能回收一个子线程,如果有多个子线程则需要循环进行回收。
多线程执行顺序
不同优先级采用抢占式调度,同一优先级使用时间片轮询调度
线程加锁之后,就会陷入堵塞状态,卡在半路除非解锁才能从头再次执行
进程调度与线程调度
不同优先级抢占式调度,同优先级时间片轮询
进程和线程切换需要保存上下文
- 通用目的寄存器
- 浮点寄存器
- 状态寄存器
- 程序计数器:存储下一条将要执行的指令
- 用户栈
- 内核栈
- 各种内核数据结构:比如描绘地址空间的页表,包含有关当前进程信息的进程表,以及包含进程已打开文件的信息的文件表。
嵌入常用工具
示波器(模拟电路)
具体电压,具体的波形结构
逻辑分析仪(数字电路)
高低电平(逻辑电压)
时序逻辑
main()函数执行之前
系统的初始化
1、设置栈指针
2、初始化static静态变量,全局变量,也就data段内容
3、将main函数的参数,argc,argv等传递给main函数,然后真正运行main函数
内存管理
内存空间分布
栈区:存放局部变量、函数形参,地址从下到上越来越大
共享区:堆与栈之间
堆区:存放动态申请的变量,地址增方向相反
代码段:只读,存放执行代码和一些只读常量(字符串常量)
全局区
数据段data:存放程序中已初始化的全局变量和静态变量
BSS段:存放程序中未初始化的全局变量和静态变量
mmu
为什么的512m物理内存·可以映射到4g虚拟内存
在操作系统中,虚拟内存的大小可以远远超过物理内存的大小。这是因为虚拟内存的主要目的是提供一个抽象的地址空间给每个进程,使得每个进程可以认为它拥有整个地址空间。虚拟内存的扩展能够让多个进程同时运行,并且每个进程可以使用连续的内存地址进行操作,而不受物理内存大小的限制。
实现虚拟内存的方式之一是使用分页机制。在分页机制中,虚拟内存和物理内存都被划分为固定大小的页或页框。每个进程的虚拟地址空间被划分为许多虚拟页,而物理内存被划分为相同大小的物理页框。操作系统通过页表来建立虚拟页和物理页框之间的映射关系。
现在来解释为什么512MB物理内存可以映射到4GB虚拟内存的情况。假设系统采用4KB的页大小,那么虚拟地址空间的范围是0到4GB,而物理内存的范围是0到512MB。
由于每个页的大小是4KB,因此虚拟地址空间中有2^32/2^12 = 2^20 = 1,048,576个虚拟页。物理内存中有512MB/4KB = 131,072个物理页框。
虚拟页和物理页框之间的映射关系由页表来管理。页表的条目数与虚拟页的数量相同,即1,048,576。每个页表条目需要存储一个物理页框的地址。由于物理页框的数量只有131,072个,所以同一个物理页框的地址可能会出现在不同的虚拟页的页表条目中。
因此,512MB物理内存可以映射到4GB虚拟内存,但并不是每个虚拟页都对应着物理内存中的一页框。在实际执行过程中,当一个进程访问虚拟页时,操作系统会根据页表的映射关系来确定该虚拟页对应的物理页框的位置,如果该物理页框已经在内存中,则直接访问;如果该物理页框不在内存中,则会进行页面调度(页面置换)操作,将所需的物理页框加载到内存中。这样,通过分页机制,操作系统可以实现将大于物理内存大小的虚拟内存映射到物理内存中,并对进程提供统一的地址空间。
注意:页表中存放的是页面之间映射关系,不是当个的映射关系,多级页表就是将将页表
每个进程都有一个自己的页表,空间是相互隔离的
注意:每个进程都有自己的页表
一级页表存放得是虚拟页与物理页之间的关系
虚拟内存的本质是什么
虚拟内存
虚拟内存的本质是一种计算机系统的内存管理技术,它通过将物理内存和磁盘空间结合起来,为每个进程提供一个抽象的、连续的地址空间。
虚拟内存的本质是将进程的虚拟地址空间与物理内存之间的映射进行管理。进程中的每个地址都被认为是虚拟地址,而不管实际上是否对应着物理内存中的位置。当进程访问虚拟地址时,操作系统通过页表等机制将虚拟地址映射到物理内存中的实际位置。
虚拟内存的本质包括以下几个方面:
地址空间隔离:每个进程拥有自己的虚拟地址空间,使得每个进程可以认为它独占整个地址空间,而不会相互干扰。进程的虚拟地址空间从0开始,可以达到非常大的范围,远远超过物理内存的大小。
分页机制:虚拟内存使用分页机制将虚拟地址空间和物理内存划分为固定大小的页或页框。每个进程的虚拟地址空间被划分为许多虚拟页,而物理内存被划分为相同大小的物理页框。操作系统通过页表来管理虚拟页和物理页框之间的映射关系。
页面置换:当物理内存不足时,虚拟内存可以将部分不常用的页面从物理内存换出到磁盘上的交换空间,从而释放物理内存供其他进程使用。当进程需要访问被置换出的页面时,操作系统会将其从磁盘加载到物理内存中。
虚拟内存管理:虚拟内存管理涉及到页表的维护、页面调度算法的选择、页面置换策略的决策等。操作系统通过这些机制来优化内存的使用,提高进程的执行效率。
多级页表:多级页表,因为内存中存在很多页表,页表中有具体页号,直接查找页很麻烦,先查找所属的页表是哪个再去找。也就是将多个页表做一个目录页目录表的每一项对应一个页表,然后再根据页表找到对应的页
总之,虚拟内存的本质是通过地址映射和页面管理技术,为每个进程提供一个抽象的、连续的地址空间,使得进程可以使用比物理内存更大的虚拟地址空间,并实现内存的高效管理和扩展能力。
内存泄漏的情况
(简单地说就是申请了一块内存空间,使用完毕后没有释放掉。)
1、申请内存空间后没有释放,malloc/new申请的内存没有主动释放
解决办法:使用free/delete释放内存,或者使用智能指针
2、如果指向动态分配内存的指针丢失或被覆盖,将无法再释放对应的内存,导致内存泄漏。
3、c++中析构函数不是虚函数
内存池
在使用内存对象的时候之前,先申请分配一段数量的内存块留作备用。当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够再继续申请新的内存。当不需要此内存时,重新将此内存放入内存池中
内存泄漏、内存溢出
内存泄漏
是指程序在申请内存后无法释放已申请的内存空间,导致系统无法及时收回内存并且分配给其他进程使用
1、动态分配的内存没有被释放:当使用关键字
new
或malloc
分配内存时,应该使用对应的delete
或free
函数来释放内存。如果没有正确释放,就会导致内存泄漏2、丢失对分配内存的指针
3、循环引用:在使用动态内存分配的数据结构时,如果对象之间存在循环引用,且没有适当地释放内存,就会导致内存泄漏。这通常发生在没有实现正确的析构函数或引用计数机制的情况下。
4、父类析构函数不是虚函数,这样子类就无法释放,导致内存泄漏
内存溢出;程序申请内存的时候,没有足够的内存供申请者使用
溢出原因:
1、 内存中加载的数据过多,
2、集合类中对对象的引用,使用完后未清空,使得不能回收
3、代码在存在是死循环或循环产生过多重复的对象实体
内存碎片
死锁
死锁是指两个或者两个以上的运算单元,相互持有对方所需的资源,导致它们无法向前推进,从而导致永久阻塞的问题
需要满足下面四个条件
1、互斥条件:运算单元对分配的资源在一段时间只能被一个运算单元持有
2、请求和保持条件:单元持有一个资源,但又提出新的资源请求而该资源被其他占据,自身也对自己持有的资源不放弃
3、不可剥夺:运算单元对已经获得的资源,在未使用完之前,不能被剥夺
4、环路等待:即运算单元正在等待另一个运算单元占用的资源,而对方又在等待自己占用的资源,从而造成环路等待的情况。
解决死锁
按照顺序加锁:尝试让所有线程按照同一顺序获取锁,从而避免死锁。
设置获取锁的超时时间:尝试获取锁的线程在规定时间内没有获取到锁,就放弃获取锁,避免因为长时间等待锁而引起的死锁。
段错误
"段错误"(Segmentation Fault)是在程序运行过程中遇到的一种错误,通常是由于程序访问了无法访问的内存段(
未分配的内存访问:当程序试图访问未分配的内存地址时,会导致段错误。
数组越界访问:如果程序尝试访问数组之外的元素,就会发生段错误。
空指针访问:如果程序使用了未初始化的指针或者指针被设置为NULL,尝试访问该指针指向的内存会导致段错误。
栈溢出:当
线程互斥锁的底层原理
底层操作基于原子操作
1、当我们把锁创建出来,并初始化以后,内存中就有一个变量叫做mutex,此时mutex = 1
2、加锁
1、线程A申请锁的时候,先把 0 放到线程某个寄存器中 (初始化寄存器)。2、将寄存器中的值和内存中mutex的值交换,线程中寄存器的值就是1
3、线程中寄存器的值变成1,mutex 等于 0表面线程已经获得了锁,可以向下访问临界资 源
要是有其他线程再次申请锁就会被挂起
3、解锁
把mutex的值赋值为1,也就是把锁还给内存中的mutex变量,然后环形等待mutex的线程。比如唤醒线程B,然后线程B就会去执行上面申请锁的汇编语句。
malloc、kalloc、valloc
kmalloc 保证分配的内存在物理上是连续的,vmalloc保证的是在虚拟地址空间上的连续,malloc申请的内存不一定连续(用户空间存储以空间链表的方式组织(地址递增),每一个链表块包含一个长度、一个指向下一个链表块的指针以及一个指向自身的存储空间指针。)
kmalloc能分配的大小有限,vmalloc与malloc能分配的空间大小相对较大。
进程上下文与中断上下文
进程上下文:
程上下文就是表示进程信息的一系列东西,包括各种变量、寄存器以及进程的运行的环境数据、用户堆栈以及共享存储区;、
寄存器上下文: 通用寄存器、程序寄存器(IP)、、栈指针(ESP)、
进程控制块task_struct、内存管理信息(mm_struct、vm_area_struct、pgd、pte)、内核栈。
中断上下文:其实也可以看作就是硬件传递过来的这些参数和内核需要保存的一些其他环境(主要是当前被打断执行的进程环境)
memcpy、strcpy的区别
1、memcpy 复制所有的类型1,strcpy复制字符串
2、strcpy一遇到\0就停止拷贝,memcpy指定拷贝长度
计算机网络
OSI七层体系结构、TCP/IP四层体系结构、五层协议体系结构
应用层:应用层是体系结构中的最高层。其任务是通过应用进程间的交互来完成特定网络应用
传输层。其任务是为两台主机中进程之间的通信提供通用的数据传输服务
网络层:其任务是负责为分组交换网上的不同主机提供通信服务。在发送数据时,网络层把传输层产生的报文段或用户数据报封装成分组(包)(Packet)进行传送。由于网络层使用IP协议,因此分组也称为IP数据报。网络层的另一个任务就是要选择合适的路由,使源主机传输层所传下来的分组,能够通过网络中的路由器找到目的主机。路由(routing)是指路由器从一个接口上收到数据包,根据数据包的目的地址进行定向并转发到另一个接口的过程。
数据链路层:两台主机之间的数据传输,总是在一段一段的链路上传送的,这就需要专门的链路层协议。在两个相邻结点之间传送数据时,数据链路层将网络层交下来的IP数据报组装成帧(framing),在两个相邻结点间的链路上传送帧(frame)。每一帧包括数据和必要的控制信息(如同步信息、地址信息、差错控制等)。用于协作 IP 数据在已有网络介质上传输的协议,典型的是ARP/RARP
tcp协议工作再哪个层
传输层
TCP与UDP的区别(被问)
TCP是面向连接的,一对一,需要三次握手才能连接,四次挥手断开,具有可靠性
UDP 无连接,不可靠,支持一对多,但是速度快,需要自己实现可靠性
Tcp是字节流,可以多次发送一次接受,udp是数据报,发送断与接受端的次数必须相同
upd 速度比TCP快
upd 速度比TCP快,tcp在传输数据时,会使用合并优化算法,将小的数据包合并成大的一并发送,这也会导致粘包问题
TCP 有堵塞控制,会调机速度
三次握手与四次挥手
三次握手:
1、(SYN)客户端发出一个带有SYN(同步)标志的TCP数据包,指示客户端要建立连接
2、(SYN+ACK)服务端收到客户端的请求后,会发送一个带有SYN和ACK(应答确认)表示确认连接
3、(ACK)客户端收到服务器确认后,会发送一个带有ACK标志的数据包,表示客户端也确认连接
socket中三次握手第一次发生在客户端向服务端发出请求申请,第二、三次发生在服务端响应
四次挥手
(FIN)当客户端想要关闭连接时,发送一个带有FIN(结束)标志的数据包,表示不再发送数据,但仍接收数据
(ACK):服务器收到客户端的关闭请求后,发送一个带有ACK标志的数据包,确认收到关闭请求。
(FIN)服务器准备关闭连接时,发送一个带有FIN标志的数据包,表示服务器不再发送数据。
(ACK)客户端收到服务器的关闭请求后,发送一个带有ACK标志的数据包,确认收到关闭请求,此时连接被彻底关闭。
tcp怎么保证传输的可靠性*(面试被问)
1、确认和重传机制:当接受到双方的数据之后,会发送一个确认消息给发送方,告诉它已经收到了信息,要是没有收到确认消息,它会重传数据,直到接受方收到数据并确认消息
2、序列号和确认号:TCP将每个数据段都分配一个序列号和确认号,序列号用来标识数据段的位置,确认号用来确认已经收到的数据段的位置,这样可以避免数据丢失或者乱序
3、流量控制:TCP使用滑动窗口协议控制发送数据的速率,接收方会告诉发送方它的缓冲区大小,发送方会根据接收方的缓冲区大小来控制发送速率,确保接收方不会因为太快而丢失数据
4、拥塞控制:TCP使用拥塞窗口控制网络拥塞,当网络拥塞时,TCP会减少发送速率,以避免过多的数据包导致堵塞
tcp数据传输格式
报文 = 首部(20字节) + 数据段将字符数据存放数据段就可以(40)
tcp协议通信流程
连接:三次握手
1、(SYN)客户端发出一个带有SYN(同步)标志的TCP数据包,指示客户端要建立连接
2、(SYN+ACK)服务端收到客户端的请求后,会发送一个带有SYN和ACK(应答确认)表示确认连接
3、(ACK)客户端收到服务器确认后,会发送一个带有ACK标志的数据包,表示客户端也确认连接
传输:这个保证了传输的可靠性
1、确认和重传机制:当接受到双方的数据之后,会发送一个确认消息给发送方,告诉它已经收到了信息,要是没有收到确认消息,它会重传数据,直到接受方收到数据并确认消息
2、序列号和确认号:TCP将每个数据段都分配一个序列号和确认号,序列号用来标识数据段的位置,确认号用来确认已经收到的数据段的位置,这样可以避免数据丢失或者乱序
3、流量控制:TCP使用滑动窗口协议控制发送数据的速率,接收方会告诉发送方它的缓冲区大小,发送方会根据接收方的缓冲区大小来控制发送速率,确保接收方不会因为太快而丢失数据
4、拥塞控制:TCP使用拥塞窗口控制网络拥塞,当网络拥塞时,TCP会减少发送速率,以避免过多的数据包导致堵塞
断开:四次我握手
FIN)当客户端想要关闭连接时,发送一个带有FIN(结束)标志的数据包,表示不再发送数据,但仍接收数据
(ACK):服务器收到客户端的关闭请求后,发送一个带有ACK标志的数据包,确认收到关闭请求。
(FIN)服务器准备关闭连接时,发送一个带有FIN标志的数据包,表示服务器不再发送数据。
(ACK)客户端收到服务器的关闭请求后,发送一个带有ACK标志的数据包,确认收到关闭请求,此时连接被彻底关闭。
tcp粘包是什么,怎么产生、怎么处理
产生愿意
- 由Nagle算法造成的发送端粘包。Nagle算法是一种改善网络传输效率的算法,但也可能造成困扰。Nagle算法简单的说,当提交一段数据给TCP时,TCP并不立刻发送此段数据,而是等待一段时间,看看在等待期间是否还有要发送的数据,若有则会一次把多段数据发送出去。这就造成了粘包。
- 接收端接收不及时造成的接收端粘包。TCP会把接收到的数据存在自己的缓冲区中,然后通知应用层取数据。当应用层由于某些原因不能及时取出TCP的数据,就会造成TCP缓冲区中存放多段数据、
怎么处理
2、在发送内容之前,加上发送内容的长度
3、封包,给一段数据加上包头,包投中有一个结构体,其中有个结构体成员变量表示包体的长度
tcp丢包问题解决
1、调整缓冲区大小
2、使用丢包重传机制
3、
tcp的设计流程
连接建立阶段:
- 主机定位:通信的两个主机确定自己的IP地址和端口号。
- 三次握手:发送方发送一个带有SYN标志的数据包给接收方,接收方收到后回复一个带有SYN和ACK标志的数据包,发送方再回复一个带有ACK标志的数据包。这三次握手确认了双方的通信能力和同意建立连接。
数据传输阶段:
- 序号和确认号:数据包中包含一个序号字段和一个确认号字段,用于标识数据包的顺序和确认已接收的数据。
- 滑动窗口:每个数据包都有一个窗口大小字段,用于指示接收方的缓冲区大小。滑动窗口机制可以控制流量和流量控制。
- 超时重传:如果发送方没有收到确认,或者确认很久没有到达,就会触发超时重传,重新发送数据。
连接终止阶段:
- 四次挥手:当一个端口要关闭连接时,它发送一个带有FIN标志的数据包,另一端回复一个带有ACK标志的数据包。然后,另一端发送一个带有FIN标志的数据包,另一端回复一个带有ACK标志的数据包,从而完成连接的关闭。
请说说socket网络编程的步骤
(1)服务器根据地址类型( ipv4, ipv6 )、 socket 类型、协议创建 socket。
(2)服务器为 socket 绑定 IP 地址和端口号。
(3)服务器 socket 监听端口号请求,随时准备接收客户端发来的连接,这时候服务器的socket 并没有被打开 。
(4)客户端创建 socket。
(5)客户端打开 socket,根据服务器 IP 地址和端口号试图连接服务器 socket。
(6)服务器 socket 接收到客户端 socket 请求,被动打开,开始接收客户端请求,直到客户端返回连接信息 。这时候 socket 进入阻塞状态,所谓阻塞即accept()方法一直到客户端返回连接信息后才返回,开始接收下一个客户端连接请求 。
(7)客户端连接成功,向服务器发送连接状态信息 。
(8)服务器 accept 方法返回,连接成功 。
(9)客户端向 socket 写入信息 。
(10)服务器读取信息 。
(11)客户端关闭 。
发送过程中检测错误的协议类型
CRC 校验,在数据位加上校验位,接收到数据后与发送数据对比
客户端失去连接服务端怎么知道
Heartbeats(心跳包):服务器可以定期向客户端发送心跳包,客户端在接收到心跳包后需要发送响应。如果服务器在一段时间内没有收到来自客户端的响应,就可以假定客户端已经失去连接。
超时设置:服务器可以为每个客户端连接设置超时时间。如果在超时时间内没有收到客户端的数据或活动,服务器会断开连接并清理资源。
http与https
https是对http进行了加密处理
超文本传输协议
TTP 协议的重要特点: 一发一收,一问一答协议工作原理:输入网址的时候会给对应的服务器发送一个HTTP请求,对应的服务器收到请求之后,经过计算
请求协议的数据格式:
响应数据格式
服务端最大连接与客户端最大连接
网络编程
select、poll、epoll
io多路复用,将任务交给内核进行处理,内核会检测出有那个缓冲区可以写入数据或者读取数据
1、select函数,可以监听的数目有限制,并且每次都调用select函数的时候都需要将fd集合从用户态拷贝到内核态,,将进程挂到每个fd的等待队列中,然后遍历fd集合消耗大
2、po'll 开销也比较大,但是因为fd使用链表这样的话,数目限制就不存在了
3、epoll将主动轮询变成被动通知,当有事情发生时,接收到通知后再去处理,也就是epoll会把哪个流发生哪种i/o事件通知我们,epoll是事件驱动(每个事件关联到fd),fd上有注册有回调函数,当网卡接收到数据时会回调该函数,同时将该fd的引用放入rdlist就绪列表中,当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可。如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户。
epoll里面都有哪些事件?
EPOLLIN:表示文件描述符准备好进行读取操作(有数据可读)。
EPOLLOUT:表示文件描述符准备好进行写入操作(可以写入数据)。
EPOLLERR:表示文件描述符发生错误。
epoll
处理数据的一般流程
1、使用epoll_create 创建一个epoll实例
2、使用epoll_ctl 将要监听的文件描述符添加到epoll实例中去,指定感兴趣的事件(如EPOLLIN、EPOLLOUT等))
3、使用epoill_wait 等待事件发生,此函数会阻塞,直到有文集描述符准备好(有事件发生)
4、一旦有文件描述符准备好,epoll_wait将返回一个包含就绪的文件描述符
5、应用程序可以遍历返回的数组,检查每个文件描述符的事件类型,然后进行相应的操作,比如,如果一个文件描述符有数据可读,可以使用read函数读取数据
6、需要的时候,可以使用
epoll_ctl
来修改文件描述符的监听事件,或者从epoll
实例中移除文件描述符。
shell编程
#!/bin/bash
shell脚本需要专门的解析器来解析然后执行,不同的脚本语言需要匹配对应的解析器才能解析执行。#!/bin/bash 表示用/bin/bash解释脚本并执行,#!/bin/sh表示用/bin/sh解释脚本并执行
数据结构与排序算法
数组
1、数组是由数据类型相同的一组元素组成的一种数据结构,在内存中顺序存放
2、通过下标索引查找元素
3、不可增加、不可删除
链表
线性表
用一组地址连续的存储单元依次存储线性表的数据元素,这种存储结构的线性表称为顺序表。
特点:逻辑上相邻的数据元素,物理次序也是相邻的。
只要确定好了存储线性表的起始位置,线性表中任一数据元素都可以随机存取,所以线性表的顺序存储结构是一种随机存取的储存结构,因为高级语言中的数组类型也是有随机存取的特性,所以通常我们都使用数组来描述数据结构中的顺序储存结构,用动态分配的一维数组表示线性表。
优点与缺点
优点:无须为表中元素之间的逻辑关系而增加额外的存储空间;可以快速的存取表中任一位置的元素。
缺点:插入和删除操作需要移动大量元素;当线性表长度较大时,难以确定存储空间的容量;造成存储空间的“碎片
链式表
链式存储时,逻辑上相邻的元素,物理存储位置则不一定相邻,对应的逻辑关系是通过指针链接来表示的,链表存放指针和一个value
栈
栈的链式结构
栈先进后出,是一个特殊的线性表,只能在一端进行操作
1、初始化栈
2、出栈
3、入栈
4、判断栈是否为空
队列
队列先进先出
队列(queue)是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。
队列是一种先进先出(First In First Out)的线性表,简称FIFO。允许插入的一端称为队尾,允许删除的一端称为队头。
队头(Front):允许删除的一端,又称队首。
队尾(Rear):允许插入的一端。
哈希表
(数组和链表)
unordered_map
树
树的定义是递归的,即在树的定义中又用到了自身,树是一种递归的数据结构。树作为一种逻辑结构,同时也是一种分层结构,具有以下两个特点:
- 树的根结点没有前驱,除根结点外的所有结点有且只有一个前驱。
- 树中所有结点可以有零个或多个后继。
红黑树
红黑树(Red-Black Tree)是一种自平衡的二叉搜索树,它在插入和删除等操作后能够自动调整树的结构,以保持树的平衡,从而保证树的查找、插入和删除等操作都能在对数时间内完成,具有较好的性能。
红黑树具有以下特性:
节点颜色: 每个节点被标记为红色或黑色。
根节点: 根节点必须为黑色。
叶子节点(空节点): 叶子节点都是黑色的。注意,这里的叶子节点是指树末端的空节点,它们不包含任何数据。
红色节点限制: 红色节点的子节点必须是黑色的。这意味着在任何路径上不能有连续的两个红色节点。
黑色节点计数: 从任何节点到其每个叶子的简单路径上的黑色节点数目必须相同,这个值被称为“黑色高度”。
冒泡排序(时间复杂度n平方,空间复杂度0(1))
比较相邻的元素,如果前一个比后一个大,就把它们两个调换位置。对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。针对所有的元素重复以上的步骤,除了最后一个。持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。注意:第二个for循环j < length - i - 1要注意哦
快排(时间复杂度nlong n,空间复杂度0(logn))
1)从序列中挑出一个元素,作为”基准”(pivot).
2)把所有比基准值小的元素放在基准前面,所有比基准值大的元素放在基准的后面(相同的数可以到任一边),这个称为分区(partition)操作。
3)对每个分区递归地进行步骤1~2,递归的结束条件是需要排序的序列的大小是0或1,这时整体已经被排好序了。
linux
linux一些常见命令
查看磁盘占用 df -h
查看pid top / ps
kill - 9
查看内存情况 free
显示打印 echo
查找文件 whereis
创建文档与查找文档 touch cat
解压 zip unzip tar.gz tar -xzvf xxx
chmod 4 读/2写/1执行
grep 筛选
GDP 调试
启动调试
list 显示代码
run 运行
break 行数 在某行打上断点
step 执行一行 进入函数内部
next 执行一行但是不进入函数内部
continue 运行到下一个断点
where 执行到了哪一行
finsh 跳出当前函数
q 退出
堆栈分析
bt /i frame
标准输入输出
输入是指从键盘输入
./my_program < input.txt
输出是指输出到屏幕上
./my_program > output.txt
makefile
规则、目标、依赖组成
当依赖不存在就会想下查找
需要定义一些变量
all 模块化执行
gcc 编译过程 ,动态库与静态库区别,怎么查看程序依赖的库,link文件吗
动态库 程序运行的时候载入,
静态库,编译的时候链接到代码中,这样代码就比较大
使用objdump命令 查看程序依赖文件
驱动开发
linux阻塞与并发竞争、同步、异步
竞争与并发
互斥锁
信号量
自旋锁
原子操作
阻塞 :等待队列(唤醒)
非阻塞:(去干别的,会一直查找是否有机会执行)轮旬调度(select、poll)epoll采用事件驱动不要传统的轮询调度
异步(线程干完吗,就主动报备)使用信号
同步与异步(消息通信机制,访问数据的方式)
同步:你问书店老板有没有数据结构这本书,老板说我找一下,然后你一直等到有结果才能离开书店
异步:你问书店老板有没有数据结构这本书,老板说我找一下,你先回家,找到了打电话通知你
堵塞与非堵塞(访问数据是否准备就绪)
堵塞:把自己挂起,直到老板告诉你有没有找到、树
非堵塞:干别的去,时不时询问老板找到了吗(轮询)
module_init 什么意思在驱动程序中
module_init
宏用于定义模块加载时要执行的初始化函数。当模块加载到内核中时,内核会在初始化过程中调用这个初始化函数,以执行特定的设置、分配资源、注册设备等操作。这个初始化函数的原型通常是
驱动中断
中断中不能直接调用中断服务函数,要触发
中断函数不能是extren 禁止外部程序调用
单片机中,中断通常是由硬件事件触发的,在内存维护着一个中断向量表,通过中断号找到对应的中断向量(中断函数入口地址)
linux中断比较复杂,中断触发有软中断和硬件中断,单片机中断不可以抢占,linux可以抢占
中断流程
中断请求、中断响应、中断处理、中断返回
裸机中中断不能传参,实时操作系统可以进行参数传递,向中断函数传递参数
具体实现
1、先知道你要使用的中断对应的中断号,使用函数从设备树中获得
2、先申请request_irq,此函数会激活中断。
3、如果不用中断了,那就释放掉,使用free_irq。
4、中断处理函数irqreturn_t (*irq_handler_t) (int, void *)。
5、使能和禁止中断,
中断上半部
硬件中断、时间快的中断下班
中断源和硬件中断:硬件设备(如硬件中断控制器)或其他触发机制(如定时器)会引发硬件中断。当硬件中断发生时,控制权从当前执行的进程或内核代码切换到中断上半部分。
硬中断处理程序:硬件中断会调用硬中断处理程序(也称为中断服务例程或ISR,Interrupt Service Routine)。硬中断处理程序是内核代码的一部分,用于处理中断并进行一些紧急的处理。硬中断通常要尽快完成,以确保系统快速响应。
禁止本地中断:为了保护中断处理程序的执行过程,内核会在执行硬中断处理程序期间禁止本地中断。这防止了在中断处理程序执行时,其他硬件中断干扰当前的处理。
中断处理程序的执行:硬中断处理程序执行所需的操作,这可能涉及读取硬件设备状态、处理设备数据、更新数据结构等。
调用上半部分的处理函数:硬中断处理程序通常会在必要时调用上半部分的处理函数。这些处理函数是内核中的回调函数,用于执行与中断相关的操作。它们可能包括更新设备状态、唤醒等待中的进程、触发软中断等。
退出中断处理程序:硬中断处理程序完成后,会重新启用本地中断,允许其他硬件中断再次发生。然后,控制权返回到原始的执行上下文,这可以是被中断的进程或内核代码。
中断下半部
可以抢占,时间长的中断
1
软中断:
1、软中断是在编译期间静态分配的。
2、最多可以有32个软中断。
3、软中断不会抢占另外一个软中断,唯一可以抢占软中断的是中断处理程序。
4、可以并发运行在多个CPU上(即使同一类型的也可以)。所以软中断必须设计为可重入的函数(允许多个CPU同时操作),因此也需要使用自旋锁来保护其数据结构。
1、tasklet基于软中断实现
2、一种特定类型的tasklet只能运行在一个CPU上,不能并行,只能串行执行
一个内核线程去执行—这个下半部分总是会在进程上下文执行,但由于是内核线程,其不能访问用户空间。最重要特点的就是工作队列允许重新调度甚至是睡眠
怎么查看i2c设备挂载情况
1、使用i2c—tools,i2cdetect
2、在/sys/bus/i2c
内核态与用户态的作用以及通信
用户态:
于用户态的 CPU 只能访问受限资源,不能直接访问内存等硬件设备,不能直接访问内存等硬件设备
内核态:CPU 可以访问任意的数据,包括外围设备,比如网卡、硬盘等
用户态与内核态的转换:
系统调用(软中断)、异常、中断
RISC 和ARM架构
RISC 和 ARM 都基于RISCV精简指令集,x86是基于复杂指令集
RISC开源兼容性,更加好
ARM发展时间长比较复杂,稳定性好
arm 是处理器架构,cortx-A7是具体架构,对应的编译器架构是
cortx-A7 具有四核,有1种用户模式+8种特权模式
i2c控制器
CPU 控制i2c适配器(也就是i2c控制器),设备一般挂接在受 CPU 控制的 I2C 适配器上, 通过 I2C 适配器与 CPU 交换数据
i2c_algorithm 是 I2C 适配器与 IIC 设备进行通信的结构体,内部核心传输函数是
i2c_transfer 实现控制器与子设备之间的传输
GPIO模式
1、开漏输出
2、推挽输出
3、复用开漏输出
4、复用推挽
c++
const与define的区别
1、变量声明并赋值的方式进行定义,而
#define
使用预处理指令进行定义。2、作用域:
const
常量具有块作用域,只在定义的作用域内有效,而#define
常量是全局的,可以在整个程序中使用存储方式:
3、存储const
常量在内存中有一份存储,每个使用该常量的地方都会使用这份存储,而#define
常量仅仅是简单的文本替换,没有存储空间4、const有编译器优化,define没有
map与hash_map或者unordered_map的区别底层实现:map是基于红黑树实现的有序容器、uborderded_map是基于哈希表实现的无序容器
map中不允许存在相同的key,unorderd_map中允许
内联函数
内联函数在编译的时候直接展开,不需要进行函数跳转,节省了的资源空间