嵌入式笔试概念
- 【硬件接口类型】
- 【常见笔试考点】
- *请评论以下这段代码*
- *指出下面函数的错误*
- *下面程序的输出*
- *如何引用一个已定义过的全局变量*
- *关键字volatile有什么含意,并给出三个不同的例子*
- *关键字static的作用*
- *关键字const的作用*
- *C语言实现程序跳转到绝对地址0x100000处执行*
- *指针的定义,使用变量a*
- *sizeof求值和 #pragma pack的使用方法*
- *单片机最小系统包括哪些*
- *内存碎片产生原因及解决办法*
- *C++实现多态的三个条件和实现原理*
- *大小端的各自的优点,哪种时候用*
- *GPIO驱动、PWM驱动、IIC驱动、SPI驱动操作集(设备)*
- *局部变量和全局变量的区别*
- *字符串的初始化注意事项*
- *定义数组的个数*
- 【Linux常用命令】
- 【Linux驱动】
- *Linux内核同步方式总结*
- *Linux中线程有哪几种的状态*
- *Linux中内核空间与用户空间的区别*
- *内核空间与用户空间的通信方式*
- *Linux中系统调用过程*
- *Linux中中断的实现机制,tasklet与workqueue的区别及底层实现区别,为什么要区分上半部和下半部*
- *Linux中的软中断*
- *Linux中的中断上下文和进程上下文*
- */dev/下的设备文件是怎么创建初来的*
- *内核申请内存的有哪几个函数,有什么区别*
- *内核函数mmap的实现原理与机制*
- *ioctl和unlock_ioctl有什么区别*
- *Linux总线、驱动和设备模型*
- *Bootloader多数有两个阶段的启动过程*
- *Linux内核的启动过程*
- 【面试问答题】
- *电路中的保护装置有哪几种*
- *TVS管和稳压二极管的作用和使用方法*
- *浮空与高组态是同一个东西吗*
- *线程与进程*
- *网络编程*
- *c语言程序运行的时候,内存分布*
- *谈谈你对堆、栈的理解*
- *C++栈溢出的原因及解决方法*
- *C使用两个栈实现一个队列*
- *C使用两个队列实现一个栈*
- *函数调用时,参数压栈顺序*
- *解释register关键字*
- *static函数与普通函数的区别*
- *IO复用三种方式*
- *GPIO的开漏输出与推挽输出*
- *怎样用I/O模拟IIC*
- *IIC为什么要用开漏输出和上拉电阻*
- *IIC最多可以挂多少个设备*
- *你觉得哪个linux命令最难*
- *管道和命名管道(FIFO)之间什么区别*
- *嵌入式系统开发和裸机开发什么优缺点*
- *使用芯片时,应该关注数据手册中的哪些信息*
【硬件接口类型】
Nor flash and nand flash 的区别
Nor falsh:
a) 读快写速度慢
b) 提供完整的地址与数据总线,允许随机存取存储器上的任何区域
c) 统一编址,可以直接CPU启动,不需要拷贝到RAM中
d) 适合小容量的启动内存和固件
Nand flash:
a) 要在Nand flash上面读写数据,要外部加主控和电路设计 (现SPI接口读取)
b) 擦写快
c) 存储密度高,体积更大,成本低,寿命较长
d) 坏块较多且随机分布,需要进行坏块处理,适合大容量的存储设备
区别:
a) Nand and Nor 的读都可以以字节为单位,但Nand的写以page为单位,而Nor可以随机写每一个字节
b) Nand flash比Nor flash体积大,成本低,寿命长
c) Nor flash 为分离的地址线和数据线,而Nand flash复用
补充:
EMMC相当于Nand Flash + 主控IC,对外的接口协议与SD、TF卡一样,提供标准接口管理闪存
常用总线对比表格
总线 | 串/并 | 同步/异步 | 速率 | 工作方式 | 线数 | 拓扑 | 通信距离 |
---|---|---|---|---|---|---|---|
UART | 串 | 异步 | 慢 | 全双工 | 2 | 总线 | 远 |
IIC | 串 | 同步 | 100K/400k | 半双工 | 2 | 总线 | 近 |
SPI | 串 | 同步 | 50M | 全双工 | 3/4 | 总线 | 远 |
USB | 串 | 异步 | 480Mb/s | 半双工 | 2 | 树形 | 最长5M |
I2C通信协议
IIC可支持0kHz~5MHz的设备:普通模式(100kHz),快速模式(400kHz),超快速模式(1Mhz),高速模式(3.4MHz)和超高速模式(5MHz)。短距离、低速。master之间不能直接通信,每个master都可以和所有的slave通信,分时使用总线。
IIC协议:
- 开始条件: 总线空闲时SCL和SDA都处于高电平。
- 开始标志: SDA由高向低跳变(多个主设备同时希望获取总线,由先拉低的主设备获取,通信期间,其他主设备的总线请求无效(总线仲裁器控制))
- 地址帧: 7位地址+一位读写控制位(I2C每次必须传输8位数据,8位传输完成,接收端获取SDA权限,发送一个ACK,成功为低电平)
- 数据帧: 每个数据帧8位传送完成,接收方在收到8位数据后,在第9个脉冲向发送方发出特定的低电平。(从最低位开始传送)
- 停止条件: 在SDA为低电平期间将SCL拉高,然后拉高SDA。
- 重复开始条件: 一次通信完成后,不产生停止条件,在SCL为低电平期间将SDA拉高,然后拉高SCL。接着master产生 一个开始信号。期间不会释放总线,不会被其他主设备夺取总线控制权。
SPI通信协议
SPI使用2条通讯总线、1根时钟线和1根片选线。(从最高位开始传送)
- MOSI:Master Output Slave Input,顾名思义,即主设备输出/从设备输入。数据从主机输出到从机,主机发送数据。
- MISO:Master Input Slave Output,主设备输入/从设备输出,数据由从机输出到主机,主机接收数据。
- SCK:即时钟信号线,用于通讯同步。该信号由主机产生,其支持的最高通讯速率为Fpclk/2,即所挂载总线速率的一半。如SPI2挂载在APB1总线上,则其最高速率为36MHz / 2 = 18MHz。类似木桶效应,两个设备之间通讯时,通讯速率受限于较低速的设备。
- NSS:即片选信号线,用于选择通讯的从设备,也可用CS表示。每个从设备都有一条独立的NSS信号线,主机通过将某个设备的NSS线置低电平来选择与之通讯的从设备。所以SPI通讯以NSS线电平置低为起始信号,以NSS线电平被拉高为停止信号。
SPI优点:
支持全双工通信、通信简单、数据传输速率快
SPI缺点:
没有指定的流控制,没有应答机制确认是否接收数据,所以跟IIC总线协议比,在数据可靠性上有一定缺陷。
UART通信协议
UART工作原理是将传输数据的每个字符一位接一位地在UART总线上传输:
- 起始位: 先发出一个逻辑”0”的信号,表示数据传输的开始。
- 数据位: 紧接着起始位之后。 可以是5~8位逻辑0或1 ,构成一个字符。通常采用ASCII码。从最低位开始传送,靠波特率进行定位。
- 奇偶校验位: 数据位加上这一位后,使得“1”的位数应为偶数(偶校验)或奇数(奇校验),以此来校验数据传送的正确性。奇偶校验位可有可无(有数据位7,无数据位8)。
- 停止位: 它是一次传输的结束标志。可以是1位、1.5位、2位的高电平。 由于数据是在传输线上定时的,并且每一个设备有其自己的时钟,很可能在通信中两台设备间出现了小小的不同步。因此停止位不仅仅是表示传输的结束,并且提供计算机校正时钟同步的机会。适用于停止位的位数越多,不同时钟同步的容忍程度越大,但是数据传输率同时也越慢。
- 波特率: 控制数据传输速率的,表示每秒钟传送的位数。常用的波特率115200 9600等。
- 空闲位: 处于逻辑1状态, 表示当前总线处于空闲状态,没有数据传送。
UART的数据发送和接收:
- 发送时: 数据被写入发送FIFO。如果UART 被使能,则会按照预先设置好的参数(波特率、数据位、停止位、校验位等)开始发送数据,一直到发送FIFO 中没有数据。所以在发送数据时要先判断发送FIFO是否为空,只有为空的情况下,才能往FIFO里面写。
- 接收数据时: UART的接收器会将接收的数据一位一位的移位到接收FIFO中,我们读接收FIFO即可以读到接收到的数据。
RS485总线:2//3/4/5线
- 485总线可分为隔离型和非隔离型,但隔离型比非隔离型在抗干扰、系统稳定性等方面都有更出色的表现。
- 为了避免总线上状态的不确定性,在 + 信号线上加上拉,- 信号线加下拉,并联120欧匹配电阻,使系统更稳定传输。
- 接口采用差分传输方式,具有一定的抗共模干扰的能力,但当电压超过RS485接收器的极限接收电压时,即大于+12V或小于-7V时,接收器就再也无法正常工作了,严重时甚至会烧毁芯片和仪器设备。( 解决此类问题的方法是通过DC-DC将系统电源和RS-485收发器的电源隔离;通过隔离器件将信号隔离,彻底消除共模电压的影响。)
- 采用DC-DC或者光耦隔离时,最好保证输入和输出不共地(内部),完全隔离则有效抑制了高共模电压的产生。
- 差分传输方式,不必须共地,但稳定性差一些。
UART出现乱码或丢包的解决方案:
硬件: 主从接口是否共地、芯片是否损坏、TTL/RS232/RS485电平是否匹配(有些盗版的串口转换芯片很容易出问题,被坑了几次)
软件: 波特率/数据位/校验位是否一致、波特率是否设置太快、串口初始化的主频是否一致
丢包的可能原因:
- 串口FIFO溢出(中断响应不及时)
- 串口线的问题,抗干扰能力弱
【常见笔试考点】
请评论以下这段代码
//下面的代码使用了__interrupt关键字去定义了一个中断服务子程序(ISR)
__interrupt double compute_area (double radius)
{
double area = PI * radius * radius;
printf("\nArea = %f", area);
return area;
}
这个函数存在如下错误:
- ISR不能返回一个值
- ISR不能传递参数
- 在许多的处理器/编译器中,浮点一般都是不可重入的。有些处理器/编译器需要让额外的寄存器入栈,有些处理器/编译器就是不允许在ISR中做浮点运算。此外,ISR应该短而有效率,在ISR中做浮点运算是不明智的。
- 不应该在中断中调用耗时的printf。
指出下面函数的错误
void test()
{
char string[10], str1[10];
int i;
for (i = 0; i < 10; i++)
str1[i] = 'a';
strcpy(string, str1);
printf("%s",string);
}
- 调用strcpy之前最后一个字符应该为’\0’, 所以str1[9] = ‘\0’即可;
- 不能去str1[10] = ‘\0’,数组越
下面程序的输出
#include <iostream.h>
#pragma pack(8)
struct example1
{
short a;
long b;
};
struct example2
{
char c;
example1 struct1;
short e;
};
#pragma pack()
int main(int argc, char* argv[])
{
example2 struct2;
cout << sizeof(example1) << endl;
cout << sizeof(example2) << endl;
cout << (unsigned int)(&struct2.struct1) - (unsigned int)(&struct2) << endl;
return 0;
}
问程序的输入结果是什么? 字节大小计算详解
8 16 4
如何引用一个已定义过的全局变量
两种方式:
- 用extern关键字方式
- 用引用头文件方式,可以在不同的C文件中声明同名的全局变量,前提是其中只有一个C文件对此变量赋初值,此时连接才不会出错。
关键字volatile有什么含意,并给出三个不同的例子
含意:“易变的”,volatile关键字的作用影响编译器的编译结果,用volatile声明的变量表示该变量随时可能发生变化,与该变量有关的运算,不进行编译优化,以免出错。
- 中断服务程序中修改的供其他程序检测的变量需要加volatile
- 多线程中,被几个任务共享的变量标志应该加volatile
- 存储区映射的硬件寄存器通常也要加volatile
关键字static的作用
1) static 局部变量:一个被声明为静态的局部变量在这一函数调用过程中,维持其值不变
2) static 全局变量:可以被模块内的所有函数访问,不能被模块外的其他函数访问
3) static 函数:这个函数被限制在声明它模块的本地范围使用,模块外不可见
关键字const的作用
const 为常量修饰符,意味着“只读操作”,const变量在声明的时候就进行其初始化,如果在声明常量的时候没有提供值,则该常量的值是不确定的,且无法修改
const主要用来修饰变量、函数形参和类成员函数
1) const常量:定义时就初始化,以后不能更改
2) const形参:func(const int a){};该形参在函数体内,不能修改
3) const类成员函数:该函数对成员变量只能进行只读操作,不能修改成员变量值
const int *a; //常量指针,无法改变变量的值
int *const a; //指针常量,无法改变变量的地址
C语言实现程序跳转到绝对地址0x100000处执行
((void (*)(void)) ox100000)();
(*(void (*)(void)) ox100000)();
a) 首先来认识一个新的数据类型,如:void (*)(void),和 int* 类似的一个数据类型,只不过int*是一个指向int型的指针,而void (*)(void)是一个指向函数的指针,且这个函数无返回值,无参数。
b) 然后给他外层加个括号,如:(void (*)(void)),这样是不是很像(int*),我们在做强制类型转换的时候需要在类型外加个括号的是吧。
c) 接着把0x100000强制转化为一个函数指针,即:(void(*)(void))0x100000();
d) 最后就是调用这个函数,外层再加个括号,后面在加一对括号(加不加 * 都是可以的,兼容旧版本)
指针的定义,使用变量a
//一个指向指针的指针,它指向的指针是指向一个整型数
int **a;
//一个有10个指针的数组,该指针是指向一个整型数
int *a[10];
//一个指向有10个整型数数组的指针
int (*a)[10];
//一个指向函数的指针,该函数有一个字符型参数并返回一个整数
int (*a)(char);
sizeof求值和 #pragma pack的使用方法
char *p1 = "12345678";
char p2[] = "12345678";
char p3[1024]="12345678";
char p4[] = {'1','2','3','4','5','6','7','8'};
sizeof(p1)=? strlen(p1)=?
sizeof(p2)=? strlen(p2)=?
sizeof(p3)=? strlen(p3)=?
sizeof(p4)=? strlen(p4)=?
分析:
- p1是一个字符指针,指向静态常量区的一个常量字符串“12345678”,所以sizeof(p1) = 4; strlen(p1) = 8(不包含’\0’);
- p2是一个字符数组,sizeof(p2)是计算数组p2的长度(包含’\0’)
- p3是一个字符数组,长度为1024,所以sizeof(p3) = 1024, strlen(p3) 计算到‘\0’前
- p4是一个字符数组,由单个字符进行初始化,不像字符串初始化包含’\0’,
所以sizeof(p4) = 8,而p4作为一个字符数组,不以’\0’结尾,所以strlen计算时,找不到’\0’,会发生字符串溢出现象。
#pragma pack (n) 作用:C编译器将按照n个字节对齐。
#pragma pack () 作用:取消自定义字节对齐方式。
#pragma pack (push,1) 作用:是指把原来对齐方式设置压栈,并设新的对齐方式设置为一个字节对齐
#pragma pack(pop) 作用:恢复对齐状态
常用方式: 定义结构体节省空间的方式,如TCP头信息
#pragma pack(1) // 按照1字节方式进行对齐
struct TCPHEADER
{
short SrcPort; // 16位源端口号
short DstPort; // 16位目的端口号
int SerialNo; // 32位序列号
int AckNo; // 32位确认号
unsigned char HaderLen : 4; // 4位首部长度
unsigned char Reserved1 : 4; // 保留6位中的4位
unsigned char Reserved2 : 2; // 保留6位中的2位
unsigned char URG : 1;
unsigned char ACK : 1;
unsigned char PSH : 1;
unsigned char RST : 1;
unsigned char SYN : 1;
unsigned char FIN : 1;
short WindowSize; // 16位窗口大小
short TcpChkSum; // 16位TCP检验和
short UrgentPointer; // 16位紧急指针
};
#pragma pack()
单片机最小系统包括哪些
单片机、电源模块、时钟模块、复位模块、 程序烧写调试口(SW、JTAG)
内存碎片产生原因及解决办法
内存碎片通常分为内部碎片和外部碎片:
- 内部碎片:由于采用固定大小的内存分区,当一个进程不能完全使用分给它的固定内存区域时就产生了内部碎片,通常内部碎片是难以完全避免的;
- 外部碎片:由于某些未分配的连续内存区域太小,以至于不能满足任意进程的内存分配请求,从而不能被进程利用的内存区域。
现在普遍采用的段页式内存分配方式就是将进程的内存区域分为不同的段,然后将每一段由多个固定大小的页组成。通过页表机制,使段内的页可以不必连续处于同一内存区域,从而减少了外部碎片,然而同一页内仍然可能存在少量的内部碎片,只是一页的内存空间本就较小,从而使可能存在的内部碎片也较少。
C++实现多态的三个条件和实现原理
多态性: 同一接口,实现不同功能
实现条件:
- 要有继承
- 要有虚函数重写
- 要有父类指针指向子类对象
实现原理:
编译器发现一个类中有虚函数,便会立即为此类生成虚函数表vtable。虚函数表的各表项为指向类里面的虚函数的指针。编译器还会在此类中隐含插入一个指针vptr指向虚函数表。调用此类的构造函数时,在类的构造函数中,编译器会隐含执行vptr与vtable的关联代码,即将vptr指向对应的vtable,将类与此类的vtable联系起来。
另外在调用类的构造函数时,指向基础类的指针此时已经变成指向具体的类的this指针,这样依靠此this指针即可得到正确的vtable,如此才能真正与函数体进行连接,这就是动态联编,实现多态的基本原理。
(C++中,构造函数不能被继承,只能被调用)
大小端的各自的优点,哪种时候用
- 大端优点: 符号位在所表示的数据的内容的第一个字节中,便于快速判断数据的正负和大小
- 小端优点: 低地址放低字节,所以在强制转换时不需要调整字节的内容。而且CPU做数值运算时内存依次从低到高取数据进行运算,直到最后刷新最高位的符号位,这样运算方式会更高效。(大端低地址存放高字节,高地址存放低字节,小端相反)
PS:
Intel x86系列芯片使用小端存储模式,ARM芯片默认小端,可切换到大端;
网络上普遍采用大端模式,使用大端的CPU: power pc、DSP……;
大小端是由CPU架构决定的,不是软件决定的!!!
GPIO驱动、PWM驱动、IIC驱动、SPI驱动操作集(设备)
GPIO:
int gpio_request(unsigned gpio, const char *label);
void gpio_free(unsigned gpio);
int gpio_direction_input(unsigned gpio);
int gpio_direction_output(unsigned gpio, int value);
int gpio_get_value(unsigned gpio);
int gpio_set_value(unsigned gpio);
PWM:
struct pwm_device *pwm_request(int pwm_id, const char *label);
void pwm_free(struct pwm_device *pwm);
int pwm_enable(struct pwm_device *pwm);
void pwm_disable(struct pwm_device *pwm);
void pwm_set_period(struct pwm_device *pwm, unsigned int period);
unsigned int pwm_get_period(struct pwm_device *pwm);
int pwm_set_polarity(struct pwm_device *pwm, enum pwm_polarity polarity);
int pwm_config(struct pwm_device *pwm, int duty_ns, int period_ns);
IIC:
struct i2c_msg msg[2];
struct i2c_client *client = (struct i2c_client *)dev->private_data;
i2c_transfer(client->adapter, msg, 2);
SPI:
struct spi_message m;
struct spi_transfer *t;
struct spi_device *spi = (struct spi_device *)dev->private_data;
gpio_set_value(dev->cs_gpio, 0); /* 片选拉低,选中 ICM20608 */
spi_sync(spi, &m); /* 同步发送 */
gpio_set_value(dev->cs_gpio, 1); /* 片选拉高,释放 ICM20608 */
局部变量和全局变量的区别
a) 作用域不同: 全局变量的作用域为整个程序,而局部变量的作用域为当前函数或循环
b) 内存存储方式不同: 全局变量存储在全局数据区中,局部变量存储在栈区
c) 生命周期不同: 全局变量的生命周期和主程序一样,随程序的销毁而销毁,局部变量在函数内部或循环体内部,随函数退出或循环的退出就不存在了
d) 使用方式不同: 全局变量在声明后程序的各个部分都可以用到,但是局部变量只能在局部使用
(函数内部会优先使用局部变量再使用全局变量)
字符串的初始化注意事项
定义 char *a = "abc"; 不能使用a[0] = ‘p’;改变字符串,会出错;(表明是字符串常量)
定义 char a[] = "abc"; 能使用a[0] = ‘p’;改变字符串,pbc;
定义数组的个数
求一个数组的大小:
一维数组:#define ARRAY_SIZE(x) (sizeof(x)/sizeof((x)[0]))
二维数组:#define ARRAY_SIZE(x) (sizeof(x)/sizeof((x)[0][0]))
因为sizeof获取的是字节数,通过总字节/单个值的字节 = 个数
【Linux常用命令】
查找文件
find / -name filename.txt #(查找根目录下的filename.txt文件)
whereis -b bash #(显示bash命令的二进制程序地址)
-m 查看帮助文件地址
which 命令(ls) # 查看可执行文件的位置
locate 命令(cd) # 配合数据库查看文件位置
文本中关键词搜素
grep -n keyword filename.txt # 在文件filename.txt中搜素keyword关键词的行
-n 显示行号 -i 不分大小写搜素
-r 递归查找 -b 将可执行文件当做文本来搜素
-v 反向选择-仅列出没有“关键词”的行
查看进程的状态
ps -aux | grep serialtcp #(通过管道符查看serialtcp程序的运行状态信息)
pidof + 程序名
pgrep + 程序名 (查看程序的PID)
杀死进程
kill -9 PID (杀死对应进程号的进程)
killall 程序名 (终止某个程序的全部进程)
pkill 程序名 (踢出某个终端)
查看文件
head -n 10 filename.txt #(查看文件头10行)
tail -n 10 filename.txt #(查看文件尾10行)
tail -f filename.txt #(显示文件新增内容,默认10行,用于查看日志)
系统负载情况
top #(动态的监视进程活动与系统负载等信息)
top -H -p PID #(进程中,线程的使用情况)
uptime #(查看系统的负载信息)
磁盘管理
fdisk 磁盘名(小于2T的磁盘) #(fdisk -l 显示系统中的所有分区内容)
parted 磁盘名(大于2T的磁盘)
df -h (查看磁盘使用情况)
du -h --max-depth=1(查看各文件夹大小, –h 显示单位)
free -h 显示当前系统中内存的使用情况
强大的文本分析工具
awk ‘script’ filename.txt
awk ‘{print $1,$4;}’ filename.txt (每行按空格或者TAB分割,输出文本中的1、4列)
文件网络传输命令
下载ftp服务器上的文件或者文件夹:
wget -P 指定文件保存位置 ftp://ip:端口/文件夹 --ftp-user=用户名 --ftp-password=密码
scp命令(文件拷贝,基于ssh的登录):
scp filename root@ip:/指定目录 (本地到远程,反过来远程到本地)
ssh运程登录:
ssh -p 端口 用户@ip地址(指定端口,ssh -p 29565 root@192.168.1.58)
ssh用户@ip地址(默认端口)
查看端口号命令
netstat -ant
-a 查看所有连接和监听端口
-n 显示IP地址和端口号,而不是域名和服务名
-t 显示TCP端口
-u 显示UDP端口
常用通配符
“*” 匹配任意长度的任意字符,可以是0个
“?” 匹配任意单个字符,必须是1个
“[ ]” 匹配指定字符范围内的任意单个字符
“[a-z,A-Z,0-9]” 匹配所有数字字母,可以不加逗号
【Linux驱动】
Linux内核同步方式总结
a) 内核中对共享资源的保护方法有哪几种?
- 原子操作: 不能再进一步分割的操作,一般原子操作作用于变量或者位操作。
- 自旋锁: 原子操作的升级,可对结构体等复杂类型操作。“原地打转”是为了等待自旋锁可以用,可以访问共享资源。(由于等待自旋锁的线程会一直处于自旋状态,这样会浪费处理器时间,降低系统性能,所以自旋锁的持有时间不能太长)
- 信号量: 相比于自旋锁,信号量可以使线程进入休眠状态。因为信号量使线程进入休眠状态以后会切换线程,所以信号量的开销要比自旋锁大。(适合那些占用资源比较久的场合;中断不能休眠,不能用于中断;共享资源持有时间比较短,不适合使用信号量,因为频繁的休眠和切换线程引起的开销要远大于信号量带来的优势)
- 互斥体: 一次只有一个线程可以访问共享资源,不能递归申请互斥体。(mutex可以导致休眠,因此不能在中断中使用mutex,中断只能使用自旋锁;和信号量一样,mutex保护的临界区可以调用引起阻塞的API函数;因一次只有一个线程可以持有mutex,因此必须由mutex的持有者释放mutex,并且mutex不能递归上锁和解锁)
b) 为什么自旋锁不能睡眠,而拥有信号量就可以?
自旋锁禁止处理器抢占,而信号量不禁止处理器抢占
c) 产生死锁的四个条件,解决死锁的方法?
发生死锁时这4个条件必然成立,只要有一个不成立将不会产生死锁:
- 互斥, 一个资源一次只能被一个进程使用;
- 占有且等待, 一个进程在申请新的资源的同时保持对原资源的占有,部分分配,占有申请;
- 不可抢占, 也就是不可抢占资源,资源只能由占有者自动释放;
- 循环等待, 进程之间相互等待
Linux中线程有哪几种的状态
- 就绪: 线程分配了CPU以外的全部资源,等待获得CPU调度
- 执行: 线程获得CPU,正在执行
- 阻塞: 线程由于发生I/O或者其他的操作导致无法继续执行,就放弃处理机,转入线程就绪队列
- 挂起: 由于终端请求,操作系统的要求等原因,导致挂起。
Linux中内核空间与用户空间的区别
Linux系统采取两级保护机制,对应两种不同的操作权限,内核空间权限高于用户空间权限。
内核空间和用户空间都有属于自己的虚拟空间。在32位系统中,cpu最高有32位寻址范围,即对应4G空间,内核空间被划分在高1G虚拟空间,用户空间在低3G。
普通应用程序运行在用户空间,执行一些贴近用户的低权限操作;系统内核程序、操作硬件的驱动程序等一些要求高级权限的程序运行在内核空间。
用户空间程序不能直接访问内核空间的数据,内核空间程序也一样不能直接访问属于用户进程空间的数据,用户空间和内核空间之间的通信必须通过一些特定的方法。
内核空间与用户空间的通信方式
常用的几种方式:
(1)系统调用。 用户空间进程通过系统调用进入内核空间,访问指定的内核空间数据。
(2)驱动程序。 用户空间进程可以使用封装后的系统调用接口访问驱动设备节点,以和运行在内核空间的驱动程序通信。
(3)proc文件系统。 proc文件系统的主要功能是在内核空间提供一套机制为方便用户空间的查询,查看,设置内核信息,多用于查询类操作。
(4)共享内存mmap。 在代码中调用接口,实现内核空间与用户空间的地址映射,在实时性要求很高的项目中为首选,省去拷贝数据的时间等资源,但缺点是不好控制。
(5)copy_to_user()、copy_from_user(), 是在驱动程序中调用接口,实现用户空间与内核空间的数据拷贝操作,应用于实时性要求不高的项目中。
Linux中系统调用过程
系统调用,如open()函数,它并不是真正的系统调用实现函数,它只是一个c库函数。
内部实现做了两件事,先把系统调用号传递给内核,最后拉起一次软中断,自此cpu进入内核态运行。
内核在软中断向量表中找出对应的中断类型,根据中断类型找到对应的软中断执行函数,然后执行函数根据系统调用号,在系统调用号表里面找到对应的系统调用函数。
Linux中中断的实现机制,tasklet与workqueue的区别及底层实现区别,为什么要区分上半部和下半部
Linux中断分为硬件中断和内部中断(异常),调用过程:外部中断产生->发送中断信号到中断控制器->通知处理器产生中断的中断号,让其进一步处理。
为了能够在中断处理过程中被新的中断打断,将中断处理程序一分为二,上半部登记新的中断,快速处理简单的任务,剩余复杂耗时的处理留给下半部处理。下半部处理过程中可以被中断,上半部处理时不可被中断。
1)softirq和tasklet都属于软中断,tasklet是softirq的特殊实现;workqueue是普通的工作队列。
2)如果推后执行的任务需要睡眠,那么就选择工作队列workqueue(工作队列是基于线程的封装)。如果推后执行的任务不需要睡眠,那么就选择tasklet。
Linux中的软中断
Linux系统中的软中断,是专为一些不是特别要紧的耗时任务而产生的一种机制,多数用在中断处理过程中,典型应用就是用于中断下半部,tasklet机制就是基于软中断的典型下半部应用。
软中断就是结合任务调度、延迟处理等让守护进程去处理一些不是特别紧急又耗时的任务。一个软中断不会去抢占另一个软中断,只有硬件中断才可以抢占软中断,所以软中断能够保证对时间的严格要求。
软中断和硬中断的区别: 在CPU的硬件中断发生之后,CPU需要将硬件中断请求通过向量表映射成具体的服务程序,这个过程是硬件自动完成的,但是软中断不是,其需要守护线程去实现这一过程,这也就是软件模拟的中断,故称之为软中断。
Linux中的中断上下文和进程上下文
- 中断上下文: 当硬件通过触发中断信号,导致内核调用中断处理程序,进入内核空间。这个过程中,硬件的一些变量和参数也要传递给内核,内核通过这些参数进行中断处理。中断上下文可以理解为硬件传递过来的这些参数和内核需要保存的一些环境,主要是被中断的进程环境。
- 进程上下文: 当创建一个进程需要控制一个外部设备时,我们编写在用户空间的代码将通过“系统调用”进入内核空间,由内核代表我们这个进程继续运行于内核空间,这时候就涉及到上下文的切换。用户空间和内存空间具有不同的地址映射,通用或专用的寄存器组,而用户空间的要传递很多变量和参数给内核,内核也要保存用户空间的一些寄存器和变量值等,以便系统调用后,继续回到用户空间执行。所谓进程上下文为一个进程在执行的时候,CPU中的所有寄存器的值,进程的状态和堆栈的内容,当内核需要切换到另一个进程时,它需要保存当前进程的所有状态,及保存当前进程的进程上下文,以便再次执行进程时,能够恢复切换时的状态,继续执行。
/dev/下的设备文件是怎么创建初来的
有三种方式:devfs机制、udev机制、手动创建设备节点。
谈谈个人见解:
a) devfs机制,从来没用过,应该是2.6以前的内核使用的;
b) udev机制,其实就是现在常用的device_create()、class_create()这一套接口,所谓udev是上层用户空间程序,是基于驱动中创建使用了这两个接口而起作用的,但是udev在日常开发中几乎接触不到,我们只需在驱动中调用创建节点的这两个API就ok了,剩下的工作就交给udev去做。
c) mknod ,新手最常用的一种创建设备节点方法,但并非入门后就再没有用途。在某些情境下,或许有人不想使用udev机制,于是把节点创建工作写在脚本里,这样也是无可厚非的。
内核申请内存的有哪几个函数,有什么区别
Linux内核管理–几个经典问题
kmalloc()、kzalloc()、vmalloc() 的共同特点是:
a) 用于申请内核空间的内存;
b) 内存以字节为单位进行分配;
c) 所分配的内存虚拟地址上连续;
kmalloc()、kzalloc()、vmalloc() 的区别是:
a) kzalloc 是强制清零的 kmalloc 操作;(以下描述不区分 kmalloc 和 kzalloc)
b) kmalloc 分配的内存大小有限制(128KB),而 vmalloc 没有限制;
c) kmalloc 可以保证分配的内存物理地址是连续的,但是 vmalloc 不能保证;
d) kmalloc 分配内存的过程可以是原子过程(使用 GFP_ATOMIC),而 vmalloc 分配内存时则可能产生阻塞;
e) kmalloc 分配内存的开销小,因此 kmalloc 比 vmalloc 要快;
一般情况下,内存只有在要被 DMA 访问的时候才需要物理上连续,但为了性能上的考虑,内核中一般使用 kmalloc(),而只有在需要获得大块内存时才使用 vmalloc()。例如,当模块被动态加载到内核当中时,就把模块装载到由 vmalloc() 分配的内存上。当使用kmalloc()申请DMA内存时,由于受分配内存大小限制且物理地址有可能不连续,一般使用专门的DMA内存分配函数进行分配。
内核函数mmap的实现原理与机制
mmap函数实现把一个文件映射到一个内存区域,从而我们可以像读写内存一样读写文件,他比单纯调用read/write也要快上许多。在某些时候我们可以把内存的内容拷贝到一个文件中实现内存备份,当然,也可以把文件的内容映射到内存来恢复某些服务。另外,mmap实现共享内存也是其主要应用之一,mmap系统调用使得进程之间通过映射同一个普通文件实现共享内存。
ioctl和unlock_ioctl有什么区别
ioctl是老的内核版本中的驱动API,unlock_ioctl是当下常用的驱动API。
区别在于ioctl调用前后,使用了大内核锁,而unlock_ioctl顾名思义就是没加大内核锁的新接口,改变的只是驱动调用的方法,用户应用程序调用的接口不变。
Linux总线、驱动和设备模型
在SoC中有些外设没有总线这个概念,Linux提出了platform这个虚拟总线:
内核使用 bus_type 结构体表示总线,通过该结构体中的match函数,匹配设备和驱动
Match中,可以看到驱动匹配的四种方法:
- OF类型的匹配,设备树采用的匹配方式
- 第二种匹配方式,ACPI
- 第三种匹配方式,id_table匹配
- 如果id_table不存在,直接比较驱动和设备的name字段
上述模型的入口函数:
int platform_driver_register(struct platform_driver *driver);
上述模型的出口函数:
void platform_driver_unregister(struct platform_driver *drver);
驱动框架三要素:
1. 入口(加载)
module_init(入口函数名insmod); static int __init xxx_func(void){}
2. 出口(卸载)
module_exit(卸载函数名rmmod); static void __exit xxx_func(void){} //进口函数名和出口函数名不需要相同
3. GPL协议声明
MODULE_LICENSE("GPL");
Bootloader多数有两个阶段的启动过程
Stage1:是汇编的 (start.S)
1) 基本的硬件初始化关闭看门狗 和中断,MMU(带操作系统),CACHE,配置系统工作时钟)
2) 为加载stage2准备RAM空间
3) 拷贝内核映像和文件系统映射到RAM中
4) 设置堆栈指针sp
5) 跳转到stage2的入口点 (start_armboot())
Stage2:是C语言的
1) 初始化本阶段要使用到的硬件设备(LED、UART等)
2) 检测系统的内存映射
3) 加载内核映射和文件系统映射
4) 设置内核的启动参数(do_boot_linux())
Linux内核的启动过程
- 首先,内核镜像自解压,解压完之后从head.s开始运行,即引导内核,在内核引导期间将会设置内核参数。
- 随后,跳转到第一个c函数start_kernel(),进入内核启动阶段,在内核启动过程中进行一些必要的硬件初始化工作。
- 在内核启动最后,挂载文件系统,然后创建第一个用户空间进程,init进程,进一步完成驱动挂载,用户服务初始化工作。
【面试问答题】
电路中的保护装置有哪几种
常用的电路保护装置有熔断器和断电器两种:
熔断器(保险丝):短路保护,其中慢熔保险丝可以起到延迟的作用
断电器(继电器):过载保护,如电机的保护可通过热继电器来实现
TVS管和稳压二极管的作用和使用方法
—
稳压二极管:
一种利用PN结处于反向击穿电压基本保持不变,但是电流可以在一定范围值内变化,稳压二极管在反向击穿电压前具有高阻态,超过这个临界点就处于低阻态。稳压二极管广泛应用于各类稳压电路、电压基准元器件等场合,它可以串联使用,这样可以获得更高的稳压值。稳压二级管最主要的几个参数:稳压值、耗散功率、最大工作电流……
TVS管:
一种瞬态抑制二极管,是一种具有浪涌吸收能力的半导体器件,它的响应时间极快,达到亚纳秒级,因此一旦电路收到瞬间高能量冲击时候它能够把这股能量抑制下来,通过吸收来自电路的瞬间大电流、高电压钳位,最终的结果的是保护后面的设备或者电路。
TVS二极管有单向和双向之分,单向多用于直流电路,双向多用于交流电路,在电路当中与被保护线路并联,一旦瞬时电压超过电路正常工作电压后,TVS二极管便发生雪崩效应,提供给瞬时电流一个超低电阻通路,从而使得被保护器件或设备避免受到损毁。因此这种TVS二极管非常适用于对ESD敏感的电路以及过压电路。
相同: 都有稳压作用
区别:
1) 封装符号基本相同,外观很难分辨
2) 全为反向接入,也就是利用它的反向特性,利用PN结雪崩效应,在反向击穿前均有一个临界电压,在反向接入电路都具有稳压作用,但也不尽相同,稳压二极管利用的是把输入电压固定在某个数值,而TVS二极管主要是防止瞬态高压对后级电路进行钳位;
3) 稳压二极管一般不会讲响应时间,更多的是关注稳压值,但对于TVS二极管不一样,他的特性决定着它拥有不同的作用,TVS二极管具有瞬态抑制高能量作用,在短时间(纳秒级)把较高能量瞬间吸收。
4) 稳压二极管的功率不是很大(1W、2W…),导致通过它的电流较小,而TVS管瞬间脉冲功率可达上千瓦,只是维持时间短
浮空与高组态是同一个东西吗
浮空: 悬空引脚,会因为接收一些杂散信号造成系统混乱或者失控。
高阻态: 输出电阻很大,该状态既不是高电平也不是低电平,当三态门处于高阻态时,无论该门的输入如何变化,都不会对其输出有贡献(相当于开路)。
线程与进程
a) 进程与线程概念,彼此之间的区别和优缺点?
进程是资源(CPU、内存等)分配的基本单位;
线程是程序执行的最小单位(CPU调度(时间片)和分配的基本单位)
(一个程序至少有一个进程,一个进程至少有一个线程)
区别:
- 地址空间: 同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。
- 资源拥有: 同一进程内的线程共享本进程的资源,如内存、I/O、CPU等,但进程之间的资源是独立的。(一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。进程切换时,消耗的资源大,所以涉及到频繁的切换时,使用线程要好于进程。同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程不能用进程)
- 执行过程: 每个独立的进程有一个程序运行的入口和顺序执行序列。但是线程不能独立执行,须依存在应用程序中,由应用程序提供多个线程执行控制。
(线程是处理器调度的基本单位,但是进程不是。两者均可并发执行)
优缺点:
线程执行开销小,但是不利于资源的管理和保护。线程适合在SMP机器(双CPU系统)上运行。进程执行开销大,但是能够很好的进行资源管理和保护。进程可以跨机器前移。
b) 何时使用多进程,何时使用多线程?
- 创建和销毁较频繁使用线程,因为创建进程花销大;
- 需要大量数据传送使用线程,因为多线程切换速度快;
- 并行操作使用线程。线程是为了实现并行操作的一个手段“合作完成大事”;
(安全稳定选进程,快速频繁选线程)
c) 多进程、多线程同步(通讯)的方法?
进程间通信:
管道/无名管道 信号 共享内存 消息队列 信号量 socket
线程间通信:
信号量 读写锁 条件变量 互斥锁 自旋锁
互斥锁用于线程的互斥,信号量用于线程的同步。同时互斥锁的作用域仅仅在于线程,信号量可以作用于线程和进程。
d) 孤儿进程、僵尸进程和守护进程的概念?
- 孤儿进程: 父进程异常结束,然后被1号进程init收养(无危害)
- 僵尸进程: 子进程退出后(exit(0)),父进程并未接收结束子进程(如调用waitpid获取
子进程的状态信息),那么子进程就一直保存在系统里,占用系统资源(有危害)。每当子进程退出,父进程都会收到SIGCHLD信号,故可通过父进程里重置signal函数。- 守护进程: 有意把父进程结束,然后被1号进程init收养(后台运行不受终端控制的
进程,网络服务大部分是守护进程)
网络编程
a) TCP/UDP的区别?
- TCP面向连接(如打电话要先拨号建立连接);UDP是无连接的,即发送数据之前不需要建立连接;
- TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付;
- TCP面向字节流,实际上是TCP把数据看成一连串无结构的字节流;UDP是面向报文的,UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如IP电话,实时视频会议等);
- 每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信
- TCP首部开销20字节;UDP的首部开销小,只有8个字节
- TCP的逻辑通信信道是全双工的可靠信道,UDP则是不可靠信道
TCP | UDP | |
---|---|---|
是否连接 | 面向连接 | 面向非连接 |
传输可靠性 | 可靠 | 不可靠 |
应用场合 | 少量数据 | 传输大量数据 |
速度 | 慢 | 快 |
TCP的状态(SYN、FIN、ACK、PSH、RST、URG)
- SYN表示建立连接
- FIN表示关闭连接
- ACK表示响应
- PSH表示有DATA数据传输
- RST表示连接重置
b) 典型网络模型
四层TCP/IP模型: 应用层 -> 传输层 -> 网络层 -> 网络接口层
五层TCP/IP模型: 应用层 -> 传输层 -> 网络层 -> 数据链路层 -> 物理层
七层OSI模型: 应用层 -> 表示层 -> 会话层 -> 传输层 -> 网络层 -> 数据链路层 -> 物理层
c) 三次握手
第一次握手: 建立连接时,客户端发送syn包(syn=x)到服务器,并进入SYN_SENT状态,等待服务器确认;SYN:同步序列编号(Synchronize Sequence Numbers);
第二次握手: 服务器收到syn包,必须确认客户的SYN(ack=x+1),同时自己也发送一个SYN包(syn=y),即SYN+ACK包,此时服务器进入SYN_RECV状态;
第三次握手: 客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=y+1),此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握手。
注意:
Server第二次握手将ACK置一,且进行资源分配
Client第三次握手将ACK置一,且进行资源分配
第一次握手: 客户端向服务器发送请求命令syn;
第二次握手: 服务器响应客户端请求ack,同时向客户端发送请求syn;
第三次握手: 客户端响应服务器的请求ack;
d) 四次挥手
1) 客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN-WAIT-1(终止等待1)状态。 TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。
2)服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。
3)客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。
4)服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。
5)客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2∗∗MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。
6)服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCP后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些。
第一次挥手: 客户端向服务器发送fin用来关闭客户端和服务器的数据传送;
第二次挥手: 服务器接收到fin并向客户端发送ack;
第三次挥手: 服务器关闭与客户端的连接,并发送fin给客户端;
第四次挥手: 客户端发送ack报文确认。
c语言程序运行的时候,内存分布
a) 代码区(.text): 该区域主要存放二进制可执行文件;
b) 数据区(.data): 数据区可详细分为三块区域,分别是只读数据区、初始化数据区、未初始化数据区;
1) 只读数据区(.ordata) : 顾名思义,这个区域存放的是一些常量,包括字符串常量、const修饰的全局变量;
2) 初始化数据区(.rwdata): 该区域用来存放初始化不为0的全局变量和static修饰的局部变量;
3) 未初始化数据区(.bss): 与初始化数据区类似,但是它存放的是初始化为0、未初始化的全局变量和static修饰的局部变量。
c) 栈: 栈存放的是程序中的局部变量,当然函数形参、返回值也都存储在栈中。栈是由系统自动管理,变量的出栈入栈操作都由系统通过移动栈顶指针来完成来完成,并不是真正的清除变量,所以当我们使用局部变量之前一定要给它赋值,否则就是一个不确定的值。
d) 堆: 堆空间解决了自动分配不灵活,对内存造成浪费等问题。可以通过malloc函数申请所需大小的空间,注意这块空间申请了之后会一直存在,所以一定要和free一起使用,等用完之后马上释放,否则会造成内存泄漏,严重会导致程序停止运行。我们使用malloc函数带来方便灵活的同时,也带来了内存碎片的问题,所以当要求程序长时间运行时,必不可免的就要解决malloc带来的内存碎片问题。
谈谈你对堆、栈的理解
栈 | 堆 | |
---|---|---|
管理方式 | 由编译器自行管理,无需程序员手工控制 | 堆空间的申请释放工作由程序员控制,容易产生内存泄漏 |
空间大小 | 栈由高地址是向低地址扩展的数据结构,是一块连续的内存区域。栈顶的地址和栈的最大容量是系统预先规定好的,当申请的空间超过栈的剩余空间时,将提示栈溢出。因此,用户能从栈获得的空间较小 | 堆是由低地址向高地址扩展的数据结构,是不连续的内存区域。因为系统是用链表来存储空间内存地址的,且链表的遍历方向是由低地址向高地址。由此可见,堆获得的空间较灵活,也较大。栈中元素都是一一对应的,不会存在一个内核块从栈中弹出的情况 |
是否产生碎片 | 不会产生碎片 | 频繁的malloc/free (new/delete)会造成内存空间的不连续,从而造成大量碎片,使程序效率降低 |
增长方向 | 向下增长 | 向上增长 |
分配方式 | 栈的分配与释放由编译器完成 | 由malloc/free函数动态申请和释放 |
分配效率 | 栈是操作系统提供的数据结构,分配专门的寄存器存放栈的地址,压栈和出栈有专门的指令,分配效率高 | 调用C的库函数,库函数会按照一定的算法在堆内存中搜素足够可用的空间(可能由于内存碎片太多,需要操作系统重新整理内存空间),机制复杂,堆的效率比栈的效率要低很多 |
C++栈溢出的原因及解决方法
原因: 操作系统给程序开出的栈的大小一般为1M~2M,如果我们定义的数组过大,占用空间就会不够了。
解决方法:
1) 把数组改为STL中的vector;
2) 使用dev c++编译时,在Linker中加入一条指令 –Wl,–stack=SIZE,其中SIZE为栈的大小;
3) 后者将其通过库函数手动分配在堆上。
C使用两个栈实现一个队列
由于栈是先进后出的结构,而队列是先进先出的结构,使用两个栈s1和s2来实现一个队列:
- 用栈s1来实现入队列的操作,栈s2来实现出队列的操作
- 入队列的操作和入栈的操作一样,将数据保存在s1中即可
- 出队列时,先判断栈s2是否为空,为空,将栈s1中的数据从栈顶取出压入s2,并删除s1中取出的数据,这样s1栈底的元素就到了s2的栈顶,然后删除s2的栈顶元素即可;不为空,直接将s2中的栈顶元素删除即可。
C使用两个队列实现一个栈
使用两个队列q1和q2来实现一个栈:
- 保持一个队列q2为空,一个队列q1不为空
- 入栈的操作和入队列是一样的,往不为空的队列q1中插入元素
- 出栈时,将不为空队列q1中的元素放入另一个为空的队列q2中,只保留一个在q1中,然后将q1中的元素弹出即可
函数调用时,参数压栈顺序
从右往左将参数依次压入栈中
解释register关键字
register: 关键字请求编译器尽可能的将变量存在CPU内部寄存器中,而不是通过内存寻址访问,以提高效率。注意是尽可能,不是绝对。
设置原因: 由于cpu和内存存取速度不一致,导致对于存储在寄存器内的常用变量比存储在内存的运行速度要高。
PS: register变量必须是能被CPU所接受的类型。这通常意味着register变量必须是一个单个的值,并且长度应该小于或者等于整型的长度。不过,有些机器的寄存器也能存放浮点数;register变量可能不存放在内存中,所以不能用“&”来获取register变量的地址;由于寄存器的数量有限,而且某些寄存器只能接受特定类型的数据(如指针和浮点数),因此真正起作用的register修饰符的数目和类型都依赖于运行程序的机器,而任何多余的register修饰符都将被编译程序所忽略;
在某些情况下,把变量保存在寄存器中反而会降低程序的运行速度。因为被占用的寄存器不能再用于其它目的;或者变量被使用的次数不够多,不足抵消以装入和存储变量所带来的额外开销。
static函数与普通函数的区别
在函数的返回类型前加上关键字static,函数就被定义成为静态函数。普通函数的定义和声明默认情况下是extern的,但静态函数只是在声明他的文件当中可见,不能被其他文件所用。因此定义静态函数有以下好处:
<1> 其他文件中可以定义相同名字的函数,不会发生冲突
<2> 静态函数不能被其他文件所用
IO复用三种方式
- IO复用的使用流程: 设置要监听的描述符以及需要监听的事件 —> 监听事件,一直阻塞到有描述符以及需要监听的时间 —> 遍历所有就绪的描述符,并进行相应处理
- IO复用的三种方式: select poll epoll (select ~ poll < epoll)
GPIO的开漏输出与推挽输出
前言:(部分硬件内部(internal)本身包含了对应的上、下拉电阻)
常见的GPIO的模式可以配置为open-drain或push-pull,具体实现上,常为通过配置对应的寄存器的某些位来配置为open-drain或是push-pull。
当我们通过CPU去设置那些GPIO的配置寄存器的某位(bit)的时候,其GPIO硬件IC内部的实现是,会去打开或关闭对应的top transistor。
相应地,如果设置为了open-d模式的话,是需要上拉电阻才能实现,也能够输出高电平的。
因此,如果硬件内部(internal)本身包含了对应的上拉电阻的话,此时会去关闭或打开对应的上拉电阻。
如果GPIO硬件IC内部没有对应的上拉电阻的话,那么你的硬件电路中,必须自己提供对应的外部(external)的上拉电阻。
而push-pull输出的优势是速度快,因为线路(line)是以两种方式驱动的。而带了上拉电阻的线路,即使以最快的速度去提升电压,最快也要一个常量的R×C的时间。
其中R是电阻,C是寄生电容(parasitic capacitance),包括了pin脚的电容和板子的电容。但是,push-pull相对的缺点是往往需要消耗更多的电流,即功耗相对大。
而open-drain所消耗的电流相对较小,由电阻R所限制,而R不能太小,因为当输出为低电平的时候,需要sink更低的transistor,这意味着更高的功耗。
(此段原文:because the lower transistor has to sink that current when the output is low; that means higher power consumption.)
而open-drain的好处之一是,允许你cshort(?)多个open-drain的电路,公用一个上拉电阻,此种做法称为wired-OR连接,此时可以通过拉低任何一个IO的pin脚使得输出为低电平。
为了输出高电平,则所有的都输出高电平。此种逻辑,就是“线与”的功能,可以不需要额外的门(gate)电路来实现此部分逻辑。
优点:
Push-Pull推挽输出:
- 可以吸电流,也可以贯电流;
- 和开漏输出相比,push-pull的高低电平由IC的电源低定,不能简单的做逻辑操作等。
Open-Drain开漏输出:
- 对于各种电压节点间的电平转换非常有用,可以用于各种电压节点的Up-translate和down-translate转换
- 可以将多个开漏输出的Pin脚,连接到一条线上,形成“与逻辑”关系,即“线与”功能,任意一个变低后,开漏线上的逻辑就为0了。这也是I2C,SMBus等总线判断总线占用状态的原理。
- 利用 外部电路的驱动能力,减少IC内部的驱动。当IC内部MOSFET导通时,驱动电流是从外部的VCC流经R pull-up ,MOSFET到GND。IC内部仅需很下的栅极驱动电流。
- 可以利用改变上拉电源的电压,改变传输电平。
缺点:
Push-Pull推挽输出:
- 一条总线上只能有一个push-pull输出的器件;
- 在CMOS电路里面应该叫CMOS输出更合适,因为在CMOS里面的push-pull输出能力不可能做得双极那么大。输出能力看IC内部输出极N管P管的面积。push-pull是现在CMOS电路里面用得最多的输出级设计方式
Open-Drain开漏输出:
开漏Pin不连接外部的上拉电阻,则只能输出低电平。当输出电平为低时,N沟道三极管是导通的,这样在Vcc’和GND之间有一个持续的电流流过上拉电阻R和三极管Q1。这会影响整个系统的功耗。采用较大值的上拉电阻可以减小电流。但是,但是大的阻值会使输出信号的上升时间变慢。 即上拉电阻R pull-up的阻值 决定了逻辑电平转换的沿的速度。阻值越大,速度越低功耗越小。反之亦然。
怎样用I/O模拟IIC
IIC为什么要用开漏输出和上拉电阻
开漏输出如果不接上拉电阻, 没有输出高电平的能力
开漏输出可防止短路(所以总线一般会使用开漏输出)和实现线与
a) IIC协议支持多个主设备与多个从设备在一条总线上, 如果不用开漏输出, 而用推挽输出, 会出现主设备之间短路的情况. 至于为什么需要上拉电阻, 那是因为IIC通信需要输出高电平的能力;
b) 为了实现多个主设备抢占总线时的仲裁IIC只有两根线(SCL和SDA), 怎么判断哪个主设备占用总线(当然是先来后到了). 假设主设备A需要启动IIC, 他需要在SCL高电平时, 将SDA由高电平转换为低电平作为启动信号. 主设备A在把SDA拉高后, 它需要再检查一下SDA的电平.
1. SDA是高电平, 说明主设备A可以占用总线, 然后主设备A将SDA拉低, 开始通信.
2. SDA是低电平, 说明有人已经捷足先登了, 主设备A不能占用总线, 结束通信.
为什么?
因为线与. 如果主设备A拉高SDA时, 已经有其他主设备将SDA拉低了. 由于 1 & 0 = 0 那么主设备A在检查SDA电平时, 会发现不是高电平, 而是低电平. 说明其他主设备抢占总线的时间比它早, 主设备A只能放弃占用总线. 如果是高电平, 则可以占用。这就是开漏输出在IIC通信中的另一个作用。
因此, 模拟IIC一定要将GPIO端口设置为开漏输出并加上上拉电阻
(硬件IIC会自动配置为开漏输出)
IIC最多可以挂多少个设备
由IIC的地址决定,7位地址,2^7 = 128, 但是地址0x00不用,理论可挂127个从器件
IIC协议没有规定总线上的device最大数目,但是规定了总线电容不超过400pF。管脚都是有输入电容的,PCB上也会有寄生电容,所以会有一个限制。实际设计中经验值大概是不超过8个器件
总线之所以规定电容大小是因为,IIC的OD要求外部有电阻上拉,电阻和总线电容产生了一个RC延时效应,电容越大信号的边沿就越缓,有可能带来信号的质量风险。
传输速度越快,信号的窗口就越小,上升沿下降沿时间要求更短更陡峭,所以RC乘积必须更小。
你觉得哪个linux命令最难
awk 文本分析工具
awk 编程用; 分隔执行语句 awk '{count++; print &0;} END{print "user count is", count}' /etc/passwd 编程借鉴了c语言的, if语句, for循环 等等
管道和命名管道(FIFO)之间什么区别
管道:
- 半双工,数据只能向一个方向流动,需要双方通信时,需要建立两个管道;
- 只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程);
- 单独构成一种独立的文件系统,管道对于管道两端的进程而言,就是一个文件,但它不是普通的文件,不属于某种文件系统,而是自立门户,单独构成一种文件系统,并且只存在于内存中;
- 数据的读出和写入,一个进程向管道中写的内容被管道另一端的进程读出。写入的内容每次都添加在管道缓冲区的末尾,并且每次都是从缓冲区的头部读出数据;(队列)
命名管道:
- 提供了一个路径名与之关联,以FIFO文件的形式存储于文件系统中,能够实现任何两个进程之间的通信;
- FIFO是一个设备文件,在文件系统中以文件名的形式存在,因此即使进程与创建FIFO的进程不存在血缘关系也依然可以通信,前提是可以访问该路径;
- FIFO总是遵循先进先出的原则
嵌入式系统开发和裸机开发什么优缺点
优点:
1) 解决了传统开发软件和硬件耦合性过高的问题
2) 多任务机制和统一程序框架,缩短了开发周期
3) 提供了丰富的网络协议栈和开发工具
4) 易裁剪和移植,并部分开源
缺点:
1) 对硬件的存储空间和运行空间要求较高
2) 错误检测机制比较麻烦,有时很难确定是应用程序的问题还是操作系统的问题
3) 系统的实时性没有裸机高
使用芯片时,应该关注数据手册中的哪些信息
根据芯片的用途来定:
如电源芯片需注意:确定输入电压和输出电压的范围、工作温度、封装、转换效率…
如A/D转换芯片:参考电压、转换位数和精度、供电电压、信噪比、采样速率、传输通道,接口类型、增益范围、工作温度、封装…
大部分公司需要对新采购的芯片进行性能的评估,所以在选择的芯片符合生产要求外,还需考虑到该芯片是否与公司的其他产品兼容等。