嵌入式LINUX常见面试问题总结
声明:本文是将常见的面试问题进行汇总,但大多数问题也是开发中较为常见的技术盲区!在此进行了汇总,以便后续进行参考!所有的答案部分是自己编写,部分问题答案引用博客客友的优秀文章!问题不全,后续工作及学习中会不断更新!
如引用下面文章对问题进行解答:
1)、嵌入式系统移植步骤详解
2)、创建守护进程的步骤
3)、TCP/IP网络编程
4)、三次握手和四次挥手
5)、32位ARM处理器的几种工作模式和工作状态
6)、按键和CPU的中断系统
7)、uboot的作用和功能
感谢以上客友的经典文章!
-
如有侵权,请联系!
1、什么是嵌入式?
A: 嵌入式系统本身是一个相对模糊的定义。目前嵌入式系统已经渗透到我们生活中的每个角落,工业、服务业、消费电子……,而恰恰由于这种范围的扩大,使得“嵌入式系统”更加难于明确定义。以下是几种常见表达方式:
-
1、执行专用功能并被内部计算机控制的设备或者系统。嵌入式系统不能使用通用型计算机,而且运行的是固化的软件,用术语表示就是固件(firmware),终端用户很难或者不可能改变固件。
-
2、凡是专用的、小型或者微型的计算机系统都是嵌入式系统,比如MP3, 手机,高清电视
-
3、比较传神和从技术人员角度来看,嵌入式系统是以应用为中心,以计算机技术为基础,并且软硬件可裁剪,适用于应用系统对功能、可靠性、成本、体积、功耗有严格要求的专用计算机系统。
2、字符设备和块设备的区别?
A:
-
1、字符设备和块设备、网络设备是一个并列的概念
-
2、字符设备按照字符流的方式被有序访问,块设备以块为单位;二者根本区别在于字符设备只能顺序被读写,块设备可以随机访问
-
3、Linux为块设备和字符设备提供了两套机制。字符设备实现的比较简单,内核例程和用户态API一一对应,用户层的read函数直接对应了内核中的read例程,这种映射关系由字符设备的file_operations维护。块设备接口相对于字符设备复杂,read、write API没有直接到块设备层, 而是通过IO请求的方式通过OS的IO请求队列实现。内核管理块设备要比管理字符设备细致得多,内核对块设备的管理却提供一个专门的提供服务的子系统。块设备对执行性能的要求很高;,LINUX内核开发者门一直致力于优化块设备的驱动。
3、进程与程序,进程与线程的区别
进程与程序的联系
A:
1. 程序是一组指令的集合,它是静态的实体,没有执行的含义。进程程序的执行过程,是一个动态的实体,有自己的生命周期,包括产生、运行、消亡的过程。除此之外,进程还有并发性和交往性。简单地说,进程是程序的一部分,程序运行的时候会产生进程。
2.所涉及到的介质不同,程序保存在存储介质,比如FLASH,硬盘等中,进程运行在RAM中内容不完全相同,程序有数据段,代码段,调试信息等,进程执行时候,有代码段,数据段,以及堆栈
线程和进程的区别:
A:
1、线程是进程的一部分,所以线程有的时候被称为是轻权进程或者轻量级进程。
2、一个没有线程的进程是可以被看作单线程的,如果一个进程内拥有多个进程,进程的执行过程不是一条线(线程)的,而是多条线(线程)共同完成的。
3、系统在运行的时候会为每个进程分配不同的内存区域,但是不会为线程分配内存(线程所使用的资源是它所属的进程的资源),线程组只能共享资源。也就是 说,出了CPU之外(线程在运行的时候要占用CPU资源),计算机内部的软硬件资源的分配与线程无关,线程只能共享它所属进程的资源。
4、标准LINUX的进程具有独立的虚拟地址空间,而一个进程里面的多个线程共享同一个虚拟内存空间,进程是系统所有资源分配时候的一个基本单位
4、嵌入式的移植过程
下面我们就来看下一个内容叫做移植的基本步骤,也就是说我们要现有一个大体的思路,如果说我作为产品开发者,或者说是作为一个系统的整体架构来说,我们拿到一款板子过后我们是如何一步一步把我们的系统用起来呢?它整个系统流程又是什么样的,我们先要有个明确目标,第一个目标是我们要保证PC也就是我们的开发机器跟目标机也就是开发板或者说最终要做成产品的板子的硬件它们俩之间的连接方式。
因为我们在嵌入式开发中有一个很麻烦的事情就是开发板的能力跟PC的能力一般是不平等的,大家都知道PC的功能很强大也很贵而板子很便宜可能一个小系统一个路由器也就几十块钱,但是我们总不能在路由器上接个键盘接个鼠标然后装一个VC,在这里是不现实的,所以说我们一般的开发环境跟ARM讲的内容都是一样的,都是在主机上开发最终把主机编译好的内容跟我们的目标机进行一个数据传输,所以这就涉及到一个非常重要的问题。
数据传输的方式。因为我们数据无外乎就是高低电平这几种,那么传播有哪些传播方式呢?
如图,所以我们如果作为一个产品的研发者来说,你第一个需要考虑的就是我们是怎么连的。
那么给大家来列出了一下,目前来说,我们的PC跟我们的开发板的连接也就大概如上图四种比较常用的方式。第一种就是我们最经典90%的板子上,都支持的方式叫异部串行接口,也就是我们所说的串口。那么这个串口传输在我们之前学习ARM的时候也学习到过,其实别看它很简单,其实它的功能很强大。它既可以输入也可以输出,所以说我们基本上完成了一个输入输出这样数据出和进的功能。
所以说串口是我们比较常用的一个接口。但是它还是有它的特点就是它的速度比较低,因为比如说我们前面所配的速度是11520那这个其实是很低的,不是很高。因为他11520B比特也就是传多少位多少个高低电平的字节,所以说这个效率不是很高但是实用性比较强就几个接口就可以。
我们举个典型的例子就是家里的路由器,如果大家有兴趣,就把家里的老路由器拆下来看一下路由器里面一般都有3个架子或4个架子,3个小插针或4个小插针无外乎就几个电压,一个是D一个是电源很多情况下,路由器都会引出这个东西。如果你的动手能力比较强,你就去市场上买一个叫DB9的一个小头子,拿个烙铁把那里面的几根线给焊上去。然后就跟PC一接,就可能会看到路由器的一些打印信息。所以说串口在我们嵌入式开发中算是一个非常经典的跟PC之间通信的一个接口。
因为大家可以想一下,这个串口既可以输出我们可以把开发板上的信息往我们的平台上去看。 甚至来说,我们还可以通过串口把PC里面的东西传到开发板中,所以他说输入输出都可以,这样的话串口也算是一个比较万能的接口,它唯一的缺陷就是速度太低,如果我们传输一些大数据比如说以后我们会看到的安卓中的一些东西,安卓中涉及到的其实跟我们所学的也是一样,他比较麻烦的就是文件系统支柱,文件系统少的可能就要几百兆,或者说压缩过后就是几十兆。那你可以想象一下,我们如果用11520去传,有的时候就要传送一二十分钟,这样很影响开发效率的。
所以说用串口如果是小文件没有关系,但大文件一般情况下用串口传输的可能性不大。如果说你的板子功能比较强,传的东西比较多,这种情况下用串口我们还是不建议。
那么现在我们就需要换一下,串口我们可以把它当作后备资源。
然后我们就要使用如上图所说的USB。随着USB的发展,从USB1.0到2.0、3.0,它的速度越来越快。那么这个传输数据我们就不用担心,它速度快是没有问题的,但是现在唯一比较担心的一个问题就是板子刚刚上电,就让他用串口去工作,这个是不现实的。所以这种情况下,我们还涉及到在开发板要把串口的驱动做好。所以说这个时候我们还要考虑驱动的问题,到底支不支持如果不支持或者开发的周期比较长,那么串口就不把它作为目标机和主机连接的主要方式了。以上就是我们的串口。
串口退而求其次就还有一种叫做网络接口,这个网络接口也是我们嵌入式开发中使用很普遍的一个接口。
如上图因为这个TCP/IP这个协议已经很成熟了。
因为我们的PC本身就是TCP/IP中很重要的一个端口,比如你可以作为服务器,也可以作为客户端,然后我们的开发板也只需要跑一个服务器或者客户端就可以跟PC以CS的模式进行数据的传输和下载。所以说这个方式也是比较通用的而且说网卡的数据和速率都比较快最少最少都是十兆而且现在百兆网卡都是非常多的。这样的话,传输速率肯定是比11520是快得多。
所以说在我们后期课程中,大家会看到我们通过网络接口去下载数据的情况是比较多的。
驱动也是一样的它也需要移植,但相对而言,它可能要比USB上要方便一点,因为USB它涉及到速率,就是说它有些时序需要调整,可能会有一些麻烦,所以说对于TCP/IP中的网卡我们一般来说是优于USB去选择的。
最后一个叫做Debug Jtag调试接口,也就是说如果你是ARM CPU的话,那么ARM中还有一些相关Debug Jtag的ICE,也就是说它内部会集成一些这样的东西,然后可能你的厂商会提供这样的调制接口。比如说ARM9就有,但是像A系列的开发板一般来说很少在市面上能够买得到它的调制接口。也就是Debug Jtag调试接口。也不是说没有,我们曾经联系ARM公司问过像Debug Jtag调试接口一台就要一万块钱,所以这样来说,如果你为了学习花一万块钱是得不偿失的。所以这种情况除非真的是你们公司去开发跟ARM公司出芯片比较多才可能去买一台Debug Jtag调试接口。
当然Debug Jtag调试接口很方便,比上面三种都更好调试,调试效率也要高的多但是就是价格太高了,所以说我们个人学习已经很多企业来说更多的还是用以上的三种。
以上就是我们移植的第一个步骤,就是关于我们主机和目标机的连接方式。
交叉编译器
有了上面那个连接方式下一个就是交叉编译器了。
交叉编译器,在我们后面就会讲到所谓的交叉编译器它其实就是我们很多情况下在开发项目中必备的一个工具。因为我们一般的开发在PC,而PC很显然大家知道它的架构是在X86,但现在很显然,我们X86的程序跟ARM程序肯定是不兼容的,这样的话我们就需要一个交叉编译器来进行相应的编译开发,也就是说我们开发的程序不能开发成X86,也就是说X86里面的二进制程序下载到ARM开发板上ARM是不认识的,这样的话,程序是不能相通的,所以说以上就是我们需要安装交叉编译器的一个道理。
如图,安装交叉编译器也有两种方法,一般情况下是第一种就是芯片厂商已经给好的因为你买的芯片一般都有。第二种就是我们自己手动的去编译,自己动手编译交叉工具链,手动编译如果大家想做可以做但是它非常耗时间甚至来说遇到的问题可能会特别特别多,他有很多不兼容的问题这个他是需要一定的功底去调试非常麻烦,所以不推荐新手去做,甚至很多公司都不会自己贸然用编译器去做产品开发。
但是如果有兴趣的同学可以下去搜索一本书“The GNU Toolchain for ARM Target HOWTO”这其实也是一本官方手册,它会告诉你如何去做一个相关的工具链。当然这个工具念在制作的时候,他的方法比较单一,思路也比较单一,那唯一不好,就是他的版本之前依赖关系比较大,你可能会需要一些手动的去修改这些要做的比较多,所以对于编译原理你要知道的很清楚去找到问题所在点去编,所以相对而言比较耗时甚至编不通过很麻烦,所以这个部分一般情况下不建议使用。
更多时候我们用的是芯片厂商提供好的,那么芯片厂商提供的芯片,一般情况下有这么几种前缀名。
如图,就说在安装过后,都会有一些前缀名,我先简单的说一下它的意义。最常用的是这一种叫做 “arm-none-linux-gnueabi-”他的意思是第一例就是说目标题结构也就是说,这个工具目的是做什么的,比如说最典型的gcc什么都没写他默认就是编译X86,如果我这里有ARM说明他这个编译器编译的是ARM,记住这个编译器他其实是个集合,我只列出了前缀也就说他这个地方实际上隐含了一个概念叫gcc,也就是说相当于用了图上加黑的那个工具,只是说,我们现在更多的时候是用的一个前缀,我们后面还会讲到很多工具集,所以我们记这个前缀而不记后面的这个命令,前缀的第一个单词就是你最终生成的编译器和生成的体系结构,第二个就是厂商名一般如果你是开源的话就是none,比如说你是三星的话那就是ARM_sanxin,一般都是none。然后第三个linux也就是我们这个程序默认编译出来的功能是针对linux操作系统去用的,也就是这个好处在于这个编译器它的内部有一些标准C库而这个C库是跟linux的接口相关的,也就是这个软件编译出来的可执行程序不能在windows下运行的。 这样的话这个工具链就专门针对于Linux操作系统运行。而且是ARM下的Linux操作系统而不是Windows,然后后面的那个词gun大家都知道是开源的,而eabi指的是我们嵌入式的标准接口,它主要针对的是嵌入式精简的一些相关库所以说是eabi,还有一个是oabi是老的,我们现在基本上都是用的eabi的系统,主要由ARM的优化选项来决定的,这个我们到时候看到ARM优化的那一章内容可以去学习一下。
一般情况下“arm-none-linux-gnueabi-”这个名字太长了我们也不想去记所以就干脆把它简称为“arm-linux-”所以有些时候你经常会看到有些编译器中都做了一个小技巧,把“arm-linux-”和“arm-none-linux-gnueabi-”做了一个快捷方式,就是软链接,把两个名字关联起来,也就是输入其中一个名字就相当于在输入另一个名字。所以这一组就是比较常用的形式。
后面还可能会接触一些,比如“arm-none-eabi-”就是没有Linux的,没有Linux这款编辑器一般代表不能在有操作系统的ARM上运行,因为它不支持操作系统,所以这就是一款比较新的,而下面这个“rm-elf-”是非常老的,也是针对于无操作系统或者可能有操作系统也支持,只是这个是非常老的,很少能见到。主要见到的是前两种。
以上就是我们安装的工具链,其实工具链一般来说找厂商要就可以或者说你实在要不到也没关系,比如说下载安卓后其实它自动就把编译器给编译出来了,你可以直接把它拷出来用也可以。或者说你可以去找一个公司,这个公司现在已经被收购了,但是大家可以注意去查叫做“codesourcery”虽然它已经被收购,但是它其实也是提供交叉编译器的工具,大家如果有兴趣也可以去网上搜一下,然后去注册一下那个收购公司的账号下下来也可以。但是一般来说,用我们光盘已经配置好的编译器也问题不大。
其上就是我们说的第二个部分安装交叉编译器。
目标机传输通道
第三个部分:搭建主机,目标机传输通道。我们就要想办法确定连接方法后就要准备搭建传输通道的一些所具备的服务和客户。比如说我们用网络,最显然就涉及到服务配置。
如图,有服务器有客户端,这样来说网络就可以成功传输数据。所以说一般情况下在嵌入式中我们用的比较多的服务就两个,在Linux下一个叫TFTP一个叫NFS,TFTP顾名思义就是FTP的一个简版,它是基于UDP传输的,相当于它的协议比较简单。而NFS它的全名叫做网络文件系统,这个网络文件系统主要是Linux和Linux之间做挂载用的,那么这个用处应该是非常大,比如说我们在后面学文件系统的调试的时候,我们很多时候都会用NFS作为我们调试的一个基本应用工具,所以这也是我们在搭建开发环境中的第三步。
烧写测试
最后,都准备好后,剩下就是一件事情也是最难的一件事情,就是把我们之前那三个子系统的功能全部做好,假如我们已经有这样的功能我们就把它们相应的编译出来,编译后剩下的最后一步就是烧写测试,最后把它整个集成放到工厂然后就开始集成化生产。
以上,就是基本的移植步骤,而具体编译三大系统就是我们后面的课程需要掌握的内容,最后我们在每一个内容中都涉及到怎么去烧写它。
5、守护进程的编写步骤
什么是守护进程?
答:守护进程是后台运行的、系统启动是就存在的、不予任何终端关联的,用于处理一些系统级别任务的特殊进程。
实现思路:
实现一个守护进程,其实就是将普通进程按照上述特性改造为守护进程的过程。
需要注意的一点是,不同版本的 Unix 系统其实现机制不同,BSD 和 Linux 下的实现细节就不同。
根据上述的特性,我们便可以创建一个简单的守护进程,这里以 Linux 系统下从终端 Shell 来启动为例。
在此有必要说一下两个概念:会话和进程组。
可点击参考会话和进程组概念
进程都有父进程,父进程也有父进程,这就形成了一个以init进程为根的家族树。除此以外,进程还有其他层次关系:进程、进程组和会话。进程组和会话在进程之间形成了两级的层次:进程组是一组相关进程的集合,会话是一组相关进程组的集合。
这样说来,一个进程会有如下ID:
·PID:进程的唯一标识。对于多线程的进程而言,所有线程调用getpid函数会返回相同的值。
·PGID:进程组ID。每个进程都会有进程组ID,表示该进程所属的进程组。默认情况下新创建的进程会继承父进程的进程组ID。
·SID:会话ID。每个进程也都有会话ID。默认情况下,新创建的进程会继承父进程的会话ID。
前面提到过,新进程默认继承父进程的进程组ID和会话ID,如果都是默认情况的话,那么追根溯源可知,所有的进程应该有共同的进程组ID和会话ID。但是调用ps axjf可以看到,实际情况并非如此,系统中存在很多不同的会话,每个会话下也有不同的进程组。
为何会如此呢?
就像家族企业一样,如果从创业之初,所有家族成员都墨守成规,循规蹈矩,默认情况下,就只会有一个公司、一个部门。但是也有些“叛逆”的子弟,愿意为家族公司开疆拓土,愿意成立新的部门。这些新的部门就是新创建的进程组。如果有子弟“离经叛道”,甚至不愿意呆在家族公司里,他别开天地,另创了一个公司,那这个新公司就是新创建的会话组。由此可见,系统必须要有改变和设置进程组ID和会话ID的函数接口,否则,系统中只会存在一个会话、一个进程组。
进程组和会话是为了支持shell作业控制而引入的概念。
当有新的用户登录Linux时,登录进程会为这个用户创建一个会话。用户的登录shell就是会话的首进程。会话的首进程ID会作为整个会话的ID。会话是一个或多个进程组的集合,囊括了登录用户的所有活动。在登录shell时,用户可能会使用管道,让多个进程互相配合完成一项工作,这一组进程属于同一个进程组。
当用户通过SSH客户端工具(putty、xshell等)连入Linux时,与上述登录的情景是类似的。
通常,会话开始于用户登录,终止于用户退出,期间的所有进程都属于这个会话。一个会话一般包含一个会话首进程、一个前台进程组和一个后台进程组,控制终端可有可无;此外,前台进程组只有一个,后台进程组可以有多个,这些进程组共享一个控制终端。
前台进程组:
该进程组中的进程可以向终端设备进行读、写操作(属于该组的进程可以从终端获得输入)。该进程组的 ID 等于控制终端进程组 ID,通常据此来判断前台进程组。
后台进程组:
会话中除了会话首进程和前台进程组以外的所有进程,都属于后台进程组。该进程组中的进程只能向终端设备进行写操作。
下图为会话、进程组、进程和控制终端之间的关系(登录 shell 进程本身属于一个单独的进程组)。
想了解更多关于会话 Sessions 内容,可以认真读一下 apue 这本书。
如果调用进程非组长进程,那么就能创建一个新会话:
该进程变成新会话的首进程
该进程成为一个新进程组的组长进程
该进程没有控制终端,如果之前有,则会被中断(会话过程对控制终端的独占性)
也就是说:组长进程不能成为新会话首进程,新会话首进程必定成为组长进程。
1、fork()创建子进程,父进程exit()退出;
这是创建守护进程的第一步。由于守护进程是脱离控制终端的,完成这一步后就会在Shell终端里造成程序已经运行完毕的假象。之后的所有工作都在子进程中完成,而用户在Shell终端里则可以执行其他命令,从而在形式上做到了与控制终端的脱离,在后台工作。
由于父进程先于子进程退出,子进程就变为孤儿进程,并由 init 进程作为其父进程收养。
2、在子进程调用setsid()创建新会话;
在调用