面试题-嵌入式

 

目录

 

OSI七层网络协议和TCP/IP协议

1、烧焦检测算法实现?

2、视频实时传输用的什么通信协议?

3、sizeof/strlen

4、PID算法

5、RW区和ZI区

5、New/delete和malloc/free

6、什么叫波特率?和比特率有什么区别?

7、PWM有占空比的概念,讲一讲占空比

8、单片机怎么实现占空比

9、怎么实现定时功能

10、讲一讲中断的概念

11、中断触发方式有哪些

12、堆和栈的区别

13、堆和栈使用上有什么区别

14、两个函数怎么共享资源

15、const和define的区别,分别在什么情况下使用

16、平时的编程环境

17、代码管理方式

18、链表是我们经常要操作的数据结构,现在给你一个单链表的链表头,实现链表的排序,说出具体过程

19、假设时钟频率128MHz,那么1us可以运行多少条指令

20、volatile的作用

关键字volatile有什么含意 并给出三个不同的例子。

21、static关键字

22、指针和引用的区别

23、C++多态的实现方式

24、C++内存分配(堆、栈、静态存储区)

25、平衡二叉树的实现原理

26、线程和进程的区别

27、进程之间相互通信的方式

28、网络IO模式(select、epoll)

29、Linux的进程调度(优先级、时间片轮转调度)

30、交叉编译

mjpg_streamer

串口通信

IIC

uart串口通信

智能指针

TCP粘包、拆包及解决办法

如何在Android系统上运行C++程序?

ARM指令体系

画出整个项目的软件流程图,多线程操作的资源保护

函数指针

类与结构体的区别

main的参数的意义

交叉编译链

我看你做了内核移植,交叉编译链是什么,那个公司的,什么版本?

能说一下具体操作系统移植的过程么?

移植遇到比较困难的问题

会shell脚本么

示例解释

字符设备的编写流程

linux调度算法有哪些

多线程为什么要设置属性,为什么要设置栈大小

进程通信有哪些,如果我需要传输大量数据使用那种方式效率最高。

为什么选择sqlite

CAMERA的工作原理

 

USB  UART  JTAG各式什么,烧程序的工作原理。

名词解释:回调函数以及用处,什么时候执行?

ASSETR是什么?什么情况下需要用

TYPEDEF的用法

全局变量,局部变量,静态局部变量各是存放在哪个位置?

中断与异常有何区别

简述SPI,UART,I2C三种传输方式

嵌入式操作系统和通用操作系统有什么差别?

C/C++实现strcpy和strcat两个功能


OSI七层网络协议和TCP/IP协议

参考:https://zhuanlan.zhihu.com/p/33959230

webp

OSI是Open System Interconnection的缩写,意为开放式系统互联。是设计和描述计算机网络通信的基本框架。OSI模型把网络通信的工作分为7层

应用层(Application):提供网络与用户应用软件之间的接口服务

表示层(Presentation):提供格式化的表示和转换数据服务,如加密和压缩

会话层(Session):提供包括访问验证和会话管理在内的建立和维护应用之间通信的机制

传输层(Transimission): 提供建立、维护和取消传输连接功能,负责可靠地传输数据(PC)

网络层(Network):处理网络间路由,确保数据及时传送(路由器)

数据链路层(DataLink) :负责无错传输数据,确认帧、发错重传等(交换机)

物理层(Physics) : 提供机械、电气、功能和过程特性(网卡、网线、双绞线、同轴电缆、中继器)

TCP/IP协议简单介绍

这里写图片描述

