Showing Posts From

Linux

Linux的OOM错误及其处理思路

对于OOM错误的简介和总结 OOM,Out Of Memory,顾名思义,也就是Linux系统在运行中因为物理内存耗尽所出现的问题。 在Linux系统的应用程序运行过程中,应用程序向Kernel不断申请内存,Kernel的内存管理子系统,会找到物理内存中的空闲页面标记为被占用状态,并且在该应用程序的页表中建立这个物理内存页面与应用程序虚拟空间所申请的虚拟内存页面之间的关联关系。当应用程序需要使用的内存过多,Linux的内存管理子系统已经找不到空闲的物理内存页来满足应用程序的内存申请要求时,就会发生这个系统运行内存不足的问题(OOM,Out Of Memory)。 对于系统运行内存不足的问题,一般有几种处理方式: - 利用SWAP分区。在系统的硬盘中存在一个独立的SWAP分区,运行内存不足且kernel已经无法再释放内存的情况下,把暂时不用的内存页交换到这个硬盘分区上,以腾出内存用作它用。后续如果要用到这个缓存的内存页,再从SWAP分区上交换回来。这就是我们在安装Linux系统时需要单独划分一个SWAP分区的原因所在。 - Linux Kernel内部有一个OOM Killer的机制,开启后在运行内存不足的情况下,通过一定的算法找到最不重要、耗费内存最多的进程,直接kil掉以确保系统运行的基本稳定性。 - 对于嵌入式系统等专用功能的系统,以上的SWAP分区和OOM killer都不适用,标准的处理方式往往就是直接系统崩溃报出OOM的错误。 SWAP分区 对于包含SWAP分区的PC以及服务器端的系统而言,当系统的物理内存不足的时候,Linux Kernel还会尝试把一部分不常用的内存页缓存到SWAP分区中,如果后续应用程序的运行要用到这个内存页的话,再从SWAP分区重新读入到内存中,这就是内存页面SWAP交换的过程。当然因为涉及到物理内存页与硬盘之间的数据拷贝,硬盘的读写是很慢的,所以一旦应用中运行中涉及到内存页的交换,应用程序的执行速度是很慢的。 但是对于嵌入式Linux系统而言,绝大多数的情况下是不支持SWAP分区的。因此在嵌入式系统中一旦出现OOM的错误,标准的处理流程就是进程Crash以及系统崩溃,所以OOM对于嵌入式Linux系统的开发而言是非常严重的错误,需要审慎处理。 - 实际上绝大多数的Linux嵌入式系统都有一定的实时性要求,如果嵌入式系统也支持SWAP分区的话,内存数据交换过程中在物理内存页和硬盘/Flash等介质之间来回拷贝数据,执行速度很难满足这个实时性要求。而且嵌入式Linux系统一般都是专用系统,其上执行的应用程序是非常固定的,所以只需要针对这些专用的应用程序的执行,评估其运行所需要的内存需求,选择合适的内存大小即可。 - 嵌入式Linux系统上不适用SWAP分区的另外一个原因则是,嵌入式系统上一般使用Flash作为存储介质,Flash的存储块的大小一般远远大于内存页的大小(4KB),而标准的SWAP分区是以内存页为单位进行交换的,这样就注定SWAP分区在Flash上的使用不会有理想的性能。 - 当然,要在嵌入式系统上开启SWAP也不是没有可能,不过嵌入式系统上所使用的SWAP是一种基于RAM的压缩文件系统zram。大致的逻辑是,提前分配一片内存作为压缩内存SWAP系统zram,在系统运行内存不足的情况下,找到使用率比较低的内存页,压缩后放入这个zram文件系统中,后续如果要用到这个内存页的话,再从zram中解压缩后释放出来。zram中保存的内存页经过一定的压缩算法处理,在内存的使用上更节约,通过这种方式达到内存交换的目的。 OOM Killer OOM Killer是Linux Kernel的一个特性,其主要的作用,就是在发生系统内部不足的情况下,Kernel通过一定的算法找到最应该kill的应用程序,把这个应该程序kill掉,然后释放出来内存,避免系统的崩溃。 因此OOM Killer的触发条件就是:在当前应用程序运行的情况下,系统已经用光了所有的物理内存;系统已经无法再释放内存来保证系统运行的稳定性。 在kernel的运行中,当OOM Killer特性开启的情况下,kernel会根据多个维度的统计数据(当前系统中的进程数量,各个进程所占用的内存空间大小,进程本身的重要性,剩余内存的多少,用户设置的oom_score_adj参数等)计算出来每个进程的oom_score。每个进程都有一个oom_score,可以通过cat /proc//oom_score命令来查看,各个进程的oom_score的分数介于0-1000之间,这个分数越高,在遇到内存不足的情况下,OOM killer会首先选择最高oom_score的进程,kill掉这个进程以后把内存释放出来。所以对于服务器和PC上运行的Linux系统而言,就是利用这个机制在系统内存负载居高不下已经影响到系统运行本身稳定性的情况下,强制kill进程来释放内存。 当然对于以上的OOM killer机制,用户也可以认为介入,设置进程的重要性,来避免OOM killer运行的时候误杀重要的进程,这就是通过oom_score_adj参数来进行调节: - 同样的,每个进程的/proc//目录下也都有一个oom_score_adj文件可以设置这个参数,如果给这个文件写入一个正值就相当于是提升这个进程被kill掉的可能性,例如echo 100 > /proc//oom_score_adj命令相当于提高这个进程的oom_score,在内存不足的情况下这个进程就更可能会被kill掉;而echo -100 > /proc//oom_score_adj命令则相当于降低这个进程的oom_score,在内存不足的情况下这个进程就更不太可能会被kill掉。 - 如果要让进程在任何时候都不会被OOM killer杀掉:echo -1000 > /proc//oom_score_adj - 而如果要选择一个优先级最低的进程,让OOM发生的时候首先被kill掉来释放内存,可以:echo 1000 > /proc//oom_score_adj 可以利用以下脚本来实时的监控系统中各个进程的oom_score和oom_score_adj: 对于OOM问题在嵌入式系统中的理解 如上所述,一般而言,对于嵌入式Linux系统而言,都会根据其应用程序的执行,提前在系统设计与评估阶段仔细的评估该应用所需要的内存大小,然后选择与之对应的系统硬件配置,所以只要系统设计阶段不出现特别大的问题,一般而言,系统硬件所提供的内存容量是足够应用程序运行所使用的。但是如果在应用程序的实现中,出现一些bug导致对内存的异常使用(最典型的就是内存泄漏,申请内存后没有及时释放,导致累计效应),在应用程序的运行中对于内存的消耗不断增加,直到系统中所有的内存耗尽从而导致OOM的错误,这一点是在嵌入式Linux系统开发中所需要面对的重点问题。 关于SWAP分区及其类似方案在嵌入式Linux系统中的应用,以上已有详细总结,SWAP分区本身并不适合嵌入式系统所使用的Flash存储介质,而zram同样会导致交换过程带来的低效率操作,难以满足嵌入式系统的实时性处理要求。 OOM Killer的设计用于在系统内存不足,存在崩溃风险的情况下,从当前系统选择消耗内存较多的非关键进程,kill掉以后来尝试恢复内存的占用状况保证系统运行的基本稳定性。这一点对于嵌入式系统而言同样不适用,因为嵌入式系统一般都是长期运行的专用应用,在系统启动后运行起来的进程都是关键进程,kill掉以后就没有办法提供基本的功能特性,这个时候即使系统本身还没有崩溃,整个产品的实用性已经大打折扣了。所以具体的实现上,不太可能依赖于OOM killer来选择进程kill掉保证系统运行的稳定性。 因此,对于嵌入式系统所面对的OOM问题来讲,关键仍然在于两点: - 系统设计和方案平台选型针对产品需求的合理性,需要确保硬件规格在满足产品需求的前提下,在内存的使用上有一定的裕量来确保系统运行的稳定性,而不是依赖于一些不可控的手段。 - 应用程序设计上一定要尽最大可能避免内存泄漏等因素导致的软件bug,对内存占用的持续积累,最终造成物理内存不足的OOM问题。当然极端情况下,也有部分产品在设计上在产品运行一段时间后,自动重启一次来尝试避免长期运行情况下出现的内存方面的问题。 参考文档: - Linux OOM (Out-of-memory) Killer. let me first explain what is OOM | by Raza | Medium

