|
沙发
楼主 |
发表于 2023-2-2 00:20:37
|
只看该作者
六.C++
1. new、delete、malloc、free关系
1.new分配空间的'同时可以初始化', malloc分配空间时'不可以直接初始化'
2.new如果不初始化会'自动用0初始化', malloc分配完就是'随机值',需
要'memset'或者'bzero'清0
3.mallco/free 是'库函数',new/delete 是C++的'关键字'
4.new分配空间时可以根据类型'自动计算空间大小',而malloc需要'手动计算传参'
5.new '返回的就是'相应类型的指针,而malloc需要'强制类型转换'---返回的是
(void *)
6.new会调用'构造函数',malloc不会
7.delete会调用'析构函数',free不会
1
2
3
4
5
6
7
8
9
2. delete与 delete []区别
//方括号里 可以写空间的大小 也可以不写,不写就是 申请了多少 就被释放多少
delete只会调用'一次''析构函数',而delete[]会调用'每一个成员'的'析构'函数。
在More Effective C++中有更为详细的解释:“当delete操作符用于数组时,它为每个数组元素调用析构函数,然后调用operator delete来释放内存。”delete与new配套,delete []与new []配套
这就说明:对于内建简单数据类型,delete和delete[]功能是相同的。对于自定义的复杂数据类型,
delete和delete[]不能互用。delete[]删除一个数组,delete删除一个指针。
简单来说,
用new分配的内存用delete删除;
用new[]分配的内存用delete[]删除。
delete[]会调用数组元素的析构函数。内部数据类型没有析构函数,所以问题不大。如果你在用delete时没用括号,delete就会认为指向的是单个对象,否则,它就会认为指向的是一个数组。
1
2
3
4
5
6
7
8
9
10
11
3. C++有哪些性质(面向对象特点)
面向对象的三大特征 : 封装 继承 多态 -----// 如果有四:加一个 抽象
1
4. 子类析构时要调用父类的析构函数吗?
'析构函数'调用的次序是先'派生类'的析构后'基类的析构',
也就是说在'基类'的的'析构调用'的时候,派生类的信息已经全部销毁了。
'定义'一个对象时先调用'基类的构造'函数、然后'调用派生类'的'构造'函数;
析构的时候'恰好相反':先调用'派生类的析构'函数、然后'调用基类的析构'函数。
1
2
3
4
5. 多态,虚函数,纯虚函数
多态:通过 父类的'指针或引用'指向 子类的对象,可以访问'子类中重写的父类中的方法'。
实现多态的必要条件:
1.继承 //---需要子类
2.父类指针或引用指向子类对象 //---需要一个指针或引用
3.虚函数 //---汇聚子类中 只有一份 公共基类的 成员
4.函数重写 //---修改原来的函数
//-----------------------------------------------------------
虚函数:以关键字 virtual 开头的成员函数。 '声明'或'定义函数'前加上 virtual
允许'在派生类'中对'基类的虚函数''重新定义'。
//-----------------------------------------------------------
纯虚函数:是一种在基类中只有声明,没有定义的函数
纯虚函数的作用:在'基类'中'为其'派生类'保留一个函数'的名字,以便'派生类''根据需要'对它进行'定义'。作为接口而存在
'纯虚函数''不具备函数'的功能,一般'不能直接被调用'。
从'基类继承'来的'纯虚函数',在'派生类'中'仍是虚函数'。
//----------------------------------------------------------
如果'一个类中'至少'有一个纯虚函数',那么这个类被称为'抽象类'(abstract class)。
'抽象类'中不仅包括'纯虚函数',也可包括'虚函数'。
抽象类'必须用作'派生其他类的'基类'
包含'纯虚函数的类' 叫做 '抽象类'
抽象类'不允许'实例化对象,否则'报错'
子类中'必须重写抽象类的纯虚函数',否则'报错'
抽象类虽然不能实例化对象,但可以通过抽象类的'指针或引用'指向子类的对象来'实现多
态'的特性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
6. 求下面函数的返回值(微软)----特么的题呢–
int func(x)
{
int countx = 0;
while(x)
{
countx ++;
x = x&(x-1);
}
return countx;
}
1
2
3
4
5
6
7
8
9
10
假定x = 9999。 答案:8
思路:将x转化为2进制,看含有的1的个数。
& : 0 与 任何数 & 都等于 0 1 与 任何数 & 都等于 任何数
由于每次都是 x 跟 x-1 进行 & 运算,所以每次,都会弄没一个 1 ,所以只要看x的最开始的二进制有几个1 ,然后就会执行几次
9999先转换成二进制数,0010 0111 0000 1111 一共8 个,8次
1
2
3
4
测试代码如下:
#include <stdio.h>
int func(x)
{
int countx = 0;
while(x)
{
countx ++;
printf("x = %#x\n",x);
printf("x-1 = %#x\n",x-1);
x = x&(x-1);
printf("后x = %#x\n",x);
puts("------------------------");
}
return countx;
}
int main()
{
int x = 12287;// %#x == 2FFF, 有13个1
int o = 0;
o = func(x);
printf("OK------o = %d\n",o);
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
7. 什么是“引用”?声明和使用“引用”要注意哪些问题?
引用就是某个目标变量的"别名"(alias),对应用的操作与对变量直接操作效果完全相同。
声明一个'引用'的时候,切记要对其进行'初始化'。
引用声明完毕后,'相当于''目标变量名'有'两个名称',即该目标'原名称'和'引用名',不能再把该引用名作为其他变量名的别名。
声明一个引用,'不是'新定义了一个变量,它只表示该'引用名'是目标变量名的一个'别名',它本身不是一种数据类型,因此引用本身不占存储单元,系统也不给引用分配存储单元。不能建立数组的引用。
1
2
3
4
5
7-8. 引用与指针的区别
1. 引用必须'初始化' ,指针可以'不初始化'
2. '引用不可以改变指向' ,指针可以
3. '不存在指向NULL的引用', 指针可以指向NULL
4. '指针'在使用前需要'检查合法性' ,引用不需要
1
2
3
4
8. 将“引用”作为 函数参数 有哪些特点?
1.传递'引用'给函数 与 传递'指针'的'效果'是一样的。
这时,被调函数的'形参'就成为原来主调函数中的'实参变量'或'对象的'一个'别名'来使用,
所以在被调函数中对'形参变量的操作'就是对其'相应的目标对象'(在主调函数中)的操作。
2.使用'引用'传递函数的参数,在内存中并没有产生实参的副本,它是'直接对实参操作';
而使用'一般变量'传递函数的参数,当发生函数调用时,需要给'形参分配存储单元',形参变量是实参变量的副本;
如果传递的是'对象',还将'调用拷贝构造'函数。因此,当参数传递的数据较大时,用'引用'比用一般'变量'传递参数的'效率和所占空间'都好。
3.使用'指针'作为函数的'参数'虽然也能达到与使用引用的效果,
但是,在被调函数中同样要给'形参分配存储单元',且需要重复使用"*指针变量名"的形式进行运算,这很容易产生错误且程序的阅读性较差;
另一方面,在主调函数的调用点处,必须用变量的地址作为实参。而引用更容易使用,更清晰。
1
2
3
4
5
6
7
8
9
10
11
9. 在什么时候需要使用“常引用”?
如果既要利用'引用'提高程序的'效率',又要'保护'传递给函数的'数据不'在函数中'被改变',就'应使用'常引用。
//有些场景下,函数中 不想修改 形参的值,可以使用 常引用
常引用声明方式:const 类型标识符 &引用名=目标变量名;
int a ;
const int &ra=a;
ra=1; //错误
a=1; //正确
1
2
3
4
5
6
7
10. 将“引用”作为函数返回值类型的格式、好处和需要遵守的规则?
格式:类型标识符 &函数名(形参列表及类型说明)
{
//函数体
}
好处:在'内存'中'不产生'被返回值的'副本';
(注意:正是因为这点原因,所以'返回一个局部变量的引用是不可取'的。因为随着该局部变量生存期的结束,相应的引用也会失效,产生runtime error!)
注意事项:
1. '不能返回局部变量的引用'。这条可以参照Effective C++[1]的Item 31。
主要原因是'局部变量'会在'函数返回'后'被销毁',因此被返回的'引用'就成为了"无所指"的引用,程序会进入未知状态。
2. '不能返回函数内部new分配的内存的引用'。这条可以参照Effective C++[1]的Item 31。
虽然不存在局部变量的被动销毁问题,可对于这种情况(返回函数内部new分配内存的引用),又面临其它尴尬局面。
例如,被函数返回的'引用'只是作为一个'临时变量'出现,而没有被赋予一个实际的变量,那么这个引用所指向的空间(由new分配)就'无法释放',造成memory leak(内存泄漏)。
3. '可以返回类成员的引用',但最好是'const'。这条原则可以参照Effective C++[1]的Item 30。
主要原因是当'对象的属性'是与某种业务规则(business rule)相关联的时候,其赋值 常常与 某些其它属性或者对象的状态有关,因此有必要将赋值操作封装在一个业务规则当中。
如果其它对象可以获得该属性的'非常量引用(或指针)',那么对该属性的单纯赋值就会破坏业务规则的完整性。
4. '流操作符重载返回值'声明为"引用"的作用:
流操作符<<(打印)和>>(提取),这两个操作符常常希望被连续使用,
例如:cout << "hello" << endl;
因此这两个操作符的'返回值'应该是一个仍然支持这两个操作符的流引用。
可选的其它方案包括:返回一个流对象和返回一个流对象指针。
但是对于返回一个'流对象',程序必须重新(拷贝)构造一个新的流对象,也就是说,连续的两个<<操作符实际上是针对不同对象的!这无法让人接受。
对于返回一个'流指针'则不能连续使用<<操作符。
因此,返回一个'流对象引用'是唯一选择。这个唯一选择很关键,它说明了'引用的重要性'以及'无可替代'性。---- '返回的自身的引用'
赋值操作符=。这个操作符象流操作符一样,是可以连续使用的,
例如:x = j = 10;或者(x=10)=100;
赋值操作符的返回值必须是一个左值,以便可以被继续赋值。因此引用成了这个操作符的惟一返回值选择。
#include<iostream.h>
int &put(int n);
int vals[10];
int error=-1;
void main()
{
put(0)=10; //以put(0)函数值作为左值,等价于vals[0]=10;
put(9)=20; //以put(9)函数值作为左值,等价于vals[9]=20;
cout<<vals[0];
cout<<vals[9];
}
int &put(int n)
{
if (n>=0 && n<=9 ) return vals[n];
else { cout<<"subscript error"; return error; }
}
5. 在另外的一些操作符中,却千万不能返回引用:
+-*/ 四则运算符。它们不能返回引用,Effective C++[1]的Item23详细的讨论了这个问题。
主要原因是这四个操作符没有side effect,因此,它们必须构造一个对象作为返回值,
可选的方案包括:返回一个对象、返回一个局部变量的引用,返回一个new分配的对象的引用、返回一个静态对象引用。
根据前面提到的引用作为返回值的三个规则,2、3两个方案都被否决了。
静态对象的引用又因为((a+b) == (c+d))会永远为true而导致错误。
所以可选的只剩下返回一个对象了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
11. 结构与联合有和区别?—C基础第四题
12. 试写出程序结果:
int a=4;
int &f(int x)
{
a=a+x;
return a;
}
int main(void)
{
int t=5;
cout<<f(t)<<endl;// a = 9
f(t)=20; //a = 20
cout<<f(t)<<endl;// t = 5,a = 20 a = 25
t=f(t); //a = 30 t = 30
cout<<f(t)<<endl; }// t = 60
}
//t = 60
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
13. 重载(overload)和重写(overried,有的书也叫做“覆盖”)的区别?
从定义上来说:
重载:是指允许存在多个同名函数,而这些函数的参数表不同(或许参数个数不同,或许参数类型不
同,或许两者都不同)。
重写:是指子类重新定义父类虚函数的方法。
从实现原理上来说:
重载:编译器根据函数不同的参数表,对同名函数的名称做修饰,然后这些同名函数就成了不同的函数
(至少对于编译器来说是这样的)。如,有两个同名函数:function func(p:integer):integer;和
function func(p:string):integer;。那么编译器做过修饰后的函数名称可能是这样的:int_func、
str_func。对于这两个函数的调用,在编译器间就已经确定了,是静态的。也就是说,它们的地址在编
译期就绑定了(早绑定),因此,重载和多态无关!
重写:和多态真正相关。当子类重新定义了父类的虚函数后,父类指针根据赋给它的不同的子类指针,
动态的调用属于子类的该函数,这样的函数调用在编译期间是无法确定的(调用的子类的虚函数的地址
无法给出)。因此,这样的函数地址是在运行期绑定的(晚绑定)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14. 有哪几种情况只能用intialization list(初始化列表) 而不能用assignment?
必须使用初始化表的场景
1. '成员变量名字'和'构造函数形参名字'冲突时;---也可以用this解决
2.类中包含' const '成员变量时 //因为 const 只能在初始化的时候赋值
3.类中包含' 引用' 成员变量 //引用 只能先指向 再操作
4.类中包含'成员子对象'(类中包含其他类对象时)时
//必须使用初始化表调用子对象的构造函数 完成对成员子对象的初始化
//构造函数 才有初始化 表
1
2
3
4
5
6
7
15. C++是不是类型安全的?
不是。两个不同类型的指针之间可以强制转换(用reinterpret cast)。C#是类型安全的
1
2
16. main 函数执行以前,还会执行什么代码?
全局对象的构造函数会在main 函数之前执行。
1
17. 描述内存分配方式以及它们的区别?
1) 从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static 变量。
2) 在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集。
3) 从堆上分配,亦称动态内存分配。程序在运行的时候用malloc 或new 申请任意多少的内存,程序员自己负责在何时用free 或delete 释放内存。动态内存的生存期由程序员决定,使用非常灵活,但问题也最多。
1
2
3
4
5
18. 分别写出BOOL,int,float,指针类型的变量a 与“零”的比较语句。
BOOL : if ( !a ) or if(a)
int : if ( a == 0)
float : const EXPRESSION EXP = 0.000001
if ( a < EXP && a >-EXP)
pointer : if ( a != NULL) or if(a == NULL)
1
2
3
4
5
19. 请说出const与#define 相比,有何优点?–C基础-11题
20. 简述数组与指针的区别?–C基础-12题
21. int (*s[10])(int) 表示的是什么?
函数指针数组,每个指针指向一个int func(int param)的函数
回调函数
1
2
22. 栈内存与文字常量区
char str1[] = "abc";
char str2[] = "abc";
const char str3[] = "abc";
const char str4[] = "abc";
const char *str5 = "abc";
const char *str6 = "abc";
char *str7 = "abc";
char *str8 = "abc";
cout << ( str1 == str2 ) << endl;//0 分别指向各自的栈内存
cout << ( str3 == str4 ) << endl;//0 分别指向各自的栈内存
cout << ( str5 == str6 ) << endl;//1指向文字常量区地址相同
cout << ( str7 == str8 ) << endl;//1指向文字常量区地址相同
结果是:0 0 1 1
解答:str1,str2,str3,str4是数组变量,它们有各自的内存空间;而str5,str6,str7,str8是指针,它们指向
相同的常量区域。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
23. 将程序跳转到指定内存地址
要对绝对地址0x100000赋值,我们可以用(unsigned int*)0x100000 = 1234;那么要是想让程序跳转到绝对地址是0x100000去执行,应该怎么做?
((void ()( ))0x100000 ) ( );
首先要将0x100000强制转换成函数指针,
即void (*)())0x100000
然后再调用它:
((void ()())0x100000)();
用typedef可以看得更直观些:
typedef void(*)() voidFuncPtr;
*((voidFuncPtr)0x100000)();
1
2
3
4
5
6
7
8
9
10
24. int id[sizeof(unsigned long)];这个对吗?为什么?
正确 这个 sizeof是编译时运算,编译时就确定了 ,可以看成和机器有关的常量。
1
27. 内存的分配方式有几种?—上面 17
28. 基类的析构函数不是虚函数,会带来什么问题?
#虚析构函数的作用:
用来指引delete关键字,正确回收空间。
--- 因为正常的析构函数 只能调用 父类 的析构
//基类指针指向子类对象时,如果析构函数不是虚函数
//那么只会调用 基类的析构函数,不调用子类的,所以可能会造成内存泄露
//如果基类的 析构函数是 虚函数
//则会调用 子类的析构函数,子类的析构函数 会默认 调用基类的析构函数,不会内存泄漏
1
2
3
4
5
6
7
29. 全局变量和局部变量有什么区别?是怎么实现的?操作系统和编译器是怎么知道的?
生命周期不同:
全局变量随主程序创建和创建,随主程序销毁而销毁;
局部变量在局部函数内部,甚至局部循环体等内部存在,退出就不存在;
使用方式不同:
通过声明后全局变量程序的各个部分都可以用到;局部变量只能在局部使用;分配在栈区。
操作系统和编译器通过'内存分配的位置'来知道的,
'全局变量'分配在'全局数据段'并且在程序开始运行的时候被加载。
'局部变量'则分配在'栈'里面 。
1
2
3
4
5
6
7
8
9
10
七. ARM体系结构编程
1. 简单描述一下ARM处理器的特点,至少说出5个以上的特点。
低功耗;
低成本,高性能,RISC结构;
体积小;
指令定长;
支持Thumb(16位)/ARM(32位)双指令集;
1
2
3
4
5
2. ARM内核有多少种工作模式?请写出这些工作模式的英文缩写,有几种异常模式,有几种特权模式,cortex_a系列有几种特权模式,几种工作模式
|--> 特权模式 |---> 异常模式 | --> FIQ模式(快速中断)
| | | --> IRQ模式(普通中断)
| | | --> SVC模式(特权模式)
工作 | | | --> Abort模式(中止异常模式)
模式 | | | --> Undef模式(未定义的异常模式)
| | | --> Monitor模式(安全监控模式)
| |---> 非异常模式---> System模式(User模式下的一种特权模式)
|
|--> 非特权模式 --> User模式
1
2
3
4
5
6
7
8
9
10
3. ARM内核有多少个寄存器,简述一下
ARM有37个寄存器,
(1)未分组寄存器:R0-R7,共8个;
(2)分组寄存器R8-R14,其中FIQ模式下有单独的一组R8-R12共5个,另外6种模式共用一组R8-R12,共5个,
(3)程序计数器PC即R15寄存器,共1个;
(4)状态寄存器CPSR,和5个备份状态寄存器SPSR,共6个
1
2
3
4
5
4. ARM通用寄存器中,有3个寄存器有特殊功能和作用,请写出它们的名字和作用。
R13:SP栈指针寄存器,用来保存程序'执行时的栈指针位置';
R14:LR返回链接寄存器,用来保存程序执行'BL指令'或'模式切换时'的'返回'原程序'继续执行的地址';
R15:PC程序计数器,指向'正在取指的指令的地址'
1
2
3
5. 请描述一下CPSR寄存器中相关Bit的情况和作用。
条件位(指令进行算术运算后的结果是否有进位,借位等),
I位(IRQ异常允许位),
F位(FIQ异常允许位),
T位(ARM/Thumb工作状态),
模式位(处理器工作模式)
1
2
3
4
5
N[31] : 指令的'运算结果为负数',N位被自动'置1',否则清0.
Z[30] : 指令的'运算结果为零',Z位被自动'置1',否则清0.
C[29] :
'加法':产生'进位',C位被自动'置1',否则清0.
'减法':产生'借位',C位被自动'清0',否则置1.
进位/借位:'低32位'向'高32位' 进位或者借位。
两个64位数相加,需要拆分成两个32位数进行加法运算,先算低32位,再算高32位,在进行高32位运算时需要考虑低32位是否有进位。
V[28] : '符号位'发生变化,V位被自动'置1',否则清0.
I[7] : IRQ屏蔽位
I = 0 不屏蔽IRQ中断
I = 1 屏蔽IRQ中断
F[6] : FIQ屏蔽位
F = 0 不屏蔽FIQ中断
F = 1 屏蔽FIQ中断
T[5] : 状态位
T = 0 : ARM状态 --》执行ARM指令集
T = 1 : Thumb状态 ---》执行Thumb指令集ARM指令集和Thumb指令集区别
ARM指令集: 每条指令占4个字节的空间
Thumb指令集:每条指令占2个字节的空间
#ARM指令集的功能性比Thumb指令集的功能更加完善。Thumb指令集的指令的密度要比ARM指令集高。
M[4:0] : 模式位
10000 User mode;
10001 FIQ mode;
10011 SVC mode;
10111 Abort mode;
11011 Undfined mode;
11111 System mode;
10110 Monitor mode;
10010 IRQ
#其他的保留
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
6. 什么是立即数?立即数的本质是什么
然后将这个数,循环 右移偶数 位,如果 可以得到 要判断的那个数,说明这个数 是一个立即数。
立即数的本质 包含在指令中的数
1
2
7. 请问BL指令跳转时LR寄存器保存的是什么内容?并请简述原因
LR中保存的是执行完'BL跳转指令的下一条指令'的地址。LR用来在'需要返回程序'时从LR中'还原程序执行的位置'继续执行。(保存现场--恢复现场)
1
8. 请描述一下什么是处理器现场,如何进行保存现场?
每种工作模式下都包含R0-R15,CPSR这17个寄存器,程序的执行当前状态就保存在这些寄存器中,称为处理器现场。
当发生'模式切换'时,由于其中的一些寄存器是多种模式下'共用的寄存器',为了'防止共用'处理器寄存器中的'值被破坏',所以需要'保存'原模式下的'处理器现场',
利用STM批量存储指令,把处理器现场对应的寄存器保存到栈上,待还原时再出栈恢复(模式和返回地址)。
其中保存现场的工作, 硬件完成了'CPSR模式的保存'和'PC返回地址的保存',其他寄存器的保存工作主要依靠软件压栈完成,其中'LR'因为'可能被异常处理程序中'的BL跳转指令'修改',所以一般都需要'软件压栈'再保存。
1
2
3
4
5
9. ATPCS默认使用的是什么栈?–满减栈(ARM也是)
10. 什么是满栈、空栈,什么是增栈、减栈?
栈的种类: (sp)
空栈:栈指针指向的栈空间没有有效的数据,
压栈时可以直接将数据压入栈空间中,
需要再次将栈指针指向一个空的空间。
满栈:栈指针指向的栈空间有有效的数据,
压栈时需要先移动栈指针指向一个空的栈空间,
在压入数据,此时栈指针指向的仍然是一个有有效数据的栈空间。
增栈:压栈时栈指针向高地址方向移动,
出栈时栈指针向低地址方向移动。
减栈:压栈时栈指针向低地址方向移动,
出栈时栈指针向高地址方向移动
1
2
3
4
5
6
7
8
9
10
11
12
11. 请写出一条完整的ARM软件中断指令,并简要描述其作用。
SWI 0x1。
SWI指令触发'软中断异常',使程序的执行流跳转到异常向量表地址0x8,0x1是软中断的中断号,
软中断处理程序可根据不同的中断号调用对应的处理子程序。一般SWI软中断都用于操作系统的系统调用。
1
2
3
12. 请描述一下ARM体系中异常向量表的概念。
异常向量表是从0x0地址开始,一共32个字节,包含8个表项,其中有1个保留不用,其他7个表项对应7种异常发生后的跳转位置,
这7种异常发生后分别对应到5种异常模式。每个表项里面放的一般都是一条跳转指令,
用来跳转到真正的异常处理程序入口,通过BL指令,或者LDR PC,[PC, #?] 的方式都可以实现此类跳转。
1
2
3
13. 请写出一个ARM程序生成的bin文件映像中包含哪些内容?
ARM生成的bin文件包含:RO,RW 两个段,注意 ZI 段一般都不在 bin 文件中占用存储空间。
1
14. 请举例说明在ARM处理器上进行一次中断处理和中断异常处理的差异。
中断处理相比异常处理,主要是中断需要初始化中断源和中断控制器,中断发生后在ISR中要清除相
应Pending位,而且要在进入中断处理程序一开始就清除。
1
2
15. 请简述异常中断处理发生时,是通过什么完成初始化步骤,这些初始化的具体步骤是什么?
当发生异常中断时,有ARM CORE完成一下工作,称作4大步三小步
1)拷贝CPSR到SPSR_mode
2)设置CPSR的位
修改处理器状态进入ARM态
修改处理器模式为相应的异常模式
禁止相应的中断(根据需要禁止FIQ或者IRQ)
3)保存返回地址到LR_MODE
4)设置PC位相应的异常向量
1
2
3
4
5
6
7
8
16. uboot的主要作用
uboot 属于bootloader的一种'引导程序',是用来引导启动内核的
初始化大部分的硬件,为内核运行提供基础,
给内核传递参数
uboot主要做了两个阶段的事:
第一个阶段:汇编
构建'异常向量表'
禁止'mmu'和'cache',禁止'看门狗'
硬件时钟的'初始化','内存'的'初始化'
清除'bss'段 #bss段是用来存储静态变量,全局变量的
完成uboot代码的自搬移
'初始化C代码'运行的'栈空间'
第二个阶段:C
完成'大部分硬件'的'初始化','串口'的初始化,'内存'的进一步的初始化,'电源的'初始化 等等必要'硬件的'初始化
根据命令是否进入'交互模式'还'自启动模式'
获取uboot的'环境变量',
执行'bootcmd中的命令',
最终'给内核传递参数'(bootargs)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
17. uboot是怎样引导启动内核的?
uboot刚开始被放到flash中,
板子上电后,会自动把'其中的一部分代码'拷到'内存'中执行,
这部分代码负责把'剩余的uboot代码'拷到'内存'中,
然后'uboot代码'再把'kernel部分代码'也拷到'内存'中,
并且启动,内核启动后,
挂载根文件系统,执行应用程序。
1
2
3
4
5
6
18. uboot的启动过程的重要干了什么
uboot启动主要分为两个阶段,
主要在start.s文件中,第一阶段主要做的是硬件的初始化,
包括,设置'处理器模式'为'SVC模式','关闭看门狗','屏蔽中断','初始化sdram','设置栈','设置时钟',从'flash拷贝代码'到'内存','清除bss段'等,bss段是用来存储静态变量,全局变量的,
然后程序跳转到start_arm_boot函数,宣告第一阶段的结束。
第二阶段比较复杂,
做的工作主要是:
1.从flash中读出内核。
2.启动内核。
start_arm_boot的主要流程为,
设置机器id,
初始化flash,
然后进入main_loop,
等待uboot命令,
uboot要启动内核,主要经过'两个函数',
第一个是's=getenv("bootcmd")',
第二个是'run_command(s...)',
所以要启动内核,需要根据'bootcmd环境变量'的内容启动,bootcmd环境变量一般指示了从'某个flash地址'读取内核到启动的内存地址,然后启动,bootm。
uboot启动的内核为uImage,这种格式的内核是由'两部分'组成:'真正的内核'和'内核头部'组成,
'头部'中包括内核中的一些信息,比如'内核的加载地址','入口地址'。
uboot在接受到启动命令后,要做的主要是,
1,'读取'内核头部,
2,'移动'内核到合适的加载地址,
3,启动内核,执行do_bootm_linux
do_bootm_linux主要做的为,
1,设置启动参数,在特定的地址,保存启动参数,函数分别为
setup_start_tag,
setup_memory_tag,
setup_commandline_tag,
setup_end_tag,
根据名字我们就知道具体的段内存储的信息,
memory中为板子的'内存大小信息',
commandline为命令行信息,
2,跳到入口地址,启动内核
启动的函数为
the_kernel(0,bd->bi_arch_number,bd->bi_boot_param)
bd->bi_arch_number为板子的机器码,
bd->bi_boot_param为启动参数的地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
19. bootcmd和bootargs两个uboot环境变量的作用
bootcmd:<设置开发板的'自启动的环境变量'>
这个参数包含了一些命令,这些命令将在倒计时结束后,进入u-boot自启动模式后执行
setenv bootcmd tftp c2000000 uImage \; tftp c4000000 stm32mp157a-fsmp1a.dtb \; bootm c2000000 - c4000000
//-------------------------------------------------------------------------------
bootargs:这个参数设置要'传递给内核的信息',主要用来告诉'内核分区信息'和'根文件系统'所在的分区
setenv bootargs root=/dev/nfs nfsroot=192.168.1.250:/home/linux/nfs/rootfs,tcp,v4 rw console=ttySTM0,115200 init=/linuxrc ip=192.168.1.222
1
2
3
4
5
6
7
8
20. linux内核的启动过程
内核启动过程中主要干了那些事?
1> uboot通过'tftp命令'将'uImage下载'到内存中(下载内核)
2> 'uImage'需要完成'自解压'
3> 获取cpu的ID号,创建页表,初始化MMU,完成物理地址到虚拟地址的映射
4> '清除BSS段',bss段是用来存储静态变量,全局变量的
5> 完成绝'大多数硬件'的'初始化',进一步对硬件初始化< 内存,时钟,串口,EMMC,nfs客户端....>
5> 从'u-boot环境变量'的内存分区'获取bootargs参数','根据'bootargs'参数',决定从哪里'挂载根文件系统'。
6> '挂载'根文件系统
7> 执行根文件系统中的'1号进程','linuxrc'程序
8> 到此开发板的linux系统启动成功
1
2
3
4
5
6
7
8
9
10
21. uImage,zImage,vmlinux的区别
内核编译(make)之后会生成两个文件,
一个'Image',一个'zImage',
其中'Image'为'内核映像'文件,
而'zImage'为'内核'的一种'映像压缩文件',
'Image'大约为4M,
而'zImage'不到2M。----因为压缩了
那么uImage又是什么的?----zImage 加了个头
它是'uboot专用'的'映像'文件,它是在'zImage'之前'加上'一个长度为'64字节'的'头',
说明这个'内核的版本'、'加载位置'、'生成时间'、'大小'等信息;其'0x40'之后与'zImage没区别'。
如何生成uImage文件?
首先在uboot的/tools目录下寻找'mkimage'文件,把其copy到系统'/usr/local/bin'目录下,这样就完成制作工具。
然后在内核目录下运行'make uImage',
如果成功,便可以在'arch/arm/boot/'目录下发现'uImage'文件,其大小比zImage多'64个字节'。
其实就是一个自动跟手动的区别,
'有了'uImage'头部的描述',u-boot就知道对应Image的信息,
如果'没有头部'则需要自己'手动去添加那些参数'。
#U-boot的U是“通用”的意思。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22. Kconfig,.config,Makefile三个文件之间的关系
编译内核文件时,先要配置.config文件(make menuconfig --> 选择需要的驱动 ),
然后Makefile在编译时通过'读取.config文件'的'配置信息'来选择要编译的文件,选择驱动的加载方式。
.config文件的生成可通过 make menuconfig ARCH=arm 或 make defconfig 方式生成,
这两种方式看上去虽然不同,但是两者的原理是一样的,都是通过Kconfig文件的配置来的。
1
2
3
4
八. 系统移植
1. Linux内核启动流程—同上 七.20
1. Linux内核'自解压过程'
uboot完成系统引导以后,执行环境变量bootm中的命令;
即,将Linux内核调入内存中并调用do_bootm函数启动内核,跳转至kernel的起始位置。如果内核没有被压缩,则直接启动;如果内核被压缩过,则需要进行解压,被压缩过的kernel头部有解压程序。
压缩过的kernel入口第一个文件源码位置在/kernel/arch/arm/boot/compressed/head.S。它
将调用decompress_kernel()函数进行解压,
解压完成后,打印出信息“UncompressingLinux...done,booting the kernel”。
解压缩完成后,调用gunzip()函数(或unlz4()、或bunzip2()、或unlz())将内核放于指定位置,开始启动内核。
2. Linux内核'启动准备阶段'
由内核链接脚本/kernel/arch/arm/kernel/vmlinux.lds可知,内核入口函数为stext(/kernel/arch/arm/kernel/head.S)。
内核解压完成后,解压缩代码调用stext函数启动内核。
P.S.:内核链接脚本vmlinux.lds在内核配置过程中产生,由/kernel/arch/arm/kernel/vmlinux.lds.S文件生成。
原因是,内核链接脚本为适应不同平台,有条件编译的需求,故由一个汇编文件来完成链接脚本的制作。
(1)关闭IRQ、FIQ中断,进入SVC模式。调用setmode宏实现;
(2)校验处理器ID,检验内核是否支持该处理器;若不支持,则停止启动内核。调用__lookup_processor_type函数实现;
(3)校验机器码,检验内核是否支持该机器;若不支持,则停止启动内核。调用__lookup_machine_type函数实现;
(4)检查uboot向内核传参ATAGS格式是否正确,调用__vet_atars函数实现;
(5)建立虚拟地址映射页表。此处建立的页表为粗页表,在内核启动前期使用。Linux对内存管理有更精细的要求,随后会重新建立更精细的页表。调用__create_page_tables函数实现。
(6)跳转执行__switch_data函数,其中调用__mmap_switched完成最后的准备工作。
1)复制数据段、清除bss段,目的是构建C语言运行环境;
2)保存处理器ID号、机器码、uboot向内核传参地址;
3)b start_kernel跳转至内核初始化阶段。
3. Linux内核'初始化阶段'
此阶段从start_kernel函数开始。start_kernel函数是所有Linux平台进入系统内核初始化的入口函数。
它的主要工作是'完成剩余与硬件平台相关的初始化'工作,在进行一系列与内核相关的初始化之后,
调用'第一个用户进程init'并等待其执行。至此,整个内核启动完成。
3.1 start_kernel函数的主要工作
start_kernel函数主要完成'内核相关的初始化'工作。具体包括以下部分:
(1)内核架构 、通用配置相关初始化
(2)内存管理相关初始化
(3)进程管理相关初始化
(4)进程调度相关初始化
(5)网络子系统管理
(6)虚拟文件系统
(7)文件系统
start_kernel函数详解。
3.2 start_kernel函数流中的关键函数
(1)setup_arch(&command_line)函数'内核架构相关的初始化'函数,是非常重要的一个初始化步骤。
其中,包含了处理器相关参数的初始化、内核启动参数(tagged list)的获取和前期处理、内存子系统的早期初始化。
command_line实质是'uboot向内核''传递的命令行启动参数',即uboot中环境变量bootargs的值。
若uboot中bootargs的值为空,command_line = default_command_line,即为内核中的默认命令行参数,其值在.config文件中配置,对应CONFIG_CMDLINE配置项。
(2)setup_command_line、parse_early_param以及parse_args函数
这些函数都是在'完成命令行参数的解析、保存'。
譬如,cmdline = console=ttySAC2,115200 root=/dev/mmcblk0p2 rw init=/linuxrc rootfstype=ext3;
解析为一下四个参数:
console=ttySAC2,115200 //指定控制台的串口设备号,及其波特率
root=/dev/mmcblk0p2 rw //指定根文件系统rootfs的路径
init=/linuxrc //指定第一个用户进程init的路径
rootfstype=ext3 //指定根文件系统rootfs的类型
(3)sched_init函数初始化进程调度器,创建运行队列,设置当前任务的空线程。
(4)rest_init函数rest_init函数的主要工作如下:
1)调用kernel_thread函数启动了2个内核线程,分别是:kernel_init和kthreadd。
kernel_init线程中'调用prepare_namespace函数'挂载根文件系统rootfs;然后调用init_post函数,执行根文件系统rootfs下的第一个用户进程init。用户进程有4个备选方案,若command_line中init的路径错误,则会执行备用方案。
第一备用:/sbin/init,
第二备用:/etc/init,
第三备用:/bin/init,
第四备用:/bin/sh。
2)调用schedule函数开启内核调度系统;
3)调用cpu_idle函数,启动空闲进程idle,完成内核启动。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
2. 什么是bootloader?在嵌入式系统当中bootloader的作用是什么?
Bootloader是'引导加载程序'的统称,#(Boot :引导 Loader : 加载)
是嵌入式系统上电后的第一段代码,其主要作用是将'硬件初始化'
到一个合适的状态并将'嵌入式操作系统''加载到内存中'执行。
1
2
3
3. 为什么汇编语言对硬件平台有依赖性而C语言却可以不依赖硬件平台?
'不同的处理器'因为'硬件的差异'其'机器码是不同'的,
'汇编语言'是将'机器码用符号'表示所以'不同的处理器''汇编语言也不同'。---解释型语言
C语言是'不能被CPU直接识别和执行'的,所以C程序需要'先被编译成机器码后'才能执行,---编译型语言--标准C库
如果我们使用的是ARM处理器那么编译的时候我们就将'C'编译成ARM的机器码,如果我们使用的是X86处理器
那么编译的时候我们就将C编译成X86的机器码,所以'C语言'可以'不区分硬件平台'。
1
2
3
4
5
6
4. 什么叫做交叉编译?
'交叉编译'就是在'一台主机'上'编辑和编译'程序,而把'编译好的程序'放到'另外一台机器'上执行。
比如嵌入式开发中我们在'电脑上编辑和编译程序',而编译好的程序'被下载到开发板'执行。
1
2
5. Linux平台下的可执行文件是什么格式?
'elf格式'的文件是linux平台下常用的'二进制格式'
1
6. 什么叫做反汇编?
因为'汇编语言'是用'符号来表示机器码',所以'汇编语言'和'机器码'是'一一对应'的,
所以我们可以将'汇编语言'编译成'机器码',同样如果有了'机器码'也可以'反推出汇编',
我们把通过机器码来推出汇编的过程叫做反汇编
1
2
3
7. 简述nfs服务的概念与作用?
NFS(Network File System)即'网络文件系统',
它允许网络中的计算机之间通过'TCP/IP网络''共享资源'。
在NFS的应用中,本地NFS的客户端应用可以透明地读写位于远端NFS服务器上的文件,就像访问本地文件一样。
1
2
3
8. 简述一个装有linux内核的开发板的启动过程?
一个装有linux内核的开发板上电后的
第一个程序一般是uboot,
uboot首先对'软硬件资源'进行'初始化',
然后将'固化'在存储器中的'内核'以及'相关文件'引导加载到'内存'中,
然后'内核开始运行',内核'首先'对'软硬件资源进行初始化',内核初始化完成后内核从指定位置去'挂载根文件系统'
根文件系统挂载完成后就可以运行上层的应用程序即完成了系统的启动。
1
2
3
4
5
6
9. 简述uboot的主要功能有哪些?
uboot最主要的功能有以下几点
1)'初始化一些硬件'为后续做准备
2)'引导'和'加载'内核
3)给'内核传参'
4)执行用户命令
1
2
3
4
5
10. uboot如何设置环境变量?
uboot中设置环境变量使用的是'setenv'命令,
比如我们想设置uboot的ipaddr环境变量为192.168.1.1就可以执行'setenv ipaddr 192.168.1.1'命令完成设置
1
2
11. 简述uboot中bootcmd环境变量的作用?–七.19
bootcmd:<设置开发板的'自启动的环境变量'>
这个参数包含了一些命令,这些命令将在倒计时结束后,进入u-boot自启动模式后执行
1
2
12. 简述uboot中bootargs环境变量的作用?–七.19
uboot除了可以引导和加载内核外还可以为内核传参,即给内核传递一些信息以便于内核的正确启动,我们可以先将这些信息(比如根文件系统位置、控制台信息等)写入到bootargs环境变量,然后uboot再将这些信息传递给内核使用。
1
13. 简述什么叫平台相关代码什么叫平台无关代码?
平台相关代码即和'硬件平台相关'的代码,当'硬件平台改变'后'这类代码'也'要做对应的修改'。
比如一些操作CPU、寄存器、引脚等相关代码,当硬件改变后这类代码就不再适用需要做对应的修改。
平台无关代码就是和'硬件平台无关'的代码,'不管硬件平台是否改变''这类代码都不用修改'。
平台相关: 跟硬件'平台有关代码'
arch
平台无关代码: 跟硬件'平台无关代码'
lib
include
drivers
toos
ipc
net
....
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
14.如何理解linux/uboot支持各种硬件平台?
一般情况下'不同的处理器'由于其'硬件的差异'其'代码是不兼容'的,
即便处理器相同外围的硬件设备不同代码也不兼容,
我们经常说linux/uboot支持各种硬件平台'并不是'其代码'真正的'能够'适用于任何平台',
而是在linux/uboot'源代码中'将其'所有支持的硬件平台'的'代码''都写了一遍',
使用的时候当前我们'使用的是什么平台'我们就'编译对应的代码'即可。
1
2
3
4
5
15. 如何配置uboot使其适合特定的开发板平台?
在uboot源码的顶层目录下执行'make <开发板名>_config'即可将其配置成'适合特定开发板平台'的'uboot'。
比如当前开发板的名字是origen那么我们在源码的顶层目录下执行make origen_config即可完成配置。
1
2
16. 如何编译uboot生成二进制文件?
在配置好的uboot源码的顶层目录下直接执行'make命令'即可编译uboot源码生成二进制的可执行文件。
1
17. 简述uboot的启动过程?
uboot主要做了两个阶段的事:
第一个阶段:汇编
构建'异常向量表'
禁止'mmu'和'cache',禁止'看门狗'
硬件时钟的'初始化','内存'的'初始化'
清除'bss'段 #bss段是用来存储静态变量,全局变量的
完成uboot代码的自搬移
'初始化C代码'运行的'栈空间'
第二个阶段:C
完成'大部分硬件'的'初始化','串口'的初始化,'内存'的进一步的初始化,'电源的'初始化 等等必要'硬件的'初始化
根据命令是否进入'交互模式'还'自启动模式'
获取uboot的'环境变量',
执行'bootcmd中的命令',
最终'给内核传递参数'(bootargs)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
18. 操作系统的作用有哪些?
内存管理
文件管理
网络管理
进程管理
设备管理
#(这是Linux内核的功能)
1
2
3
4
5
6
19. 如何配置linux源码使其适合特定的处理器?
在linux源码中执行'make <处理器名>_defconfig'即可将其配置成'适合特定的处理器'的代码。
比如现在使用的处理器是exynos,那么我们在linux源码顶层目录下执行命令make exynos_defconfig即可完成配置。
1
2
20. 在make menuconfig界面下有些驱动可以被选成三种状态即“Y”,“N”,“M”这三种状态分别是什么含义?
Y--选配'到内核里',编译后的内核就包含了该驱动程序,同样内核的体积也会随之增大
N--'不会被编译'进内核,即编译后的内核也不支持该驱动
M--'模块化编译'到内核里,要将该驱动编译成模块,在编译内核的时候该驱动'不会被编译',但可以单独编译成一个驱动模块使用的时候临时加载该模块。
1
2
3
21. 如何编译被选中为“M”选项的驱动模块?
在linux源码的顶层目录下执行'make modules'即可将选为“M”的驱动编译成驱动模块。
insmod 可以安装这个驱动
lsmod 查看驱动
rmmod卸载驱动
1
2
3
4
22. 简述设备树的作用?
设备树(Device Tree)是种'描述硬件的数据结构',在操作系统(OS)'引导阶段'进设备'初始化'的时候,数据结构中的'硬件信息''被检测并传递'给'操作系统'。
内核中的'驱动程序''没有'开发板'硬件信息',比如'管脚的信息'、'寄存器的地址'等,所以驱动程序'不能正确的驱动'一个具体的硬件设备工作,设备树是'专门'描述开发板'硬件信息'的'文件',所以有了'设备树','驱动程序'和开发板上'具体的硬件'设备就能'建立起关系'使驱动正常工作。
1
2
3
23. 编写设备树文件的主要依据是什么?
设备树文件是描述开发板硬件信息的文件,
所以编写设备树的'主要依据'是根据开发板硬件的信息。
1
2
24. 简述如何将一个内核源码中已有的驱动程序编译到内核中?
首先在'make menuconfig'中'选中'我们想要的驱动,
因为内核自带的驱动程序中'没有开发板的硬件信息',
所以我们还要'按照实际的硬件信息'去'修改设备树'文件,
然后'重新编译'内核和设备树就可以将驱动编译到内核。
1
2
3
4
25. 简述如何将一个自己编写的驱动程序编译到内核中?
首先我们将自己编写的'驱动程序'放入到'内核源码'中,
然后修改对应的'Kconfig文件'使自己写的驱动能在'make menuconfig'界面中显示出来,
然后还需要修改'对应的Makefile'使驱动能'正确编译',
以上步骤完成后 编译就可以将'驱动编译进内核'。
1
2
3
4
26.在内核启动过程中如果控制台已经初始化我们一般采用什么方式来调试内核?
如果控制台已经初始化我们一般使用'printk函数'来打印我们自己的信息
1
27. linux内核在启动过程中遇到什么情况会打印系统崩溃报告Oops?
当linux内核在启动过程中出现以下几种问题的时候会打印'系统崩溃报告'
1)内存访问'越界' 2)使用'非法指针' 3)使用了'NULL指针' 4)使用了'不正确的指针'
1
2
28. linux内核在启动过程中遇到某些问题会打印系统崩溃报告Oops,报告中主要打印哪些内容?
报告中可以将CPU中'各个寄存器的值'、'页描述符表的位置'以及'其他信息'打印出来。
1
29. 简述什么叫文件系统?什么叫根文件系统?
文件系统是一种'管理和访问磁盘'的'软件机制',其'本质'是'软件',不同'文件系统'管理和访问磁盘的'机制不同'。
根文件系统是'存放运行系统'所必须的'各种工具软件'、'库文件'、'脚本'、'配置'等文件的地方
'实质'就是'一些文件'
1
2
3
4
30. 开发板中为什么一般不需要安装静态库?
库可以分为'静态库'和'动态库'
'静态库'一般是'编译程序'的时候'使用'
而'动态库'一般是'程序运行'的时候'使用'
在嵌入式开发中一般我们使用'交叉编译'的方式,即我们编辑和编译程序是在电脑下而程序编译完成后下载到开发板执行,所以'一般不会在开发板编译程序',所以开发板'一般不需要安装静态库'。
1
2
3
4
5
九. 驱动开发
1. 什么是模块?
Linux 内核的整体结构已经非常庞大,而其包含的组件也非常多。
这会导致两个问题,
一是生成的'内核会很大',
二是如果我们要在现有的'内核中新增或删除功能',将不得不'重新编译内核'。
Linux 提供了这样的一种机制,这种机制被称为'模块(Module)'。使得编译出的内核本身并'不需要包含所有功能',而在这些功能需要'被使用的时候',其'对应的代码'被'动态地加载到内核'中。
1
2
3
4
5
6
2. 驱动类型有几种
字符设备驱动、块设备驱动、网络设备驱动
1
3. 字符设备驱动框架编程流程?
模块加载部分:
1- 生成设备号
2- 注册设备号
3- 初始化字符设备对象,编写填充file_operations结构体集合
4- 添加注册字符设备
模块卸载部分:
1- 取消cdev注册
2- 取消设备号注册
1
2
3
4
5
6
7
8
9
4. 什么是并发,驱动中产生竞态的原因有哪些?
并发(concurrency)指的是'多个执行单元''同时、并行'被'执行',而并发的执行单元对'共享资源'(硬件资源和软件上的全局变量、静态变量等)的'访问'则很容易导致竞态(race conditions)
产生竟态的原因:当'多个进程'同时访问'同一个'驱动的'临界资源'的时候竞态就产生了。
1.对于'单核CPU'来说,如果支持'进程抢占',就会产生竞态。
2.对于'多核CPU'来说,核与核之间'本身'就会产生竞态
3.中断和进程间 会产生竞态
#(arm)中断和中断间会产生竞态 (###错误的###)
1
2
3
4
5
6
7
5. 解决竞态的途径有哪些?分别有什么特点?
1.顺序执行
2.互斥执行
中断屏蔽:都只能禁止和使能本CPU内的中断,因此,并不能解决SMP多CPU引发的竞态
自旋锁 :又叫忙等待锁。自旋锁期间不能有睡眠的函数存在,也不能主动放弃cpu的调度权,也不能进行耗时操作。否则容易造成死锁。
信号量 :是内核中用来保护临界资源的一种,与应用层信号量理解一致
3.互斥体
4.原子操作
1
2
3
4
5
6
7
6. 驱动中IO模型有几种?
(1)阻塞式IO 最简单,最常用,效率最低的io操作
(2)非阻塞式IO 需要不断的轮询。
(3)多路IO复用 解决多路输入输出的阻塞问题
(4)信号驱动IO 异步通信机制,类似于中断。
1
2
3
4
7. 设计linux设备模型的主要作用?
实现'硬件地址信息'与'软件驱动'分离 ----这特么是设备树吧
1
8. 字符设备驱动框架与linux设备模型是否矛盾?
字符设备驱动框架,主要为了使'应用程序'能够经过'层层调用',访问'底层硬件'。
linux设备模型实现'硬件地址信息'与'软件驱动'分离
所以二者并不矛盾。
1
2
3
9. platform架构分别分为哪个部分?他们通过什么进行匹配
platform运用的'分离'的思想,将'设备信息'和'设备驱动'分离,分离后借助'总线'模型 'devicebus driver'完成匹配的过程。
匹配'成功'之后执行'驱动中的probe函数',在probe中操作硬件即可
如果两者'分离'执行驱动中的'remove函数'。bus就完成匹配的工作(bus是内核实现的)。
设备,驱动,总线;
1- 按名称匹配
2- 按id_table表进行匹配
1
2
3
4
5
6
7
10. 设备树与platform架构是否有矛盾?
没有矛盾
'设备树'是对'设备模型'中,'硬件资源'描述部分,进行简化升级。将以前的资源'结构体',改成'设备树节点'的形式进行描述,降低了难度系数
1
2
11. 为什么要将中断分为上下半部?上下半部机制有哪些?
在中断处理函数只能做'简短不耗时'的操作,但是'有的时候'又希望在中断到来的时候'做相对耗时的操作',这样就产生了矛盾,linux内核为了解决这个矛盾引入了'中断底半部'的概念。
将这些'耗时操作'放到中断底半部完成。---但是还是不能有延时函数,因为虽然是中断底半部,但也属于中断,优先级比一般进程要高--。
#例如:当网卡中断到来的时候需要去接受网络传递过来的数据,此时就是一个耗时操作应该放到中断底半部完成。中断底半部的机制 软中断 , tasklet , 工作队列
1
2
3
12. 工作队列与tasklet的区别?
tasklet:
tasklet是'基于'软中断实现的,本身是通过'链表'实现的,因此'没有个数'限制。
tasklet工作在'中断上下文',它是中断的'一个部分','不能脱离'中断执行。
可以做'相对耗时'操作,但是'不能'做休眠的操作。
工作队列:
在内核启动的时候会启动一个'events'的线程这个线程默认处于'休眠'状态,在这个线程中维护一个'工作队列'。
如果想'使用工作队列'就向队列中'提交队列项',然后'唤醒events线程',让它去调用队列项中的'底半部处理函数'即可。
工作队列'可以脱离'中断执行,'没有个数限制',工作于'进程上下文'。在底半部处理函数中可以做延时,耗时,'甚至'休眠的操作。
#-------------------------------------------------------
区别:
工作队列的'使用方法'和tasklet非常'相似'
tasklet运行于'中断上下文'
工作队列运行于'进程上下文'
tasklet处理函数中'不能睡眠',而工作队列处理函数中'允许睡眠'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
13. 内核中内存分配函数分别有哪些?分别有什么特点?
按页(page)分配 __get_free_pages ()
调用者指定'所需整页'的阶数作为参数之一来请求
所申请的页将会'被加入内核空间'
所分配的'物理RAM空间'是'连续'的
kmalloc
除了是'请求精确字节'的'内存'外,与按页分配相同
vmalloc
除了'物理内存不一定连续'外,与kmalloc同
1
2
3
4
5
6
7
8
9
10
14. 内核调试
a.可以使用printk打印内核信息,printk的调试级别如下
#define KERN_EMERG "0" /* system is unusable */
#define KERN_ALERT "1" /* action must be taken immediately */
#define KERN_CRIT "2" /* critical conditions */
#define KERN_ERR "3" /* error conditions */
#define KERN_WARNING "4" /* warning conditions */
#define KERN_NOTICE "5" /* normal but significant condition */
#define KERN_INFO "6" /* informational */
#define KERN_DEBUG "7" /* debug-level messages */
//打印级别的作用是用于过滤打印信息的。
<0> ------------ <7>
最大级别 最小级别
只有当'消息的级别'大于'终端的级别'的时候消息'才会'在终端上'显示'
可以通过'/proc/sys/kernel/printk' cat 这个文件来查看
cat /proc/sys/kernel/printk
4 4 1 7
终端的级别 默认消息的级别 终端的最大级别 终端的最小级别
//注意 : 修改级别的时候 只能使用 echo 重定向,不能使用 vim
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
b. gdb 和 addr2line 调试内核模块
大致流程如下
1.编写的makefile中的gcc加上-Wall -g选项,方便后续调试
2.加载内核模块的时候产生oops,利用dmesg 来查看 panic 内容 (例如:dmesg |tail -20)
3.查看日志内容,找到oops 发生的关键日志,(注意:出现 oops 的是从模块的基地址偏移出来的地址)
4.找到oops发生的基地址,cat /proc/modules |grep oops
5.使用 addr2line 找到 oops 位置,(addr2line -e oops.o 0x14,偏移量=偏移地址-基地址,这里的,0x14就是偏移量)
6.上面的运行结果返回的是源码某个.c文件里的行号,也就是知道了代码导致 oops 的位置是第几行
7.通过objdump 来查找oops 位置(objdump -dS --adjust-vma=0xffffffffa0358000oops.ko)
8.终端打印的结果可以看到反汇编出来的c代码,也就是知道了产生oops的内存地址对应的c代码是哪一句
1
2
3
4
5
6
7
8
9
c. 使用函数BUG_ON(),BUG()和dump_stack()调试内核
使用方法:
1.编写内核驱动模块的时候,在想要查看具体信息的函数中调用BUG_ON(),BUG()或者dump_stack()
2.执行sudo insmode xxx.ko后在kernle日志下可以看到调用函数的具体信息,带绝对路径的文件名和行号等
1
2
3
15. 字符设备驱动的框架
字符设备:提供连续的数据流,应用程序可以顺序读取,通常不支持随机存取,支持按字节/字符来读写
数据。
块设备:应用程序可以随机访问设备数据,程序可自行确定读取数据的位置。应用程序可以寻址磁盘上
的任何位置,并由此读取数据。此外,数据的读写只能以块(通常是512B)的倍数进行。与字符设备不
同,块设备并不支持基于字符的寻址。
网络设备:网络设备是特殊设备的驱动,它负责接收和发送帧数据,可能是物理帧,也可能是ip数据
包,这些特性都有网络驱动决定。它并不存在于/dev下面,所以与一般的设备不同。网络设备是一个
net_device结构,并通过register_netdev注册到系统里,最后通过ifconfig -a的命令就能看到。
1
2
3
4
5
6
7
8
16. 字符设备和块设备和网络设备的区别
字符设备:
提供连续的'数据流',应用程序可以'顺序读取',通常'不支持随机存取',支持按'字节/字符'来读写数据。
块设备:
应用程序可以'随机访问设备数据',程序可'自行确定'读取数据的'位置'。应用程序可以'寻址'磁盘上的'任何位置',并由此读取数据。
此外,数据的读写'只能以块(通常是512B)的倍数'进行。与字符设备不同,块设备并'不支持'基于字符的寻址。
网络设备:
网络设备是'特殊设备'的驱动,它负责'接收和发送'帧数据,可能是'物理帧',也可能是'ip数据包',这些特性都有网络驱动决定。
它并不存在于/dev下面(因为在写Linux内核之前网络协议就有了),所以与一般的设备不同。网络设备是一个net_device结构,并通过'register_netdev'注册到系统里,最后通过'ifconfig -a'的命令就能看到。
1
2
3
4
5
6
7
8
9
10
17. 并发和竞态概念,那些情况会出现竟态,解决竟态的方法,以及区别,使用场景。—上面第4题
并发(concurrency)是指'多个执行单元''同时、并行'的'被执行',而并发执行单元对共享资源的访问很容导致竞态(race condition)。
并发与竞态发生的条件:
对称多处理器(SMP)的多个CPU;单CPU内进程与抢占它的进程;中断与进程之间。
当'多个进程'同时访问'同一个'驱动的'临界资源'的时候竞态就产生了。
1.对于'单核CPU'来说,如果支持'进程抢占',就会产生竞态。
2.对于'多核CPU'来说,核与核之间'本身'就会产生竞态
3.中断和进程间 会产生竞态
解决并发与竞态的途径:
访问共享资源的代码区域称为'临界区域(critical section)'',解决竞态的根本途径就是对临界区的互斥访问,方法主要有中断屏蔽、原子操作、自旋锁、信号量、互斥体。
1. 中断屏蔽
单CPU避免竞态的'简单办法'就是'中断屏蔽',保证可以防止中断与进程间竞态条件的发生(所有中断被屏蔽后进程间切换的基础时钟中断也被屏蔽掉了)。
2.原子操作
原子操作是指在执行过程中不会被别的代码路径所中断,可分为整形原子操作和位原子操作。
3.自旋锁
自旋锁主要针对SMP或单CPU且内核可抢占的情况,自旋锁可以保证临界区不受别的CPU和本CPU内的抢占进程打扰。当临界区可能受到中断和底半步影响时,应该使用自旋锁的衍生操作。
自旋锁是忙等待锁,当锁不可用时,CPU会不停地循环测试而不能做其它的工作,因此自旋锁会降低系统的性能。
如果临界区域发生阻塞,可能会导致死锁,因此在自旋锁占有期间内不能调用copy_from_user(),copy_to_user(), kmalloc()等函数。
4、信号量
信号量和自旋锁不同的地方在于当进程得不到信号量时,进程会进入休眠或其它状态。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
18. 自旋锁和信号量的区别
自旋锁:
当一个程序获取到自旋锁后,另外一个程序也想获取这把锁,此时后一个进程处于自旋状态(忙等,原地打
转)
1.针对'多核处理器'有效
2.自旋的时候需要'消耗cpu资源'
3.自旋锁会'导致死锁'。//---> 加锁了以后 又加一次,然后就会死锁
4.自旋锁可以工作在'中断处理函数'中
5.在自旋锁'内部不能有'延时,耗时,甚至休眠的操作。还'不能
有'copy_to_user/copy_from_user函数。
6.自旋锁在'上锁的时候'会'关闭抢占'
信号量:
信号量:当一个程序获取到信号量后,另外一个程序也想获取这个信号量,此时后一个进程处于休眠状态。
1.等待获取信号量的进程'不占用cpu资源'
2.针对'多核设计'的
3.信号量'不会产生死锁'
4.信号量保护的'临界区可以很大',里面'可以有'延时、耗时,甚至休眠的操作
5.信号量'不能用于'中断处理函数中
#------------------------------------------------------------------------
当临界区'执行时间比较小'时,采用自旋锁,否则采用信号量;
'自旋锁'绝对不能在临界区包含可能'引起阻塞的代码',信号量可以;
如果临界区的代码在'中断中'执行,应该使用'自旋锁'或'信号量的down_trylock()函数'。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
19. 谈谈你对中断上下文,进程上下文的理解
进程上下文:
进程上文:
其是指进程由'用户态'切换到'内核态'是需要'保存用户态时cpu寄存器中的值','进程状态'以及'堆栈上的内容',
即保存'当前进程的进程上下文',以便'再次执行'该进程时,能够恢复切换时的状态,继续执行。
进程下文:
其是指'切换'到'内核态'后执行的程序,即进程运行在内核空间的部分。
#--------------------------------------------------------------------------------
中断上下文:
中断上文:
硬件通过'中断'触发信号,导致内核调用中断处理程序,进入'内核'空间。这个过程中,硬件的一些'变量和参数'也要传递给'内核',内核通过这些参数进行中断处理。
中断上文可以看作就是'硬件传递'过来的这些'参数'和内核需要保存的一些其他环境(主要是当前被中断的进程环境。)
中断下文:
执行在内核空间的中断服务程序。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
20. 中断低半部主要做了什么
为了在中断执行'时间尽可能短'和中断处理'需完成大量工作'之间找到一个平衡点,Linux将中断处理程序分解为两个半部:顶半部(top half)和底半部(bottom half)。
顶半部完成尽可能少的比较紧急的功能,它往往只是简单地读取寄存器中的中断状态并清除中断标志后就进行“登记中断”的工作。
“登记中断”意味着将底半部处理程序挂到该设备的底半部执行队列中去。这样,顶半部执行的速度就会很快,可以服务更多的中断请求。
现在,中断处理工作的重心就落在了底半部的头上,它来完成中断事件的绝大多数任务。
底半部几乎做了中断处理程序所有的事情,而且可以被新的中断打断,这也是底半部和顶半部的最大不同,因为顶半部往往被设计成不可中断。底半部则相对来说并不是非常紧急的,而且相对比较耗时,不在硬件中断服务程序中执行。
尽管顶半部、底半部的结合能够改善系统的响应能力,但是,僵化地认为Linux设备驱动中的中断处理一定要分两个半部则是不对的。如果中断要处理的工作本身很少,则完全可以直接在顶半部全部完成。
其实上面这一段大致说明一个问题,那就是:中断要尽可能耗时比较短,尽快恢复系统正常调试,所以把中断触发、中断执行分开,也就是所说的“上半部分(中断触发)、底半部(中断执行)”,其实就是我们后面说的中断上下文。下半部分一般有tasklet、工作队列实现,触摸屏中中断实现以工作队列形式实现的
中断下半部的处理对于一个中断,如何划分出上下两部分呢?哪些处理放在上半步,哪些放在下半部?
这里有一些经验可供借鉴:
如果一个任务对时间十分敏感,将其放在上半部。
如果一个任务和硬件有关,将其放在上半部。
如果一个任务要保证不被其他中断打断,将其放在上半部。
其他所有任务,考虑放在下半部。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
21. Platfprm平台总线驱动模型
相对于USB、PCI、I2C、SPI等物理总线来说,platform总线是一种'虚拟、抽象'出来的总线,实际中并不存在这样的总线。
那为什么需要platform总线呢?
其实是Linux设备驱动模型为了保持设备驱动的统一性而虚拟出来的总线。
因为对于usb设备、i2c设备、pci设备、spi设备等等,他们与cpu的通信都是直接挂在相应的总线下面与我们的cpu进行数据交互的,但是在我们的嵌入式系统当中,并不是所有的设备都能够归属于这些常见的总线,在嵌入式系统里面,SoC系统中集成的独立的外设控制器、挂接在SoC内存空间的外设却不依附与此类总线。
所以Linux驱动模型为了保持完整性,将这些设备挂在一条虚拟的总线上(platform总线),而不至于使得有些设备挂在总线上,另一些设备没有挂在总线上。
platform总线管理
(1)两个结构体platform_device和platform_driver
(2)两组接口函数(driver\base\platform.c)
// 用来注册设备驱动
int platform_driver_register(struct platform_driver *);
// 用来卸载设备驱动
void platform_driver_unregister(struct platform_driver *);
// 用来注册设备
int platform_device_register(struct platform_device *);
// 用来卸载设备
void platform_device_unregister(struct platform_device *);
不管是'先注册设备'还是'先注册设备驱动'都会进行一次'设备与设备驱动'的匹配过程,匹配成功之后就会调用'probe函数',
匹配的'原理'就是去'遍历'总线下的相应的'链表'来找到挂接在他下面的设备或者设备驱动。
(3) platform总线下的匹配函数,platform_match函数platform总线下设备与设备驱动的匹配原理就是通过名字进行匹配的,先去匹配platform_driver中的id_table表中的各个名字与platform_device->name名字是否相同,
如果相同表示匹配成功直接返回,
否则直接匹配platform_driver->nameplatform_driver->name是否相同,相同则匹配成功,否则失败。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
22. IIC子系统驱动框架
23. 输入子系统驱动框架
子系统的组成
输入子系统的事件处理机制示意图
输入子系统剖析
文章知识点与官方知识档案匹配,可进一步学习相关知识
————————————————
版权声明:本文为CSDN博主「细雨青峦」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qz652219228/article/details/120630417
|
|