TCP(Transmission Control Protocol 传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议,是互联网的基础,也是每个程序员必备的基本功。TCP/IP参考模型分为四层,从上到下分别是:应用层、传输层、网络互连层、网络接口层。

TCP功能

将数据进行分段打包传输
对每个数据包编号控制顺序
运输中丢失、重发和丢弃处理
流量控制避免拥塞

1、烧焦检测算法实现?

烧焦实际上肉眼就能发现,然后实际上我们做的工作主要是返回烧焦原因并推荐5/10组参数给用户。根据从经验师傅那里得到的经验来设计程序

激光器烧焦原因:功率过高、频率、脉宽

2、视频实时传输用的什么通信协议?

http通讯协议,mjpg-streamer 可以通过文件或者是HTTP方式访问linux UVC兼容摄像头

wifi:获取端口号和ip地址,获取wifi服务,创建http客户端:HttpURLConnection 和 Apache HTTP。

数据处理通过socket实现:

就是两个进程,跨计算机,他俩需要通讯的话,需要通过网络对接起来。
这就是 socket 的作用。打个比方吧,两个进程在两个计算机上,需要有一个进程做被动方,叫做服务器。另一个做主动方,叫做客户端。他们位于某个计算机上,叫做主机 host ,在网络上有自己的 ip 地址。一个计算机上可以有多个进程作为服务器,但是 ip 每个机器只有一个,所以通过不同的 port 数字加以区分。
因此,服务器程序需要绑定在本机的某个端口号上。客户端需要声明自己连接哪个地址的那个端口。两个进程通过网络建立起通讯渠道,然后就可以通过 recv send 来收发一些信息,完成通讯。

所以 socket 就是指代承载这种通讯的系统资源的标识。

https://blog.csdn.net/bailang_zhizun/article/details/78327974

3、sizeof/strlen

sizeof

a---数组名单独出现,代表整个数组。int a[5] = {1,2,3,4};printf("%d\n",sizeof(a));//20

&a—表示的是整个数组的地址(地址的大小为4,单位为字节。)printf("%d\n",sizeof(a+0));//4 printf("%d\n",sizeof(&a+1));//4

解引用 *&a---代表整个数组的内容printf("%d\n",sizeof(*&a));//20,*&a---代表整个数组的内容,所以为5*4

strlen

char arr[] = {'a','b','c','d','e','f'};

printf("%d\n", strlen(arr));//随机值

因为在此数组中没有’ \0 ’,函数无法知道在哪里停下来。printf("%d\n",strlen(&arr+1));//随机值-6.但&数组名加上其他操作符—代表整个数组的地址。再接着执行。跳过整个数组,所以大小差6.

char arr[] = "abcdef";

printf("%d\n", strlen(arr));//6

printf("%d\n", strlen(arr+0));//6

二维数组

int a[3][4] = {0};

printf("%d\n",sizeof(a));//48 a---数组名单独出现,代表整个数组。一共有12个元素,每个元素为4字节。12*4

printf("%d\n",sizeof(*a));//16二维数组降级变为一维数组,成为第一行。所以第一行内容的大小为4*4

printf("%d\n",sizeof(a[3]));//16表示第三行的数组长度,元素个数为4,4*4

4、PID算法

参考:https://zhuanlan.zhihu.com/p/52846678

横滚角、俯仰角和偏航角

PID经常用于保持环境的稳定,一般它通过关注3种误差和设置3个可调参数最后进行加权求和作为输出,3个误差分别是当前误差、历史误差和近期误差,当前误差用于大幅度的调控,历史误差用于累积收敛,近期误差用于调控整体趋势,再用3个可调参数分别相乘再相加,也就是加权求和,就可以作为控制的输出。

首先,什么是控制?我们以飞控中的姿态控制为例。这里,我们要给定飞行器一个目标值,比如roll角为30度,则希望飞行器的roll转到30度左右,这个过程就是控制。

控制的输入输出?这里就涉及到四旋翼飞行器的飞行原理了,前面建模篇有介绍过,这里不再赘述。姿态控制中,控制输入为30度的目标角度,控制器输出的是电机的转速差值,这个差值会经过代码里的混控模块,最终得到4个电调的PWM指令,最终驱动电机转起来,最终体现成飞行器倾斜30度。

控制的目标?控制器的目标就是使得输入和反馈值的误差趋于0,也就是说目标值30度,反馈得到当前飞行器的姿态假设也是30度,那这个误差就为0,也就达到了控制目标,飞行器的姿态为30度。

PID基础:前面讲过控制的目标,就是保证误差趋于0,那最直观的感受就是引入一个比例项,与误差直接相乘,得到控制量。误差越大,这个控制量就越大,误差越小,这个控制量就越小。比如目标值30度,当前反馈的姿态为0度,所以误差为30度,乘以一个比例项,比如10,则得到控制量为300,即转速差值为300,映射到电机输出。

只有P控制有什么问题?如果比例项给的参数过小,则控制作用偏弱,在飞控中,直观的感受就是不跟手。如果参数过大,则会越界,即超过设定的30度,俗称“超调”。太大的话,就会引起震荡,飞行器不稳定。也就是说,最好的情况就是“临界稳定”,但是始终不会收敛到设定值上

关于越界的直观理解,就是飞行器是有一定的惯性的,当姿态到达30度的时候,这时候控制作用为0,但是由于惯性的存在,导致它又超过了30度,这时候又会产生一个反向的控制作用,如此进行反复。

PID各自的作用?

PID控制采用了数据的三种信息,分别是当前、过去和未来三种状态。

P 快速反应

I 去除稳态误差

D 超前控制

积分抗饱和

积分控制在实际应用的时候要非常小心。举个例子,假设我们给一个姿态角30度的目标值,但是你人为的让飞机还是保持水平,那这时候,控制器里面的积分会不断的累加这个误差,时间越长,这个积分项的数值越大。前面我们讲过,最终控制器的输出是要由执行机构执行的,我们这里指的是电机。假设电机PWM波的行程最大范围接收到1000,那你控制器的输出,由于积分的累加,假设到了2000,自然是没办法执行的。

5、RW区和ZI区

https://i-blog.csdnimg.cn/blog_migrate/ae3eaaaaf56f2374ed46e10c8230495a.jpeg

全局区的初始化区和未初始化区

https://i-blog.csdnimg.cn/blog_migrate/80d7223310b0763f119b756cd195eee1.png

Code是程序代码所占的字节,即代码区;

RO-data 代表只读数据,程序中所定义的常量数据和字符串等都位于此处,即常量区;

RW-data 代表已初始化的读写数据,程序中定义并且初始化的全局变量和静态变量位于此处,一部分静态区(全局区);

ZI-data 代表未初始化的读写数据,程序中定义了但没有初始化的全局变量和静态变量位于此处,另一部分的静态区(全局区)。ZI英语是zero initial,就是程序中用到的变量并且被系统初始化为0的变量的字节数,keil编译器默认是把你没有初始化的变量都赋值一个0,这些变量在程序运行时是保存在RAM中的。

5、New/delete和malloc/free

https://i-blog.csdnimg.cn/blog_migrate/4c78fb2df91919de6c61f7eb7d595df9.png

指针左移64位:(char *)ptr -= 64;

6、什么叫波特率?和比特率有什么区别?

波特率是传输一个码元的时间,而比特率是传输一个二进制位的时间,区别在于,一个码元不一定是一个二进制位(虽然经常都是),比如当传输的状态有4种的时候,需要2个二进制位表示全部状态,此时发送一个状态为一个码元,一个码元其实就是2个二进制位,此时波特率是比特率的两倍,一个通用的公式是比特率=波特率*一个码元对应的二进制位数

7、PWM有占空比的概念,讲一讲占空比

占空比就是在一个脉冲宽度中,通电时间或者高电平时间所占的比例

8、单片机怎么实现占空比

可以用定时功能实现,设置好脉冲宽度和占空比后,计算定时的溢出时间,这段时间就作为高电平的输出时间,其余时间输出低电平

9、怎么实现定时功能

单片机都会有定时器,用定时器和中断就可以了

10、讲一讲中断的概念

中断是用于处理紧急事件的,系统只要预先设置好中断源、中断触发方式、中断服务函数、中断屏蔽位等等就可以使用中断了,当中断源满足中断条件的时候就会触发中断,此时CPU会停止当前工作,然后保护现场,接着跳转到中断服务函数执行,最后恢复现场

11、中断触发方式有哪些

外部中断一般是上升沿、下降沿或者两者都触发,而内部中断是由程序行为触发的,比如未定义行为像数组越界、除数是0等等

12、堆和栈的区别

首先是管理方式不同,堆由程序员负责申请和释放,栈是编译器负责的,然后是结构不同,堆是一种从底部往顶部扩展的结构,栈是从顶部到底部刚好相反的结构,最后是效率有很大的不同,堆的申请和释放都需要经过算法计算,因为要减少内存碎片和提高内存使用率,而栈由编辑器负责,速度非常快,在这点上堆的效率比较低

13、堆和栈使用上有什么区别

因为栈是编译器负责的,所以程序员只需要在函数内部定义变量直接使用,堆是程序员负责的,申请的时候会在内存开辟一块匿名区域,然后返回地址,必须使用指针去访问,所以访问堆都是通过传递指针的方式,最后需要程序员去释放堆空间

14、两个函数怎么共享资源

可以用全局变量,不够由于没有约束,会使得函数变得不可重入,也可以使用指针,以参数的形式传递一个指针,就可以共享指针所指向的资源

15、const和define的区别,分别在什么情况下使用

在编译器的角度下,const其实给出了地址,而define给出了立即数,然后const是有类型的,表达式运算的时候会进行类型安全检查,而define没有,最后const只在第一次使用的时候访问内存,往后的使用都是访问符号表,define则是普通的字符串替换,具有多个副本。

const一般用于函数的参数保护,以免函数内部不小心修改了只读变量,define其实比较自由,按照Linux的风格,define是有崇高的地位,很多短小精悍的功能都由define完成,如果就定义常量而言,const和define都可以,不过我个人认为const定义的常量比define要高效

我只要一听到被面试者说:“const意味着常数,我就知道我正在和一个业余者打交道。去年Dan Saks已经在他的文章里完全概括了const的所有用法,因此ESP(译者:Embedded Systems Programming)的每一位读者应该非常熟悉const能做什么和不能做什么.
如果你从没有读到那篇文章,只要能说出const意味着只读就可以了。尽管这个答案不是完全的答案,但我接受它作为一个正确的答案。(如果你想知道更详细的答案,仔细读一下Saks的文章吧。)如果应试者能正确回答这个问题,我将问他一个附加的问题:下面的声明都是什么意思? const int a;
int const a;
const int *a;
int *const a;
int const *a const;

前两个的作用是一样,a是一个常整型数。第三个意味着a是一个指向常整型数的指针(也就是,整型数是不可修改的,但指针可以)。第四个意思a是一个指向整型数的常指针(也就是说,指针指向的整型数是可以修改的,但指针是不可修改的)。最后一个意味着a是一个指向常整型数的常指针(也就是说,指针指向的整型数是不可修改的,同时指针也是不可修改的)。如果应试者能正确回答这些问题,那么他就给我留下了一个好印象。顺带提一句,也许你可能会问,即使不用关键字 const,也还是能很容易写出功能正确的程序,那么我为什么还要如此看重关键字const呢?我也如下的几下理由:
1). 关键字const的作用是为给读你代码的人传达非常有用的信息,实际上,声明一个参数为常量是为了告诉了用户这个参数的应用目的。如果你曾花很多时间清理其它人留下的垃圾,你就会很快学会感谢这点多余的信息。(当然,懂得用const的程序员很少会留下的垃圾让别人来清理的。)
2). 通过给优化器一些附加的信息,使用关键字const也许能产生更紧凑的代码。
3). 合理地使用关键字const可以使编译器很自然地保护那些不希望被改变的参数,防止其被无意的代码修改。简而言之,这样可以减少bug的出现。