对于Signal 11 SIGSEGV错误的简单总结

SIGSEGV问题的简介以及原因总结 Signal 11,也就是SIGSEGV(Signal Segmentation Violation),是Unix/Linux系统在应用程序运行中,遇到内存访问错误的情况下,向应用程序所发出的错误信号。一般是应用程序代码在运行中尝试访问和读写非法内存地址时,由系统内核向应用程序发出这个信号。这类错误一般发生在可以通过指针等访问底层内存地址的C/C++语言代码中,发生后可能会导致数据污染、系统崩溃或者其他不可预计的行为,应该是嵌入式C/C++应用开发过程中最普遍,但是也最严重的问题。 导致SIGSEGV的原因一般是: - 引用空指针或者野指针。如果指针在引用前,既没有进行正确的初始化和设置,也没有做必要的检查,直接引用这些指针就会导致程序代码对内存的访问处于失控的状态,报出SIGSEGV的错误。 - Buffer溢出。无论是在堆空间、栈空间还是全局变量空间的Buffer或者队列,当访问这些buffer超出其地址范围时,会导致内存buffer溢出的问题。 - 内存访问权限问题。各个应用程序运行过程中所分配的内存区域,以及其对应的虚拟内存页,都因其类型的不同有着不同的访问权限,最典型的就是代码段内存空间是只读的,如果向只读类型的内容空间尝试写入时,也会发生SIGSEGV的错误。 - 栈溢出。如果在子函数的内部栈空间上申请大量的内存,子函数在执行的过程中对于其栈空间内存的使用超过其限制后,发生栈溢出的问题,也会导致SIGSEGV的问题。 从Linux Kernel的角度看待SIGSEGV信号 对于Linux系统中运行的应用程序而言,每个进程都对应于一个独立的虚拟地址空间,其所占用的内存按照类型来区分,包含有多个段,例如代码段、数据段、堆空间、栈空间等。各个段又按照4KB为单位,分为很多个虚拟内存页,每个虚拟内存页都有自己的地址和权限设置。这些虚拟地址页再通过MMU以及内核中维护的各个进程的页表来与物理内存页一一对应。每个应用程序进程在kernel中都有自己的页表,保存着自己的虚拟内存空间与实际占用的物理内存页之间的对应关系。 那么对于Linux Kernel而言,什么时候会向一个进程发出SIGSEGV信号呢? 当应用程序对某个虚拟内存地址发出读写访问的请求时,这个请求会转到内核空间,此时: - Kernel会查询这个进程对应的页表,寻找与请求虚拟地址对应的页表及其权限,如果请求的虚拟地址页包含在页表中,并且访问权限匹配正确,则kernel会按照应用程序的请求访问指定的内存地址。如果权限不一致,kernel就会向应用程序发出一个SIGSEGV信号,表示这个内存访问的权限出错。 - 如果Kernel在页表中找不到与应用程序指定虚拟地址的虚拟地址页,则会发生page fault的错误。针对page fault会存在三种情况,分别由page fault handler进行处理: - Major Page Fault,也称为硬缺页错误(hard page fault),要访问的虚拟内存地址在物理内存中没有对应的页帧,这个时候需要从硬盘等设备中载入(例如读写硬盘上的文件到内存,或者把硬盘上保存的cache交换到内存中),然后再由MMU建立物理内存页与虚拟内存页之间的映射关系。因为要从硬盘上读入文件,这个处理过程当然就比较慢,一般是异步操作。 - Minor Page Fault,也称为软缺页错误(soft page fault),是指要访问的内存页不在当前进程对应的虚拟地址空间中,但是在物理内存中,这个时候就只需要MMU在当前进程的虚拟地址空间与物理地址空间建立关联关系即可,无需从硬盘读取内容,这个过程的处理一般是多个进程之间共享一部分物理内存空间(例如共享动态库)。因此这个过程的响应速度会比较快,一般是同步操作。 - 无论是Major Page Fault还是Minor Page Fault都属于正常操作,kernel不会像应用程序上报异常的SIGSEGV信号。 - page fault还有一种就完全是无效的缺页错误了。最典型的就是用户进程访问的内存地址越界所导致的非法内存访问、对空地址的引用,此时Kernel就会向应用程序上报一个SIGSEGV的信号,默认行为是终止应用程序运行。 调试SIGSEGV问题的思路 1. 编译开启所有的warning配置 首先一点,就是应该在编译应用程序的时候,养成开启所有的warning message,并解决所有导致warning message的应用程序代码问题。因为编译器在编译的过程中,如果产生了warning message,极大概率这个警告消息背后隐藏着一定的bug,即使这个bug短期内没有发现明显的测试问题,在某些应用逻辑的运行下,就有概率会导致出现一些几率性的问题,非常难以定位和解决。所以养成习惯去自动开启所有的warning message,并且提前解决掉这些问题,可以防患于未然,提前把可能导致问题的bug解决掉。 例如当使用gcc进行编译的时候,可以通过-Wall以及-Wextra等编译选项开启所有的warning message,这些错误在实践中一定都相对比较好解决: !image.png 2. 静态扫描分析 在开发流程中导入静态分析工具的思路与以上在编译过程中开启warning配置的思路一致,都是尝试在软件代码运行之前,想办法通过扫描和编译静态代码的过程中提前侦测可能的问题,并提前解决之。只不过,代码的静态扫描和分析工具完全是对静态源代码本身进行扫描,是在编译流程之前完成的工作;而编译过程中开启warning配置,则是利用编译工具在编译代码的过程中同步进行的代码扫描和分析,两者大同小异。 目前无论是开源还是商业的代码静态扫描分析工具,均已经非常成熟。例如针对嵌入式C、C++应用代码的扫描,开源的cppcheck,商业软件的Coverity、Fortify等工具在实践中都得到了非常普遍的应用,确实是可以很高效的提升团队开发的代码质量,提前解决掉导致测试问题的代码缺陷。 因此,在开发实践中,从开发质量和效率的提升角度考虑,一定要积极的把静态分析工具的扫描及其扫码问题的解决流程导入到代码提交的开发流程中。 3. 使用Valgrind进行动态内存分析 不同于cppcheck和Coverity这类静态代码扫描和分析工具,Valgrind是一种动态分析工具,也就意味着,Valgrind是在应用程序的运行中对其内存的使用情况进行监控和分析的:Valgrind会跟待分析的应用程序一起运行,在后台监控待分析应用程序的内存使用情况,更准确的给出内存出错的位置和原因。 当然,Valgrind的作用不仅仅是对内存的使用情况进行动态跟踪和分析,还可以对程序进行各种性能分析、代码覆盖测试、堆栈分析及CPU的Cache命中率、丢失率分析等,是一个非常强大的开源工具。 利用Valgrind的memcheck可以实现对应用程序运行中的内存覆盖、内存泄漏、内存越界检测等方面的检测。 Valgrind在嵌入式系统调试中的使用步骤大致如下: - 首先需要编译Valgrind。 - 使用Valgrind加上memcheck选项启动应用程序的运行:valgrind —tool=memcheck a.out 但是,对于valgrind在嵌入式系统中的应用而言,存在比较大的实用性的问题,尤其是考虑到大部分的嵌入式系统在某些环节的处理上都有实时或者一定时序的严格要求。这是因为Valgrind的运行要实时的监控应用程序的内存使用情况,会导致应用程序的运行速度比不开启valgrind慢20-30倍,这一点在绝大多数嵌入式应用程序的调试中是不可接受的。 所以,valgrind更大的用于之地还是在PC、服务器这类资源比较充裕的环境中,如果要使用valgrind对嵌入式应用程序的内存进行分析,比较现实的做法是对软件的架构做更合理的设计和实现,可以让部分软件模块运行在PC中,然后在PC中利用valgrind来针对这些软件模块做独立的单元测试或者模块级别的测试。 4. 借助于coredump 当Linux的coredump功能开启的时候,应用程序运行中出现各种异常或者严重bug导致程序退出的时候,Linux系统会把该程序运行时的内存、寄存器状态、堆栈指针、内存管理信息、各种函数的堆栈调用信息保存到一个core文件中。对这个core文件进行分析和跟踪就可以大致定位出来导致内存问题的具体原因。 首先需要开启core dump功能:ulimit -c unlimited。 然后运行自己的应用程序,当应用程序的内存错误导致程序退出的时候,就会在当前目录下自动产生一个core文件。然后再用gdb来分析这个core文件,在gdb的交互式环境中,通过bt命令查看程序退出之前的函数调用栈的信息,就可以大致的定位出来导致这个内存错误的具体位置和原因。 5. 检查Kernel Log定位问题 当发生段错误的时候,内核同步会在其kernel log buffer里面写一条日志,通过对这个日志进行分析也可以得到一些有用的信息,基于这些信息可以初步定位出来发生段错误的原因和大致的参考位置。kernel的运行log一般保存在/var/log/syslog里面。 - 当然要kernel的运行log要自动记录再/var/log/syslog这个文件里面,需要在后台运行一个rsyslog的服务,才能看到并利用这个文件对问题进行分析处理。 下面是一个发生段错误以后在kernel log文件中看到的信息: !image.png - at :表示应用层代码在发生段错误时所尝试访问的虚拟内存地址。 - ip :错误发生时,应用层程序代码在内存中执行的位置。 - sp :错误发生时的应用程序堆栈指针。 - error  :错误发生时应用程序正在执行的内存操作类型。主要的类型包括以下: - 4:表示程序正在尝试读尚未分配的内存地址。 - 5:表示程序正在尝试读write-only的内存地址。 - 6:表示程序正在尝试写尚未分配的内存地址。 - 7:表示程序正在尝试写没有写权限的内存地址。 参考文档 - What is Signal 11 SIGSEGV Error? | phoenixNAP KB

基于Linux UAC gadget驱动实现Audio Control处理-Kernel层GET

基于对SigmaStar Webcam方案源代码的学习,通过三篇文章的篇幅,整体总结了SigmaStar提供的在基于Linux kernel的UAC gadget设备中增加audio control控制功能的完整工作流程: 1. 应用层 2. 内核层1-Audio Control Get类命令的处理流程 3. 内核层2-Audio Control Set类命令的处理流程 以下为内核层GET类命令的完整总结。 kernel部分UAC的实现主要用到三个文件:drivers/usb/gadget/function目录下的f_uac1.c和u_audio.c,和drivers/usb/gadget/legacy下的audio.c文件。 Audio control在kernel中的实现流程需要从f_uac1结构的定义开始进行分析: 以上使用Control Set Command字符串标准的这部分就是在Linux标准uac实现的基础上增加的,以提供对audio control功能的支持。 以上数据结构中增加了一个列表定义cs,用于保存该设备可以支持的unit基本的audio control命令队列: - uac规范把audio control command分为多个不同的unit,每个unit中又包含有多个command。所以每次device收到host发出的audio control命令以后,首先要找到这个命令归属于那个unit,然后在这个unit中再找到对应的command进行处理。 - 这里的cs保存的是unit的列表。 在f_uac1.c文件中f_audio_alloc函数的最后通过control_selector_init函数来实现两个队列的初始化,后续在接收到host的audio control命令后需要对这两个队列进行轮询。在下面的例子中,提供了一个包含在Feature Unit中的volume control音量控制功能的例子: 以上初始化中用到的capture_fu_controls和capture_volume_control结构的定义如下: 接下来开始分析Device端接收到Host的Audio Control命令的完整处理流程。 按照UAC规范的定义,Audio Control命令的处理逻辑与UVC的Video Control的处理类似,都是通过在端点0上发送的控制传输类型来进行实现的。 那么在f_uac1.c中,每次Device端收到端点0上发过来的控制传输类型的数据后,自动调用的回调函数是f_audio_setup: 所以对这个f_audio_setup函数执行流程的分析就是整个audio control命令处理的起点所在。 - 首先对控制传输结构数据中包含的BRequestType字段进行解析,针对它的方向(IN还是OUT)、目的类型(端点还是接口)分为四类调用不同的函数进行处理; - 因此audio control命令都是发给audio control interface的,所以对于Set类型的audio control,我们只需要关注audio_set_intf_req;Get类型只需要关注audio_get_intf_req的实现即可。 - audio_set_intf_req和audio_get_intf_req执行完成后,返回这个控制传输后面的data stage要传输的字节数。如果大于0的话,需要再调用usb_ep_queue进行data stage的收发处理。 - 例如对于上面的volume control的例子,GET类型的命令处理上,在这个阶段会把当前的volume value返回给Host;SET类型的命令处理上,则在这个阶段接收Host新设置的volume value数据。 Audio Control Get类型命令的处理接口audio_get_intf_req: - 注意:对于get类audio command的处理,实际上在找到对应的audio command并调用其con->get函数,得到它的当前设置值,并返回以后,整个流程实际上就结束了。这个函数返回后,在f_audio_setup中会把返回的当前设置值通过usb_ep_queue发回给Host。因此在这里注册的这个f_audio_complete回调实际上没有实际作用,根本没有执行有意义的处理流程。f_audio_complete实际上主要应用在Set类型audio control的处理上,可以参考后面对Set类型命令的处理流程。 Get类型命令的实际处理入口实际上就是上面在轮询中找到的con->get函数,也就是generic_get_cmd: - 所以说这个函数实际上只是返回这个audio control中包含的当前设置值而已。那么这个当前设置值在哪儿初始化呢?答案是在前面的control_selector_init中就对所有的audio control的参数初始值进行了初始化。并且在此后收到Set类型命令以后,也会对这个con->data进行赋值,确保其中包含的参数是最新的。 至此,整个Get类型的audio control命令的处理流程就完整的分析完了。需要注意的一点是:所有audio control的当前运行参数都是在内核中进行维护和管理的,因此get类型的命令,直接从内核中就可以得到这些参数并直接返回,不需要到应用层去获取这些数据。