16、平时的编程环境

如果是51或者Arduino或者STM的话,因为都有丰富的集成开发环境IDE,所以直接在PC编程烧录,如果是ARM的开发,我一般是PC上用sourceinsight编程,然后进入虚拟机Linux编译调试,再通过NFS连接ARM的板载Linux传输数据进行驱动安装

17、代码管理方式

因为代码量都不是很大,不太需要github来管理,所以都是直接保存PC本地,调试的时候直接修改源代码,编译烧录整个过程重复一遍

18、链表是我们经常要操作的数据结构,现在给你一个单链表的链表头,实现链表的排序,说出具体过程

https://www.cnblogs.com/tenosdoit/p/3666585.html

19、假设时钟频率128MHz,那么1us可以运行多少条指令

时钟周期:时钟周期也称为振荡周期,定义为时钟脉冲的倒数(时钟周期就是单片机外接晶振的倒数,例如12M的晶振,它的时钟周期就是1/12us),是计算机中的最基本的、最小的时间单位。在一个时钟周期内,CPU仅完成一个最基本的动作。时钟脉冲是计算机的基本工作脉冲,控制着计算机的工作节奏。时钟频率越高,工作速度就越快。8051单片机把一个时钟周期定义为一个节拍(用P表示),二个节拍定义为一个状态周期(用S表示)。

机器周期:计算机中,常把一条指令的执行过程划分为若干个阶段,每一个阶段完成一项工作。每一项工作称为一个基本操作,完成一个基本操作所需要的时间称为机器周期。8051系列单片机的一个机器周期由6个S周期(状态周期)组成。 一个S周期=2个节拍(P),所以8051单片机的一个机器周期=6个状态周期=12个时钟周期。例如外接24M晶振的单片机,他的一个机器周期=12/24M 秒;

指令周期:执行一条指令所需要的时间,一般由若干个机器周期组成。指令不同,所需的机器周期也不同。

20、volatile的作用

编译器优化常用的方法有:将内存变量缓存到寄存器;调整指令顺序充分利用CPU

volatile的本意是“易变的” 因为访问寄存器要比访问内存单元快的多,所以编译器一般都会作减少存取内存的优化,但有可能会读脏数据(脏数据就是在物理上临时存在过,但在逻辑上不存在的数据)。当要求使用volatile声明变量值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。精确地说就是,遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问;如果不使用valatile,则编译器将对所声明的语句进行优化。(简洁的说就是:volatile关键词影响编译器编译的结果,用volatile声明的变量表示该变量随时可能发生变化,与该变量有关的运算,不要进行编译优化,以免出错)

关键字volatile有什么含意 并给出三个不同的例子。

一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。下面是volatile变量的几个例子:
1). 并行设备的硬件寄存器(如:状态寄存器)
2). 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)
3). 
多线程应用中被几个任务共享的变量
回答不出这个问题的人是不会被雇佣的。我认为这是区分C程序员和嵌入式系统程序员的最基本的问题。嵌入式系统程序员经常同硬件、中断、RTOS等等打交道,所用这些都要求volatile变量。不懂得volatile内容将会带来灾难。
假设被面试者正确地回答了这是问题(嗯,怀疑这否会是这样),我将稍微深究一下,看一下这家伙是不是直正懂得volatile完全的重要性。
1). 一个参数既可以是const还可以是volatile吗?解释为什么。
2). 一个指针可以是volatile 吗?解释为什么。
3). 下面的函数有什么错误:
int square(volatile int *ptr)
{ return *ptr * *ptr;
}

下面是答案:
1). 是的。一个例子是只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。
2). 是的。尽管这并不很常见。一个例子是当一个中服务子程序修该一个指向一个buffer的指针时。
3). 这段代码的有个恶作剧。这段代码的目的是用来返指针*ptr指向值的平方,但是,由于*ptr指向一个volatile型参数,编译器将产生类似下面的代码:
int square(volatile int *ptr)
{
int a,b;
a = *ptr;
b = *ptr;
return a * b;
}

由于*ptr的值可能被意想不到地该变,因此ab可能是不同的。结果,这段代码可能返不是你所期望的平方值!正确的代码如下:
long square(volatile int *ptr)
{
int a;
a = *ptr;
return a * a;
}

21、static关键字

静态局部变量使用static修饰符定义,即使在声明时未赋初值,编译器也会把它初始化为0。且静态局部变量存储于进程的全局数据区,即使函数返回,它的值也会保持不变。

全局变量定义在函数体外部,在全局数据区分配存储空间,且编译器会自动对其初始化。普通全局变量对整个工程可见,其他文件可以使用extern外部声明后直接使用。也就是说其他文件不能再定义一个与其相同名字的变量了(否则编译器会认为它们是同一个变量)。静态全局变量仅对当前文件可见,其他文件不可访问,其他文件可以定义与其同名的变量,两者互不影响。在定义不需要与其他文件共享的全局变量时,加上static关键字能够有效地降低程序模块之间的耦合,避免不同文件同名变量的冲突,且不会误使用。

函数的使用方式与全局变量类似,在函数的返回类型前加上static,就是静态函数。其特性有:静态函数只能在声明它的文件中可见,其他文件不能引用该函数不同的文件可以使用相同名字的静态函数,互不影响。非静态函数可以在另一个文件中直接引用,甚至不必使用extern声明

22、指针和引用的区别

对象是指一块能存储数据并具有某种类型的内存空间,一个对象a,它有值和地址&a,运行程序时,计算机会为该对象分配存储空间,来存储该对象的值,我们通过该对象的地址,来访问存储空间中的值

指针p也是对象,它同样有地址&p和存储的值p,只不过,p存储的数据类型是数据的地址。如果我们要以p中存储的数据为地址,来访问对象的值,则要在p前加解引用操作符"*",即*p。

对象有常量(const)和变量之分,既然指针本身是对象,那么指针所存储的地址也有常量和变量之分,指针常量是指,指针这个对象所存储的地址是不可以改变的,而指向常量的指针的意思是,不能通过该指针来改变这个指针所指向的对象。