基于Linux UAC gadget驱动实现Audio Control处理-Kernel层SET

基于对SigmaStar Webcam方案源代码的学习,通过三篇文章的篇幅,整体总结了SigmaStar提供的在基于Linux kernel的UAC gadget设备中增加audio control控制功能的完整工作流程: 1. 应用层 2. 内核层1-Audio Control Get类命令的处理流程 3. 内核层2-Audio Control Set类命令的处理流程 以下为内核层SET类命令的完整总结。 延续前面的Get类命令的分析。下面来继续分析Audio Control Set类型命令的处理接口audio_set_intf_req: - 整体的工作逻辑与Get类的处理基本类似。最大的不同就是通过轮询两个队列,找到对应的audio control以后,只是把这个audio control结构体赋值给uac1->set_con,并没有做具体的set操作。这个set操作需要在req->complete上注册的回调函数f_audio_complete中执行。 - 放到f_audio_complete中执行的原因是,set类型的命令需要收到data stage发过来的设置参数,而audio_set_intf_req执行的时候这个data stage还没发过来。所以只能在这里先记下来,然后等整个控制传输结束,这个时候会自动回调req->complete上注册的f_audio_complete函数。因此在f_audio_complete中处理实际的set命令比较合适。 因此Set类型命令的实际处理入口函数就是f_audio_complete: - 可以看到,上面最终执行的set功能仍然是audio control command自己的set函数。这个set函数在对应的audio control结构体的定义中已经设置好。例如可以查看前面的capture_volume_control变量定义的时候给set赋值的处理函数是generic_set_cmd。 generic_set_cmd的分析: - 前面的f_audio_alloc函数中定义了一个schedule work,对应的执行函数是mixer_cmd_work。因此这里的schedule_work(&uac1->cmd_work)实际上就是给kernel的调度器提交了一个执行mixer_cmd_work的执行任务。 通过调度器执行的mixer_cmd_work函数来负责audio control功能在应用层的生效处理,其执行分析如下: g_audio_notify利用linux声卡驱动的ALSA架构来实现与应用层之间的通信。在u_audio.c文件中定义: - 需要注意,上面的g_audio_notify的实现非常简略。实际上只提供了一个volume调整上传应用层的机制,如果需要增加其他audio control的控制,就要在以上代码的基础上增加对应的处理逻辑,可以通过id把不同的命令区分开来,告诉应用层究竟需要对哪个audio control进行控制。 那么问题来了,上面的g_audio->uac->volume_ctl究竟是什么?为什么snd_ctl_notify函数可以向应用层发出volume control变化的通知? 这是因为UAC的实现借助了Linux的ALSA网卡驱动框架。ALSA网卡驱动框架提供了一个kcontrol的机制来实现在应用层和kernel层进行音频参数控制功能。 每个音频参数的设置都对应一个kcontrol结构体,也就是所谓的struct snd_kcontrol,把这个结构体定义并填充好以后,注册到内容中。应用层就可以访问到kernel中注册的所有kcontrol的列表,需要控制哪个参数,就通过对应的kcontrol控制的接口来进行控制即可。 那么对于volume control这个kcontrol而言,其定义及其在kernel中的注册流程如下: - 所以如果还要增加新的audio control的控制类型,就需要像上面的volume control一样,先定义清楚这个新的audio control对应的snd_kcontrol_new结构体,然后调用snd_ctl_add把这个kcontrol注册到内核中。这样后续就可以在g_audio_notify中通过snd_ctl_notify函数向应用层发出audio control command了。