引用可以理解成变量的别名。定义一个引用的时候,程序把该引用和它的初始值绑定在一起,而不是拷贝它。计算机必须在声明r的同时就要对它初始化,并且,r一经声明,就不可以再和其它对象绑定在一起了。引用的一个优点是它一定不为空,因此相对于指针,它不用检查它所指对象是否为空,这增加了效率。

23、C++多态的实现方式

1、覆盖

覆盖是指子类重新定义父类的虚函数的做法。当子类重新定义了父类的虚函数后,父类指针根据赋值给它的不同的子类指针,动态的调用属于子类的该函数,这样函数调用在编译期间是无法确定的,这样的函数地址在运行期间绑定称为动态联编。

2、重载

重载是指允许存在多个同名函数,而这些函数的参数表不同(或许参数个数不同,或许参数类型不同,或许两者都不同)。编译器根据函数不同的参数表,对同名函数的名称做修饰,然后这些同名函数就成了不同的函数。

24、C++内存分配(堆、栈、静态存储区)

,就是那些由编译器在需要的时候分配,在不需要的时候自动清除的变量的存储区。里面的变量通常是局部变量、函数参数等。在一个进程中,位于用户虚拟地址空间顶部的是用户栈,编译器用它来实现函数的调用。和堆一样,用户栈在程序执行期间可以动态地扩展和收缩。

,就是那些由 new 分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个 new 就要对应一个 delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。堆可以动态地扩展和收缩。

自由存储区,就是那些由 malloc 等分配的内存块,他和堆是十分相似的,不过它是用 free 来结束自己的生命的。

全局/静态存储区,全局变量和静态变量被分配到同一块内存中,在以前的 C 语言中,全局变量又分为初始化的和未初始化的(初始化的全局变量和静态变量在一块区域,未初始化的全局变量与静态变量在相邻的另一块区域,同时未被初始化的对象存储区可以通过 void* 来访问和操纵,程序结束后由系统自行释放),在 C++ 里面没有这个区分了,他们共同占用同一块内存区。

常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改(当然,你要通过非正当手段也可以修改,而且方法很多

 

https://i-blog.csdnimg.cn/blog_migrate/ae3eaaaaf56f2374ed46e10c8230495a.jpeg

25、平衡二叉树的实现原理

平衡二叉树引入了平衡因子BF(Balance Factor),根据平衡因子可以构造平衡二叉树以及判断该二叉树是否一棵平衡二叉树!BF的定义:二叉树上的结点上的左子树的深度值与右子树的深度值之差,如果绝对值小于等于1,表示该树是平衡二叉树!

26、线程和进程的区别

进程和线程的主要差别在于它们是不同的操作系统资源管理方式进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。

1) 简而言之,一个程序至少有一个进程,一个进程至少有一个线程.

2) 线程的划分尺度小于进程,使得多线程程序的并发性高。

3) 另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。

4) 线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。

5) 从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。

27、进程之间相互通信的方式

1. 管道pipe:管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
2. 命名管道FIFO:有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
4. 消息队列MessageQueue:消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
5. 共享存储SharedMemory:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。
6. 信号量Semaphore:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
7. 套接字Socket:套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。
8. 信号 ( sinal ) 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。

28、网络IO模式(select、epoll)

I/O 多路复用( IO multiplexing):selectpollepoll,有些地方也称这种IO方式为event driven IOselect/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是selectpollepoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。但selectpollepoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。

int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

select 函数监视的文件描述符分3类,分别是writefdsreadfds、和exceptfds。调用后select函数会阻塞,直到有描述副就绪(有数据 可读、可写、或者有except),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。当select函数返回后,可以 通过遍历fdset,来找到就绪的描述符。

poll

int poll (struct pollfd *fds, unsigned int nfds, int timeout);

不同与select使用三个位图来表示三个fdset的方式,poll使用一个 pollfd的指针实现。

struct pollfd {
    int fd; /* file descriptor */
    short events; /* requested events to watch */
    short revents; /* returned events witnessed */
};

pollfd结构包含了要监视的event和发生的event,不再使用select“参数-传递的方式。同时,pollfd并没有最大数量限制(但是数量过大后性能也是会下降)。 select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符。

epoll

epoll是在2.6内核中提出的,是之前的selectpoll的增强版本。相对于selectpoll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。

29、Linux的进程调度(优先级、时间片轮转调度)

1、先来先服务调度算法

先来先服务(FCFS)调度算法是一种最简单的调度算法,该算法既可用于作业调度,也可用于进程调度。当在作业调度中采用该算法时,每次调度都是从后备作业队列中选择一个或多个最先进入该队列的作业,将它们调入内存,为它们分配资源、创建进程,然后放入就绪队列。在进程调度中采用FCFS算法时,则每次调度是从就绪队列中选择一个最先进入该队列的进程,为之分配处理机,使之投入运行。该进程一直运行到完成或发生某事件而阻塞后才放弃处理机。

2、短作业(进程)优先调度算法

短作业(进程)优先调度算法,是指对短作业或短进程优先调度的算法。它们可以分别用于作业调度和进程调度。短作业优先(SJF)的调度算法是从后备队列中选择一个或若干个估计运行时间最短的作业,将它们调入内存运行。而短进程优先(SPF)调度算法则是从就绪队列中选出一个估计运行时间最短的进程,将处理机分配给它,使它立即执行并一直执行到完成,或发生某事件而被阻塞放弃处理机时再重新调度。

3、时间片轮转法

在早期的时间片轮转法中,系统将所有的就绪进程按先来先服务的原则排成一个队列,每次调度时,把CPU分配给队首进程,并令其执行一个时间片。时间片的大小从几ms到几百ms。当执行的时间片用完时,由一个计时器发出时钟中断请求,调度程序便据此信号来停止该进程的执行,并将它送往就绪队列的末尾;然后,再把处理机分配给就绪队列中新的队首进程,同时也让它执行一个时间片。这样就可以保证就绪队列中的所有进程在一给定的时间内均能获得一时间片的处理机执行时间。换言之,系统能在给定的时间内响应所有用户的请求。

30、交叉编译

 交叉编译是在一个平台上生成另一个平台上的可执行代码

常用:gcc

mjpg_streamer

介绍:mjpg_streamer是在Linux上运行的视频服务器,可以将摄像头采集到的视频数据通过网络传输到客户端,实现视频监控,mjpg_streamer是开源项目。

参考:https://blog.csdn.net/yi412/article/details/37649641?depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-5&utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-5

mjpg_streamer主要由三部分构成,主函数mjpg_streamer.c和输入、输出组件,其中输入、输出组件通常是input_uvc.so和output_http.so,他们是以动态链接库的形式在主函数中调用的。

主函数的主要功能有:

1.解析命令行的各个输入参数。2.判断程序是否需要成为守护进程,如果需要,则执行相应的操作。3.初始化global全局变量。4.打开并配置输入、输出插件。5.运行输入、输出插件。

输入插件将采集到的视频送到全局缓存中,输出插件则从全局缓存中读取数据并发送。输出插件的实现是一个http服务器,这里就不介绍了。输入插件的视频采集功能主要是通过Linux的V4L2接口实现的,主要是4个函数input_init()、 input_stop()、 input_run()和 input_cmd()。其中iniput_run()函数创建了线程cma_thread(),这个线程很重要,该函数的作用是抓取一帧的图像,并复制到全局缓冲区。

串口通信

参考:https://blog.csdn.net/huwei2003/article/details/36418471

串口是串行接口(serial port)的简称,也称为串行通信接口或COM接口。

串口通信是指采用串行通信协议(serial communication)在一条信号线上将数据一个比特一个比特地逐位进行传输的通信模式。串口按电气标准及协议来划分,包括RS-232-C、RS-422、RS485等。

IIC

参考:https://blog.csdn.net/u010650845/article/details/73467586

概念:由数据线 SDA和时钟线SCL两根线构成的串行总线,可发送和接收数据。在CPU与被控IC之间、IC与IC之间进行双向传送
IIC数据传输速率有标准模式(100 kbps)、快速模式(400 kbps)和高速模式(3.4 Mbps),另外一些变种实现了低速模式(10 kbps)和快速+模式(1 Mbps)

uart串口通信

参考:https://blog.csdn.net/weixin_43664511/article/details/103038303

UART是一种通用串行数据总线,用于异步通信。该总线双向通信,可以实现全双工传输和接收。串口的通信方式为串行通信,按位发送和接收字节,将并行数据转换为串行数据流发送出去,将接受的串行数据流转换为并行数据。完成串口通信,只需要三根线,接收(rx)、发送(tx)和地线。确定通信双方的波特率数据格式一致,是实现串口通信的前提。

智能指针

参考:https://www.cnblogs.com/wxquare/p/4759020.html

shared_ptr、unique_ptr、weak_ptr

shared_ptr多个指针指向相同的对象。shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。每使用他一次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,自动删除所指向的堆内存。shared_ptr内部的引用计数是线程安全的,但是对象的读取需要加锁。

  • 初始化。智能指针是个模板类,可以指定类型,传入指针通过构造函数初始化。也可以使用make_shared函数初始化。不能将指针直接赋值给一个智能指针,一个是类,一个是指针。例如std::shared_ptr<int> p4 = new int(1);的写法是错误的
  • 拷贝和赋值。拷贝使得对象的引用计数增加1,赋值使得原对象引用计数减1,当计数为0时,自动释放内存。后来指向的对象引用计数加1,指向后来的对象。
  • get函数获取原始指针
  • 注意不要用一个原始指针初始化多个shared_ptr,否则会造成二次释放同一内存
  • 注意避免循环引用,shared_ptr的一个最大的陷阱是循环引用,循环,循环引用会导致堆内存无法正确释放,导致内存泄漏。循环引用在weak_ptr中介绍。

unique_ptr“唯一”拥有其所指对象,同一时刻只能有一个unique_ptr指向给定对象(通过禁止拷贝语义、只有移动语义来实现)。相比与原始指针unique_ptr用于其RAII的特性,使得在出现异常的情况下,动态资源能得到释放。unique_ptr指针本身的生命周期:从unique_ptr指针创建时开始,直到离开作用域。离开作用域时,若其指向对象,则将其所指对象销毁(默认使用delete操作符,用户可指定其他操作)。unique_ptr指针与其所指对象的关系:在智能指针生命周期内,可以改变智能指针所指对象,如创建智能指针时通过构造函数指定、通过reset方法重新指定、通过release方法释放所有权、通过移动语义转移所有权。

weak_ptr是为了配合shared_ptr而引入的一种智能指针,因为它不具有普通指针的行为,没有重载operator*和->,它的最大作用在于协助shared_ptr工作,像旁观者那样观测资源的使用情况。weak_ptr可以从一个shared_ptr或者另一个weak_ptr对象构造,获得资源的观测权。但weak_ptr没有共享资源,它的构造不会引起指针引用计数的增加。使用weak_ptr的成员函数use_count()可以观测资源的引用计数,另一个成员函数expired()的功能等价于use_count()==0,但更快,表示被观测的资源(也就是shared_ptr的管理的资源)已经不复存在。weak_ptr可以使用一个非常重要的成员函数lock()从被观测的shared_ptr获得一个可用的shared_ptr对象, 从而操作资源。但当expired()==true的时候,lock()函数将返回一个存储空指针的shared_ptr。

 

 

 C++程序设计中使用堆内存是非常频繁的操作,堆内存的申请和释放都由程序员自己管理。程序员自己管理堆内存可以提高了程序的效率,但是整体来说堆内存的管理是麻烦的,C++11中引入了智能指针的概念,方便管理堆内存。使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,程序发生异常时内存泄露等问题等,使用智能指针能更好的管理堆内存。

理解智能指针需要从下面三个层次:

从较浅的层面看,智能指针是利用了一种叫做RAII(资源获取即初始化)的技术对普通的指针进行封装,这使得智能指针实质是一个对象,行为表现的却像一个指针。
智能指针的作用是防止忘记调用delete释放内存和程序异常的进入catch块忘记释放内存。另外指针的释放时机也是非常有考究的,多次释放同一个指针会造成程序崩溃,这些都可以通过智能指针来解决。
智能指针还有一个作用是把值语义转换成引用语义。
智能指针在C++11版本之后提供,包含在头文件<memory>中,shared_ptr、unique_ptr、weak_ptr

auto_ptr
class Test
{
public:
    Test(string s)
    {
        str = s;
       cout<<"Test creat\n";
    }
    ~Test()
    {
        cout<<"Test delete:"<<str<<endl;
    }
    string& getStr()
    {
        return str;
    }
    void setStr(string s)
    {
        str = s;
    }
    void print()
    {
        cout<<str<<endl;
    }
private:
    string str;
};

int main()
{
    auto_ptr<Test> ptest(new Test("123"));
    ptest->setStr("hello ");
    ptest->print();
    ptest.get()->print();
    ptest->getStr() += "world !";
    (*ptest).print();
    ptest.reset(new Test("123"));
    ptest->print();
    return 0;
}


如上面的代码:智能指针可以像类的原始指针一样访问类的public成员,成员函数get()返回一个原始的指针,成员函数reset()重新绑定指向的对象,而原来的对象则会被释放。注意我们访问auto_ptr的成员函数时用的是“.”,访问指向对象的成员时用的是“->”。我们也可用声明一个空智能指针auto_ptr<Test>ptest();

当我们对智能指针进行赋值时,如ptest2 = ptest,ptest2会接管ptest原来的内存管理权,ptest会变为空指针,如果ptest2原来不为空,则它会释放原来的资源,基于这个原因,应该避免把auto_ptr放到容器中,因为算法对容器操作时,很难避免STL内部对容器实现了赋值传递操作,这样会使容器中很多元素被置为NULL。判断一个智能指针是否为空不能使用if(ptest == NULL),应该使用if(ptest.get() == NULL)。

unique_ptr
unique_ptr 是一个独享所有权的智能指针,它提供了严格意义上的所有权,包括:

1、拥有它指向的对象

2、无法进行复制构造,无法进行复制赋值操作。即无法使两个unique_ptr指向同一个对象。但是可以进行移动构造和移动赋值操作

3、保存指向某个对象的指针,当它本身被删除释放的时候,会使用给定的删除器释放它指向的对象