基于Linux UAC gadget驱动实现Audio Control处理-应用层

基于对SigmaStar Webcam方案源代码的学习,通过三篇文章的篇幅,整体总结了SigmaStar提供的在基于Linux kernel的UAC gadget设备中增加audio control控制功能的完整工作流程: 1. 应用层 2. 内核层1-Audio Control Get类命令的处理流程 3. 内核层2-Audio Control Set类命令的处理流程 以下为应用层部分的完整总结。详细解析了SigmaStar SDK中提供的在应用层通过ALSA KControl机制实现audio control命令的通知和功能实现。 Linux USB Gadget中的UAC部分与kernel部分的交互,实际上使用了Linux的ALSA音频驱动框架。这一点有点类似于在UVC Gadget中使用了v4l2摄像头驱动框架一样: - v4l2摄像头框架原本的设计是要实现一个内核中的摄像头驱动,以方便应用层以统一的流程和接口从kenel中读取摄像头的数据;而在UVC中使用v4l2的图像流的走向刚好与原本的v4l2框架相反,是把应用层的图像流通过v4l2机制发到内核,然后由内核调用USB底层驱动发给Host; - 在UAC上使用ALSA音频驱动框架的逻辑也是类似。ALSA原本的设计,是通过ALSA框架定义的标准,从kernel中读取麦克风的数据到应用层,向kernel中写入需要通过喇叭来播放的音频数据,以及通过kcontrol机制实现对音频硬件的控制;UAC中对于ALSA的使用刚好相反,是把应用层整理好的音频数据使用SNDRV_PCM_IOCTL_WRITEI_FRAMES ioctl code写入kernel,然后由kernel调用USB硬件发给Host,以及把USB Host发过来的音频数据通过使用SNDRV_PCM_IOCTL_READI_FRAMES ioctl code读取到应用层,再调用SDK应用层的API把这个声音从喇叭中播放出来,而USB Host所发过来的audio control类的命令则是通过kcontrol机制返回给应用层,由应用层做针对的处理。 应用层部分的实现逻辑 应用层实现audio control的大致流程,与ALSA声卡驱动框架的kcontrol一致: - 调用mixer_open函数打开audio control设备文件,以及通过这个设备文件读取kernel中支持kcontrol列表; - 调用mixer_subscribe_events函数开启内核audio control事件的订阅; - 循环执行: - 调用mixer_wait_event函数等待audio control设备文件是否有新的command通知上来; - 调用mixer_ctl_get_event和mixer_ctl_get_value获取事件详细信息及其参数; - 利用从内核设备文件中读取到的事件和参数,通过SigmaStar SDK应用层的MI_AI_SetVqeVolume接口设置新的音量参数。 mixer_open函数的解析: mixer_subscribe_events mixer_subscribe_events的实现比较简单,实际上就是通过ioctl code SNDRV_CTL_IOCTL_SUBSCRIBE_EVENTS开启或者关闭对内核ALSA control事件的订阅; Audio Control消息处理循环结构 - 通过在事件监听和处理循环中,通过mixer_ctl_get_event可以知道哪个audio control的参数发生了变化,然后通过mixer_ctl_get_value函数读取这个audio control在kernel中的当前设置值,然后就可以在应用层调用对应的audio control的功能函数,把Host发过来的audio control功能实现出来。 - 以上例子代码只是实现了一个volume control的例子。如果要增加其他的audio control功能,可以在mixer_ctl_get_value函数中进行判断和识别,究竟是哪个audio control的参数发生了变化,然后对应的在mixer_ctl_get_value函数中读取其内核中的设置值,并在应用层设置功能。