unique_ptr 可以实现如下功能:

1、为动态申请的内存提供异常安全

2、讲动态申请的内存所有权传递给某函数

3、从某个函数返回动态申请内存的所有权

4、在容器中保存指针

5、auto_ptr 应该具有的功能

unique_ptr<Test> fun()
{
    return unique_ptr<Test>(new Test("789"));
}
int main()
{
    unique_ptr<Test> ptest(new Test("123"));
    unique_ptr<Test> ptest2(new Test("456"));
    ptest->print();
    ptest2 = std::move(ptest);//不能直接ptest2 = ptest
    if(ptest == NULL)cout<<"ptest = NULL\n";
    Test* p = ptest2.release();
    p->print();
    ptest.reset(p);
    ptest->print();
    ptest2 = fun(); //这里可以用=,因为使用了移动构造函数
    ptest2->print();
    return 0;
}

share_ptr
从名字share就可以看出了资源可以被多个指针共享,它使用计数机制来表明资源被几个指针共享。可以通过成员函数use_count()来查看资源的所有者个数。出了可以通过new来构造,还可以通过传入auto_ptr, unique_ptr,weak_ptr来构造。当我们调用release()时,当前指针会释放资源所有权,计数减一。当计数等于0时,资源会被释放。

int main()
{
    shared_ptr<Test> ptest(new Test("123"));
    shared_ptr<Test> ptest2(new Test("456"));
    cout<<ptest2->getStr()<<endl;
    cout<<ptest2.use_count()<<endl;
    ptest = ptest2;//"456"引用次数加1,“123”销毁
    ptest->print();
    cout<<ptest2.use_count()<<endl;//2
    cout<<ptest.use_count()<<endl;//2
    ptest.reset();
    ptest2.reset();//此时“456”销毁
    cout<<"done !\n";
    return 0;
}


weak_ptr
weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。

class B;
class A
{
public:
    shared_ptr<B> pb_;
    ~A()
    {
        cout<<"A delete\n";
    }
};
class B
{
public:
    shared_ptr<A> pa_;
    ~B()
    {
        cout<<"B delete\n";
    }
};
 
void fun()
{
    shared_ptr<B> pb(new B());
    shared_ptr<A> pa(new A());
    pb->pa_ = pa;
    pa->pb_ = pb;
    cout<<pb.use_count()<<endl;
    cout<<pa.use_count()<<endl;
}
 
int main()
{
    fun();
    return 0;
}

TCP粘包、拆包及解决办法

参考:https://zhuanlan.zhihu.com/p/108822858

UDP 是基于报文发送的,UDP首部采用了 16bit 来指示 UDP 数据报文的长度,因此在应用层能很好的将不同的数据报文区分开,从而避免粘包和拆包的问题。而 TCP 是基于字节流的,虽然应用层和 TCP 传输层之间的数据交互是大小不等的数据块,但是 TCP 并没有把这些数据块区分边界,仅仅是一连串没有结构的字节流;另外从 TCP 的帧结构也可以看出,在 TCP 的首部没有表示数据长度的字段,基于上面两点,在使用 TCP 传输数据时,才有粘包或者拆包现象发生的可能。

什么是粘包、拆包?

假设 Client 向 Server 连续发送了两个数据包,用 packet1 和 packet2 来表示,那么服务端收到的数据可以分为三种情况,现列举如下:

第一种情况,接收端正常收到两个数据包,即没有发生拆包和粘包的现象。

第二种情况,接收端只收到一个数据包,但是这一个数据包中包含了发送端发送的两个数据包的信息,这种现象即为粘包。这种情况由于接收端不知道这两个数据包的界限,所以对于接收端来说很难处理。

第三种情况,这种情况有两种表现形式,如下图。接收端收到了两个数据包,但是这两个数据包要么是不完整的,要么就是多出来一块,这种情况即发生了拆包和粘包。这两种情况如果不加特殊处理,对于接收端同样是不好处理的。

为什么会发生 TCP 粘包、拆包?

  • 要发送的数据大于 TCP 发送缓冲区剩余空间大小,将会发生拆包。
  • 待发送数据大于 MSS(最大报文长度),TCP 在传输前将进行拆包。
  • 要发送的数据小于 TCP 发送缓冲区的大小,TCP 将多次写入缓冲区的数据一次发送出去,将会发生粘包。
  • 接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。

粘包、拆包解决办法

由于 TCP 本身是面向字节流的,无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过上层的应用协议栈设计来解决,根据业界的主流协议的解决方案,归纳如下:

  • 消息定长:发送端将每个数据包封装为固定长度(不够的可以通过补 0 填充),这样接收端每次接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。
  • 设置消息边界:服务端从网络流中按消息边界分离出消息内容。在包尾增加回车换行符进行分割,例如 FTP 协议。
  • 将消息分为消息头和消息体:消息头中包含表示消息总长度(或者消息体长度)的字段。
  • 更复杂的应用层协议比如 Netty 中实现的一些协议都对粘包、拆包做了很好的处理。

如何在Android系统上运行C++程序?

使用 NDK,通过 JNI 的方式来调用 C++ 的方法。

流程:

Gradle 调用您的外部构建脚本 CMakeLists.txt。
CMake 按照构建脚本中的命令将 C++ 源文件 native-lib.cpp 编译到共享的对象库中,并命名为 libnative-lib.so,Gradle 随后会将其打包到 APK 中。
运行时,应用的 MainActivity 会使用 System.loadLibrary() 加载原生库。现在,应用可以使用库的原生函数 stringFromJNI()。
MainActivity.onCreate() 调用 stringFromJNI(),这将返回“Hello from C++”并使用这些文字更新 TextView。
要手动配置 Gradle 以关联到您的原生库,需要将 externalNativeBuild {} 块添加到模块级 build.gradle 文件中,并使用 cmake {} 或 ndkBuild {} 对其进行配置。

ARM指令体系

arm和Thumb

画出整个项目的软件流程图,多线程操作的资源保护

参考:https://blog.csdn.net/u012724150/article/details/52238017

这里写图片描述

函数指针

参考:https://blog.csdn.net/qq_28386947/article/details/73480831?depth_1-utm_source=distribute.pc_relevant.none-task-blog-OPENSEARCH-4&utm_source=distribute.pc_relevant.none-task-blog-OPENSEARCH-4

函数指针: 是指向函数的指针变量
在程序运行中,函数是程序算法指令部分,他们和数组一样也占用内存空间,也都有相应的地址。我们程序员可以使用指针变量指向数组的首地址,同样,也可以使用指针变量指向函数代码的首地址, 指向函数代码首地址的指针变量,称为函数指针。

函数指针有两个用途:调用函数和做函数的参数

函数指针和指针函数的区别:函数指针是一个指向函数的指针。他的本质是一个指针,而指针函数只是说明他是一个返回值为指针的函数,它本质是一个函数,

类与结构体的区别

参考:https://blog.csdn.net/yuechuxuan/article/details/81673953

类是一种“引用类型”。创建类的对象时,对象赋值到的变量只保存对该内存的引用。将对象引用赋给新变量时,新变量引用的是原始对象。通过一个变量做出的更改将反映在另一个变量中,因为两者引用同一数据。

 结构是一种值类型。创建结构时,结构赋值到的变量保存该结构的实际数据。将结构赋给新变量时,将复制该结构。因此,新变量和原始变量包含同一数据的两个不同的副本。对一个副本的更改不影响另一个副本。

类通常用于对较为复杂的行为建模,或对要在创建类对象后进行修改的数据建模。结构最适合一些小型数据结构,这些数据结构包含的数据以创建结构后不修改的数据为主。结构与类共享大多数相同的语法,但结构比类受到的限制更多。

C++中的struct对C中的struct进行了扩充,它已经不再只是一个包含不同数据类型的数据结构了,它已经获取了太多的功能。
struct能包含成员函数吗? 能!struct能继承吗? 能!!struct能实现多态吗? 能!!!既然这些它都能实现,那它和class还能有什么区别?最本质的一个区别就是默认的访问控制:默认的继承访问权限,struct是public的,class是private的。

struct作为数据结构的实现体,它默认的数据访问控制是public的,而class作为对象的实现体,它默认的成员变量访问控制是private的

我依旧强调struct是一种数据结构的实现体,虽然它是可以像class一样的用。我依旧将struct里的变量叫数据,class内的变量叫成员,虽然它们并无区别。

class”这个关键字还用于定义模板参数,就像“typename”。但关键字“struct”不用于定义模板参数。这一点在Stanley B.Lippman写的Inside the C++ Object Model有过说明。

从上面的区别,我们可以看出,struct更适合看成是一个数据结构的实现体,class更适合看成是一个对象的实现体。

main的参数的意义

main()函数称之为主函数,一个C程序总是从main()函数开始执行的

main()函数的返回值类型是int型的,而程序最后的 return 0; 正与之遥相呼应,0就是main()函数的返回值。那么这个0返回到那里呢?返回给操作系统,表示程序正常退出。因为return语句通常写在程序的最后,不管返回什么值,只要到达这一步,说明程序已经运行完毕。而return的作用不仅在于返回一个值,还在于结束函数。

C编译器允许main()函数没有参数,或者有两个参数(有些实现允许更多的参数,但这只是对标准的扩展)。这两个参数,一个是int类型,一个是字符串类型。第一个参数是命令行中的字符串数。按照惯例(但不是必须的),这个int参数被称为argc(argument count)。大家或许现在才明白这个形参为什么要取这么个奇怪的名字吧,呵呵!至于英文的意思,自己查字典吧。第二个参数是一个指向字符串的指针数组。命令行中的每个字符串被存储到内存中,并且分配一个指针指向它。按照惯例,这个指针数组被称为argv(argument value)。系统使用空格把各个字符串格开。一般情况下,把程序本身的名字赋值给argv[0],接着,把最后的第一个字符串赋给argv[1],等等。

交叉编译链

gcc

在一种计算机环境(称为host machine)中运行的编译程序,能编译出在另外一种环境(称为target machine)下运行的代码,叫做交叉编译。实现这个交叉编译的一系列工具,包括C函数库,内核文件,编译器,链接器,调试器,二进制工具……称为交叉编译工具链。
    实际上在进行嵌入式开发时,我们通常都会在主机上(host machine)使用开发板厂商提供的编译器,调试器。比如在windows上装环境调试51,61单片机,在Linux上用arm-gcc写arm开发板的程序……
    搭建交叉编译环境是一个非常繁琐并细致的过程。笔者就已嵌入式Linux交叉编译工具链的搭建为例,介绍下交叉编译工具的创建过程和原理。
 

我看你做了内核移植,交叉编译链是什么,那个公司的,什么版本?


答:免费版目前有三大主流工具商提供,第一是GNU(提供源码,自行编译制作),第二是 Codesourcery,第三是Linora。作为学习,答到以上三个就行。

能说一下具体操作系统移植的过程么?

答:内核启动流程:uboot->kernel->rootfs uboot基本都不会做修改,直接拿来烧在板上即可,uboot主要在调试模式修改一些环境变量,包括tftp的下载地址(serverip、ipaddr),包括bootcmd传给内核的参数,传给根文件系统的参数bootargs,并熟悉相应的命令使用(nand scrub、set pri、boot);内核源码编译,内核参考开发板的参数进行修改(开发板和母版的参数差异),make menuconfig进行模块选择,ulmage的制作;然后内核利用bootargs参数进行根文件系统启动,再就是根文件目录树的创建及其基本文件的创建。
(以上基本就能应付,很少有面试官深入,因为应届毕业生做arm移植的太少了=-=,更深的问题:为什么内核初始化地址是0x30008000,ulmage和zlmage的差别,make menuconfig添加模块的依据,)

移植遇到比较困难的问题

我说我当时移植QT的库时 ,发现版子上分区内存不够,当时也不知道怎么解决 就去学习分区挂载的知识,用df查看分区,发现有分区但是没挂载,导致空间不够,但是又不想在系统启动每次手动去挂载,于是就研究根文件启动init的过程,在配置文件inittab添加挂载命令,系统就会自启动并挂载分区了。

接着问,如果开发板上只有十兆空间,但是你的移植的QT库有二十兆,你会怎么做?
我说的扩容,他说除了扩容呢,我接着说移植的文件中有一个stripped属性,用strip命令去除库文件中的符号表,会小很多,他说QT编译一般会做这个工作,还有别的方法么…我就答不上来了。查了一下看了别人的方法https://blog.csdn.net/yming0221/article/details/6548349

会shell脚本么

我不会…慢慢补一下,但是这个很加分,即使他不问你,你自己说出来也很加分,做嵌入式真的要shell脚本,提高办事效率用的。

看个例子吧:

#!/bin/sh
cd ~
mkdir shell_tut
cd shell_tut

for ((i=0; i<10; i++)); do
	touch test_$i.txt
done

示例解释

  • 第1行:指定脚本解释器,这里是用/bin/sh做解释器的
  • 第2行:切换到当前用户的home目录
  • 第3行:创建一个目录shell_tut
  • 第4行:切换到shell_tut目录
  • 第5行:循环条件,一共循环10次
  • 第6行:创建一个test_0…9.txt文件
  • 第7行:循环体结束

mkdir, touch都是系统自带的程序,一般在/bin或者/usr/bin目录下。for, do, done是sh脚本语言的关键字。

字符设备的编写流程

主要答probe函数的实现,platform总线的思想
设备号分配------->cdev结构体的初始化(绑定fops)------->cdev结构体的注册

linux调度算法有哪些

SCHED_OTHER 分时调度策略,
SCHED_FIFO实时调度策略,先到先服务
SCHED_RR实时调度策略,时间片轮转

多线程为什么要设置属性,为什么要设置栈大小

我答的不够用所以要分配更多的空间…后来百度了一下,linux线程默认分配8M的调用栈,那么太多的线程会导致栈溢出的问题,所以可以调用pthread_attr_setstack自动分配合理的大小防止栈溢出。当然线程还有其他的一些属性。

进程通信有哪些,如果我需要传输大量数据使用那种方式效率最高。