Linux的磁盘加密技术LUKS

Linux系统的磁盘加密技术LUKS简介 - LUKS:Linux Unified Key Setup。 - LUKS是Linux系统为硬盘分区进行加密操作提供的一个标准,能够通用于各种Linux系统发布版本中。 - 使用LUKS技术对磁盘分区进行加密后,必须要先输入密码对其进行解密后才能把这个加密分区挂载到文件系统中进行访问,没有密码的情况下无法挂载,也就限制了在没有访问密码的情况下,即使把硬盘拿下来也是没有办法访问硬件中原有的数据,这样就保证了硬盘数据的安全性。 - 在Linux系统中使用LUKS技术对磁盘分区进行加密需要提前安装cryptsetup软件包。 - 使用cryptsetup对分区进行了加密后,这个分区就不再允许直接挂载。LUKS也是一种基于device mapper 机制的加密方案。如果要使用这个分区,必须对这个分区做一个映射,映射到/dev/mapper这个目录里去,我们只能挂载这个映射才能使用。然而做映射的时候是需要输入解密密码的。 - 使用LUKS和cryptsetup工具对磁盘分区进行加密的特点: - 加密后不能直接进行挂载,必须先使用cryptsetup命令+输入访问密码把这个加密分区的设备文件映射到device mapper下,然后才能把device mapper的映射文件挂载到文件系统中访问; - 加密后硬盘丢失也不用担心数据被盗,没有访问密码的情况下,无法正常的挂载到新的系统中; - 加密后必须先输入密码进行device mapper映射才能挂载成功并正常使用。 - 可以使用cryptsetup工具对一个磁盘分区设置多达8个访问密钥,并且可以在后期使用命令灵活的管理增加以及删除访问密钥; cryptsetup的基本使用 cryptsetup命令的常用参数包括: - luksFormat:对磁盘分区设置密码 - cryptsetup luksFormat [device file path] - 执行中需要设置一个访问密码,作为以后访问这个磁盘分区的密钥; - luksOpen:建立加密分区设备文件到/dev/mapper下的映射文件 - cryptsetup luksOpen [device file path] [mapper file name] - 执行中需要输入磁盘分区的密钥; - 执行成功后会在/dev/mapper下创建一个以[mapper file name]命名的设备映射文件; - status:查看映射设备文件的状态 - cryptsetup status [mapper file name] - 查看加密分区的挂载状态以及元信息; - luksClose:关闭之前在/dev/mapper下的映射文件 - cryptsetup luksClose [mapper file name] - 执行前需要先把这个分区umount掉,执行 luksClose命令完成后会删除在device mapper下创建的映射文件; - luksAddKey:为磁盘分区增加一个访问密钥 - cryptsetup luksAddKey [device file path] - 最多可以为一个磁盘分区设置8个密钥; - 除了通过键盘输入字符串密钥以外,也可以指定一个磁盘上已经存在的文件为加密磁盘分区的访问密钥:sudo cryptsetup luksAddKey [device file path] [any other file path] - luksReoveKey:从磁盘分区已有的访问密钥列表中删除指定的访问密钥,此后就不能再用这个密钥访问文件系统了 - cryptsetup luksReoveKey [device file path] - 执行时输入需要删除的密钥 在VMware虚拟机上进行分区加密及自动挂载实验 - VMware版本:VMware Workstation 15 Player。 - 操作系统:Debian 10.0。 虚拟Linux系统中原有一块虚拟硬盘40GB,对应于/dev/sda,内部包含两个分区/dev/sda1和/sda2; 在VMware的当前虚拟机系统中通过选项界面再增加一块SCSI硬盘,大小设置为10GB: !Untitled.png 新建后启动虚拟机,可以在/dev下看到一个新的设备文件sdb,下一步计划在这个硬盘上创建LUKS加密分区。 1.在Debian上安装cryptsetup sudo apt-get install cryptsetup 2.使用fdisk工具对新磁盘进行分区 直接基于新创建的sdb硬盘创建一个主分区,对应于设备文件/dev/sdb1; !Untitled.png 3.使用cryptsetup工具对新建立的sdb1分区进行加密; !Untitled.png 4.在/dev/mapper下创建sdb1的映射磁盘分区 - crtpt_disk是随便取的,执行cryptsetup luksOpen命令成功后,就会在/dev/mapper下创建与之对应的磁盘分区映射文件,实际上是/dev/dm-0的映射文件; - 通过以上命令创建的这个/dev/mapper/crypt_disk文件在系统重启后会丢失; - _此后就可以像使用/dev/sda1一样的方式来使用/dev/mapper/crypt_disk了;_ 5.对加密分区进行格式化 此处使用ext4分区格式对/dev/mapper/crtpt_disk进行格式化: 6.挂载并开始使用加密分区 - 注意,对于这类加密分区而言,不能直接挂载其分区文件/dev/sdb1,而是要挂载前面通过cryptsetup luksOpen命令创建的映射文件/dev/mapper/crypt_disk; - 此后就可以在/mnt中使用这个分区进行数据的读写操作了; 7.开机自动加载并自动输入密码 - 按照以上流程设置后,开机过程中会自动创建/dev/sdb1的device mapper映射文件crypt_disk,在加载过程中自动输入密码,并自动挂载到挂载点/mnt下,此后就可以在/mnt下正常使用加密分区了; 参考资料: - 1-14 文件系统的特性与磁盘加密技术 - Linux下的磁盘加密LUKS - linux使用luks加密硬盘或u盘