管道,命名管道,消息队列,共享内存,信号量,socket unix域,数据很大的时候,用共享内存是最好的,不用从用户态到内核态的频繁切换和拷贝数据,直接从内存中读取就可以,速度最快,但是共享内存开辟空间就必须要使用同步机制,这是他的缺点。

为什么选择sqlite

轻量级、跨平台。

(我说了使用信号signal)信号的使用方法
signal函数和signalcation函数还有常用的信号。

CAMERA的工作原理

CAMERA从硬件上说起,他有VSYN HSYN MCLK 还有PCLK,这几个信号分别是MCLK是一个BB给CAM工作的时钟,大家都知道所有的DSP都要是基于时钟来运行的,这个就是他的基本时钟,内部很多的时钟都是通过这个时钟进行分频或倍频得到的。VSYN是一场的同步。HSYN是一行的同步。PCLK是一个点一个时钟,这个是用来与基带抓数据同步的。这里的行,场如果你不明白,你可以看黑白电视机原理。打字太麻烦,如果要解释,我们可以电话或其它的方式聊。CAM分为Preview与Capture。在Preview时CAM输出为YUV的格式,然后经过RGB的转换,并将此数据存到到一个BUF里,这个BUF也就是我们做MMI都知道的LAYER,然后与OSD的LAYER进行叠加,当然什么时候刷,这就是前面提到的VSYN了,这样就可以知道了,为什么我们的刷屏速度是会影响我们的preview的速率了.数据是怎么抓到BB上来的,这个就是MTK上说的ISP了.ISP后还要进行一个RESIZE,这个东西就是会将他RESIZE到我们想要的尺寸.RESIZE的数据才是放到LAYER BUF里的数据.这打字太麻烦了,简单的说一下Capture吧,他实际上就是MMI通知驱动层,我现在要一张多大的image.怎么通知的,就是MMI通过消息带下来的参数,如照片大小.还有照片格式.等一些参数.实际上在如果设置下来的图片大小与现在正在Preview的大小一样的情况下,就会将最后一个Preview的帧进行编码然后以文件的形式存起来.如果实际要的图片SIZE与PREVIEW的不同时,这时他会将PREVIEW的最后一帧后的一帧进行RESIZE得到合适的SIZE后,就进行编码,并以文件的形式保存下来.
还有一点就是VSYN与HSYN是会让BB产生中断的.自己看代码就能懂了,还有进CAM时会丢掉几帧.

 

USB  UART  JTAG各式什么,烧程序的工作原理。

我只说一下JTAG,哪些USB,UART我就不说了,大家都天天在用,不说了
JTAG是ARM公司用来调试ARM核的一个协议.
烧程序的工作原理:MTK内部有ROM的,大家都知道.他有一个内部代码在开机时监视UART上接收到的数据,然后决定执行ROM的代码还是进入下载BIN的模式.

名词解释:回调函数以及用处,什么时候执行?

回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用为调用它所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。

ASSETR是什么?什么情况下需要用

提示你的代码在错误的情况执行了.在你认为正常情况下不可能执行到的地方就得放这东西..这东西写代码的人要记得经常用,多用.没了

TYPEDEF的用法

用来声明自定义数据类型,主要是为了达到简便,还有就是在换编译环境时这东西比较有用.因为不同的编译环境数据类型占的位数有可能不一样的.

全局变量,局部变量,静态局部变量各是存放在哪个位置?

全局变量静态局部变量: 存放在静态RAM中.
局部变量:存放在动态区.而这东西在函数调用时会压栈的.他可能这一次运行存放在一个地址,下一次运行时就在另一个地址了,是一个不固定的.写过汇编的都知道.

中断与异常有何区别

异常在处理的时候必须考虑与处理器的时钟同步,实际上异常也称为同步中断,在处理器执行到因编译错误而导致的错误指令时,或者在执行期间出现特殊错误,必须靠内核处理的时候,处理器就会产生一个异常;所谓中断是指外部硬件产生的一个电信号从CPU的中断引脚进入,打断CPU的运行。所谓异常是指软件运行过程中发生了一些必须作出处理的事件,CPU自动产生一个陷入来打断CPU的运行。

简述SPI,UART,I2C三种传输方式

    SPI:高速同步串行口,首发独立,可同步进行

    SPI接口主要应用在EEPROM,Flash,实时时钟,A/D转化器,数字信号处理,是一种全双工同步通讯总线,该接口一般使用四条线:串行时钟线(sck),主出从入线,主入从出线,低电平有效地的从机选择线。

    I2C协议:是单片机与其他芯片进行通讯的协议:

A、只要求两条总线线路,一条是串行时钟线,一条是串行数据线; 

B、通过软件设定地址  

C、是一个多主机总线,如果两个或更多主机同时初始化数据传送可通过冲突检测和仲裁防止数据破坏; 

D、I2C总线传输的是数据的总高位

    UART:主要是由一个modem(调制解调器),可以将模拟信号量转化成数字信号量。

嵌入式操作系统和通用操作系统有什么差别?

答案:多优先级,抢占型,实时操作系统。嵌入式操作系统一般没有UI,体积小,实时性强,对稳定性要求更高。嵌入式操作系统强调实时性,并且可裁减。要求系统资源的消耗要尽可能的小。

C/C++实现strcpy和strcat两个功能

strcpy实现

char* myStrcpy(char* pre, const char* next)
{
    if (pre == nullptr || next == nullptr) //空指针直接返回
    {
        return nullptr;
    }
    if (pre == next)                       // 两者相等也无需拷贝了
        return pre;

    while ((*pre++ = *next++) != '\0');    // 依次赋值给主字符数组

    return pre;
}

//另一种形式,这种标准点
char* _strcpy(char* dest, const char* src) {
    assert(dest != NULL && src != NULL);
    char* temp = dest;
    while (*src != '\0')
    {
        *dest = *src;
        dest++;
        src++;
    }
    *dest = '\0';
    return dest;
}


int main() {

    char s2[] = "efieji";
    char s1[] = "123";

    _strcpy(s2, s1);
    cout << s2 << endl;
    cout << strlen(s2) << endl;
}

strcat实现

char* myStrcat(char* pre, const char* next)
{
    if (pre == nullptr || next == nullptr) // 如果有一个为空指针,直接返回pre
        return pre;
    char* tmp_ptr = pre + strlen(pre); //strlen计算字符数,需要包含都文件string.h,当然也可以自己实现

    while ( (*tmp_ptr++ = *next++) != '\0'); // 依次接着赋值

    return pre;
}
#include<iostream>
#include<string>
#include<string.h>

using namespace std;

char* myStrcat(char* pre, const char* next)
{
    if (pre == nullptr || next == nullptr)
        return pre;
    char* tmp_ptr = pre + strlen(pre);

    while ( (*tmp_ptr++ = *next++) != '\0');

    return pre;
}

char* myStrcpy(char* pre, const char* next)
{
    if (pre == nullptr || next == nullptr)
    {
        return nullptr;
    }
    if (pre == next)
        return pre;

    while ((*pre++ = *next++) != '\0');

    return pre;
}

int main()
{
    char str1[100] = "12345";
    char str2[20] = "hello world";
    myStrcat(str1, str2);
    myStrcpy(str1, str2);
    printf("%s\n", str1);

    return 0;
}

 

  • 4
    点赞
  • 62
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值