Linux IPC机制之消息队列

消息队列基础 - Linux系统中的消息队列机制是SystemV IPC提供的三大进程间IPC机制之一:消息队列,信号量,共享内存; - Linux内核中会为每个IPC结构(包括消息队列、信号量和共享内存等)都使用一个非负整数的标识符来加以引用,而无论何时创建或者使用一个已经存在于内核中的IPC结构,都需要指定一个关键字(key_t),使用这个关键字对不同的IPC结构进行区分; - 消息队列中每一个消息都需要遵守系统的:消息最大长度限制MSGMAX,每个消息队列的总的字节数限制MSGMNB,系统上消息队列的总数限制MSGMNI。 - 可以进入/proc/sys/kernel/查看具体的限制数据; - 在Linux中可以使用ipcs命令来查看系统内核中存在的消息队列的状态以及统计数据; 消息队列的工作原理 从本质上讲,消息队列实际上就是位于内核空间的链表。应用空间定义和声明的一个消息队列实际上会对应内核空间的一组(多个)链表,用户层收发的每一条消息都对应于链表中的一个节点,具有相同消息类型的消息节点按照写入的顺序通过指针链接在一起,因此每一个消息类型都对应有一个链表。 应用层对于消息队列的发送和接收从本质上来讲,都是基于其中的某个链表,而非整个消息队列。 内核中消息队列的数据结构的原理如下图所示: !Untitled.png - 消息类型的ID用一个整数来表示,而且这个ID必须大于0,每个消息类型都有一个属于自己的链表; - 应用层发出和接收消息必须要指定一个消息类型ID,这样内核才能知道把这个消息放入哪个链表,或者从哪个链表上取出数据; - 消息类型ID为0的节点用于记录整个消息队列中所有消息加入消息队列的顺序,即上图用红色线条标注的链条; - 在应用中如果我们需要按照顺序读取整个消息队列中的所有消息,即要求消息队列中无论消息类型的ID是多少都返回的时候,就可以在读取的时候指定读取的消息类型ID为0,这样就会把整个消息队列的所有消息全部按照顺序返回; 消息队列的主要API ftok函数 - 用于基于参数生成一个消息队列的key,其原型为: - key_t ftok(const char *pathname, int proj_id); - 参数: - pathname:存在于文件系统的某个文件或者目录的路径名称,可以随意设置,但是这个路径名称对应的文件/目录必须存在; - proj_id:根据自己的约定随意设置,Unix系统中取值范围为0-255; - 功能:ftok函数根据参数传递的路径名,提取出该路径下文件inode在系统内的信息,根据这些文件信息与参数传递的project ID合成一个独一无二的消息队列key; msgget函数 - 用于创建/访问一个消息队列,其原型为: - int msgget(key_t key, int msgflag); - 参数: - key:消息队列的key,一般使用ftok()产生; - msgflag:有两个选项,IPC_CREAT和IPC_EXCL; - 单独使用IPC_CREAT:消息队列不存在的话就创建消息队列,存在的话就打开返回; - 使用IPC_CREAT | IPC_EXCL:消息队列不存在的话就创建消息队列,存在的话就出错返回; - 返回值: - 如果执行成功就返回一个非负整数,表示这个消息队列的ID,后续收发消息对需要引用这个消息队列ID;如果执行失败则返回-1; msgctl函数 - 用于控制和设置消息队列,其原型为: - int msgctl(int msqid, int cmd, struct msqid_ds *buf); - 参数: - msqid:由以上msgget函数执行返回的消息队列的ID; - cmd:有三个选项,IPC_STAT,IPC_SET,IPC_RMID; - IPC_STAT:获取消息队列的内部属性信息数据(uid,gid,字节数,消息数等),赋值给参数传递的msqid_ds结构中; - IPC_SET:使用参数传递的设置msqid_ds结构数据设置消息队列中可以设置的属性信息;(需要足够的权限,并且只能设置可设置的部分) - IPC_RMID:删除消息队列; - 返回值: - 执行成功返回0,失败返回-1; msgsnd函数: - 向消息队列发送一条消息,其原型为: - int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg); - 参数: - msqid:由以上msgget函数执行返回的消息队列的ID; - msgp:要发送的消息内容; - 消息内容的前sizeof(long int)个字节一定要是消息类型的ID,否则内核收到这个消息以后不知道应该放到哪个链表上进行处理; - msgsz:要发送的消息长度; - 这个消息长度应该减去消息类型ID的长度,即sizeof(long int); - msgflag: - 默认为0,表示消息队列满的时候阻塞等待; - 设置为IPC_NOWAIT,当消息队列满的时候直接返回错误; - 返回值: - 执行成功返回0,失败返回-1; - 注意事项: - 发出消息的大小必须要小于系统规定的上限,即MSGMAX参数; - 消息结构体必须要以一个long int长整型开始,内核根据消息的这个值来判断应该放入哪个链表,而消息的接收者也根据这个值来确定要接收的消息的类型; msgrcv函数 - 用于从消息队列中接收消息,其原型为: - ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg); - 参数: - msqid:由以上msgget函数执行返回的消息队列的ID; - msgp:接收到的消息头指针; - msgsz:接收到的消息的长度; - msgtyp:指定要接收的消息的消息类型; - msgtyp = 0:表示获取整个消息队列中的第一条消息,实际上反复的读取msgtyp = 0的消息,会把整个消息队列的消息按照顺序读取出来,不依赖于具体的消息类型; - msgtyp > 0:表示获取类型为 msgtyp 的第一条消息,除非指定了 msgflg 为MSG_EXCEPT,这表示获取除了 msgtyp 类型以外的第一条消息。 - 可以结合上面的消息队列链表结构来对以上内容进行理解。 - msgflag: - 默认为0,表示消息队列中没有指定消息类型的消息的时候,阻塞等待; - 设置为IPC_NOWAIT,当消息队列中没有指定消息类型的消息时直接返回错误; - 返回值: - 执行成功则返回实际接收到接收缓冲区里的字符个数,失败返回-1; 示例代码 这里设计一个比较典型的消息队列通信的案例:可以支持向同一个消息队列的不同消息类型链表发送不同消息内容的消息,并能够正确的接收和解析; - 创建一个消息队列,send进程向该详细队列中写入消息,recv进程从消息队列中读取消息; - send进程一次执行向消息队列的不同消息类型发送消息; - 相当于是向一个消息队列的多个链表分别发送消息; - send进程一次执行向消息队列的同一消息类型发送不同的消息内容; - 相当于是向一个消息队列的同一个消息链表写入类型不同的消息数据; msg.h: send.c: recv.c: 通过send进程发出消息后,读取消息类型为1的所有消息: 通过send进程发出消息后,读取整个消息队列中的所有消息(无论消息类型是多少): 对于Linux应用层的多模块应用程序中使用消息队列的一些思考 - 多模块应用程序一般从架构上可以分为一个主模块和多个子模块,应用启动过程中首先启动主模块,主模块根据配置或者启动参数决定启动各个子模块; - 主模块在退出时也需要负责对所有的消息队列做删除操作,避免下次启动造成状态混乱; - 每个模块都有自己的独立的消息队列,所有的消息队列在主模块启动时创建,各个子模块启动后,把主模块和自己的消息队列通过msgget挂接进来; - 所有的子模块只会与主模块之间通过消息队列直接通信,不会给其他模块直接发送消息,消息通过主模块来进行总体的管理和中转,以降低子模块之间的耦合; - 因此对于每个子模块而言,只需要挂接自己的消息队列和主模块的消息队列;而对于主模块,因为有可能与所有的子模块通信,因此需要创建/挂接自己和所有子模块的消息队列; - 所有的模块(包括主模块)都有一个专门用于接收和处理自己消息队列中信息的消息处理线程,在该线程中通过while循环+msgrcv阻塞接收的方式等待接收和处理发送给自己的消息,此处msgrcv函数应该使用msgtype=0的接收参数,这样就可以不加区分的接收和处理整个消息队列各个链表中的所有消息,收到消息后,再针对不同的消息类型做不同的处理逻辑; - 所有的模块(包括主模块)在需要向其他模块发出消息时只需要调用msgsnd发送消息即可,在消息类型参数msgtype可以指定不同的消息类型id,来对不同的消息内容进行区分; - 如以上例子代码所示,不同消息类型可以包含不同的消息数据结构,但总的来讲,因为消息队列本身有总的字节数的限制,因此如果有比较大的数据,不建议使用消息队列直接传输,进程之间可以配合共享内存来对大块的数据结构进行传输,线程之间则可以通过全局数据结构来进行消息数据的传递; 参考资料 - 【Linux】进程间通信之消息队列 - Linux系统编程—消息队列