浩淼56
浩淼56
逆天,从脚下开始
关注数: 18
粉丝数: 57
发帖数: 518
关注贴吧数: 26
力高澜湖咋样 没人吗?想问一下这个房子怎么样,对面是蓝光和万科, 他们家的物业怎样啊? 还有合肥有没有其他的楼盘,可以去看看他们的物业
Linux ALSA声卡驱动之一:ALSA架构简介 一. 概述 ALSA是Advanced Linux Sound Architecture 的缩写,目前已经成为了linux的主流音频体系结构,想了解更多的关于ALSA的这一开源项目的信息和知识,请查看以下网址:http://tieba.baidu.com/mo/q/checkurl?url=http%3A%2F%2Fwww.alsa-project.org%2F&urlrefer=68d083e769e876a29a860ad513abc48f。 在内核设备驱动层,ALSA提供了alsa-driver,同时在应用层,ALSA为我们提供了alsa-lib,应用程序只要调用alsa-lib提供的API,即可以完成对底层音频硬件的控制。图 1.1 alsa的软件体系结构 由图1.1可以看出,用户空间的alsa-lib对应用程序提供统一的API接口,这样可以隐藏了驱动层的实现细节,简化了应用程序的实现难度。内核空间中,alsa-soc其实是对alsa-driver的进一步封装,他针对嵌入式设备提供了一些列增强的功能。本系列博文仅对嵌入式系统中的alsa-driver和alsa-soc进行讨论。 二. ALSA设备文件结构 我们从alsa在linux中的设备文件结构开始我们的alsa之旅. 看看我的电脑中的alsa驱动的设备文件结构: $ cd /dev/snd $ ls -l crw-rw----+ 1 root audio 116, 8 2011-02-23 21:38 controlC0 crw-rw----+ 1 root audio 116, 4 2011-02-23 21:38 midiC0D0 crw-rw----+ 1 root audio 116, 7 2011-02-23 21:39 pcmC0D0c crw-rw----+ 1 root audio 116, 6 2011-02-23 21:56 pcmC0D0p crw-rw----+ 1 root audio 116, 5 2011-02-23 21:38 pcmC0D1p crw-rw----+ 1 root audio 116, 3 2011-02-23 21:38 seq crw-rw----+ 1 root audio 116, 2 2011-02-23 21:38 timer $ 我们可以看到以下设备文件: controlC0 --> 用于声卡的控制,例如通道选择,混音,麦克风的控制等 midiC0D0 --> 用于播放midi音频 pcmC0D0c --〉 用于录音的pcm设备 pcmC0D0p --〉 用于播放的pcm设备 seq --〉 音序器 timer --〉 定时器 其中,C0D0代表的是声卡0中的设备0,pcmC0D0c最后一个c代表capture,pcmC0D0p最后一个p代表playback,这些都是alsa-driver中的命名规则。从上面的列表可以看出,我的声卡下挂了6个设备,根据声卡的实际能力,驱动实际上可以挂上更多种类的设备,在include/sound/core.h中,定义了以下设备类型: [c-sharp] view plaincopy? #define SNDRV_DEV_TOPLEVEL ((__force snd_device_type_t) 0) #define SNDRV_DEV_CONTROL ((__force snd_device_type_t) 1) #define SNDRV_DEV_LOWLEVEL_PRE ((__force snd_device_type_t) 2) #define SNDRV_DEV_LOWLEVEL_NORMAL ((__force snd_device_type_t) 0x1000) #define SNDRV_DEV_PCM ((__force snd_device_type_t) 0x1001) #define SNDRV_DEV_RAWMIDI ((__force snd_device_type_t) 0x1002) #define SNDRV_DEV_TIMER ((__force snd_device_type_t) 0x1003) #define SNDRV_DEV_SEQUENCER ((__force snd_device_type_t) 0x1004) #define SNDRV_DEV_HWDEP ((__force snd_device_type_t) 0x1005) #define SNDRV_DEV_INFO ((__force snd_device_type_t) 0x1006) #define SNDRV_DEV_BUS ((__force snd_device_type_t) 0x1007) #define SNDRV_DEV_CODEC ((__force snd_device_type_t) 0x1008) #define SNDRV_DEV_JACK ((__force snd_device_type_t) 0x1009) #define SNDRV_DEV_LOWLEVEL ((__force snd_device_type_t) 0x2000) 通常,我们更关心的是pcm和control这两种设备。 三. 驱动的代码文件结构 在Linux2.6代码树中,Alsa的代码文件结构如下: sound /core /oss /seq /ioctl32 /include /drivers /i2c /synth /emux /pci /(cards) /isa /(cards) /arm /ppc /sparc /usb /pcmcia /(cards) /oss /soc /codecs core 该目录包含了ALSA驱动的中间层,它是整个ALSA驱动的核心部分 core/oss 包含模拟旧的OSS架构的PCM和Mixer模块 core/seq 有关音序器相关的代码 include ALSA驱动的公共头文件目录,该目录的头文件需要导出给用户空间的应用程序使用,通常,驱动模块私有的头文件不应放置在这里 drivers 放置一些与CPU、BUS架构无关的公用代码 i2c ALSA自己的I2C控制代码 pci pci声卡的顶层目录,子目录包含各种pci声卡的代码 isa isa声卡的顶层目录,子目录包含各种isa声卡的代码 soc 针对system-on-chip体系的中间层代码 soc/codecs 针对soc体系的各种codec的代码,与平台无关
硬件抽象层笔记汇总 Android的硬件抽象层,简单来说,就是对Linux内核驱动程序的封装,向上提供接口,屏蔽低层的实现细节。也就是说,把对硬件的支持分成了两层,一层放在用户空间(User Space),一层放在内核空间(Kernel Space),其中,硬件抽象层运行在用户空间,而Linux内核驱动程序运行在内核空间。访问路线:app——>框架(framework)——>外部库及runtime——>硬件抽象层——>内核 Android硬件抽象层程序开发过程:(Linux硬件驱动开发相似) 第一步:硬件抽象层模块开发: a 定义模块id b 定义设备id c 定义模块结构体 第一个成员变量必须是标准的hw_module_t 结构体,相当于是定义一个hw_module_t子类 d 定义设备结构体 第一个成员变量必须是hw_device_t结构体,相当于是定义一个hw_device_t的子类; e 定义符号HAL_MODULE_INFO_SYM,类型为自定义的寂寞开结构体 f 实现设备打开接口和关闭接口; g 实现设备访问接口(可选) 每一个硬件抽象层模块都使用结构体hw_module_t来描述。 每一个硬件设备都使用结构体hw_device_t来描述。hw_module_t (1)硬件抽象层中的每一个模块都必须自定义一个硬件抽象层模块结构体,而且它的第一个成员变量的类型必须为hw_module_t。 (2)硬件抽象层中的每一个模块都必须存在一个导出符号,来指向一个自定义的硬件抽象层模块结构体 HAL_MODULE_INFO_SYM,即“HMI”。 (3)结构体hw_module_t的成员变量tag的值必须设置为HARDWARE_MODULE_TAG,即设置为一个常量值('H'<<24 | 'W'<<16 | 'M'<<8 | 'T'),用来标志这是一个硬件抽象层模块结构体。 (4)结构体hw_module_t的成员变量dso用来保存加载硬件抽象层模块后得到的句柄值。每一个硬件抽象层模块都对应有一个动态链接库文件。加载硬件抽象层模块的过程实际上就是调用dlopen函数来加载与其对应的动态链接库文件的过程。在调用dlclose函数来卸载这个硬件抽象层模块时,要用到这个句柄值,因此,我们在加载时需要将它保存起来。 (5)结构体hw_module_t的成员变量methods定义了一个硬件抽象层模块的操作方法列表。 hw_module_methods_t结构体hw_module_methods_t只有一个成员变量,它是一个函数指针,用来打开硬件抽象层模块中的硬件设备。其中,参数module表示要打开的硬件设备所在的模块;参数ID表示要打开的硬件设备的ID;参数device是一个输出参数,用来描述一个已经打开的硬件设备。由于一个硬件抽象层模块可能会包含多个硬件设备,因此,在调用结构体hw_module_methods_t的成员变量open来打开一个硬件设备时,我们需要指定它的ID。hw_device_t (1)硬件抽象层模块中的每一个硬件设备都必须自定义一个硬件设备结构体,而且它的第一个成员变量的类型必须为hw_device_t。 (2)结构体hw_device_t的成员变量tag的值必须设置为HARDWARE_DEVICE_TAG,即设置为一个常量值('H'<<24 | 'W'<<16 | 'D'<<8 | 'T'),用来标志这是一个硬件抽象层中的硬件设备结构体。 (3)结构体hw_device_t的成员变量close是一直函数指针,它用来关闭一个硬件设备。 注:硬件抽象层中的硬件设备是由其所在的模块提供接口来打开的,而关闭则是由硬件设备自身提供接口来完成的。 第二步Android硬件访问服务开发: a 定义硬件访问接口IXXX 使用AIDL语言定义,编译后会生成一个IXXX_StubD类 b 实现硬件访问服务XXX 从IXXX_Stub类中继承,实现硬件访问接口IXXX,通过JNI访问硬件抽象成模块; c 实现硬件访问服务XXX的JNI接口 调用函数hw_get_module加载硬件抽象层模块,打开硬件设备; d 启动硬件访问服务 在system_server进程中穿件一个XXX实例,调用servermanager.addserver 接口将XXX实例注册到servermanager中。
对memcpy函数的改进 对memcpy函数的改进: 改进思想: 大部分认为memcpy是一个char到char的拷贝的循环,担心它的效率。实际上,memcpy是一个效率最高的内存拷贝函数,他不会那么傻,来做一个一个字节的内存拷贝,在地址不对齐的情况下,他是一个字节一个字节的拷,地址对齐以后,就会使用CPU字长来拷(和dma类似),32bit或64bit,还会根据cpu的类型来选择一些优化的指令来进行拷贝。总的来说,memcpy的实现是和CPU类型、操作系统、cLib相关的。毫无疑问,它是内存拷贝里效率最高的,请放心使用。 [cpp] view plain copy void *mymemcpy(void *dst,const void *src,size_t num) { assert((dst!=NULL)&&(src!=NULL)); int wordnum = num/4;//计算有多少个32位,按4字节拷贝 int slice = num%4;//剩余的按字节拷贝 int * pintsrc = (int *)src; int * pintdst = (int *)dst; while(wordnum--)*pintdst++ = *pintsrc++; while (slice--)*((char *)pintdst++) =*((char *)pintsrc++); return dst; }
华为上机题练习(自己写的代码,验证通过的) 明明想在学校中请一些同学一起做一项问卷调查,为了实验的客观性,他先用计算机生成了N个1到1000之间的随机整数(N≤1000),对于其中重复的数字,只保留一个,把其余相同的数去掉,不同的数对应着不同的学生的学号。然后再把这些数从小到大排序,按照排好的顺序去找同学做调查。请你协助明明完成“去重”与“排序”的工作。 Input Param n 输入随机数的个数 inputArray n个随机整数组成的数组 Return Value OutputArray 输出处理后的随机整数 注:测试用例保证输入参数的正确性,答题者无需验证。测试用例不止一组。 输入描述: 输入多行,先输入随机整数的个数,再输入相应个数的整数 输出描述: 返回多行,处理后的结果 #include <stdio.h> #include <stdlib.h> #define N 1000 void input(int *arr,int len) { int i=0; for(;i< len; i++) scanf("%d",&arr[i]); } void paixu(int *arr, int len) { int i=0; int j=0; int tmp=0; for(i=0; i<len; i++) for(j=i+1; j<len; j++) { if(arr[i] > arr[j]) { tmp = arr[i]; arr[i]=arr[j]; arr[j]=tmp; } } } void output(int *arr , int len) { int i=0; printf("%d\n",arr[i]); for(i=1; i<len; i++) { if(arr[i] != arr[i-1]) printf("%d\n",arr[i]); } } int main(void) { int len=0; int arr[N]= {0}; while(scanf("%d",&len)!= EOF) { input(arr,len); paixu(arr,len); output(arr,len); } return 0; }
中断下半部的的三种机制 中断下半部 断处理流程都会分为两部分:上半部分(tophalf)和下半部分(bottomhalf)。 1.中断可以随时的打断处理机对其他程序的执行,如果被打断的代码对系统很重要,那么此时中断处理程序的执行时间应该是越短越好。 2.通过上文我们知道,中断处理程序正在执行时,会屏蔽同条中断线上的中断请求;而更严重的是,如果设置了IRQF_DISABLED,那么该中断服务程序执行是会屏蔽所有其他的中断请求。那么此时应该让中断处理程序执行的越快越好。 这样划分是有一定原因的,因为我们必须有一个快速、异步而且简单的处理程序专门来负责对硬件的中断请求做出快速响应,与此同时也要完成那些对时间要求很严格的操作。而那些对时间要求相对宽松,其他的剩余工作则会在稍候的任意时间执行,也就是在所谓的下半部分去执行。 下半部可以通过多种机制来完成:小任务(tasklet),工作队列,软中断。不管是那种机制,它们均为下半部提供了一种执行机制,比上半部灵活多了。至于何时执行,则由内核负责。 如果该任务对时间比较敏感,将其放在上半部中执行。 如果该任务和硬件相关,一般放在上半部中执行。 如果该任务要保证不被其他中断打断,放在上半部中执行(因为这是系统关中断)。 其他不太紧急的任务,一般考虑在下半部执行。 tasklet tasklet(小任务)机制是中断处理下半部分最常用的一种方法,其使用也是非常简单的。正如在前文中你所知道的那样,一个使用tasklet的中断程序首先会通过执行中断处理程序来快速完成上半部分的工作,接着通过调用tasklet使得下半部分的工作得以完成。可以看到,下半部分被上半部分所调用,至于下半部分何时执行则属于内核的工作。 tasklet由tasklet_struct结构体来表示,每一个这样的结构体就表示一个tasklet。在<linux/interrupt.h>中可以看到如下的定义: tasklet_struct { structtasklet_struct *next; 链表中的下一个tasklet unsigned long state; 此刻tasklet的状态 TASKLET_STATE_SCHED(准备运行) TASKLET_STATE_RUN(正在运行) atomic_t count; count成员是一个引用计数器,只有当其值为0时候,tasklet才会被激活;否则被禁止,不能被执行。 void (*func)(unsigned long); 指向tasklet处理函数 unsigned long data; 处理函数的唯一参数为data }; 在使用tasklet前,必须首先创建一个tasklet_struct类型的变量。通常有两种方法:静态创建和动态创建。如同上半部分的中断处理程序一样,这个函数需要我们自己来实现。 void tasklet_handler(unsigned long data) 然后在小任务的调度来执行. 在上半部中通过调用tasklet,使得对时间要求宽松的那部分中断程序推后执行。 工作队列 工作队列(work queue)可以实现一些tasklet不能实现的工作,比如工作队列机制可以睡眠。这种差异的本质原因是,在工作队列机制中,将推后的工作交给一个称之为工作者线程(worker thread)的内核线程去完成。因此,在该机制中,当内核在执行中断的剩余工作时就处在进程上下文(process context)中。也就是说由工作队列所执行的中断代码会表现出进程的一些特性,最典型的就是可以重新调度甚至睡眠。 进程上下文:一般的进程运行在用户态,如果这个进程进行了系统调用,那么此时用户空间中的程序就进入了内核空间,并且称内核代表该进程运行于内核空间中。由于用户空间和内核空间具有不同的地址映射,并且用户空间的进程要传递很多变量、参数给内核,内核也要保存用户进程的一些寄存器、变量等,以便系统调用结束后回到用户空间继续执行。这样就产生了进程上下文。 所谓的进程上下文,就是一个进程在执行的时候,CPU的所有寄存器中的值、进程的状态以及堆栈中的内容。当内核需要切换到另一个进程时(上下文切换),它需要保存当前进程的所有状态,即保存当前进程的进程上下文,以便再次执行该进程时,能够恢复切换时的状态继续执行。上述所说的工作队列所要做的工作都交给工作者线程来处理,因此它可以表现出进程的一些特性,比如说可以睡眠等。 内核中通过下述结构体来表示一个具体的工作: struct work_struct { unsigned long pending;//这个工作是否正在等待处理 struct list_head entry;//链接所有工作的链表,形成工作队列 void (*func)(void *);//处理函数 void *data;//传递给处理函数的参数 void *wq_data;//内部使用数据 struct timer_list timer;//延迟的工作队列所用到的定时器 }; 而这些工作(结构体)链接成的链表就是所谓的工作队列。工作者线程会在被唤醒时执行链表上的所有工作,当一个工作被执行完毕后,相应的work_struct结构体也会被删除。当这个工作链表上没有工作时,工作线程就会休眠。 软中断 软中断 (softirq)是用软件方式模拟硬件中断的概念,实现宏观上的异步执行效果。softirq 是基本的下半部机制,需要互斥使用。只能使用一种软件中断,其优先级低于硬件中断的,但是高于普通的进程优先级. 软中断不会抢占另一个软中断,只有中断处理函数才能抢占一个软中断 每个注册的软中断占据数组的一项,因此,总共有NR_SOFTIRQS个注册的软中断。软中断的数目是在编译时静态决定的,不能动态更改。内核中软中断个数的限制是32个,但在当前内核中,只有9个。 一个注册的软中断必须被标记后,才能运行。这称之为触发,实质上就是将其标记为未决状态。通常,中断处理函数会触发一个软中断,然后返回。在合适的时间,软中断会执行。 软中断----“谁触发,谁执行”(Whomarks, who runs),也就是说,每个CPU都单独负责它所触发的软中断,互不干扰。 3 几种下半部机制的比较 Linux内核提供的几种下半部机制都用来推后执行你的工作,但是它们在使用上又有诸多差异,各自有不同的适用范围,使用时应该加以区分。 Linux 2.6内核提供的几种软中断机制都贯穿着“谁触发,谁执行”的思想,但是它们各自有不同的特点。softirq是整个软中断框架体系的核心,是最底层的一种机制,内核程序员很少直接使用它,大部分应用,我们只需要使用tasklet就行了。
4G虚拟内存分布详解 一、进程与内存 所有进程(执行的程序)都必须占用一定数量的内存,它或是用来存放从磁盘载入的程序代码,或是存放取自用户输入的数据等等。不过进程对这些内存的管理方式因内存用途不一而不尽相同,有些内存是事先静态分配和统一回收的,而有些却是按需要动态分配和回收的。对任何一个普通进程来讲,它都会涉及到5种不同的数据段; 代码段:代码段是用来存放可执行文件的操作指令,也就是说是它是可执行程序在内存中的镜像。代码段需要防止在运行时被非法修改,所以只准许读取操作,而不允许写入(修改)操作——它是不可写的。 数据段:数据段用来存放可执行文件中已初始化全局变量,换句话说就是存放程序静态分配的变量和全局变量。 BSS段:BSS段包含了程序中未初始化的全局变量,在内存中 bss段全部置零。 堆(heap):堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减) 栈:栈是用户存放程序临时创建的局部变量,也就是说我们函数括弧“{}”中定义的变量(但不包括static声明的变量,static意味着在数据段中存放变量)。除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。由于栈的先进先出特点,所以栈特别方便用来保存/恢复调用现场。从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。 上述几种内存区域中数据段、BSS和堆通常是被连续存储的——内存位置上是连续的,而代码段和栈往往会被独立存放。有趣的是,堆和栈两个区域关系很“暧昧”,他们一个向下“长”(i386体系结构中栈向下、堆向上),一个向上“长”,相对而生。但你不必担心他们会碰头,因为他们之间间隔很大
位操作技巧大全 20160710 位运算的运算分量只能是整型,位运算把运算对象看作是由二进制位组成的位串信息,按位完成指定的运算,得到位串信息的结果。 在计算机程序中,数据的位是可以操作的最小数据单位,理论上可以用“位运算”来完成所有的运算和操作。一般的位操作是用来控制硬件的,或者做数据变换使用,但是,灵活的位操作可以有效地提高程序运行的效率。C语言提供了位运算的功能,这使得C语言也能像汇编语言一样用来编写系统程序。 例如:9|5可写算式如下: 00001001|00000101 00001101 (十进制为13) 可见9|5=13 基本的位操作符有与、或、异或、取反、左移、右移这6种,它们的运算规则如下所示: 符号 描述 运算规则 & 与 两个位都为1时,结果才为1 | 或 两个位都为0时,结果才为0 ^ 异或 两个位相同为0,相异为1 ~ 取反 0变1,1变0 << 左移 各二进位全部左移若干位,高位丢弃,低位补0 >> 右移 各二进位全部右移若干位,对无符号数,高位补0,有符号数,各编译器处理方法不一样,有的补符号位(算术右移),有的补0(逻辑右移) 位运算等价 x+y (x|y)+(x&y) x-y (x|~y)-(~x&y) x^y (x|y)-(x&y) x|y (x&~y)+y x&y (~x|y)-~x x==y (x-y|y-x) x!=y x-y|y-x x< y (x-y)^((x^y)&((x-y)^x)) x< y (~x&y)|((~x|y)&(x-y))//无符号x,y比较 x<=y (~x|y)&((x^y)|~(y-x)) //无符号x,y比较 #define BIT(n) (0x01<<n) set_bit(n) { a |=BIT(n);} 位置1 clear_bit(n) {a &= ~BIT(n);} 位清0 判断奇偶 只要根据最未位是0还是1来决定,为0就是偶数,为1就是奇数。 因此可以用if (a& 1 == 0)代替if (a % 2 == 0)来判断a是不是偶数。 不使用第三变量的两数交换 void swap(int &a, int &b) { if (a != b) { a ^= b; b ^= a; a ^= b; } } 改变符号 变换符号就是正数变成负数,负数变成正数。可以利用求补码的方法(按位取反+1)来处理。 如对于-11和11,可以通过下面的变换方法将-11变成11 1111 0101(二进制) –取反-> 0000 1010(二进制) –加1-> 0000 1011(二进制) 同样可以这样的将11变成-11 0000 1011(二进制) –取反-> 0000 1010(二进制) –加1-> 1111 0101(二进制) 可以得到如下代码: int changeSign(int n) { return ~n + 1; } 取绝对值 对于任何数,与0异或都会保持不变,与-1即0xFFFFFFFF异或就相当于取反,因此,a与i异或后再减i(因为i为0或-1,所以减i即是要么加0要么加1)也可以得到绝对值因此可以得到如下代码: int abs(int n) { return (n ^ (n >> 31)) - (n >> 31); } abs( x ) { y=x>>31; return(x^y)-y;//也可写作 (x+y)^y } 高低位互换 给出一个32位的无符号整数。称这个二进制数的前16位为“高位”,后16位为“低位”。现在写一程序将它的高低位交换。例如,数0x1234ABCD用二进制表示为: 0001 0010 0011 0100 1010 1011 1100 1101 将它的高低位进行交换,我们得到了一个新的二进制数: 1010 1011 1100 1101 0001 0010 0011 0100 它即是0xABCD1234。 这个问题用位操作解决起来非常方便,设x=0x1234ABCD由于x为无符号数,右移时会执行逻辑右移即高位补0,因此x右移16位将得到0000 0000 0000 0000 0001 0010 0011 0100。而x左移8位将得到0000 00000000 0000 1010 1011 1100 1101。可以发现只要将x>>16与x<<16这两个数相或就可以得到结果。代码如下 int exchangeBits(unsigned int n) { return (n >> 16) | (n << 16); } 二进制逆序 我们知道如何对字符串求逆序,现在要求计算二进制的逆序,如数34520用二进制表示为: 10000110 11011000 00000000 00000000 逆序 00000000 00000000 00011011 01100001 即是十进制的7009。 回顾下字符串的逆序的方法,可以从字符串的首尾开始,依次交换两端的数据。在二进制逆序我们也可以用这种方法,但运用位操作的高低位交换来处理二进制逆序将会得到更简洁的方法。类似于归并排序的分组处理,可以通过下面4步得到32位数据的二进制逆序: 第一步:每2位为一组,组内高低位交换 00 00 00 00 00 00 00 00 10 00 01 10 11 01 10 00 → 00 00 00 0000 00 00 00 01 00 10 01 11 10 01 00 第二步:每4位为一组,组内高低位交换 0000 0000 0000 0000 0100 1001 1110 0100 → 0000 00000000 0000 0001 0110 1011 0001 第三步:每8位为一组,组内高低位交换 00000000 00000000 00010110 10110001 → 0000000000000000 01100001 00011011 第四步:每16位为一组,组内高低位交换 0000000000000000 0110000100011011 →0110000100011011 0000000000000000 对第一步,可以依次取出每2位作一组,再组内高低位交换,这样有点麻烦,下面介绍一种非常有技巧的方法。先分别取10000110 11011000的奇数位和偶数位,空位以下划线表示。 原 数 00000000 00000000 10000110 11011000 奇数位 0_0_0_0_0_0_0_0_ 1_0_0_1_ 1_0_1_0_ 偶数位 _0_0_0_ 0_0_0_0_ 0 _0_0_1_0 _1_1_0_0 将下划线用0填充,可得 原 数 00000000 00000000 10000110 11011000 奇数位 00000000 00000000 10000010 10001000 偶数位 00000000 00000000 00000100 01010000 再将奇数位右移一位,偶数位左移一位,此时将这两个数据相与即可以达到奇偶位上数据交换的效果了。 原 数 00000000 00000000 10000110 11011000 奇数位右移 00000000 00000000 01000011 01101100 偶数位左移 00000000 00000000 00001000 10100000 相或得到 00000000 00000000 01001000 11100100 可以看出,结果完全达到了奇偶位的数据交换,再来考虑代码的实现—— 取x的奇数位并将偶数位用0填充用代码实现就是x& 0xAAAAAAAA 取x的偶数位并将奇数位用0填充用代码实现就是x& 0×55555555 因此,第一步就用代码实现就是: x = ((x & 0xAAAAAAAA) >> 1) | ((x &0×55555555) << 1); 类似可以得到如下代码: int revertBits(unsigned int n) { n = ((n & 0xAAAAAAAA)>> 1 ) | ((n & 0x55555555) << 1); n = ((n & 0xCCCCCCCC)>> 2 ) | ((n & 0x33333333) << 2); n = ((n & 0xF0F0F0F0)>> 4 ) | ((n & 0x0F0F0F0F) << 4); n = ((n & 0xFF00FF00)>> 8 ) | ((n & 0x00FF00FF) << 8); n = ((n & 0xFFFF0000)>> 16 ) | ((n & 0x0000FFFF) << 16); return n; } 32位整数前导零的个数 int preZero(unsigned int n) { int count = 0; if (n == 0) return(32); if ((n >> 16) == 0) count = count + 16; n = n << 16; if ((n >> 24) == 0) count = count + 8; n =n << 8; if ((n >> 28) == 0) count = count + 4; n =n << 4; if ((n >> 30) == 0) count = count + 2; n =n << 2; if ((n >> 31) == 0) count = count + 1; n =n << 1; return count; } 二进制中1的个数 方法1: 考虑到n-1会把n的二进制表示中最低位的1置0并把其后的所有0置1,同时不改变此位置前的所有位,那么n&(n-1)即可消除这个最低位的1。这样便有了比顺序枚举所有位更快的算法:循环消除最低位的1,循环次数即所求1的个数。此算法的时间复杂度为O(n的二进制表示中的1的个数),最坏情况下的复杂度O(n的二进制表示的总位数)。 int count1(unsigned int n) { int count = 0; while(n) { n &= (n - 1); count++; } return count; }
【资料整理】C语言位运算总结 位操作基础 基本的位操作符有与、或、异或、取反、左移、右移这6种,它们的运算规则如下所示: 符号 描述 运算规则 & 与 两个位都为1时,结果才为1 | 或 两个位都为0时,结果才为0 ^ 异或 两个位相同为0,相异为1 ~ 取反 0变1,1变0 << 左移 各二进位全部左移若干位,高位丢弃,低位补0 >> 右移 各二进位全部右移若干位,对无符号数,高位补0,有符号数,各编译器处理方法不一样,有的补符号位(算术右移),有的补0(逻辑右移) 注意以下几点: 在这6种操作符,只有~取反是单目操作符,其它5种都是双目操作符。 位操作只能用于整形数据,其他类型进行位操作会被编译器报错。 对于移位操作,在微软的VC6.0和VS2008编译器都是采取算术称位即算术移位操作,算术移位是相对于逻辑移位,它们在左移操作中都一样,低位补0即可,但在右移中逻辑移位的高位补0而算术移位的高位是补符号位。如下面代码会输出-4和3。 位操作应用 位运算的简单应用 表达式 位运算等价 x+y (x|y)+(x&y) x-y (x|~y)-(~x&y) x^y (x|y)-(x&y) x|y (x&~y)+y x&y (~x|y)-~x x==y (x-y|y-x) x!=y x-y|y-x x< y (x-y)^((x^y)&((x-y)^x)) x< y (~x&y)|((~x|y)&(x-y)) //无符号x,y比较 x<=y (~x|y)&((x^y)|~(y-x)) //无符号x,y比较 判断奇偶 只要根据最未位是0还是1来决定,为0就是偶数,为1就是奇数。因此可以用if (a & 1 == 0)代替if (a % 2 == 0)来判断a是不是偶数。可以得到如下代码: 1 bool isEven(int n) { 2 if (n & 1) { 3 return true; 4 } else { 5 return false; 6 } 7 } 不使用第三变量的两数交换 1 void swap(int &a, int &b) { 2 3 if (a != b) { 4 a ^= b; 5 b ^= a; 6 a ^= b; 7 } 8 } 可以这样理解: 1)a^=b 即a=(a^b); 2)b^=a 即b=b^(a^b),由于^运算满足交换律,b^(a^b)=b^b^a。由于一个数和自己异或的结果为0并且任何数与0异或都会不变的,所以此时b被赋上了a的值。 3)a^=b 就是a=a^b,由于前面二步可知a=(a^b),b=a,所以a=a^b即a=(a^b)^a。故a会被赋上b的值。 再来个实例说明下以加深印象。int a = 13, b = 6; a的二进制为 13=8+4+1=1101(二进制) b的二进制为 6=4+2=110(二进制) 第一步 a^=b a = 1101 ^ 110 = 1011; 第二步 b^=a b = 110 ^ 1011 = 1101;即b=13 第三步 a^=b a = 1011 ^ 1101 = 110;即a=5 改变符号 变换符号就是正数变成负数,负数变成正数。可以利用求补码的方法(按位取反+1)来 处理。 如对于-11和11,可以通过下面的变换方法将-11变成11 1111 0101(二进制) –取反-> 0000 1010(二进制) –加1-> 0000 1011(二进制) 同样可以这样的将11变成-11 0000 1011(二进制) –取反-> 0000 1010(二进制) –加1-> 1111 0101(二进制) 可以得到如下代码: 1 int changeSign(int n) { 2 return ~n + 1; 3 } 取绝对值 对于任何数,与0异或都会保持不变,与-1即0xFFFFFFFF异或就相当于取反,因此,a与i异或后再减i(因为i为0或-1,所以减i即是要么加0要么加1)也可以得到绝对值因此可以得 到如下代码: 1 int abs(int n) { 2 return (n ^ (n >> 31)) - (n >> 31); 3 } 高低位互换 给出一个32位的无符号整数。称这个二进制数的前16位为“高位”,后16位为“低位”。现在写一程序将它的高低位交换。例如,数0x1234ABCD用二进制表示为: 0001 0010 0011 0100 1010 1011 1100 1101 将它的高低位进行交换,我们得到了一个新的二进制数: 1010 1011 1100 1101 0001 0010 0011 0100 它即是0xABCD1234。 这个问题用位操作解决起来非常方便,设x=0x1234ABCD由于x为无符号数,右移时会执行逻辑右移即高位补0,因此x右移16位将得到0000 0000 0000 0000 0001 0010 0011 0100。而x左移8位将得到0000 0000 0000 0000 1010 1011 1100 1101。可以发现只要将x>>16与x<<16这两个数相或就可以得到结果。代码如下 1 int exchangeBits(unsigned int n) { 2 return (n >> 16) | (n << 16); 3 } 二进制逆序 我们知道如何对字符串求逆序,现在要求计算二进制的逆序,如数34520用二进制表示为:10000110 11011000 00000000 00000000 将它逆序,我们得到了一个新的二进制数:00000000 00000000 00011011 01100001 它即是十进制的7009。 回顾下字符串的逆序的方法,可以从字符串的首尾开始,依次交换两端的数据。在二进制逆序我们也可以用这种方法,但运用位操作的高低位交换来处理二进制逆序将会得到更简洁的方法。类似于归并排序的分组处理,可以通过下面4步得到32位数据的二进制逆序: 第一步:每2位为一组,组内高低位交换 00 00 00 00 00 00 00 00 10 00 01 10 11 01 10 00 → 00 00 00 00 00 00 00 00 01 00 10 01 11 10 01 00 第二步:每4位为一组,组内高低位交换 0000 0000 0000 0000 0100 1001 1110 0100 → 0000 0000 0000 0000 0001 0110 1011 0001 第三步:每8位为一组,组内高低位交换 00000000 00000000 00010110 10110001 → 00000000 00000000 01100001 00011011 第四步:每16位为一组,组内高低位交换 0000000000000000 0110000100011011 → 0110000100011011 0000000000000000 对第一步,可以依次取出每2位作一组,再组内高低位交换,这样有点麻烦,下面介绍一种非常有技巧的 方法。先分别取10000110 11011000的奇数位和偶数位,空位以下划线表示。 原 数 00000000 00000000 10000110 11011000 奇数位 0_0_0_0_ 0_0_0_0_ 1_0_0_1_ 1_0_1_0_ 偶数位 _0_0_0_ 0 _0_0_0_ 0 _0_0_1_0 _1_1_0_0 将下划线用0填充,可得 原 数 00000000 00000000 10000110 11011000 奇数位 00000000 00000000 10000010 10001000 偶数位 00000000 00000000 00000100 01010000 再将奇数位右移一位,偶数位左移一位,此时将这两个数据相与即可以达到奇偶位上数据交换的效果了。 原 数 00000000 00000000 10000110 11011000 奇数位右移 00000000 00000000 01000011 01101100 偶数位左移 00000000 00000000 00001000 10100000 相或得到 00000000 00000000 01001000 11100100 可以看出,结果完全达到了奇偶位的数据交换,再来考虑代码的实现—— 取x的奇数位并将偶数位用0填充用代码实现就是x & 0xAAAAAAAA 取x的偶数位并将奇数位用0填充用代码实现就是x & 0×55555555 因此,第一步就用代码实现就是: x = ((x & 0xAAAAAAAA) >> 1) | ((x & 0×55555555) << 1); 类似可以得到如下代码: 1 int revertBits(unsigned int n) { 2 n = ((n & 0xAAAAAAAA) >> 1 ) | ((n & 0x55555555) << 1); 3 n = ((n & 0xCCCCCCCC) >> 2 ) | ((n & 0x33333333) << 2); 4 n = ((n & 0xF0F0F0F0) >> 4 ) | ((n & 0x0F0F0F0F) << 4); 5 n = ((n & 0xFF00FF00) >> 8 ) | ((n & 0x00FF00FF) << 8); 6 n = ((n & 0xFFFF0000) >> 16 ) | ((n & 0x0000FFFF) << 16); 7 8 return n; 9 } 32位整数前导零的个数 01 int preZero(unsigned int n) { 02 int count = 0; 03 04 if (n == 0) 05 return(32); 06 if ((n >> 16) == 0) 07 count = count + 16; n = n << 16; 08 if ((n >> 24) == 0) 09 count = count + 8; n = n << 8; 10 if ((n >> 28) == 0) 11 count = count + 4; n = n << 4; 12 if ((n >> 30) == 0) 13 count = count + 2; n = n << 2; 14 if ((n >> 31) == 0) 15 count = count + 1; n = n << 1; 16 17 return count; 18 } 二进制中1的个数 方法1: 考虑到n-1会把n的二进制表示中最低位的1置0并把其后的所有0置1,同时不改变此位置前的所有位,那么n&(n-1)即可消除这个最低位的1。这样便有了比顺序枚举所有位更快的算法:循环消除最低位的1,循环次数即所求1的个数。此算法的时间复杂度为O(n的二进制表示中的1的个数),最坏情况下的复杂度O(n的二进制表示的总位数)。 1 int count1(unsigned int n) { 2 int count = 0; 3 while(n) { 4 n &= (n - 1); 5 count++; 6 } 7 return count; 8 } 方法2: 通过下面四步来计算其二进制中1的个数二进制中1的个数。 第一步:每2位为一组,组内高低位相加 00 00 00 00 00 00 00 00 10 00 01 10 11 01 10 00 → 00 00 00 00 00 00 00 00 01 00 01 01 10 01 01 00 第二步:每4位为一组,组内高低位相加 0000 0000 0000 0000 0100 0101 1001 0100 → 0000 0000 0000 0000 0001 0010 0011 0001 第三步:每8位为一组,组内高低位相加 00000000 00000000 00010010 00110001 → 00000000 00000000 00000011 00000100 第四步:每16位为一组,组内高低位相加 0000000000000000 0000001100000100 → 0000000000000000 0000000000000111 代码如下: 1 int count1_2(unsigned int n) { 2 n = ((n & 0xAAAAAAAA) >> 1 ) + (n & 0x55555555); 3 n = ((n & 0xCCCCCCCC) >> 2 ) + (n & 0x33333333); 4 n = ((n & 0xF0F0F0F0) >> 4 ) + (n & 0x0F0F0F0F); 5 n = ((n & 0xFF00FF00) >> 8 ) + (n & 0x00FF00FF); 6 n = ((n & 0xFFFF0000) >> 16 ) + (n & 0x0000FFFF); 7 return n; 8 }
C51 位运算符 在对单处机进行编程的过程中,对位的操作是经常遇到的。C51对位的操控能力是非常强大的。从这一点上, 可以看出C不光具有高级语言的灵活性,又有低级语言贴近硬件的特点。这也是在各个领域中都可以看到C的重 原因。在这一节中将详细讲解C51中的位操作及其应用。 1、位运算符 C51提供了几种位操作符,如下表所示: 1)“按位与”运算符(&) 参加运算的两个数据,按二进位进行“与”运算。原则是全1为1,有0为0,即: 0&0=0; 0&1=0; 1&0=0; 1&1=1; 如下例: a=5&3; //a=(0b 0101) & (0b 0011) =0b 0001 =1 那么如果参加运算的两个数为负数,又该如何算呢?会以其补码形式表示的二进制数来 与运算。 a=-5&-3; //a=(0b 1011) & (0b1101) =0b 1001 =-7 在实际的应用中与操作经常被用于实现特定的功能: 1.清零 “按位与”通常被用来使变量中的某一位清零。如下例: a=0xfe; //a=0b 11111110 a=a&0x55; //使变量a的第1位、第3位、第5位、第7位清零 a= 0b 01010100 2.检测位 要知道一个变量中某一位是‘1’还是‘0’,可以使用与操作来实现。 a=0xf5; //a=0b 11110101 result=a&0x08; //检测a的第三位,result=0 3.保留变量的某一位 要屏蔽某一个变量的其它位,而保留某些位,也可以使用与操作来实现。 a=0x55; //a=0b 01010101 运算符 含义 运算符 含义 & 按位与 ~ 取反 | 按位或 << 左移 ^ 按位异或 >> 右移 a=a&0x0f; //将高四位清零,而保留低四位 a=0x05 2)“按位或”运算符(|) 参与或操作的两个位,只要有一个为‘1’,则结果为‘1’。即有‘1’为‘1’ ‘0’为‘0’。 0|0=0; 0|1=1; 1|0=1; 1|1=1; 例如: a=0x30|0x0f; //a=(0b00110000)|(0b00001111)=(0b00111111)=0x3f “按位或”运算最普遍的应用就是对一个变量的某些位置‘1’。如下例: a=0x00; //a=0b 00000000 a=a|0x7f; //将a的低7位置为1,a=0x7f 3)“异或”运算符(^) 异或运算符^又被称为XOR运算符。当参与运算的两个位相同(‘1’与‘1’或 与‘0’)时结果为‘0’。不同时为‘1’。即相同为0,不同为1。 0^0=0; 0^1=1; 1^0=1;1^1=0; 例如: a=0x55^0x3f; //a=(0b01010101)^(0b00111111)=(0b01101010)=0x6a 异或运算主要有以下几种应用: 1.翻转某一位 当一个位与‘1’作异或运算时结果就为此位翻转后的值。如下例: 当一个位与‘1’作异或运算时结果就为此位翻转后的值。如下例: a=0x35; //a=0b00110101 a=a^0x0f; //a=0b00111010 a的低四位翻转 关于异或的这一作用,有一个典型的应用,即取浮点的相反数,具体的实现 下: f=1.23; //f为浮点型变量 值为1.23 f=f*-1; //f乘以-1,实现取其相反数,要进行一次乘运算 f=1.23; ((unsigned char *)&f)[0]^=0x80; //将浮点数f的符号位进行翻转实现取相反数 2.保留原值 当一个位与‘0’作异或运算时,结果就为此位的值。如下例: a=0xff; //a=0b11111111 a=a^0x0f; //a=0b11110000 与0x0f作异或,高四位不变,低四位翻转 3.交换两个变量的值,而不用临时变量 要交换两个变量的值,传统的方法都需要一个临时变量。实现如下: void swap(unsigned char *pa,unsigned char *pb) { unsigned char temp=*pa;//定义临时变量,将pa指向的变量值赋给它 *pa=*pb; *pb=temp; //变量值对调 } 而使用异或的方法来实现,就可以不用临时变量,如下: void swap_xor(unsigned char *pa,unsigned char *pb) { *pa=*pa^*pb; *pb=*pa^*pb; *pa=*pa^*pb; //采用异或实现变量对调 } 从上例中可以看到异或运算在开发中是非常实用和神奇的。 4)“取反”运算符(~) 与其它运算符不同,“取反”运算符为单目运算符,即它的操作数只有一个 它的功能就是对操作数按位取反。也就是是‘1’得‘0’,是‘0’得‘1’。 ~1=0; ~0=1; 如下例: a=0xff; //a=0b11111111 a=~a; //a=0b00000000 1.对小于0的有符号整型变量取相反数 d=-1; //d为有符号整型变量,赋值为-1,内存表示为0b 11111111 11111111 d=~d+1; //取d的相反数,d=1,内存表示0b 00000000 00000001 此例运用了负整型数在内存以补码方式来存储的这一原理来实现的。负数的补码方式是这样 的:负数的绝对值的内存表示取反加1,就为此负数的内存表示。如-23如果为八位有 符号整型数,则其绝对值23的内存表示为0b00010111,对其取反则为0b11101000 再加1为0b11101001,即为0XE9,与Keil仿真结果是相吻合的: 2.增强可移植性 关于“增强可移植性”用以下实例来讲解: 假如在一种单片机中unsigned char类型是八个位(1个字节),那么一个此 型的变量a=0x67,对其最低位清零。则可以用以下方法: a=0x67; //a=0b 0110 0111 a=a&0xfe; //a=0b 0110 0110 上面的程序似乎没有什么问题,使用0xfe这一因子就可以实现一个unsigned char型的变量最低位清 零。但如果在另一种单片机中的unsigned char类型被定义为16个位(两个字节), 那么这种方法就会出错,如下: b=0x6767; //假设b为另一种单片机中的unsigned char 类型变量,值为0b 0110 0111 0110 0111 b=b&0xfe; //如果此时因子仍为0xfe的话,则结果就为0b 0000 0000 0110 0110 0x0066,而与0x6766不相吻合 上例中的问题就是因为不同环境中的数据类型差异所造成的,即程序的可移植性不好。对于这种 况可以采用如下方法来解决: a=0x67; //a=0b 0110 0111 a=a&~1; //在不同的环境中~1将自动匹配运算因子,实现最后一位清零 a=0x66 其中~1为 0b 11111110 b=0x6767; //a=0b 0110 0111 0110 0111 b=a&~1; //~1=0b 1111 1111 1111 1110,b=0b 0110 0111 0110 0110 ,即0x6766 5)左移运算符(<<) 左移运算符用来将一个数的各位全部向左移若干位。如: a=a<<2 表示将a的各位左移2位,右边补0。如果a=34(0x22或0b00100010),左移2位得0b10001000,即十 的136。高位在左移后溢出,不起作用。 从上例可以看到,a被左移2位后,由34变为了136,是原来的4倍。而如果左 移1位,就为0b01000100,即十进制的68,是原来的2倍,很显然,左移N位,就等 乘以了2N。但一结论只适用于左移时被溢出的高位中不包含‘1’的情况。比如 a=64; //a=0b 0100 0000 a=a<<2; //a=0b 0000 0000 其实可以这样来想,a为unsigned char型变量,值为64,左移2位后等于乘以了4,即64X4=256, 种类型的变量在表达256时,就成为了0x00,产生了一个进位,即溢出了一个 ‘1’。 在作乘以2N这种操作时,如果使用左移,将比用乘法快得多。因此在程序中 适应的使用左移,可以提高程序的运行效率。 6)右移运算符 右移与左移相类似,只是位移的方向不同。如: a=a>>1 表示将a的各位向右移动1位。与左移相对应的,左移一位就相当于除以2,右移N位,就相当于除以 2N。 在右移的过程中,要注意的一个地方就是符号位问题。对于无符号数右移时 边高位移和‘0’。对于有符号数来说,如果原来符号位为‘0’,则左边高位为 入‘0’,而如果符号位为‘1’,则左边移入‘0’还是‘1’就要看实际的编译 了,移入‘0’的称为“逻辑右移”,移入‘1’的称为“算术右移”。Keil中采用 “算术右移”的方式来进行编译。如下: d=-32; //d为有符号整型变量,值为-32,内存表示为0b 11100000 d=d>>1;//右移一位 d为 0b 11110000 即-16,Keil采用"算术逻辑"进行编译 7)位运算赋值运算符 在对一个变量进行了位操作中,要将其结果再赋给该变量,就可以使用位 算赋值运算符。位运算赋值运算符如下: &=, |=,^=,~=,<<=, >>= 例如:a&=b相当于a=a&b,a>>=2相当于a>>=2。 8)不同长度的数据进行位运算 如果参与运算的两个数据的长度不同时,如a为char型,b为int型,则编译 将二者按右端补齐。如果a为正数,则会在左边补满‘0’。若a为负数,左边补满 ‘1’。如果a为无符号整型,则左边会添满‘0’。 a=0x00; //a=0b 00000000 d=0xffff; //d=0b 11111111 11111111 d&=a; //a为无符号型,左边添0,补齐为0b 00000000 00000000,d=0b 00000000 00000000
c语言位操作的一些注意事项 c语言位操作的一些注意事项 1. 位操作尽量使用unsigned char,而不是char,否则会使你混乱 如果你使用char,那么一个普通的字符,0xe3,因为首位是1,所以当他被转换为16位长时,成了0xffffffe3,而不是我们想要的0x000000e3,因为他是一个有符号的负数。 举例如下: #include <stdlib.h> #include <stdio.h> int main() { // char buf[10] = {0}; unsigned char buf[10] = {0}; char sbuf[10] = {0}; buf[0] = 0xe3; buf[1] = 0xb4; sbuf[0] = 0xe3; sbuf[1] = 0xb4; unsigned short pid1, pid2, pid3; /*bit operations with unsigned chars*/ printf("bit operations with unsigned chars:\n"); pid1= (buf[0]&0x1f); pid2= ((buf[0]&0x1f)<<8); pid3= ((buf[0]&0x1f)<<8)|buf[1]; printf( "pid1 = %x\n", pid1 ); printf( "pid2 = %2x\n", pid2 ); printf( "pid3 = %2x\n", pid3 ); /*bit operations with signed chars*/ printf("bit operations with signed chars:\n"); pid1= (sbuf[0]&0x1f); pid2= ((sbuf[0]&0x1f)<<8); pid3= ((sbuf[0]&0x1f)<<8)|sbuf[1]; printf( "pid1 = %x\n", pid1 ); printf( "pid2 = %2x\n", pid2 ); printf( "pid3 = %2x\n", pid3 ); } 结果如下: [shaoting@serverbj6:/user/shaoting/DVB-T]$ ./a.out bit operations with unsigned chars: pid1 = 3 pid2 = 300 pid3 = 3b4 bit operations with signed chars: pid1 = 3 pid2 = 300 pid3 = ffb4 可见,pid3的两次取值,因为一个是针对unsigned char的buffer,另一个是针对char的buffer而使结果不同。 2. 每次操作最好用括号括起来,不要随意猜想其算术优先级 位操作的优先级比算数运算优先级低,如果记不清楚,就将其括起来,不要想当然,例子: #include <stdlib.h> #include <stdio.h> int main() { unsigned char buf[10] = {0}; buf[0] = 0xf0; buf[1] = 0x03; unsigned short pid3, pid4; pid3= 5+ ((buf[0]&0x0f)<<8)|buf[1]; pid4 =5+ (((buf[0]&0x0f)<<8)|buf[1]); printf( "pid3 = %2x\n", pid3 ); printf( "pid4 = %2x\n", pid4 ); } 结果: [shaoting@serverbj6:/user/shaoting/DVB-T]$ ./a.out pid3 = 7 pid4 = 8 可见,我们以为pid3和pid4结果应该是一样的,都是8,但我们错了,pid3的计算结果其实是等于 (5+ ((buf[0]&0x0f)<<8))|buf[1],即先进行了加法计算,在进行了位与计算。
OpenCV函数学习之cvAbsDiff 函数名:cvAbsDiff 功能: calculates absolute difference between two arrays. 用法:void cvAbsDiff(const CvArr* src1, const CvArr* src2, CvArr* dst); 说明:src1 The first source array src2 The second source array dst The destination array dst(i)c = |src1(I)c − src2(I)c | All the arrays must have the same data type and the same size (or ROI size). 它可以把两幅图的差的绝对值输出到另一幅图上面来。在QQ游戏里面有一款叫做"我们来找茬",就是要找两幅图的不同点,下面是代码实现: [cpp] view plain copy print? #include<stdlib.h> #include <stdio.h> #include <cv.h> #include <highgui.h> int main(int argc, char*argv[]) { IplImage* img1, *img2,*img3; if(argc<3){ printf("Usage: main <image-file-name>\n\7"); exit(0); } img1=cvLoadImage(argv[1]); img2=cvLoadImage(argv[2]); if(!img1||!img2){ printf("Could not load image file\n"); exit(0); } img3 = cvCreateImage(cvGetSize(img1),img1->depth,img1->nChannels); cvAbsDiff(img1,img2,img3); cvNamedWindow("img1", CV_WINDOW_AUTOSIZE); cvNamedWindow("img2", CV_WINDOW_AUTOSIZE); cvNamedWindow("img3", CV_WINDOW_AUTOSIZE); cvShowImage("img1", img1 ); cvShowImage("img2", img2 ); cvShowImage("img3", img3 ); cvWaitKey(0); cvReleaseImage(&img1); cvReleaseImage(&img2); cvReleaseImage(&img3); return0; }
减去背景 减去背景是通过两幅图像代数相减 //减去背景 int main() { IplImage* pFrame = NULL; IplImage* pFrImg = NULL; IplImage* pBkImg = NULL; CvMat* pFrameMat = NULL; CvMat* pFrMat = NULL; CvMat* pBkMat = NULL; CvCapture* pCapture = NULL; int nFrmNum = 0; //创建窗口 cvNamedWindow("video", 1); cvNamedWindow("background",1); cvNamedWindow("foreground",1); pCapture = cvCaptureFromFile("media.avi"); while(pFrame = cvQueryFrame( pCapture )) { nFrmNum++; //如果是第一帧,需要申请内存,并初始化 if(nFrmNum == 1) { pBkImg = cvCreateImage(cvSize(pFrame->width, pFrame->height), IPL_DEPTH_8U,1); pFrImg = cvCreateImage(cvSize(pFrame->width, pFrame->height), IPL_DEPTH_8U,1); pBkMat = cvCreateMat(pFrame->height, pFrame->width, CV_32FC1); pFrMat = cvCreateMat(pFrame->height, pFrame->width, CV_32FC1); pFrameMat = cvCreateMat(pFrame->height, pFrame->width, CV_32FC1); //转化成单通道图像再处理 cvCvtColor(pFrame, pBkImg, CV_BGR2GRAY); cvCvtColor(pFrame, pFrImg, CV_BGR2GRAY); cvConvert(pFrImg, pFrameMat); cvConvert(pFrImg, pFrMat); cvConvert(pFrImg, pBkMat); } else { cvCvtColor(pFrame, pFrImg, CV_BGR2GRAY); cvConvert(pFrImg, pFrameMat); //当前帧跟背景图相减 cvAbsDiff(pFrameMat, pBkMat, pFrMat); //二值化前景图 cvThreshold(pFrMat, pFrImg, 60, 255.0, CV_THRESH_BINARY); //更新背景 cvRunningAvg(pFrameMat, pBkMat, 0.003, 0); //将背景转化为图像格式,用以显示 cvConvert(pBkMat, pBkImg); cvShowImage("video", pFrame); cvShowImage("background", pBkImg); cvShowImage("foreground", pFrImg); if( cvWaitKey(2) >= 0 ) break; } } cvDestroyWindow("video"); cvDestroyWindow("background"); cvDestroyWindow("foreground"); cvReleaseImage(&pFrImg); cvReleaseImage(&pBkImg); cvReleaseMat(&pFrameMat); cvReleaseMat(&pFrMat); cvReleaseMat(&pBkMat); cvReleaseCapture(&pCapture); return 0; }
使用OpenCV识别直线和圆 过程基本分四步或者三步走。第一步,加载图片。第二步,灰度化图片。第三步,Canny边缘化,第四步,检测直线或者圆。 首先,说明下OpenCv表示图片的结构是IplImage。这个东西代表了很多有用的东西。接下来的几步,都有该结构的指针对应图片。 第一步, 加载图 片。可以直接加载成灰度图,或者加载成彩色图,或者加载为原有图片的格式,第二步再灰度化。 直接加载为灰度图,IplImage* imgZero = cvLoadImage(szZero, CV_LOAD_IMAGE_GRAYSCALE);加载为彩色图,则把cvLoadImage函数的第二个参数改为CV_LOAD_IMAGE_COLOR,改为CV_LOAD_IMAGE_ANYCOLOR则保持图片原有格式。 第二步,如果第一步不是按灰度图加载,那么在这一步需要 灰度化 。代码如下,IplImage *imgGray = NULL;imgGray = cvCreateImage(cvGetSize(imgZero ),IPL_DEPTH_8U,1);cvCvtColor(imgZero, imgGray ,CV_BGR2GRAY);这几句代码的意思是根据原图片大小创建单通道位深为8的图片,然后把原有图片转换为创建的单通道8位深灰度图片。 第三步, canny处理 。代码如下,IplImage *imgCanny = cvCreateImage(cvGetSize(imgZero ),IPL_DEPTH_8U,1);cvCanny(imgGray , imgCanny, 50, 100);这几句代码的意思是根据原图片大小创建单通道位深为8的图片,然后对灰度图片进行canny处理,处理结果存储在新建立的imgCanny图片中。 第四步,使用识别函数进行 识别 直线或者圆。识别直线的函数是cvHoughLines2。识别圆的函数是cvHoughCircles。进行识别之前,先创建存储区。CvMemStorage *storage = cvCreateMemStorage(0); 识别直线的代码如下, cvClearMemStorage(storage); CvPoint* line; CvSeq* lines; lines = cvHoughLines2(imgCanny, storage, CV_HOUGH_PROBABILISTIC, 1, CV_PI/180, 50, 50, 10 ); line = (CvPoint*)cvGetSeqElem(lines, 0); cvLine(imgZero, line[0], line[1], CV_RGB(255,0,0), 3, CV_AA, 0 ); cout<<"端点1:"<< line[0].x << "," << line[0].y<<endl; cout<<"端点2:"<< line[1].y << "," << line[1].y<<endl; pts[1].fX = line[0].x; pts[1].fY = line[0].y; 识别圆的代码如下, cvClearMemStorage(storage); CvSeq * cir=NULL; cir = cvHoughCircles(imgCanny, storage, CV_HOUGH_GRADIENT, 1, imgRecog->width/10 ,80,40, 50); float * p=(float *)cvGetSeqElem(cir, 0); CvPoint pt = cvPoint(cvRound(p[0]),cvRound(p[1])); cvCircle(imgRecog,pt,cvRound(p[2]),CV_RGB(0,255,0)); cvCircle(imgRecog, pt, 5, CV_RGB(0,255,0), -1, 8, 0 );//绘制圆心 cout<<"圆心:"<<pt.x<<","<<pt.y<<endl; cout<<"半径:"<<p[2]<<endl; pts[0].fX = pt.x; pts[0].fY = pt.y; fR = p[2]; 下面再提供一个识别圆和直线的程序的完整代码。识别直线和圆的参数设置,需要自己调节。 #include <opencv/cv.h> #include <opencv/highgui.h> #include <math.h> #include <iostream> #include <fstream> using namespace std; #pragma comment(lib, "opencv_core220.lib") #pragma comment(lib, "opencv_highgui220.lib") #pragma comment(lib, "opencv_imgproc220.lib") struct Point { double fX; double fY; }; Point pts[4]; double fR; int main() { //打开输出文件 ofstream outf("out.txt"); //获取cout默认输出 streambuf *default_buf=cout.rdbuf(); //重定向cout输出到文件 cout.rdbuf( outf.rdbuf() ); char* szZero = "仪表盘0.bmp"; char* szOne = "仪表盘1.bmp"; char* szRecog = "仪表盘.bmp"; IplImage* imgZero = cvLoadImage(szZero, 1); if (!imgZero) return -1; IplImage* imgOne = cvLoadImage(szOne, 1); if (!imgOne) return -1; IplImage* imgRecog = cvLoadImage(szRecog, 1); if (!imgRecog) return -1; IplImage *imgEdge = NULL; imgEdge = cvCreateImage(cvGetSize(imgRecog),IPL_DEPTH_8U,1); IplImage *imgCanny = cvCreateImage(cvGetSize(imgRecog),IPL_DEPTH_8U,1); CvMemStorage *storage = cvCreateMemStorage(0);//内存采用默认大小 cvCvtColor(imgZero, imgEdge,CV_BGR2GRAY); cvCanny(imgEdge, imgCanny, 50, 100); //hough变化检测刻度0直线 cvClearMemStorage(storage); CvPoint* line; CvSeq* lines; lines = cvHoughLines2(imgCanny, storage, CV_HOUGH_PROBABILISTIC, 1, CV_PI/180, 50, 50, 10 ); line = (CvPoint*)cvGetSeqElem(lines, 0); cvLine(imgZero, line[0], line[1], CV_RGB(255,0,0), 3, CV_AA, 0 ); cout<<"端点1:"<< line[0].x << "," << line[0].y<<endl; cout<<"端点2:"<< line[1].y << "," << line[1].y<<endl; pts[1].fX = line[0].x; pts[1].fY = line[0].y; cvCvtColor(imgOne, imgEdge,CV_BGR2GRAY); cvCanny(imgEdge, imgCanny, 50, 100); //hough变化检测刻度1直线 cvClearMemStorage(storage); lines = cvHoughLines2(imgCanny, storage, CV_HOUGH_PROBABILISTIC, 1, CV_PI/180, 50, 50, 10 ); line = (CvPoint*)cvGetSeqElem(lines, 0); cvLine(imgOne, line[0], line[1], CV_RGB(255,0,0), 3, CV_AA, 0 ); cout<<"端点1:"<< line[0].x << "," << line[0].y<<endl; cout<<"端点2:"<< line[1].y << "," << line[1].y<<endl; pts[2].fX = line[0].x; pts[2].fY = line[0].y; cvCvtColor(imgRecog, imgEdge,CV_BGR2GRAY); cvCanny(imgEdge, imgCanny, 50, 100); //hough变化圆检测 cvClearMemStorage(storage); CvSeq * cir=NULL; cir = cvHoughCircles(imgCanny, storage, CV_HOUGH_GRADIENT, 1, imgRecog->width/10 ,80,40, 50); float * p=(float *)cvGetSeqElem(cir, 0); CvPoint pt = cvPoint(cvRound(p[0]),cvRound(p[1])); cvCircle(imgRecog,pt,cvRound(p[2]),CV_RGB(0,255,0)); cvCircle(imgRecog, pt, 5, CV_RGB(0,255,0), -1, 8, 0 );//绘制圆心 cout<<"圆心:"<<pt.x<<","<<pt.y<<endl; cout<<"半径:"<<p[2]<<endl; pts[0].fX = pt.x; pts[0].fY = pt.y; fR = p[2]; //hough变化检测指针直线 lines = cvHoughLines2(imgCanny, storage, CV_HOUGH_PROBABILISTIC, 1, CV_PI/180, 50, 50, 10 ); line = (CvPoint*)cvGetSeqElem(lines, 0); cvLine(imgRecog, line[0], line[1], CV_RGB(255,0,0), 3, CV_AA, 0 ); cout<<"端点1:"<< line[0].x << "," << line[0].y<<endl; cout<<"端点2:"<< line[1].y << "," << line[1].y<<endl; pts[3].fX = line[0].x; pts[3].fY = line[0].y; static const double PI = atan(1.0) * 4; double fKZero = (pts[1].fY - pts[0].fY) / (pts[1].fX - pts[0].fX); double fThetaZero = 180.0 / PI * atan(fKZero); if (pts[1].fX + 10 < pts[0].fX)//左半部分 { fThetaZero += 180.0; } fThetaZero += 360.0; if (fThetaZero > 360.0) { fThetaZero -= 360.0; } double fKOne = (pts[2].fY - pts[0].fY) / (pts[2].fX - pts[0].fX); double fThetaOne = 180.0 / PI * atan(fKOne); if (pts[2].fX + 10 < pts[0].fX)//左半部分 { fThetaOne += 180.0; } fThetaOne += 360.0; if (fThetaOne > 360.0) { fThetaOne -= 360.0; } double fKRecog = (pts[3].fY - pts[0].fY) / (pts[3].fX - pts[0].fX); double fThetaRecog = 180.0 / PI * atan(fKRecog); if (pts[3].fX + 10 < pts[0].fX)//左半部分 { fThetaRecog += 180.0; } fThetaRecog += 360.0; if (fThetaRecog > 360.0) { fThetaRecog -= 360.0; } char szAns[100]; sprintf(szAns, "0刻度的角度为:%f,1刻度的角度为:%f,识别刻度的角度为:%f,识别结果为:%.2f", fThetaZero, fThetaOne, fThetaRecog, (fThetaRecog - fThetaZero) / (fThetaOne - fThetaZero)); cout << szAns << endl; cvNamedWindow("检测", CV_WINDOW_AUTOSIZE); cvShowImage("检测", imgRecog); cvNamedWindow("刻度0", CV_WINDOW_AUTOSIZE); cvShowImage("刻度0", imgZero); cvNamedWindow("刻度1", CV_WINDOW_AUTOSIZE); cvShowImage("刻度1", imgOne); cvWaitKey(0); cvDestroyWindow("检测"); cvDestroyWindow("刻度0"); cvDestroyWindow("刻度1"); cvReleaseImage(&imgRecog); cvReleaseImage(&imgZero); cvReleaseImage(&imgOne); cvReleaseImage(&imgEdge); cvReleaseImage(&imgCanny); cvReleaseMemStorage(&storage); }
指针的步长问题,int a[5]。a+1跟&a+1跟(int)a+1的区别 /*指针的移动有个步长,步长等于sizeof(指针指向的元素类型) * /#include<stdio.h> int main(void) { int a[5] = {1, 2, 3, 4, 5}; int *ptr1 = (int*)(&a + 1); //&a指针指向的元素为整个数组,故加为sizeof(数组) int *ptr2 = (int*)((int)a + 1); //a地址再加一个字节,直接地址值相加而不是指针 int *ptr3 = (int*)(a + 1); //a为数组首元素的地址,a+1为数组第二个元素的地址 / * 数组a在内存的存放形式为 *01000000 020000000 03000000 04000000 05000000 *ptr2指向01000000的第二个字节,故*ptr2=00000002 * */ printf("%x %x %x\n", ptr1[-1], *ptr2, *ptr3); return 0; }
Linux内存管理基本概念 1.基本概念 1.1地址 (1)逻辑地址:指由程序产生的与段相关的偏移地址部分。在C语言指针中,读取指针变量本身值(&操作),实际上这个值就是逻辑地址,它是相对于你当前进程数据段的地址。 (2)线性地址:段中的偏移地址(逻辑地址),加上相应段的基地址就生成了一个线性地址。 (3)物理地址: 放在寻址总线上的地址。 (4)虚拟地址:保护模式下段和段内偏移量组成的地址,而逻辑地址就是代码段内偏移量,或称进程的逻辑地址。 1.2内存 (1)虚拟内存:计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。与没有使用虚拟内存技术的系统相比,使用这种技术的系统使得大型程序的编写变得更容易,对真正的物理内存(例如RAM)的使用也更有效率。 (2)物理内存:实际的内存。物理地址被分成离散的单元,成为页(page)。目前大多数系统的页面大小都为4k。 1.3地址转换 Linux采用段页式管理机制,有两个部件用于地址转换:分段部件和分页部件。 (1)分段部件:将逻辑地址转换为线性地址。分段提供了隔绝各个代码、数据和堆栈区域的机制,因此多个程序(任务)可以运行在同一个处理器上而不会互相干扰。 (2)分页部件:将线性地址转换为物理地址(页表和页目录),若没有启用分页机制,那么线性地址直接就是物理地址。2.内存分配 Malloc,kmalloc 和vmalloc区别? (1)kmalloc和vmalloc是分配的是内核的内存,malloc分配的是用户的内存。 (2)kmalloc保证分配的内存在物理上是连续的,vmalloc保证的是在虚拟地址空间上的连续。 (3)kmalloc申请的内存比较小,一般小于128 K。它是基于slab(内存池)的,以加快小内存申请效率。 3.常见问题 (1)调用malloc函数后,OS会马上分配实际的内存空间吗? 答:不会,只会返回一个虚拟地址,待用户要使用内存时,OS会发出一个缺页中断,此时,内存管理模块才会为程序分配真正的内存。 (2)段式管理和页式管理的优缺点? 在段式存储管理中,将程序的地址空间划分为若干个段(segment),这样每个进程有一个二维的地址空间,相互独立,互不干扰。程序通过分段划分为多个模块,如代码段、数据段、共享段。这样做的优点是:可以分别编写和编译源程序的一个文件,并且可以针对不同类型的段采取不同的保护,也可以按段为单位来进行共享。段式存储管理的优点是:没有内碎片,外碎片可以通过内存紧缩来消除;便于实现内存共享。 在页式存储管理中,将程序的逻辑地址空间划分为固定大小的页(page),而物理内存划分为同样大小的页框(pageframe)。程序加载时,可将任意一页放人内存中任意一个页框,这些页框不必连续,从而实现了离散分配。这种管理方式的优点是,没有外碎片,且一个程序不必连续存放。这样就便于改变程序占用空间的大小。 页式和段式系统有许多相似之处。比如,两者都采用离散分配方式,且都通过地址映射机构来实现地址变换。但概念上两者也有很多区别,主要表现在: [1] 页是信息的物理单位,分页是为了实现离散分配方式,以减少内存的外零头,提高内存的利用率。或者说,分页仅仅是由于系统管理的需要,而不是用户的需要。段是信息的逻辑单位,它含有一组其意义相对完整的信息。分段的目的是为了更好地满足用户的需要。 [2] 页的大小固定且由系统决定,把逻辑地址划分为页号和页内地址两部分,是由机器硬件实现的。段的长度不固定,且决定于用户所编写的程序,通常由编译系统在对源程序进行编译时根据信息的性质来划分。 [3]页式系统地址空间是一维的,即单一的线性地址空间,程序员只需利用一个标识符,即可表示一个地址。分段的作业地址空间是二维的,程序员在标识一个地址时,既需给出段名,又需给出段内地址。 (3)Malloc在什么情况下调用mmap? 从操作系统角度来看,进程分配内存有两种方式,分别由两个系统调用完成:brk和mmap(不考虑共享内存)。brk是将数据段(.data)的最高地址指针_edata往高地址推,mmap是在进程的虚拟地址空间中(一般是堆和栈中间)找一块空闲的。这两种方式分配的都是虚拟内存,没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系。 在标准C库中,提供了malloc/free函数分配释放内存,这两个函数底层是由brk,mmap,munmap这些系统调用实现的。 默认情况下,malloc函数分配内存,如果请求内存大于128K(可由M_MMAP_THRESHOLD选项调节),那就不是去推_edata指针了,而是利用mmap系统调用,从堆和栈的中间分配一块虚拟内存。这样子做主要是因为brk分配的内存需要等到高地址内存释放以后才能释放(例如,在B释放之前,A是不可能释放的),而mmap分配的内存可以单独释放。 (4)32位系统,通常情况下,最大虚拟地址和物理地址空间为多少? 不使用PAE情况下,最大虚拟地址和物理地址空间均为4G,若果使用PAE,最大虚拟地址仍为4G,而物理地址空间可变为64G(x86, 32为变36位)。 (5)怎样实现malloc和free? Malloc实现可考虑采用buddy算法+slob算法,free类似。
v4l2 编程接口(一) — ioctl 在应用程序获取视频数据的流程中,都是通过 ioctl 命令与驱动程序进行交互,常见的 ioctl 命令有: VIDIOC_QUERYCAP /* 获取设备支持的操作 */ VIDIOC_G_FMT /* 获取设置支持的视频格式 */ VIDIOC_S_FMT /* 设置捕获视频的格式 */ VIDIOC_REQBUFS /* 向驱动提出申请内存的请求 */ VIDIOC_QUERYBUF /* 向驱动查询申请到的内存 */ VIDIOC_QBUF /* 将空闲的内存加入可捕获视频的队列 */ VIDIOC_DQBUF /* 将已经捕获好视频的内存拉出已捕获视频的队列 */ VIDIOC_STREAMON /* 打开视频流 */ VIDIOC_STREAMOFF /* 关闭视频流 */ VIDIOC_QUERYCTRL /* 查询驱动是否支持该命令 */ VIDIOC_G_CTRL /* 获取当前命令值 */ VIDIOC_S_CTRL /* 设置新的命令值 */ VIDIOC_G_TUNER /* 获取调谐器信息 */ VIDIOC_S_TUNER /* 设置调谐器信息 */ VIDIOC_G_FREQUENCY /* 获取调谐器频率 */ VIDIOC_S_FREQUENCY /* 设置调谐器频率 */ 1、struct v4l2_capability 与 VIDIOC_QUERYCAP VIDIOC_QUERYCAP 命令通过结构 v4l2_capability 获取设备支持的操作模式: [cpp] view plain copy struct v4l2_capability { __u8 driver[16]; /* i.e. "bttv" */ __u8 card[32]; /* i.e. "Hauppauge WinTV" */ __u8 bus_info[32]; /* "PCI:" + pci_name(pci_dev) */ __u32 version; /* should use KERNEL_VERSION() */ __u32 capabilities; /* Device capabilities */ __u32 reserved[4]; }; 其中域 capabilities 代表设备支持的操作模式,常见的值有 V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_STREAMING 表示是一个视频捕捉设备并且具有数据流控制模式;另外 driver 域需要和 struct video_device 中的 name 匹配。 2、struct v4l2_format 与 VIDIOC_G_FMT、VIDIOC_S_FMT、VIDIOC_TRY_FMT 通常用 VIDIOC_S_FMT 命令通过结构 v4l2_format 初始化捕获视频的格式,如果要改变格式则用 VIDIOC_TRY_FMT 命令: [cpp] view plain copy struct v4l2_format { enum v4l2_buf_type type; union { struct v4l2_pix_format pix; /* V4L2_BUF_TYPE_VIDEO_CAPTURE */ struct v4l2_window win; /* V4L2_BUF_TYPE_VIDEO_OVERLAY */ struct v4l2_vbi_format vbi; /* V4L2_BUF_TYPE_VBI_CAPTURE */ struct v4l2_sliced_vbi_format sliced; /* V4L2_BUF_TYPE_SLICED_VBI_CAPTURE */ __u8 raw_data[200]; /* user-defined */ } fmt; }; 其中 enum v4l2_buf_type { V4L2_BUF_TYPE_VIDEO_CAPTURE = 1, V4L2_BUF_TYPE_VIDEO_OUTPUT = 2, V4L2_BUF_TYPE_VIDEO_OVERLAY = 3, ... V4L2_BUF_TYPE_PRIVATE = 0x80, }; struct v4l2_pix_format { __u32 width; __u32 height; __u32 pixelformat; enum v4l2_field field; __u32 bytesperline; /* for padding, zero if unused */ __u32 sizeimage; enum v4l2_colorspace colorspace; __u32 priv; /* private data, depends on pixelformat */ }; 常见的捕获模式为 V4L2_BUF_TYPE_VIDEO_CAPTURE 即视频捕捉模式,在此模式下 fmt 联合体采用域 v4l2_pix_format:其中 width 为视频的宽、height 为视频的高、pixelformat 为视频数据格式(常见的值有 V4L2_PIX_FMT_YUV422P | V4L2_PIX_FMT_RGB565)、bytesperline 为一行图像占用的字节数、sizeimage 则为图像占用的总字节数、colorspace 指定设备的颜色空间。 3、struct v4l2_requestbuffers 与 VIDIOC_REQBUFS VIDIOC_REQBUFS 命令通过结构 v4l2_requestbuffers 请求驱动申请一片连续的内存用于缓存视频信息: [cpp] view plain copy struct v4l2_requestbuffers { __u32 count; enum v4l2_buf_type type; enum v4l2_memory memory; __u32 reserved[2]; }; 其中 enum v4l2_memory { V4L2_MEMORY_MMAP = 1, V4L2_MEMORY_USERPTR = 2, V4L2_MEMORY_OVERLAY = 3, }; count 指定根据图像占用空间大小申请的缓存区个数,type 为视频捕获模式,memory 为内存区的使用方式。 4、struct v4l2_buffer与 VIDIOC_QUERYBUF VIDIOC_QUERYBUF 命令通过结构 v4l2_buffer 查询驱动申请的内存区信息: [cpp] view plain copy struct v4l2_buffer { __u32 index; enum v4l2_buf_type type; __u32 bytesused; __u32 flags; enum v4l2_field field; struct timeval timestamp; struct v4l2_timecode timecode; __u32 sequence; /* memory location */ enum v4l2_memory memory; union { __u32 offset; unsigned long userptr; } m; __u32 length; __u32 input; __u32 reserved; }; index 为缓存编号,type 为视频捕获模式,bytesused 为缓存已使用空间大小,flags 为缓存当前状态(常见值有 V4L2_BUF_FLAG_MAPPED | V4L2_BUF_FLAG_QUEUED | V4L2_BUF_FLAG_DONE,分别代表当前缓存已经映射、缓存可以采集数据、缓存可以提取数据),timestamp 为时间戳,sequence为缓存序号,memory 为缓存使用方式,offset 为当前缓存与内存区起始地址的偏移,length 为缓存大小,reserved 一般用于传递物理地址值。 另外 VIDIOC_QBUF 和 VIDIOC_DQBUF 命令都采用结构 v4l2_buffer 与驱动通信:VIDIOC_QBUF 命令向驱动传递应用程序已经处理完的缓存,即将缓存加入空闲可捕获视频的队列,传递的主要参数为 index;VIDIOC_DQBUF 命令向驱动获取已经存放有视频数据的缓存,v4l2_buffer 的各个域几乎都会被更新,但主要的参数也是 index,应用程序会根据 index 确定可用数据的起始地址和范围。
Linux之V4L2基础编程 1. 定义 V4L2(Video For Linux Two) 是内核提供给应用程序访问音、视频驱动的统一接口。 2. 工作流程: 打开设备-> 检查和设置设备属性-> 设置帧格式-> 设置一种输入输出方法(缓冲 区管理)-> 循环获取数据-> 关闭设备。 3. 设备的打开和关闭:#include <fcntl.h>int open(const char *device_name, int flags);#include <unistd.h>int clo se(int fd); 例: int fd=open(“/dev/video0”,O_RDWR); // 打开设备close(fd); // 关闭设备 注意:V4L2 的相关定义包含在头文件<linux/videodev2.h> 中. 4. 查询设备属性: VIDIOC_QUERYCAP 相关函数: int ioctl(int fd, int request, struct v4l2_capability *argp); 相关结构体: 按 Ctrl+C 复制代码按 Ctrl+C 复制代码 capabilities 常用值: V4L2_CAP_VIDEO_CAPTURE // 是否支持图像获取 例:显示设备信息 struct v4l2_capability cap;ioctl(fd,VIDIOC_QUERYCAP,&cap);printf(“Driver Name:%s\nCard Name:%s\nBus info:%s\nDriver Version:%u.%u.%u\n”,cap.driver,cap.card,cap.bus_info,(cap.version>>16)&0XFF, (cap.version>>8)&0XFF,cap.version&0XFF);5. 设置视频的制式和帧格式 制式包括PAL,NTSC,帧的格式个包括宽度和高度等。 相关函数: int ioctl(int fd, int request, struct v4l2_fmtdesc *argp);int ioctl(int fd, int request, struct v4l2_format *argp); 相关结构体: v4l2_cropcap 结构体用来设置摄像头的捕捉能力,在捕捉上视频时应先先设置 v4l2_cropcap 的 type 域,再通过 VIDIO_CROPCAP 操作命令获取设备捕捉能力的参数,保存于 v4l2_cropcap 结构体中,包括 bounds(最大捕捉方框的左上角坐标和宽高),defrect (默认捕捉方框的左上角坐标和宽高)等。 v4l2_format 结构体用来设置摄像头的视频制式、帧格式等,在设置这个参数时应先填 好 v4l2_format 的各个域,如 type(传输流类型),fmt.pix.width(宽), fmt.pix.heigth(高),fmt.pix.field(采样区域,如隔行采样),fmt.pix.pixelformat(采 样类型,如 YUV4:2:2),然后通过 VIDIO_S_FMT 操作命令设置视频捕捉格式。如下图所示:5.1 查询并显示所有支持的格式:VIDIOC_ENUM_FMT 相关函数: int ioctl(int fd, int request, struct v4l2_fmtdesc *argp); 相关结构体: 按 Ctrl+C 复制代码按 Ctrl+C 复制代码 例:显示所有支持的格式struct v4l2_fmtdesc fmtdesc; fmtdesc.index=0; fmtdesc.type=V4L2_BUF_TYPE_VIDEO_CAPTURE; printf("Support format:\n");while(ioctl(fd, VIDIOC_ENUM_FMT, &fmtdesc) != -1){printf("\t%d.%s\n",fmtdesc.index+1,fmtdesc.description);fmtdesc.index++;}5.2 查看或设置当前格式: VIDIOC_G_FMT, VIDIOC_S_FMT 检查是否支持某种格式:VIDIOC_TRY_FMT 相关函数: int ioctl(int fd, int request, struct v4l2_format *argp); 相关结构体: 按 Ctrl+C 复制代码按 Ctrl+C 复制代码 例:显示当前帧的相关信息struct v4l2_format fmt; fmt.type=V4L2_BUF_TYPE_VIDEO_CAPTURE; ioctl(fd, VIDIOC_G_FMT, &fmt);printf(“Current data format information:\n\twidth:%d\n\theight:%d\n”,fmt.fmt.pix.width,fmt.fmt.pix.height);struct v4l2_fmtdesc fmtdesc; fmtdesc.index=0; fmtdesc.type=V4L2_BUF_TYPE_VIDEO_CAPTURE; while(ioctl(fd,VIDIOC_ENUM_FMT,&fmtdesc)!=-1){if(fmtdesc.pixelformat & fmt.fmt.pix.pixelformat){printf(“\tformat:%s\n”,fmtdesc.description);break;}fmtdesc.index++;}例:检查是否支持某种帧格式 struct v4l2_format fmt; fmt.type=V4L2_BUF_TYPE_VIDEO_CAPTURE; fmt.fmt.pix.pixelformat=V4L2_PIX_FMT_RGB32; if(ioctl(fd,VIDIOC_TRY_FMT,&fmt)==-1) if(errno==EINVAL)printf(“not support format RGB32!\n”);6. 图像的缩放 VIDIOC_CROPCAP 相关函数: int ioctl(int fd, int request, struct v4l2_cropcap *argp);int ioctl(int fd, int request, struct v4l2_crop *argp);int ioctl(int fd, int request, const struct v4l2_crop *argp); 相关结构体: Cropping 和 scaling 主要指的是图像的取景范围及图片的比例缩放的支持。Crop 就 是把得到的数据作一定的裁剪和伸缩,裁剪可以只取样我们可以得到的图像大小的一部分, 剪裁的主要参数是位置、长度、宽度。而 scale 的设置是通过 VIDIOC_G_FMT 和 VIDIOC_S_FMT 来获得和设置当前的 image 的长度,宽度来实现的。看下图我们可以假设 bounds 是 sensor 最大能捕捉到的图像范围,而 defrect 是设备默认 的最大取样范围,这个可以通过 VIDIOC_CROPCAP 的 ioctl 来获得设备的 crap 相关的属 性 v4l2_cropcap,其中的 bounds 就是这个 bounds,其实就是上限。每个设备都有个默 认的取样范围,就是 defrect,就是 default rect 的意思,它比 bounds 要小一些。这 个范围也是通过 VIDIOC_CROPCAP 的 ioctl 来获得的 v4l2_cropcap 结构中的 defrect 来表示的,我们可以通过 VIDIOC_G_CROP 和 VIDIOC_S_CROP 来获取和设置设备当前的 crop 设置。 6.1 设置设备捕捉能力的参数 相关函数: int ioctl(int fd, int request, struct v4l2_cropcap *argp); 相关结构体: 按 Ctrl+C 复制代码按 Ctrl+C 复制代码6.2 设置窗口取景参数 VIDIOC_G_CROP 和 VIDIOC_S_CROP
Arm-linux-gcc-4.3.2安装步骤 安装交叉编译工具链: 1、首先以root用户登入 2、复制arm-linux-gcc-4.3.2.tgz到根目录下tmp文件夹里 3、解压命令tar xvzf arm-linux-gcc-4.3.2 -C /注意以上命令必须要有-C而且是大写,后边有个空格也要注意。 4、配置下编译环境路径 在控制台下输入 gedit /root/.bashrc 等一会出来文本编辑器后在文件最后(最后一行)加上下面代码。export PATH=/usr/local/arm/4.3.2/bin:$PATH保存关闭后,注销当前用户,用root账号重新登录系统(使刚刚添加的环境变量生效)。 此时你可以在控制台输入: arm-linux-gcc -v 如果安装成功将会输出 arm-linux-gcc的版本号。 若想让它在非超级用户下使用那。首先,以非超级用户登入。 1、 输入命令:vi ~/.bashrc编辑.bashrc文件,在文件末尾加入如上面的内容 export PATH=/usr/local/arm/4.3.2/bin:$PATH 2、输入命令:gedit /etc/profile在文件的末尾加上:PATH=/usr/local/arm/4.3.2/bin:$PATH保存对profile的修改后,执行source /etc/profile就OK了,好了通样先注销当前用户再登录后进入控制台执行arm-linux-gcc -v 看看能否执行成功就可以了。
linux内存管理--虚拟内存管理理解 一 为什么需要使用虚拟内存 大家都知道,进程需要使用的代码和数据都放在内存中,比放在外存中要快很多。问题是内存空间太小了,不能满足进程的需求,而且现在都是多进程,情况更加糟糕。所以提出了虚拟内存,使得每个进程用于3G的独立用户内存空间和共享的1G内核内存空间。(每个进程都有自己的页表,才使得3G用户空间的独立)这样进程运行的速度必然很快了。而且虚拟内存机制还解决了内存碎片和内存不连续的问题。为什么可以在有限的物理内存上达到这样的效果呢? 二 虚拟内存的实现机制 首先呢,提一个概念,交换空间(swap space),这个大家应该不陌生,在重装系统的时候,会让你选择磁盘分区,就比如说一个硬盘分几个部分去管理。其中就会分一部分磁盘空间用作交换,叫做swap space。其实就是一段临时存储空间,内存不够用的时候就用它了,虽然它也在磁盘中,但省去了很多的查找时间啊。当发生进程切换的时候,内存与交换空间就要发生数据交换一满足需求。所以啊,进程的切换消耗是很大的,这也说明了为什么自旋锁比信号量效率高的原因。 那么我们的程序里申请的内存的时候,linux内核其实只分配一个虚拟内存( 线性地址),并没有分配实际的物理内存。只有当程序真正使用这块内存时,才会分配物理内存。这就叫做延迟分配和请页机制。释放内存时,先释放线性区对应的物理内存,然后释放线性区;"请页机制"将物理内存的分配延后了,这样是充分利用了程序的局部性原来,节约内存空间,提高系统吞吐;就是说一个函数可能只在物理内存中呆了一会,用完了就被清除出去了,虽然在虚拟地址空间还在。(不过虚拟地址空间不是事实上的存储,所以只能说这个函数占据了一段虚拟地址空间,当你访问这段地址时,就会产生缺页处理,从交换区把对应的代码搬到物理内存上来) 三 物理内存与虚拟内存的布局左边是物理地址分配,与实际的CPU相关。4KB的这些都是一些控制器所占有,比如lcdc sd卡,他们的寄存器地址就是这样定死的。但是呢,我们要访问这些寄存器的时候,还是不能直接用,要使用内存管理的规则,使用虚拟地址去访问它,所以在驱动等内核程序中需要使用虚拟地址访问寄存器。如果有人直接使用物理地址访问寄存器,那么唯一的解释就是没有开mmu。不过这样你的进程就没有4G内存可以用了。 物理地址分布:这是偷的别人的图啦,物理地址有896M直接映射到虚拟地址的内存空间,这是一一对应的映射,只有起始地址不一样,偏移是一样的。这个大小大多是固定的,哪怕你的内存超过一个G,太小了就另外说了。注意:用户区的代码也是放在这段物理地址里面的,就是说物理地址可以进行二次映射。但不管怎么样,这段物理地址都是受内核管理。当你内存很大的时候,超过896M时,剩余的那些内存怎么办呢?这多出来的叫做高端内存,如果你使用vmalloc申请空间,就会在高端内存中分配,如果你使用kmalloc申请空间,就会在小于896的内存中分配。所以还是很讲究的啊!!如果你的程序需要使用高端内存,就要调用内核API来分配,所以高端内存并不是想用就能用的哦。不过通过系统把一些应用常住在高端内存到是个好注意。不过前提是你的内存灰常大啊。 为什么要这样做呢?先看看这里面放些什么?虚拟地址分布:关于0-3G用户空间内存的分布:谈到段式分布,就要说说逻辑地址,线性地址与物理地址的关系:linux通过段机制把逻辑地址转换为虚拟地址(就是线性地址),再通过页机制把虚拟地址转换为物理地址。所谓分段就是基址不同,偏移一样,比如说32位,一般程序里面都不会使用这么多的位,可以把前12位用作基址,后20位用作偏移,这样在特定段就可以只使用偏移寻址了。寻址很方便,不过linux页基址做的更好。 最后呢再说几个点: 1 线性地址空间:指linux系统中的虚拟地址空间。 2 cpu寻址是属于物理地址。所以在使用cpu寻址前要把地址转换好。 3 物理内存中的高端内存是DDR减去896M后多出来的那一段。虚拟地址里面的高端内存是指用于映射高端内存的虚拟地址空间。不过高端内存被映射到用户空间,那就是另外一回事了吧。 4 内核空间是可以访问用户空间的,级别高就是好啊。不过不是通过虚拟地址直接访问的。
五大常用算法之五:分支限界法 一、基本描述 类似于回溯法,也是一种在问题的解空间树T上搜索问题解的算法。但在一般情况下,分支限界法与回溯法的求解目标不同。回溯法的求解目标是找出T中满足约束条件的所有解,而分支限界法的求解目标则是找出满足约束条件的一个解,或是在满足约束条件的解中找出使某一目标函数值达到极大或极小的解,即在某种意义下的最优解。 (1)分支搜索算法 所谓“分支”就是采用广度优先的策略,依次搜索E-结点的所有分支,也就是所有相邻结点,抛弃不满足约束条件的结点,其余结点加入活结点表。然后从表中选择一个结点作为下一个E-结点,继续搜索。 选择下一个E-结点的方式不同,则会有几种不同的分支搜索方式。 1)FIFO搜索 2)LIFO搜索 3)优先队列式搜索 (2)分支限界搜索算法 二、分支限界法的一般过程 由于求解目标不同,导致分支限界法与回溯法在解空间树T上的搜索方式也不相同。回溯法以深度优先的方式搜索解空间树T,而分支限界法则以广度优先或以最小耗费优先的方式搜索解空间树T。 分支限界法的搜索策略是:在扩展结点处,先生成其所有的儿子结点(分支),然后再从当前的活结点表中选择下一个扩展对点。为了有效地选择下一扩展结点,以加速搜索的进程,在每一活结点处,计算一个函数值(限界),并根据这些已计算出的函数值,从当前活结点表中选择一个最有利的结点作为扩展结点,使搜索朝着解空间树上有最优解的分支推进,以便尽快地找出一个最优解。 分支限界法常以广度优先或以最小耗费(最大效益)优先的方式搜索问题的解空间树。问题的解空间树是表示问题解空间的一棵有序树,常见的有子集树和排列树。在搜索问题的解空间树时,分支限界法与回溯法对当前扩展结点所使用的扩展方式不同。在分支限界法中,每一个活结点只有一次机会成为扩展结点。活结点一旦成为扩展结点,就一次性产生其所有儿子结点。在这些儿子结点中,那些导致不可行解或导致非最优解的儿子结点被舍弃,其余儿子结点被子加入活结点表中。此后,从活结点表中取下一结点成为当前扩展结点,并重复上述结点扩展过程。这个过程一直持续到找到所求的解或活结点表为空时为止。 三、回溯法和分支限界法的一些区别 有一些问题其实无论用回溯法还是分支限界法都可以得到很好的解决,但是另外一些则不然。也许我们需要具体一些的分析——到底何时使用分支限界而何时使用回溯呢? 回溯法和分支限界法的一些区别: 方法对解空间树的搜索方式 存储结点的常用数据结构 结点存储特性常用应用 回溯法深度优先搜索堆栈活结点的所有可行子结点被遍历后才被从栈中弹出找出满足约束条件的所有解 分支限界法广度优先或最小消耗优先搜索队列、优先队列每个结点只有一次成为活结点的机会找出满足约束条件的一个解或特定意义下的最优解
内核编程与用户态空间编程的差别 对于从事应用程序开发来说,用户空间的任务调度与同步之间的关系相对简单,无需过多考虑需要同步的原因。这一是因为在用户空间中各个进程都拥有独立的运行空间,进程内部的数据对外不可见,所以各个进程即使并发执行也不会产生对数据访问的竞争。第二是因为用户空间与内核空间独立,所以用户进程不会与内核任务交错执行,因此用户进程不存在与内核任务并发的可能。以上两个原因使得用户同步仅仅需要在进程间通讯和多线程编程时需要考虑。
Linux 引导过程内幕 从主引导记录到第一个用户空间应用程序的指导 引导 Linux® 系统的过程包括很多阶段。不管您是引导一个标准的 x86 桌面系统,还是引导一台嵌入式的 PowerPC® 机器,很多流程都惊人地相似。本文将探索 Linux 的引导过程,从最初的引导到启动第一个用户空间应用程序。在本文介绍的过程中,您将学习到各种与引导有关的主题,例如引导加载程序、内核解压、初始 RAM 磁盘以及 Linux 引导的其他一些元素。 3 评论 M. Tim Jones (
[email protected]
), 顾问工程师, Emulex 2006 年 7 月 26 日内容在 IBM Bluemix 云平台上开发并部署您的下一个应用。 开始您的试用 早期时,启动一台计算机意味着要给计算机喂一条包含引导程序的纸带,或者手工使用前端面板地址/数据/控制开关来加载引导程序。尽管目前的计算机已经装备了很多工具来简化引导过程,但是这一切并没有对整个过程进行必要的简化。 让我们先从高级的视角来查看 Linux 引导过程,这样就可以看到整个过程的全貌了。然后将回顾一下在各个步骤到底发生了什么。在整个过程中,参考一下内核源代码可以帮助我们更好地了解内核源代码树,并在以后对其进行深入分析。 概述 图 1 是我们在 20,000 英尺的高度看到的视图。 图 1. Linux 引导过程在 20,000 英尺处的视图当系统首次引导时,或系统被重置时,处理器会执行一个位于已知位置处的代码。在个人计算机(PC)中,这个位置在基本输入/输出系统(BIOS)中,它保存在主板上的闪存中。嵌入式系统中的中央处理单元(CPU)会调用这个重置向量来启动一个位于闪存/ROM 中的已知地址处的程序。在这两种情况下,结果都是相同的。因为 PC 提供了很多灵活性,BIOS 必须确定要使用哪个设备来引导系统。稍后我们将详细介绍这个过程。 当找到一个引导设备之后,第一阶段的引导加载程序就被装入 RAM 并执行。这个引导加载程序在大小上小于 512 字节(一个扇区),其作用是加载第二阶段的引导加载程序。 当第二阶段的引导加载程序被装入 RAM 并执行时,通常会显示一个动画屏幕,并将 Linux 和一个可选的初始 RAM 磁盘(临时根文件系统)加载到内存中。在加载映像时,第二阶段的引导加载程序就会将控制权交给内核映像,然后内核就可以进行解压和初始化了。在这个阶段中,第二阶段的引导加载程序会检测系统硬件、枚举系统链接的硬件设备、挂载根设备,然后加载必要的内核模块。完成这些操作之后启动第一个用户空间程序(init),并执行高级系统初始化工作。 这就是 Linux 引导的整个过程。现在让我们深入挖掘一下这个过程,并深入研究一下 Linux 引导过程的一些详细信息。 回页首 系统启动 系统启动阶段依赖于引导 Linux 系统上的硬件。在嵌入式平台中,当系统加电或重置时,会使用一个启动环境。这方面的例子包括 U-Boot、RedBoot 和 Lucent 的 MicroMonitor。嵌入式平台通常都是与引导监视器搭配销售的。这些程序位于目标硬件上的闪存中的某一段特殊区域,它们提供了将 Linux 内核映像下载到闪存并继续执行的方法。除了可以存储并引导 Linux 映像之外,这些引导监视器还执行一定级别的系统测试和硬件初始化过程。在嵌入式平台中,这些引导监视器通常会涉及第一阶段和第二阶段的引导加载程序。 提取 MBR 的信息 要查看 MBR 的内容,请使用下面的命令: # dd if=/dev/hda of=mbr.bin bs=512 count=1 # od -xa mbr.bin 这个 dd 命令需要以 root 用户的身份运行,它从 /dev/hda(第一个 IDE 盘) 上读取前 512 个字节的内容,并将其写入 mbr.bin 文件中。od 命令会以十六进制和 ASCII 码格式打印这个二进制文件的内容。 在 PC 中,引导 Linux 是从 BIOS 中的地址 0xFFFF0 处开始的。BIOS 的第一个步骤是加电自检(POST)。POST 的工作是对硬件进行检测。BIOS 的第二个步骤是进行本地设备的枚举和初始化。 给定 BIOS 功能的不同用法之后,BIOS 由两部分组成:POST 代码和运行时服务。当 POST 完成之后,它被从内存中清理了出来,但是 BIOS 运行时服务依然保留在内存中,目标操作系统可以使用这些服务。 要引导一个操作系统,BIOS 运行时会按照 CMOS 的设置定义的顺序来搜索处于活动状态并且可以引导的设备。引导设备可以是软盘、CD-ROM、硬盘上的某个分区、网络上的某个设备,甚至是 USB 闪存。 通常,Linux 都是从硬盘上引导的,其中主引导记录(MBR)中包含主引导加载程序。MBR 是一个 512 字节大小的扇区,位于磁盘上的第一个扇区中(0 道 0 柱面 1 扇区)。当 MBR 被加载到 RAM 中之后,BIOS 就会将控制权交给 MBR。 回页首 第一阶段引导加载程序 MBR 中的主引导加载程序是一个 512 字节大小的映像,其中包含程序代码和一个小分区表(参见图 2)。前 446 个字节是主引导加载程序,其中包含可执行代码和错误消息文本。接下来的 64 个字节是分区表,其中包含 4 个分区的记录(每个记录的大小是 16 个字节)。MBR 以两个特殊数字的字节(0xAA55)结束。这个数字会用来进行 MBR 的有效性检查。 图 2. MBR 剖析主引导加载程序的工作是查找并加载次引导加载程序(第二阶段)。它是通过在分区表中查找一个活动分区来实现这种功能的。当找到一个活动分区时,它会扫描分区表中的其他分区,以确保它们都不是活动的。当这个过程验证完成之后,就将活动分区的引导记录从这个设备中读入 RAM 中并执行它。 回页首 第二阶段引导加载程序 次引导加载程序(第二阶段引导加载程序)可以更形象地称为内核加载程序。这个阶段的任务是加载 Linux 内核和可选的初始 RAM 磁盘。 GRUB 阶段引导加载程序 /boot/grub 目录中包含了 stage1、stage1.5 和stage2 引导加载程序,以及很多其他加载程序(例如,CR-ROM 使用的是 iso9660_stage_1_5)。 在 x86 PC 环境中,第一阶段和第二阶段的引导加载程序一起称为 Linux Loader(LILO)或 GRand Unified Bootloader(GRUB)。由于 LILO 有一些缺点,而 GRUB 克服了这些缺点,因此下面让我们就来看一下 GRUB。(有关 GRUB、LILO 和相关主题的更多内容,请参阅本文后面的 参考资料 部分的内容。) 关于 GRUB,很好的一件事情是它包含了有关 Linux 文件系统的知识。GRUB 不像 LILO 一样使用裸扇区,而是可以从 ext2 或 ext3 文件系统中加载 Linux 内核。它是通过将两阶段的引导加载程序转换成三阶段的引导加载程序来实现这项功能的。阶段 1 (MBR)引导了一个阶段 1.5 的引导加载程序,它可以理解包含 Linux 内核映像的特殊文件系统。这方面的例子包括reiserfs_stage1_5(要从 Reiser 日志文件系统上进行加载)或 e2fs_stage1_5(要从 ext2 或 ext3 文件系统上进行加载)。当阶段 1.5 的引导加载程序被加载并运行时,阶段 2 的引导加载程序就可以进行加载了。 当阶段 2 加载之后,GRUB 就可以在请求时显示可用内核列表(在 /etc/grub.conf 中进行定义,同时还有几个软符号链接/etc/grub/menu.lst 和 /etc/grub.conf)。我们可以选择内核甚至修改附加内核参数。另外,我们也可以使用一个命令行的 shell 对引导过程进行高级手工控制。 将第二阶段的引导加载程序加载到内存中之后,就可以对文件系统进行查询了,并将默认的内核映像和 initrd 映像加载到内存中。当这些映像文件准备好之后,阶段 2 的引导加载程序就可以调用内核映像了。 回页首 内核GRUB 中的手工引导 在 GRUB 命令行中,我们可以使用 initrd 映像引导一个特定的内核,方法如下: grub> kernel /bzImage-2.6.14.2 [Linux-bzImage, setup=0x1400, size=0x29672e] grub> initrd /initrd-2.6.14.2.img [Linux-initrd @ 0x5f13000, 0xcc199 bytes] grub> boot Uncompressing Linux... Ok, booting the kernel. 如果您不知道要引导的内核的名称,只需使用斜线(/)然后按下 Tab 键即可。GRUB 会显示内核和 initrd 映像列表。 当内核映像被加载到内存中,并且阶段 2 的引导加载程序释放控制权之后,内核阶段就开始了。内核映像并不是一个可执行的内核,而是一个压缩过的内核映像。通常它是一个 zImage(压缩映像,小于 512KB)或一个 bzImage(较大的压缩映像,大于 512KB),它是提前使用 zlib 进行压缩过的。在这个内核映像前面是一个例程,它实现少量硬件设置,并对内核映像中包含的内核进行解压,然后将其放入高端内存中,如果有初始 RAM 磁盘映像,就会将它移动到内存中,并标明以后使用。然后该例程会调用内核,并开始启动内核引导的过程。 当 bzImage(用于 i386 映像)被调用时,我们从 ./arch/i386/boot/head.S 的 start汇编例程开始执行(主要流程图请参看图 3)。这个例程会执行一些基本的硬件设置,并调用./arch/i386/boot/compressed/head.S 中的 startup_32 例程。此例程会设置一个基本的环境(堆栈等),并清除 Block Started by Symbol(BSS)。然后调用一个叫做decompress_kernel 的 C 函数(在 ./arch/i386/boot/compressed/misc.c 中)来解压内核。当内核被解压到内存中之后,就可以调用它了。这是另外一个 startup_32 函数,但是这个函数在 ./arch/i386/kernel/head.S 中。 在这个新的 startup_32 函数(也称为清除程序或进程 0)中,会对页表进行初始化,并启用内存分页功能。然后会为任何可选的浮点单元(FPU)检测 CPU 的类型,并将其存储起来供以后使用。然后调用 start_kernel 函数(在 init/main.c 中),它会将您带入与体系结构无关的 Linux 内核部分。实际上,这就是 Linux 内核的 main 函数。 图 3. Linux 内核 i386 引导的主要函数流程通过调用 start_kernel,会调用一系列初始化函数来设置中断,执行进一步的内存配置,并加载初始 RAM 磁盘。最后,要调用kernel_thread(在 arch/i386/kernel/process.c 中)来启动 init 函数,这是第一个用户空间进程(user-space process)。最后,启动空任务,现在调度器就可以接管控制权了(在调用 cpu_idle 之后)。通过启用中断,抢占式的调度器就可以周期性地接管控制权,从而提供多任务处理能力。 在内核引导过程中,初始 RAM 磁盘(initrd)是由阶段 2 引导加载程序加载到内存中的,它会被复制到 RAM 中并挂载到系统上。这个initrd 会作为 RAM 中的临时根文件系统使用,并允许内核在没有挂载任何物理磁盘的情况下完整地实现引导。由于与外围设备进行交互所需要的模块可能是 initrd 的一部分,因此内核可以非常小,但是仍然需要支持大量可能的硬件配置。在内核引导之后,就可以正式装备根文件系统了(通过 pivot_root):此时会将 initrd 根文件系统卸载掉,并挂载真正的根文件系统。 decompress_kernel 输出 函数 decompress_kernel 就是显示我们通常看到的解压消息的地方: Uncompressing Linux... Ok, booting the kernel. initrd 函数让我们可以创建一个小型的 Linux 内核,其中包括作为可加载模块编译的驱动程序。这些可加载的模块为内核提供了访问磁盘和磁盘上的文件系统的方法,并为其他硬件提供了驱动程序。由于根文件系统是磁盘上的一个文件系统,因此 initrd 函数会提供一种启动方法来获得对磁盘的访问,并挂载真正的根文件系统。在一个没有硬盘的嵌入式环境中,initrd 可以是最终的根文件系统,或者也可以通过网络文件系统(NFS)来挂载最终的根文件系统。 回页首 Init 当内核被引导并进行初始化之后,内核就可以启动自己的第一个用户空间应用程序了。这是第一个调用的使用标准 C 库编译的程序。在此之前,还没有执行任何标准的 C 应用程序。 在桌面 Linux 系统上,第一个启动的程序通常是 /sbin/init。但是这不是一定的。很少有嵌入式系统会需要使用 init 所提供的丰富初始化功能(这是通过 /etc/inittab 进行配置的)。在很多情况下,我们可以调用一个简单的 shell 脚本来启动必需的嵌入式应用程序。 回页首 结束语 与 Linux 本身非常类似,Linux 的引导过程也非常灵活,可以支持众多的处理器和硬件平台。最初,加载引导加载程序提供了一种简单的方法,不用任何花架子就可以引导 Linux。LILO 引导加载程序对引导能力进行了扩充,但是它却缺少文件系统的感知能力。最新一代的引导加载程序,例如 GRUB,允许 Linux 从一些文件系统(从 Minix 到 Reise)上进行引导。
Linux 下没有conio.h 已解决 #include <stdio.h> //#include <conio.h> void main(){ char ch; for(;;){ // system("stty -echo"); ch = getch(); if(ch==27) break; if(ch==13) continue; putch(ch); } } Linux实现conio.h中的getch()功能 在windows下写C程序时有时会用到conio.h这个头文件中的getch()功能,即读取键盘字符但是不显示出来(without echo) 后来发现含有conio.h的程序在linux无法编译通过,因为linux没有这个头文件,今天突然发现可以用其他方法代替,贴出来 //in windows #include<stdio.h> #include<conio.h> int mian(){ char c; printf("input a char:"); c=getch(); printf("You have inputed:%c \n",c); return 0; } //in linux #include<stdio.h> int main(){ char c; printf("Input a char:"); system("stty -echo"); c=getchar(); system("stty echo"); printf("You have inputed:%c \n",c); return 0; } 这样就可以了,注:linux中stty -echo是不显示输入内容的意思
进程与线程的区别联系20160305 一。什么是进程 进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。 线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。 系统资源:线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行。 进程的作用和定义:进程是为了提高CPU的执行效率,减少因为程序等待带来的CPU空转以及其他计算机软硬件资源的浪费而提出来的。进程是为了完成用户任务所需要的程序的一次执行过程和为其分配资源的一个基本单位,是一个具有独立功能的程序段对某个数据集的一次执行活动。 二。线程和进程的区别: 1、 线程是进程的一部分,所以线程有的时候被称为是轻权进程或者轻量级进程。 2、 一个没有线程的进程是可以被看作单线程的,如果一个进程内拥有多个进程,进程的执行过程不是一条线(线程)的,而是多条线(线程)共同完成的。 3、 系统在运行的时候会为每个进程分配不同的内存区域,但是不会为线程分配内存(线程所使用的资源是它所属的进程的资源),线程组只能共享资源。那就是说,除了CPU之外(线程在运行的时候要占用CPU资源),计算机内部的软硬件资源的分配与线程无关,线程只能共享它所属进程的资源。 4、 与进程的控制表PCB相似,线程也有自己的控制表TCB,但是TCB中所保存的线程状态比PCB表中少多了。 5、 进程是系统所有资源分配时候的一个基本单位,拥有一个完整的虚拟空间地址,并不依赖线程而独立存在。 三。线程相对进程的优点 进程切换比线程切换开销大是因为进程切换时要切页表,而且往往伴随着页调度,因为进程的数据段代码段要换出去,以便把将要执行的进程的内容换进来。本来进程的内容就是线程的超集。而且线程只需要保存线程的上下文(相关寄存器状态和栈的信息)就好了,动作很小。 四。进程与程序的区别: 程序是一组指令的集合,它是静态的实体,没有执行的含义。而进程是一个动态的实体,有自己的生命周期。一般说来,一个进程肯定与一个程序相对应,并且只有一个,但是一个程序可以有多个进程,或者一个进程都没有也可以只有一个进程。除此之外,进程还有并发性和交往性。简单地说,进程是程序的一部分,程序运行的时候会产生进程。总结:线程是进程的一部分,进程是程序的一部分。
Linux系统信息查看命令大全(查看内存使用情况) 系统 # uname -a # 查看内核/操作系统/CPU信息 # head -n 1 /etc/issue # 查看操作系统版本 # cat /proc/cpuinfo # 查看CPU信息 # hostname # 查看计算机名 # lspci -tv # 列出所有PCI设备 # lsusb -tv # 列出所有USB设备 # lsmod # 列出加载的内核模块 # env # 查看环境变量 资源 # free -m # 查看内存使用量和交换区使用量 # df -h # 查看各分区使用情况 # du -sh <目录名> # 查看指定目录的大小 # grep MemTotal /proc/meminfo # 查看内存总量 # grep MemFree /proc/meminfo # 查看空闲内存量 # uptime # 查看系统运行时间、用户数、负载 # cat /proc/loadavg # 查看系统负载 磁盘和分区 # mount | column -t # 查看挂接的分区状态 # fdisk -l # 查看所有分区 # swapon -s # 查看所有交换分区 # hdparm -i /dev/hda # 查看磁盘参数(仅适用于IDE设备) # dmesg | grep IDE # 查看启动时IDE设备检测状况 网络 # ifconfig # 查看所有网络接口的属性 # iptables -L # 查看防火墙设置 # route -n # 查看路由表 # netstat -lntp # 查看所有监听端口 # netstat -antp # 查看所有已经建立的连接 # netstat -s # 查看网络统计信息 进程 # ps –ef #查看所有进程 # top #实时显示进程状态 用户 # w # 查看活动用户 # id <用户名> # 查看指定用户信息 # last # 查看用户登录日志 # cut -d: -f1 /etc/passwd # 查看系统所有用户 # cut -d: -f1 /etc/group # 查看系统所有组 # crontab -l # 查看当前用户的计划任务 服务 # chkconfig –list # 列出所有系统服务 # chkconfig –list | grep on # 列出所有启动的系统服务 程序 # rpm -qa # 查看所有安装的软件包 在Linux下查看内存我们一般用free命令
Linux系统中的进程通信方式和进程通信方式(20160305) Linux系统中的进程通信方式主要一下几种: 同一主机上的进程通信方式: Uinx进程间通信方式 A、管道(PIPE) 管道是一种办丧共的通信方式,数据只能单向流动,而且只能在具有亲缘关系(父子进程)的进程间使用。另外管道传送的是无格式的字节流,并且管道缓冲区的大小是有限的。 B、有名管道(FIFO)有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。 C、 信号(signal)信号是一种比较复杂的通信方式,用于通知接收进程某个时间已经发生 System v进程通信方式: A、信号量(semaphore)信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。 B、消息队列Messgar queue 消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少,管道只能承载无格式字节流以及缓冲区大小受限制等缺点。 C、 共享内存Shared memory共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最宽的IPC方式,他是针对其他进程间通信方式运行效率低而专门设计的,他往往与其他通信机制,如信号量,配合使用该,来实现进程间的同步和通信 网络主机间的进程通信方式: A、套接字socket套接字也是进程通信机制,与其他的通信机制不同,他可用于不同主机间的进程通信。 Linux系统中的线程通信方式主要以下几种方式; 线程间的通信目的主要是用于线程同步。所以线程没有像进程通信中的用于数据交换的通信机制。 锁机制: A、互斥锁提供了以排他方式防止书籍结构被并发修改的方法 B、条件变量 可以以原子烦人方式阻塞进程,直到某个特定条件为真为止。对条件的测试时在互斥锁的保护下进行的,条件变量始终与互斥锁一起使用。 C、 读写锁允许多个线程同时读共享数据,而对写操作是互斥的 信号量机制 包括无名线程信号量和命名线程信号量 信号机制 类似进程的信号处理。
linux内存池的原理和实现 (20160305) 一、 内存池的原理和实现 Linux采用的“按需调页”算法,支持三层也是存储管理策略。将每个用户进程4GB长度的虚拟内存划分成固定大小的页面。其中0支3GB是用户空间,3GB到4GB是内核空间,由所有进程共享,但只有内核态进程才能访问。 定义:内存池(MemoryPool)是一种内存分配方式。通常我们习惯直接使用new、malloc等API申请分配内存,这样做的缺点在于:由于所申请内存块的大小不定,当频繁使用时会造成大量的内存碎片并进而降低性能。 内存池:内存池是动态分配内存的设备,只能被特定的内核成分使用。拥有者通常不直接使用内存池,当普通内存分配失败时,内核才调用特定的内存池函数来提取内存池,以得到所需的额外内存。因此内存池只是内核内存的一个储备,用在特定的时刻。这样做的一个显著优点是尽量避免内存碎片看,是的内存分配效率得到提升。 Linux内核的内存管理采取狼藉分配机制,即初始化时将内存池按照内存的大小分成多个级别,8字节的倍数,每个几倍都预先分配20块内存。如果用户申请的内存单位元大于预定义的级别128字节,则直接调用malloc从堆中分配内存。而如果申请的内存小于128字节,则从最相近的内存大小中申请,如果改组的内存存储量小于一定的值,就会根据算法,再次从堆中申请一部分内存加入内存池,保证内存池中有一定量的内存可以使用。 内核内存池初始时从缓冲区申请一定量的内存块,需要使用时从池中顺序查找空闲内存块并返回申请者。回收时也是直接将内存插入池中,如果池已经满了,则直接释放。内存池没有动态增加大小的能力,如果内存池中的内存消耗殆尽,则只能直接从缓存区申请内存,内存池的容量不会随着使用量的增加而增加。 (c++)整个内存池其实就是一个单链表,表头指向第一个没有使用的节点,我们可以吧整个单链表想象成一段链条,调用方法new就是从链条的一端取走一个节点,调用的方法delete就是在链表的一端前面插入一个节点,新插入的节点就是链表的表头。 内存池有点:分析linux2.6内核中的内存池的数据结构及实现,研究表明,使用内存池可以减少内存碎片,提高分配速度,便于内存管理,防止内存泄漏。将内存池的分配方法应用与用户程序,可以提高效率。但是放分配的内存大小增大的时候,使用内存池分配内存的速度有可能会有所降低。
字符串函数汇总大全 比较函数: 1.intbcmp(const void *s1, const void *s2, int n); 功能:比较字符串s1和s2的前n个字节是否相等 说明:如果s1=s2或n=0则返回零,否则返回非零值。bcmp不检查NULL 2. int memcmp(void *buf1, void *buf2, unsigned int count); 功能:比较内存区域buf1和buf2的前count个字节。 说明:当buf1<buf2时,返回值<0 当buf1=buf2时,返回值=0 当buf1>buf2时,返回值>02.1. int memicmp(void *buf1, void *buf2, unsigned int count); 功能:比较内存区域buf1和buf2的前count个字节但不区分字母的大小写。 说明:memicmp同memcmp的唯一区别是memicmp不区分大小写字母。 当buf1<buf2时,返回值<0 当buf1=buf2时,返回值=0 当buf1>buf2时,返回值>0 3. int strcmp(char *s1,char * s2); 功能:比较字符串s1和s2。 说明: 当s1<s2时,返回值<0 当s1=s2时,返回值=0 当s1>s2时,返回值>0 3.1. int stricmp(char *s1,char * s2); 功能:比较字符串s1和s2,但不区分字母的大小写。 说明:strcmpi是到stricmp的宏定义,实际未提供此函数。 当s1<s2时,返回值<0 当s1=s2时,返回值=0 当s1>s2时,返回值>0 4. int strcmp(char *s1,char * s2,int n); 功能:比较字符串s1和s2的前n个字符。 说明: 当s1<s2时,返回值<0 当s1=s2时,返回值=0 当s1>s2时,返回值>0 4.1. int stricmp(char *s1,char * s2); 功能:比较字符串s1和s2,但不区分字母的大小写。 说明:strcmpi是到stricmp的宏定义,实际未提供此函数。 当s1<s2时,返回值<0 当s1=s2时,返回值=0 当s1>s2时,返回值>0 5. int strncmp(char *s1,char * s2,int n); 功能:比较字符串s1和s2的前n个字符。 说明: 当s1<s2时,返回值<0 当s1=s2时,返回值=0 当s1>s2时,返回值>0 5.1. int strnicmp(char *s1,char * s2,int n); 功能:比较字符串s1和s2的前n个字符但不区分大小写。 说明:strncmpi是到strnicmp的宏定义 当s1<s2时,返回值<0 当s1=s2时,返回值=0 当s1>s2时,返回值>0 拷贝移动函数: 1. void bcopy(const void *src, void *dest,int n); 功能:将字符串src的前n个字节复制到dest中 说明:bcopy不检查字符串中的空字节NULL,函数没有返回值。 2. void *memccpy(void *dest, void *src, unsigned char ch, unsigned int count); 功能:由src所指内存区域复制不多于count个字节到dest所指内存区域,如果遇到字符ch则停止复制。 说明:返回指向字符ch后的第一个字符的指针,如果src前n个字节中不存在ch则返回NULL。ch被复制。3. void *memcpy(void *dest, void *src, unsigned int count); 功能:由src所指内存区域复制count个字节到dest所指内存区域。 说明:src和dest所指内存区域不能重叠,函数返回指向dest的指针。 4. void *memmove(void *dest, const void *src,unsigned int count); 功能:由src所指内存区域复制count个字节到dest所指内存区域。 说明:src和dest所指内存区域可以重叠,但复制后src内容会被更改。函数返回指向dest的指针。 4.1. void movmem(void *src, void *dest,unsigned int count); 功能:由src所指内存区域复制count个字节到dest所指内存区域。 说明:src和dest所指内存区域可以重叠,但复制后src内容会被更改。函数返回指向dest的指针。 5. char *stpcpy(char *dest,char *src); 功能:把src所指由NULL结束的字符串复制到dest所指的数组中。 说明:src和dest所指内存区域不可以重叠且dest必须有足够的空间来容纳src的字符串。 返回指向dest结尾处字符(NULL)的指针。 5.1.char*strcpy(char *dest,char *src); 功能:把src所指由NULL结束的字符串复制到dest所指的数组中。 说明:src和dest所指内存区域不可以重叠且dest必须有足够的空间来容纳src的字符串。 返回指向dest的指针。 6. char *strdup(char *s); 功能:复制字符串s 说明:返回指向被复制的字符串的指针,所需空间由malloc()分配且可以由free()释放。 7. char *strncpy(char *dest, char *src, intn); 功能:把src所指由NULL结束的字符串的前n个字节复制到dest所指的数组中。 说明: 如果src的前n个字节不含NULL字符,则结果不会以NULL字符结束。 如果src的长度小于n个字节,则以NULL填充dest直到复制完n个字节。 src和dest所指内存区域不可以重叠且dest必须有足够的空间来容纳src的字符串。 返回指向dest的指针。 初始化函数: 1. void bzero(void *s, int n); 功能:置字节字符串s的前n个字节为零。 说明:bzero无返回值。 2.void *memset(void *buffer, int c, int count); 功能:把buffer所指内存区域的前count个字节设置成字符c。 说明:返回指向buffer的指针。 3. void setmem(void *buf, unsigned int count,char ch); 功能:把buf所指内存区域前count个字节设置成字符ch。 说明:返回指向buf的指针。 4. char *strset(char *s, char c); 功能:把字符串s中的所有字符都设置成字符c。 说明:返回指向s的指针。查找函数:1. void *memchr(void *buf, char ch, unsigned count); 功能:从buf所指内存区域的前count个字节查找字符ch。 说明:当第一次遇到字符ch时停止查找。如果成功,返回指向字符ch的指针;否则返回NULL。 2. char *strchr(char *s, char c); 功能:查找字符串s中首次出现字符c的位置 说明:返回首次出现c的位置的指针,如果s中不存在c则返回NULL。 3.intstrcspn(char *s1,char *s2); 功能:在字符串s1中搜寻s2中所出现的字符。 说明:返回第一个出现的字符在s1中的下标值,亦即在s1中出现而s2中没有出现的子串的长度。 4. char *strpbrk(char *s1, char *s2); 功能:在字符串s1中寻找字符串s2中任何一个字符相匹配的第一个字符的位置,空字符NULL不包括在内。 说明:返回指向s1中第一个相匹配的字符的指针,如果没有匹配字符则返回空指针NULL。 5. char *strstr(char *haystack, char *needle); 功能:从字符串haystack中寻找needle第一次出现的位置(不比较结束符NULL)。 说明:返回指向第一次出现needle位置的指针,如果没找到则返回NULL。增减颠倒函数: 1. char *strcat(char *dest,char *src); 功能:把src所指字符串添加到dest结尾处(覆盖dest结尾处的'\0')并添加'\0'。 说明:src和dest所指内存区域不可以重叠且dest必须有足够的空间来容纳src的字符串。 返回指向dest的指针。 2. char *strncat(char *dest,char *src,int n); 功能:把src所指字符串的前n个字符添加到dest结尾处(覆盖dest结尾处的'\0')并添加'\0'。 说明:src和dest所指内存区域不可以重叠且dest必须有足够的空间来容纳src的字符串。 返回指向dest的指针。 3. char *strtok(char *s, char *delim); 功能:分解字符串为一组标记串。s为要分解的字符串,delim为分隔符字符串。 说明:首次调用时,s必须指向要分解的字符串,随后调用要把s设成NULL。 strtok在s中查找包含在delim中的字符并用NULL('\0')来替换,直到找遍整个字符串。 返回指向下一个标记串。当没有标记串时则返回空字符NULL。 4.char*strrev(char *s); 功能:把字符串s的所有字符的顺序颠倒过来(不包括空字符NULL)。 说明:返回指向颠倒顺序后的字符串指针。 大小写函数: 1. char *strlwr(char *s); 功能:将字符串s转换为小写形式 说明:只转换s中出现的大写字母,不改变其它字符。返回指向s的指针。 2. char *strupr(char *s); 功能:将字符串s转换为大写形式 说明:只转换s中出现的小写字母,不改变其它字符。返回指向s的指针。 1. int strlen(char *s); 功能:计算字符串s的长度 说明:返回s的长度,不包括结束符NULL。
Linux安全攻略 如何才能让内存不再泄漏 Linux安全攻略 如何才能让内存不再泄漏 本文将介绍内存泄漏的检测方法以及现在可以使用的工具。针对内存泄漏的问题,本文提供足够的信息,使我们能够在不同的工具中做出选择。 内存泄漏 在此,谈论的是程序设计中内存泄漏和错误的问题,不过,并不是所有的程序都有这一问题。首先,泄漏等一些内存方面的问题在有的程序语言中是不容易发生的。这些程序语言一般都认为内存管理太重要了,所以不能由程序员来处理,最好还是由程序语言设计者来处理这些问题,这样的语言有Perl、Java等等。 然而,在一些语言(最典型的就是C和C++)中,程序语言的设计者也认为内存管理太重要,但必需由开发人员自己来处理。内存泄漏指的是程序员动态分配了内存,但是在使用完成后却忘了将其释放。除了内存泄漏以外,在开发人员自己管理内存的开发中,缓冲溢出、悬摆指针等其它一些内存的问题也时有发生。 问题缘何产生 为了让程序能够处理在编译时无法预知的数据占用内存的大小,所以程序必需要从操作系统实时地申请内存,这就是所谓的动态内存。这时候,就会出现程序申请到内存块并且使用完成后,没有将其归还给操作系统的错误。更糟的情况是所获取的内存块的地址丢失,从而系统无法继续识别、定位该内存块。还有其它的问题,比如试图访问已经释放的指针(悬摆指针),再如访问已经被使用了的内存(内存溢出)的问题。 后果不容忽视 对于那些不常驻内存的程序来说,由于执行过程很短,所以即使有漏洞可能也不会导致特别严重的后果。不过对于一些常驻内存的程序(比如Web服务器Apache)来说,如果出现这样的问题,后果将非常严重。因为有问题的程序会不断地向系统申请内存,并且不释放内存,最终可能导致系统内存耗尽而导致系统崩溃。此外,存在内存泄漏问题的程序除了会占用更多的内存外,还会使程序的性能急剧下降。对于服务器而言,如果出现这种情况,即使系统不崩溃,也会严重影响使用。 悬摆指针会导致一些潜在的隐患,并且这些隐患不容易暴发。它非常不明显,因此很难被发现。在这三种存在的问题形式中,缓冲溢出可能是最危险的。事实上,它可能会导致很多安全性方面的问题(一个安全的程序包含很多要素,但是最重要的莫过于小心使用内存)。正如上面所述,有时也会发生同一内存块被多次返还给系统的问题,这显然也是程序设计上的错误。一个程序员非常希望知道在程序运行的过程中,使用内存的情况,从而能够发现并且修正问题。 如何处理 现在已经有了一些实时监测内存问题的技术。内存泄漏问题可以通过定时地终止和重启有问题的程序来发现和解决。在比较新的Linux内核版本中,有一种名为OOM(Out Of Memory )杀手的算法,它可以在必要时选择执行Killed等程序。悬摆指针可以通过定期对所有已经返还给系统的内存置零来解决。解决内存溢出问题的方法则多种多样。 事实上,在程序运行时来解决这些问题,显然要麻烦得多,所以我们希望能够在开发程序时就发现并解决这些问题。下面介绍一些可用的自由软件。 工具一:垃圾回收器(GC) 在GCC(下载)工具包中,有一个“垃圾回收器(GC)”,它可以轻松检测并且修正很多的内存问题。目前该项目由HP的Hans-J.Boehm负责。 使用的技术 GC使用的是名为Boehm-Demers-Weiser的可以持续跟踪内存定位的技术。它的算法通过使用标准的内存定位函数来实现。程序使用这些函数进行编译,然后执行,算法就会分析程序的操作。该算法非常著名并且比较容易理解,不会导致问题或者对程序有任何干扰。 性能 该工具有很好的性能,故可以有效提高程序效率。其代码非常少并且可以直接在GCC中使用。 该工具没有界面,使用起来比较困难,所以要想掌握它还是要花一些工夫的。一些现有的程序很有可能无法使用这个编辑器进行配置。此外,为了让所有的调用能被捕获,所有的内存调用(比如malloc()和free())都必须要使用由GC提供的相应函数来代替。我们也可以使用宏来完成这一工作,但还是觉得不够灵活。 结论 如果你希望能够有跨平台(体系结构、操作系统)的解决方案,那么就是它了。
C++标准转换运算符const_cast C++标准转换运算符const_cast 前面讲了C++继承并扩展C语言的传统类型转换方式,最后留下了一些关于指针和引用上的转换问题,没有做详细地讲述。C++相比于C是一门面向对象的语言,面向对象最大的特点之一就是具有“多态性(Polymorphism)”。 要想很好的使用多态性,就免不了要使用指针和引用,也免不了会碰到转换的问题,所以在这一篇,就把导师讲的以及在网上反复查阅了解的知识总结一下。 C++提供了四个转换运算符: const_cast <new_type> (expression) static_cast <new_type> (expression) reinterpret_cast <new_type> (expression) dynamic_cast <new_type> (expression) 它们有着相同的结构,看起来像是模板方法。这些方法就是提供给开发者用来进行指针和引用的转换的。 其实我很早就想写这篇内容的,自己不断地查看导师发来的资料,也在网上不停地看相关的知识,却一直迟迟不能完全理解C++转换运算符的用法,倒是看了那些资料后先写了一篇传统转换方面的内容。虽然从字面上很好理解它们大致是什么作用,但是真正像使用起来,却用不知道他们具体的用途,只会不断的被编译器提醒Error。所以如果出现理解不到位或错误的地方,还希望前人或来者能够指正。 在我看来这些标准运算符的作用就是对传统运算符的代替,以便做到统一。就像我们用std::endl来输出换行,而不是'\n'。我会用代码来说明相应的传统转换可以如何这些标准运算符。当然,这这是大致的理解,在标准运算符上,编译器肯定有做更多的处理,特别是dynamic_cast是不能用传统转换方式来完全实现的。 在这一篇文章里,我会先讲讲我对const_cast运算符的理解。 const_cast (expression) const_cast转换符是用来移除变量的const或volatile限定符。对于后者,我不是太清楚,因为它涉及到了多线程的设计,而我在这方面没有什么了解。所以我只来说const方面的内容。 用const_cast来去除const限定 对于const变量,我们不能修改它的值,这是这个限定符最直接的表现。但是我们就是想违背它的限定希望修改其内容怎么办呢? 下边的代码显然是达不到目的的:const int constant = 10; int modifier = constant; 因为对modifier的修改并不会影响到constant,这暗示了一点:const_cast转换符也不该用在对象数据上,因为这样的转换得到的两个变量/对象并没有相关性。 只有用指针或者引用,让变量指向同一个地址才是解决方案,可惜下边的代码在C++中也是编译不过的:const int constant = 21; int* modifier = &constant // Error: invalid conversion from 'const int*' to 'int*' (上边的代码在C中是可以编译的,最多会得到一个warning,所在在C中上一步就可以开始对constant里面的数据胡作非为了) 把constant交给非const的引用也是不行的。const int constant = 21; int& modifier = constant; // Error: invalid initialization of reference of type 'int&' from expression of type 'const int' 于是const_cast就出来消灭const,以求引起程序世界的混乱。 下边的代码就顺利编译功过了:const int constant = 21; const int* const_p = &constant; int* modifier = const_cast<int*>(const_p); *modifier = 7; 传统转换方式实现const_cast运算符 我说过标:准转换运算符是可以用传统转换方式实现的。const_cast实现原因就在于C++对于指针的转换是任意的,它不会检查类型,任何指针之间都可以进行互相转换,因此const_cast就可以直接使用显示转换(int*)来代替:const int constant = 21; const int* const_p = &constant; int* modifier = (int*)(const_p); 或者我们还可以把他们合成一个语句,跳过中间变量,用const int constant = 21; int* modifier = (int*)(&constant); 替代const int constant = 21; int* modifier = const_cast<int*>(&constant); 为何要去除const限定 从前面代码中已经看到,我们不能对constant进行修改,但是我们可以对modifier进行重新赋值。 但是但是,程序世界真的混乱了吗?我们真的通过modifier修改了constatn的值了吗?修改const变量的数据真的是C++去const的目的吗? 如果我们把结果打印出来:cout << "constant: "<< constant <<endl; cout << "const_p: "<< *const_p <<endl; cout << "modifier: "<< *modifier <<endl; /** constant: 21 const_p: 7 modifier: 7 **/ constant还是保留了它原来的值。 可是它们的确指向了同一个地址呀: cout << "constant: "<< &constant <<endl; cout << "const_p: "<< const_p <<endl; cout << "modifier: "<< modifier <<endl; /** constant: 0x7fff5fbff72c const_p: 0x7fff5fbff72c modifier: 0x7fff5fbff72c **/ 这真是一件奇怪的事情,但是这是件好事:说明C++里是const,就是const,外界千变万变,我就不变。不然真的会乱套了,const也没有存在的意义了。 IBM的C++指南称呼“*modifier = 7;”为“未定义行为(Undefined Behavior)”。所谓未定义,是说这个语句在标准C++中没有明确的规定,由编译器来决定如何处理。 位运算的左移操作也可算一种未定义行为,因为我们不确定是逻辑左移,还是算数左移。 再比如下边的语句:v[i] = i++; 也是一种未定义行为,因为我们不知道是先做自增,还是先用来找数组中的位置。 对于未定义行为,我们所能做的所要做的就是避免出现这样的语句。对于const数据我们更要这样保证:绝对不对const数据进行重新赋值。 如果我们不想修改const变量的值,那我们又为什么要去const呢? 原因是,我们可能调用了一个参数不是const的函数,而我们要传进去的实际参数确实const的,但是我们知道这个函数是不会对参数做修改的。于是我们就需要使用const_cast去除const限定,以便函数能够接受这个实际参数。 #include <iostream>using namespace std;void Printer (int* val,string seperator = "\n"){cout << val<< seperator;}int main(void) {const int consatant = 20;//Printer(consatant);//Error: invalid conversion from 'int' to 'int*'Printer(const_cast<int *>(&consatant));return 0;} 出现这种情况的原因,可能是我们所调用的方法是别人写的。还有一种我能想到的原因,是出现在const对象想调用自身的非const方法的时候,因为在类定义中,const也可以作为函数重载的一个标示符。有机会,我会专门回顾一下我所知道const的用法,C++的const真的有太多可以说的了。 在IBM的C++指南中还提到了另一种可能需要去const的情况: #include <iostream>using namespace std;int main(void) {int variable = 21;int* const_p = &variable;int* modifier = const_cast<int*>(const_p);*modifier = 7cout << "variable:" << variable << endl;return 0;} /**variable:7**/ 我们定义了一个非const的变量,但用带const限定的指针去指向它,在某一处我们突然又想修改了,可是我们手上只有指针,这时候我们可以去const来修改了。上边的代码结果也证实我们修改成功了。 不过我觉得这并不是一个好的设计,还是应该遵从这样的原则:使用const_cast去除const限定的目的绝对不是为了修改它的内容,只是出于无奈。(如果真像我说是种无奈,似乎const_cast就不太有用到的时候了,但的确我也很少用到它)
Android的HAL层 关注嵌入式物联网行业及人才培养,每日更新,欢迎订阅及留言讨论~~~ 作者:倪键树,嵌入式物联网讲师。 Android的HAL层分析 1、Android的HAL是为了一些硬件提供商提出的“保护proprietary”的驱动程序而产生的东东,简而言之,就是为了避开Linuxkernal的GPL license的束缚。Android把控制硬件的动作都放到了user space中,而在kernel driver里面只有最简单的读写寄存器的操作,而完全去掉了各种功能性的操作(比如控制逻辑等),这些能够体现硬件特性的操作都放到了Android的HAL层,而Android是基于Aparch的license,因此硬件厂商可以只提供二进制代码,所以说Android知识一个开放的平台,并不是一个开源的平台。 2、Android的HAL的实现需要通过JNI(Java Native Intereface),JNI简单说就是java程序可以调用C/C++写的动态链接库,这样的话,HAL可以使用C/C++语言编写,效率更高。而Android的app可以直接调用.so,也可以通过app->app_manager->service(java)->service(jni)->HAL来调用。第二种方法看上去很复杂,但是更加符合android的架构结构。 3、 1)Kernel Driver 这里的kernel driver相对于linux真正的driver形式上是一样的,也提供open,read,write,ioctl,mmap等接口,就可以只作成往寄存器写操作,至于如何写,为什么写,这些工作都会在HAL层进行的,而一般用户是看不到这些代码的。这也是linux mainstream把android的kernel踢出去的原因,因为这些driver根本无法用在其他的linux平台上。 2)这一层就位于kernel之上的user space了,一般来说这里需要涉及的是两个结构体:hw_module_t和hw_device_t,第一个结构体是当这个hardware stub被load的时候(hw_get_module())提供的初始化操作,比如提供stub的open(module->methods->open())操作,而第二个结构体是提供该硬件stub具有的操作硬件的接口,,在jollen的mokoid工程里,主要提供打开和关闭led的操作,文件就是led.h和文件led.c,这两个文件最后会被编译成动态链接库,不如libled.so被放到/system/libs/hw/,当service调用hw_get_module(hardware/libhardware/hardware.c)时,会在/system/libs/hw/里面寻找对应的动态链接库,然后提供给service对应的操作接口。 Process and Lifecycle该优先权分五层。下面的列表显示优先权层次顺序: 前景进程 可视进程 服务进程 背景进程 空进程 4、学习不同的语言如何进行协作,尤其是如何实现垃圾回收和多线程。把一个虚拟机实现整合到用C/C++写的程序中。系统环境代指本地操作系统,它有自己的本地库和CPU指令集。本地程序(Native Applications)使用C/C++这样的本地语言来编写,被编译成只能在本地系统环境下运行的二进制代码,并和本地库链接在一起。本地程序和本地库一般地会依赖于一个特定的本地系统环境。只有在同一进程中调用本地代码时,使用JNI。 5、使用JAVA程序调用C函数来打印“、helloworld!",这个过程包含下面几步: 1.创建一个类(Hello World.java)声明本地方法。在Java代码中声明本地方法必须有“native”标识符,native修饰的方法,在java代码中只作为声明存在。在调用本地方法前,必须首先装载含有该方法的本地库。如HelloWorld.java中所示,置于static块中,在Java VM初始化一个类时,首先执行这部分代码,这可保证用本地方法前,装载了本地库。 2.使用javac编译源文件HelloWorld.java,产生HelloWorld.class。使用javah -jni来生成C头文件(HelloWorld.h),这个头文件里面包含了本地方法的函数原型。 3.用C代码写函数原型的实现。 4.把C函数实现编译成一个本地库,创建Hello-World.dll或者libHello-World.so。 5.使用java命令运行HelloWorld程序,类文件Hello-World.class和本地库在运行时被加载。 5、开发者使用JNI时最常问到的是JAVA和C/C++之间如何传递数据,以及数据类型之间如何互相映射。 Java_HelloWorld_print(JNIEnv*,jobject);该函数声明,接受两个参数,而对应的Java代码对该函数的声明没有参数,第一个参数是指向JNIEnv结构的指针;第二个函数,为HelloWorld对象自身,即this指针。JNIEnv是JNI核心数据库之一,地位非常崇高,所有对JNI的调用都要通过此结构体。请注意:jni.h文件必须被包含,该文件定义了JNI所有的函数声明和数据类型。生成本地库的名字,必须与System.loadLibrary("HelloWorld“);待装载库的名字相同。-MD:保证与Win32多线程C库连接(win分为静态、动态、动态多线程。。。C库)-LD:生成动态链接库 6、一对一映射和shared stubs的对比 shared stubs的主要优点是程序员不必在本地代码中写一大堆的stub函数,一对一映射的优点是高效,因为它不需要太多的附加的数据类型转换.
LWIP UDP 协议分析 一、udp.c实现的函数 1、void udp_input(struct pbuf *p, struct netif *inp) 说明:处理接收到的udp数据包。 参数:p数据包缓存区;inp网络接口。 2、err_t udp_send(struct udp_pcb *pcb, struct pbuf *p) 说明:发送udp包。这个函数直接调用udp_sendto()函数。 参数:pcb协议控制块;p数据包发送缓存区。 返回:ERR_OK发送成功;ERR_MEM发送溢出;ERR_RTE不能发送到指定ip;其它表示发送失败。 3、err_t udp_sendto(struct udp_pcb *pcb, struct pbuf *p, struct ip_addr *dst_ip, u16_t dst_port) 说明:发送udp包到指定ip地址。 参数:pcb协议控制块;p数据包发送缓存区;dst_ip目的ip地址;dst_port目的端口号。 4、err_t udp_sendto_if(struct udp_pcb *pcb, struct pbuf *p, struct ip_addr *dst_ip, u16_t dst_port, struct netif *netif) 说明:按照指定的网络接口和ip地址发送udp包。 参数:pcb协议控制块;p数据包发送缓存区;dest_ip目的ip地址;dst_port目的端口号,netif网络接口。 5、err_t udp_bind(struct udp_pcb *pcb, struct ip_addr *ipaddr, u16_t port) 说明:在协议控制块中绑定本地ip地址和本地端口号 参数:pcb协议控制块;ipaddr本地ip地址;port本地端口号。 返回:ERR_OK成功;ERR_USE已经被占用。 6、err_t udp_connect(struct udp_pcb *pcb, struct ip_addr *ipaddr, u16_t port) 说明:与远端udp主机建立连接。 参数:pcb所需连接的协议控制块;ipaddr远端ip地址;port远端端口号。 7、void udp_disconnect(struct udp_pcb *pcb) 说明:断开指定连接。 参数:pcb所需断开连接的协议控制块。 8、void udp_recv(struct udp_pcb *pcb, void (* recv)(void *arg, struct udp_pcb *upcb, struct pbuf *p, struct ip_addr *addr, u16_t port), void *rev_arg) 说明:设置接收到数据包时调用的回调函数及其参数。 参数:pcb协议控制块;recv回调函数名(地址);rev_arg回调函数参数。 这个函数直接修改pcb->recv和pcb->recv_arg的值。 9、void udp_remove(struct udp_pcb *pcb) 说明:删除指定udp协议控制块,从协议控制链表中删除并释放内存资源。 参数:pcb所要删除的协议控制块。 10、struct udp_pcb * udp_new(void) 说明:创建udp协议控制块,并不分配资源。 返回:协议控制块指针,指向NULL。 - UDP functions err_t udp_bind(struct udp_pcb *pcb, struct ip_addr *ipaddr, u16_t port) 函 数遍历整个UDP PCB链表,以排除在没有设置REUSE_ADDR或者REUSE_PORT标志的情况下绑定到一个以相同port绑定的pcb或者以相同port及ip 绑定的pcb。如果需要绑定的port无效,则分配最小可用port。如果该pcb未在原来PCB链表中,则加入链表。具体流程参看流程图。 err_t udp_connect(struct udp_pcb *pcb, struct ip_addr *ipaddr, u16_t port) 连接到远程端口。如果还未分配本地port,则分配一个空闲port。然后将一下两种地址绑定类型进行转换: a. *.local_port foreign_ip.foreign_port: 调用ip_router确定本地ip。 b. *.* *.foreign_port: 转换为 *.local_port *.foreign_port err_t udp_sendto(struct udp_pcb *pcb, struct pbuf *p, struct ip_addr *dst_ip, u16_t dst_port) 该函数借用当前的pcb调用udp_send发送UDP包,完成后,回复原来pcb内容。 err_t udp_send(struct udp_pcb *pcb, struct pbuf *p) 如 果pcb未绑定,则调用udp_bind获取一个可用的port绑定之。然后构造UDP包,查找能够到达remote_ip的router接口,如果有必 要,将该接口的本地ip作为UDP的src ip。如果UDP需要校验和,则调用inet_chksum_pseudo函数,计算校验和。最后调用ip_output_if将UDP包传送到下层IP 层发送。 void udp_input(struct pbuf *p, struct netif *inp) 该函数接受来自ip层的UDP包。将所有PCB都遍历,如果有多个绑定,则给每一个进程复制一份数据报,实际调用pcb->recv()。详细流程参看流程图。 其中的数据报的地址绑定匹配优先级和协议上的略有区别: Local Foreign local_ip(*).local_port foreign_ip(*).foreign_port local_ip(*).local_port *.*
lwIP源代码分析1-------内存管理模块的分析 因为lwIP主要用于嵌入式系统,内存要求比较高,所以要对那些小对象进行池化之类的处理来加快分配速度,减少内存碎片产生。 lwIP中主要有memp.h, memp_std.h, memp.c, mem.h, mem.c几个类组成了内存管理模块。 memp.c 动态内存池管理器, lwip拥有各种不同的内存池来为各个模块的小对象分配内存。 一个内存池主要有name,description,number(内存池里的内存节点的数目)和size(内存池里的内存节点的大小) 内存节点的struct描述: struct memp { struct memp *next; // 指向下一个内存节点的指针 #if MEMP_OVERFLOW_CHECK // 如果进行内存溢出检测, const char *file; // 发生溢出时调用函数的文件名 int line; // 发生溢出时调用函数的行号 #endif /* MEMP_OVERFLOW_CHECK */ }; lwip中各类内存池的描述在memp_std.h文件中: 该文件主要由三种内存池组成: 1. LWIP_MEMPOOL(标准的内存池) 2. LWIP_MALLOC_MEMPOOL(被mem.c中mem_malloc使用的内存池,需要在lwippools.h自定义不同大小的内存池,稍后会讲到) 3. LWIP_PBUF_MEMPOOL(PBUF的内存池)。其中2,3两种内存池均是通过调用第一种内存池来实现的,所以我们来看下第一种内存池,也就是lwip的标准内存池。 我们来看一个TCP_PCB的内存池定义: LWIP_MEMPOOL(TCP_PCB, MEMP_NUM_TCP_PCB, sizeof(struct tcp_pcb), "TCP_PCB") 其中TCP_PCB是内存池名称,MEMP_NUM_TCP_PCB是节点的数目,sizeof(struct tcp_pcb)是每个节点大小, "TCP_PCB"是内存池的描述。 而在memp.c中通过不断定义这些描述(宏)来保存内存池中各种不同的信息到相应的结构中去: 1. 通过各种内存池的唯一的名称定义一个enum,并且在最后插入MEMP_MAX来得到内存池的总数。 typedef enum { #define LWIP_MEMPOOL(name,num,size,desc) MEMP_##name, #include "lwip/memp_std.h" MEMP_MAX } memp_t; 2.定义数组memp_size[MEMP_MAX]存放每个内存池的节点大小。 /** This array holds the element sizes of each pool. */ #if !MEM_USE_POOLS && !MEMP_MEM_MALLOC static #endif const u16_t memp_sizes[MEMP_MAX] = { #define LWIP_MEMPOOL(name,num,size,desc) LWIP_MEM_ALIGN_SIZE(size), #include "lwip/memp_std.h" }; 3. 定义数组memp_num[MEMP_MAX]存放每个内存池的节点数目。 /** This array holds the number of elements in each pool. */ static const u16_t memp_num[MEMP_MAX] = { #define LWIP_MEMPOOL(name,num,size,desc) (num), #include "lwip/memp_std.h" }; 4. 定义数组memp_num[MEMP_MAX]存放每个内存池的描述。 /** This array holds a textual description of each pool. */ #ifdef LWIP_DEBUG static const char *memp_desc[MEMP_MAX] = { #define LWIP_MEMPOOL(name,num,size,desc) (desc), #include "lwip/memp_std.h" }; #endif /* LWIP_DEBUG */ /** This array holds a textual description of each pool. */ #ifdef LWIP_DEBUG static const char *memp_desc[MEMP_MAX] = { #define LWIP_MEMPOOL(name,num,size,desc) (desc), #include "lwip/memp_std.h" }; #endif /* LWIP_DEBUG */ 5. 定义数组memp_memory,这个数组所占有的内存就是所有内存池用的实际内存块。 数组长度为各个池的需要的内存总和,即每个内存池+ ( (num) * (MEMP_SIZE + MEMP_ALIGN_SIZE(size) ) ), 其中num为节点数目,MEMP_SIZE为每个节点结构所需要的额外内存,size为内存池提供给上层用户的内存大小。 这些大小都是要对齐的,还有个MEM_ALIGNMENT - 1是为了对齐数组memp_memory,因为定义的memp_memory是不 一定对齐的,最坏的情况就要MEM_ALIGNMENT - 1。 /** This is the actual memory used by the pools. */ static u8_t memp_memory[MEM_ALIGNMENT - 1 #define LWIP_MEMPOOL(name,num,size,desc) + ( (num) * (MEMP_SIZE + MEMP_ALIGN_SIZE(size) ) ) #include "lwip/memp_std.h" ]; 内存对齐的概念我想大家都懂,这儿就不多做解释了,在lwip的mem.h头文件定义了下面两个宏来进行size和内存地址的对齐。 #ifndef LWIP_MEM_ALIGN_SIZE #define LWIP_MEM_ALIGN_SIZE(size) (((size) + MEM_ALIGNMENT - 1) & ~(MEM_ALIGNMENT-1)) #endif #ifndef LWIP_MEM_ALIGN #define LWIP_MEM_ALIGN(addr) ((void *)(((mem_ptr_t)(addr) + MEM_ALIGNMENT - 1) & ~(mem_ptr_t)(MEM_ALIGNMENT-1))) #endif mem.c: 如果使用pool的话就是运用内存池管理器进行内存管理。 否则的话主要实现了一个实现了一个类似于C malloc的heap的轻量级内存管理器。 下面开始讲述每个函数的功能,工作原理和要点: memp.h memp.c memp_init(): 初始化每个内存池,通过struct memp作为节点以单链表的形式串联起来。 memp_malloc(): 从相应的内存池中取出内存。 memp_free(): 归还内存到相应的内存池,需要检测溢出。 memp_overflow_init(): 溢出检测的初始化,主要是在用户申请的内存区域前后插入一些填充符0xcd。 memp_overflow_check_element(): 节点溢出检测,在用户申请的内存区域前后检查填充符0xcd是否被覆盖,是则溢出。 memp_overflow_check_all(): 所有节点溢出检测,安全,但是相对慢。 一个内存池的一个结点的内存分配情况: ------------------------------------------------------------------------------------------------------------------------------------ struct memp所占用的内存 + check_overflow_before_region + 用户申请的内存 +check_overflow_after_region ------------------------------------------------------------------------------------------------------------------------------------ memp_memory的内存分配情况: ------------------------------------------------------------------------------------------------------------------------------------ 对齐地址的内存空间(0->MEM_ALIGNMENT - 1) + 各个内存池所占内存 ------------------------------------------------------------------------------------------------------------------------------------ memp_std.h: 防止多次定义错误,因为memp.c使用#include memp_std.h并且定义以下的宏的技巧,所以每次需要undef这些宏. mem.h mem.c #if MEM_USE_POOLS: mem_malloc: 从相应的提供给malloc用的不同size的pool中取一个合适的一个,并且将这个pool的size保存到相应的结构memp_malloc_helper中。 可以在lwippools.h自定义pool,详情请见代码memp_std.h和memp.h文件的注释: LWIP_MALLOC_MEMPOOL_START LWIP_MALLOC_MEMPOOL(20, 256) LWIP_MALLOC_MEMPOOL(10, 512) LWIP_MALLOC_MEMPOOL(5, 1512) LWIP_MALLOC_MEMPOOL_END mem_free: 释放到相应的内存池中, 取出memp_malloc_helper中的size放入相应的内存池中去。 #else /* MEM_USE_POOLS: */ 实现了一个类似于C malloc的heap的轻量级内存管理器。 /* 采用索引双链表的形式来管理heap */ /* 节点结构 */ struct mem { /** index (-> ram[next]) of the next struct */ mem_size_t next; // 下一个struct的索引 /** index (-> ram[next]) of the next struct */ mem_size_t prev; // 上一个struct的索引 /** 1: this area is used; 0: this area is unused */ u8_t used; // 是否被使用 }; mem_init: 初始化heap,并且设置lfree为最低地址的空闲节点,以加快搜索速度。 mem_malloc: 从heap上分配用户所需size的内存。从lfree节点开始搜索空闲并且可以容纳SIZEOF_STRUCT_MEM + size的空间, 如果找到这样的节点mem,则分为两种情况,如果可容纳的空间可以容纳SIZEOF_STRUCT_MEM + size + (SIZEOF_STRUCT_MEM + MIN_SIZE_ALIGNED),则说明可以新建一个空闲节点,于是将建立新的空闲节点并且插入到mem和mem->next之间,将mem的used标志为1,否则的话不建立节点,只是设置mem的used标志为1。 mem_free: 释放的时候会改used标志为0,表示空闲,并且调用plug_holes()函数来进行前后合并空闲空间。 mem_calloc: 使用mem_malloc申请size×count大小的地址,并且初始化清0所分配的内存。 mem_realloc: 重新分配new_size的内存,lwip目前不支持扩大size的情况。 如果当前节点后面的那个节点也是free的话,那么可以合并多余剩下的节点和后面的那个free节点。 如果不是空闲的则看看剩余的空间是否足够新建一个空闲节点,能则建之,并将其插入到链表中去。
extern C的作用详解 extern "C"的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言的进行编译,而不是C++的。由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般之包括函数名。 这个功能十分有用处,因为在C++出现以前,很多代码都是C语言写的,而且很底层的库也是C语言写的,为了更好的支持原来的C代码和已经写好的C语言库,需要在C++中尽可能的支持C,而extern "C"就是其中的一个策略。 这个功能主要用在下面的情况: 1、C++代码调用C语言代码 2、在C++的头文件中使用 3、在多个人协同开发时,可能有的人比较擅长C语言,而有的人擅长C++,这样的情况下也会有用到 给出一个我设计的例子: moduleA、moduleB两个模块,B调用A中的代码,其中A是用C语言实现的,而B是利用C++实现的,下面给出一种实现方法: //moduleA头文件 #ifndef __MODULE_A_H //对于模块A来说,这个宏是为了防止头文件的重复引用 #define __MODULE_A_H int fun(int, int); #endif //moduleA实现文件moduleA.C //模块A的实现部分并没有改变 #include"moduleA" int fun(int a, int b) { return a+b; } //moduleB头文件 #idndef __MODULE_B_H //很明显这一部分也是为了防止重复引用 #define __MODULE_B_H #ifdef __cplusplus //而这一部分就是告诉编译器,如果定义了__cplusplus(即如果是cpp文件, extern "C"{ //因为cpp文件默认定义了该宏),则采用C语言方式进行编译 #include"moduleA.h" #endif … //其他代码 #ifdef __cplusplus } #endif #endif //moduleB实现文件 moduleB.cpp //B模块的实现也没有改变,只是头文件的设计变化了 #include"moduleB.h" int main() { cout<#ifndef __INCvxWorksh 、#define __INCvxWorksh、#endif"(即上面代码中的蓝色部分)的作用是为了防止该头文件被重复引用 那么 #ifdef __cplusplus (其中__cplusplus是cpp中自定义的一个宏!!!) extern "C"{ #endif #ifdef __cplusplus } #endif 的作用是什么呢? extern "C"包含双重含义,从字面上可以知道,首先,被它修饰的目标是"extern"的;其次,被它修饰的目标代码是"C"的。 被extern "C"限定的函数或变量是extern类型的 extern是C/C++语言中表明函数和全局变量的作用范围的关键字,该关键字告诉编译器,其申明的函数和变量可以在本模块或其他模块中使用。 记住,下面的语句: extern int a; 仅仅是一个变量的声明,其并不是在定义变量a,并未为a分配空间。变量a在所有模块中作为一种全局变量只能被定义一次,否则会出错。 通常来说,在模块的头文件中对本模块提供给其他模块引用的函数和全局变量以关键字extern生命。例如,如果模块B要引用模块A中定义的全局变量和函数时只需包含模块A的头文件即可。这样模块B中调用模块A中的函数时,在编译阶段,模块B虽然找不到该函数,但并不会报错;它会在链接阶段从模块A编译生成的目标代码中找到该函数。 extern对应的关键字是static,static表明变量或者函数只能在本模块中使用,因此,被static修饰的变量或者函数不可能被extern C修饰。 被extern "C"修饰的变量和函数是按照C语言方式进行编译和链接的:这点很重要!!!! 上面也提到过,由于C++支持函数重载,而C语言不支持,因此函数被C++编译后在符号库中的名字是与C语言不同的;C++编译后的函数需要加上参数的类型才能唯一标定重载后的函数,而加上extern "C"后,是为了向编译器指明这段代码按照C语言的方式进行编译 未加extern "C"声明时的链接方式: //模块A头文件 moduleA.h #idndef _MODULE_A_H #define _MODULE_A_H int foo(int x, int y); #endif 在模块B中调用该函数: //模块B实现文件 moduleB.cpp #include"moduleA.h" foo(2,3); 实际上,在链接阶段,连接器会从模块A生成的目标文件moduleA.obj中找_foo_int_int这样的符号!!!,显然这是不可能找到的,因为foo()函数被编译成了_foo的符号,因此会出现链接错误。 常见的做法可以参考下面的一个实现: moduleA、moduleB两个模块,B调用A中的代码,其中A是用C语言实现的,而B是利用C++实现的,下面给出一种实现方法: //moduleA头文件 #ifndef __MODULE_A_H //对于模块A来说,这个宏是为了防止头文件的重复引用 #define __MODULE_A_H int fun(int, int); #endif //moduleA实现文件moduleA.C //模块A的实现部分并没有改变 #include"moduleA" int fun(int a, int b) { return a+b; } //moduleB头文件 #idndef __MODULE_B_H //很明显这一部分也是为了防止重复引用 #define __MODULE_B_H #ifdef __cplusplus //而这一部分就是告诉编译器,如果定义了__cplusplus(即如果是cpp文件, extern "C"{ //因为cpp文件默认定义了该宏),则采用C语言方式进行编译 #include"moduleA.h" #endif … //其他代码 #ifdef __cplusplus } #endif #endif //moduleB实现文件 moduleB.cpp //B模块的实现也没有改变,只是头文件的设计变化了 #include"moduleB.h" int main() { cout< } 4. 不可以将extern "C" 添加在函数内部 5. 如果函数有多个声明,可以都加extern "C", 也可以只出现在第一次声明中,后面的声明会接受第一个链接指示符的规则。 6. 除extern "C", 还有extern "FORTRAN" 等。
《LwIP协议栈源码详解——TCP/IP协议的实现》TCP终结与小结 TCP还有最后一点东西需要扫尾。现在我们要跳出tcp_process函数,继续回到tcp_input中,前面说过,tcp_input接收IP层递交上来的数据包,并根据数据包查找相应TCP控制块,并根据相关控制块所处的状态调用函数tcp_timewait_input、tcp_listen_input或tcp_process进行处理。如果是调用的前两个函数,则tcp_input在这两个函数返回后就结束了,但若调用的是tcp_process函数,则函数返回后,tcp_input还要进行许多相应的处理。 要继续往下讲就得看看一个很重要的全局变量recv_flags,前面说TCP全局变量的时候也简单说到过。这个变量与控制块中的flags字段相似,都是用来描述当前TCP控制块的所处状态的。flags字段可以设置的各个标志位及其意义如下宏定义所示: #define TF_ACK_DELAY (u8_t)0x01U // 延迟回复ACK包 #define TF_ACK_NOW (u8_t)0x02U // 立即发送ACK包 #define TF_INFR (u8_t)0x04U // 处于快速重传状态 #define TF_FIN (u8_t)0x20U // 本地上层应用关闭连接 #define TF_NODELAY (u8_t)0x40U // 禁止Nagle算法禁止 #define TF_NAGLEMEMERR (u8_t)0x80U // 发送缓存空间不足 上面的各个字段基本都已经涉及过了,再来看看全局变量recv_flags可以设置的各个标志位及其意义,如下宏定义所示: #define TF_RESET (u8_t)0x08U // 接收到RESET包 #define TF_CLOSED (u8_t)0x10U // 在LAST_ACK状态收到ACK包,连接成功关闭 #define TF_GOT_FIN (u8_t)0x20U // 接收到FIN包 为什么要用两个字段来描述相应TCP控制块的状态呢,不是很明了?个人理解有两个原因:一是由于控制块中的flags字段本身是8位的,若以每位描述一种状态,则不足以描述上面的9种状态;二是上面的9种描述状态很明显可以分为两类,第一类的6种与TCP的数据包处理密切相关,第二类的3种与TCP的状态转换密切相关。 在tcp_input每次调用tcp_process之前,recv_flags都会被初始化为0,在tcp_process的处理中,相关控制块在完成状态转换后,该全局变量与状态转换相关的位则会被置位,在函数返回到tcp_input后,tcp_input还会根据相应设置好的recv_flags值对控制块做后续处理。 if (recv_flags & TF_RESET) { // TF_RESET标志表示接收到了对端的RESET包 TCP_EVENT_ERR(pcb->errf, pcb->callback_arg, ERR_RST); // 若注册了回调函数 // 则调用该函数通知上层 tcp_pcb_remove(&tcp_active_pcbs, pcb); // 将控制块从链表中删除 memp_free(MEMP_TCP_PCB, pcb); // 释放控制块内存空间 } else if (recv_flags & TF_CLOSED) { // TF_CLOSED表示服务器成功关闭连接 tcp_pcb_remove(&tcp_active_pcbs, pcb); // 将控制块从链表中删除 memp_free(MEMP_TCP_PCB, pcb); // 释放控制块内存空间 } else { err = ERR_OK; if (pcb->acked > 0) { // 如果收到的数据包确认了unacked队列中的数据 TCP_EVENT_SENT(pcb, pcb->acked, err); //则可调用自定义的函数发送数据包 } if (recv_data != NULL) { // 若成功的接收了数据包中的数据 if(flags & TCP_PSH) { //全局变量 flags保存的是TCP头部中的标志字段 recv_data->flags |= PBUF_FLAG_PUSH; // 将数据包pbuf字段 } // 设置PUSH标志 TCP_EVENT_RECV(pcb, recv_data, ERR_OK, err); // 调用自定义的函数接收数据 if (err != ERR_OK) { //若上层接收数据失败 pcb->refused_data = recv_data; //用控制块refused_data字段暂存数据 } } // if (recv_data != NULL) if (recv_flags & TF_GOT_FIN) { // 如果接收到FIN包 TCP_EVENT_RECV(pcb, NULL, ERR_OK, err); // 调用自定义的函数接收数据 } if (err == ERR_OK) { //若处理正常则 tcp_output(pcb); // 试图往外发数据包 } } //else 有两个地方需要说一下,首先是函数tcp_pcb_remove,源码如下所示,代码里面比较重要的两个函数是TCP_RMV和tcp_pcb_purge,这里就不再仔细说明了。 void tcp_pcb_remove(struct tcp_pcb **pcblist, struct tcp_pcb *pcb) { TCP_RMV(pcblist, pcb); // 从某链表上移除PCB控制块 tcp_pcb_purge(pcb); // 清空控制块的数据缓冲队列,释放内存空间 if (pcb->state != TIME_WAIT && //如果该控制块上有被延迟的ACK,则立即发送 pcb->state != LISTEN && pcb->flags & TF_ACK_DELAY) { pcb->flags |= TF_ACK_NOW; tcp_output(pcb); } pcb->state = CLOSED; // 置状态 } 还有个需要注意的地方是回调函数的调用:如上面TCP_EVENT_XXX所示。在实际应用程序中,我们可以通过回调函数的方式与LWIP内核交互,在初始化一个PCB控制块的时候,可以设定控制块中相应函数指针字段的初始值,包括sent、recv、connected、accept、poll、errf等。在内核处理中,会在TCP_EVENT_XXX处调用我们预先注册的函数,从而完成应用程序与协议栈之间的交互。关于应用程序与协议栈间接口的问题,又是一个庞大的工程,也是我们以后会继续讨论的重点。 到这里,TCP部分就基本讲完了,当然TCP层中还有一些东西没有讲到,如tcp_write等函数。TCP层学习的关键是了解整个TCP层运行的机制,在这个基础上去阅读源代码,应该不会存在什么的问题的。从《TCP建立与断开》到《TCP终结与小结》,可以看出TCP的篇幅实在是太多了,实际源代码也是如此,从代码量上看,TCP部分占了整个协议栈代码量的一半左右。注意,一般讲TCP协议时都会谈到UDP,但在这里我还不想涉及UDP协议,原因是在一般嵌入式产品中,都需要提供有效可靠的网络服务,而UDP的本质特点让其无法满足这一要求。所以,如果真是要将LWIP用于我们的产品中,则使用的基本是TCP协议,在后续的讲解中,我们会看到应用程序怎样利用LWIP建立一个Web服务器,这使得我们可以远程的通过http访问我们的设备了。接下来要说的就是协议栈与应用程序间的接口问题了。
修改LWIP里面一些可以节省内存的地方 移植了LWIP,发现内存不够用了。 1.MEM_SIZE 在OPT.H里面 2.LWIP里每个线程的堆栈空间的大小和线程数目。这个影响是挺大的,往往新增一个带1K堆栈的线程,SRAM的占用可不止多了1K。 3.MEMP_NUM_NETCONN 最多的SOCKET的数量,在OPT.H里面
常用的shell命令 2.Linux shell命令 2.1date 作用:获取或者设置日期 用法:date [选项] 显示时间格式(以+开头,后面接格式) 举例: (1)以固定格式显示时间:date + “%Y%m%d%H” (2)显示明天的日期:date -d “tomorrow” +”%Y-%m-%d” (3)显示前天的日志:date -d “1 days ago” +”%Y-%m-%d” 2.2cut 作用:从输入文件或者命令的输出中析取出各种域 用法:cut –c{字符串范围} –d{字段间分割符} –f{字段索引编号} 举例: (1)查看在线用户:who | cut –c1-8 (2)从系统文件/etc/passwd中获取用户名列表:cut –d: -f1 /etc/passwd 2.3.paste 作用:将多个域合并 用法:cut –d{字段间分割符} 举例: paste –d’:’ filename1 filename2 2.4.sort 作用:排序 用法: -t 指定分隔符, 默认为空格 -r 以降序来排列 -u 去掉重复行 -d 以字典序来排列,包括字母,数字,符号等 -n 以数字序来排列 +positon1 -positon2 从第position1 字段到position2字段,包括position1,不包括position2。positon1从0开始。 -k KeyDefinition 指定排序关键字。KeyDefinition 选项的格式为: [ FStart [ .CStart ] ] [ Modifier ] [ , [ FEnd [ .CEnd ] ][ Modifier ] ] 排序关键字包括任何以 FStart 变量指定的字段和 CStart 变量指定的列开头的字符及以 FEnd 变量指定的字段和 CEnd 变量指定的列结束的字符。Modifier 变量的值能够是 b、d、f、i、n 或 r。修饰符和同一字母的标志等价。 举例: (1)对/etc/passwd 文件的第三项进行排序(userid): sort -t: -k 3n /etc/passwd (2)基于ip地址对/etc/hosts文件排序: sort -t . -k 1,1n -k 2,2n -k 3,3n -k 4,4n /etc/hosts 2.5.uniq 作用:对数据进行去重 用法:先要对数据进行排序,然后再去重 -d:输出重复行 -c:对数据进行计数 举例: 找出/etc/passwd文件中的重复用户名:sort /etc/passwd | cut –f1 –d: | uniq –d 2.6.sed 作用:编辑数据 用法: sed command file -n选项:指定行号或者行号范围,如果未指定,表示任意一行;用p表示打印 举例: sed –n ‘-1,2p’ file.txt #显示前两行 sed –n ‘/UNIX/p’ filename #显示包含“UNIX“的行 d命令:删除数据 举例: sed ‘1,2d’ intro #删掉前两行 sed ‘/UNIX/d’ intro #删掉包含“UNIX“的行 s命令:替换 举例 Sed ‘s/Unix/UNIX/g’ intro > temp #将文件intro中“Unix“替换为UNIX, 并将结果保存到temp文件中 2.7.vi (1) 光标移动 nG:光标移至第n行首 n+:光标下移n行 n-:光标上移n行 n$:光标移至第n行尾 (2) 删除命令 do:删至行首 d$:删至行尾 ndd:删除当前行及其后n-1行 :n1,n2 d:将n1行到n2行之间的内容删除 (3)搜索替换 /pattern:从光标开始处向文件尾搜索pattern ?pattern:从光标开始处向文件首搜索pattern :s/p1/p2/g:将当前行中所有p1均用p2替代 :n1,n2s/p1/p2/g:将第n1至n2行中所有p1均用p2替代 :g/p1/s//p2/g:将文件中所有p1均用p2替换 (4)复制粘贴 yy:复制当前行,将光标移动到某一行,p粘贴 n1,n2 co n3:n1为起始行,n2为结束行,n3为粘贴行(其中,co是copy的简称,也可以直接用copy代替) (5)文件保存与退出 :w :保存当前文件 :q!:不保存文件并退出vi (6)多个文件之间的复制粘贴 先在开始处做标志mk (注:m是做标注的命令,语法是m[字母],[字母]为该行的标注) 然后在末尾用”ay k (光标自动回到开始处,此时已经把你的内容放到缓冲区了) 其中a表示缓冲区a 然后用ex转义到你的文件B中,然后用”ap命令粘贴就行 即:vi 1.txt 2.txt mk“ay kex 2.txt ”ap (7)vi切分窗口 :split two.c:打开另一个窗口并用该窗口编辑另一个指定的文件 :vsplit:垂直分割窗口 CTRL-W:跳转窗口 (8)比较两个文件的不同之处 vimdiff 1.txt 2.txt 2.8.diff和patch (1)diff命令 功能是用来比较两个文件的不同,然后记录下来,也就是所谓的 diff 补丁 选项 -r 是一个递归选项 -u 选项以统一格式创建补丁文件,这种格式比缺省格式更紧凑些。 (2)patch命令 patch 就是利用 diff 制作的补丁来实现源文件(夹) 和目的文件(夹) 的转换。 选项: -p0 选项要从当前目录查找目的文件(夹) -p1 选项 要忽略掉第一层目录,从当前目录开始查找。 -E 选项说明如果发现了空文件,那么就删除它 -R 选项说明在补丁文件中的 “ 新 ” 文件和 “ 旧 ” 文件现在要调换过来了(实际上就是给新版本打补丁,让它变成老版本) 举例: 单个文件: diff –uN from-file to-file >to-file.patch patch –p0 < to-file.patch patch –RE –p0 < to-file.patch 多个文件: diff –uNr from-docu to-docu >to-docu.patch patch –p1 < to-docu.patch patch –R –p1 <to-docu.patch 2.9.find find pathname -options [-print -exec -ok ...] (1)含义解释 pathname: find命令所查找的目录路径。例如用.来表示当前目录,用/来表示系统根目录。 -print: find命令将匹配的文件输出到标准输出。 -exec: find命令对匹配的文件执行该参数所给出的shell命令。相应命令的形式为’command’ { } \;,注意{ }和\;之间的空格。 -ok: 和-exec的作用相同,只不过以一种更为安全的模式来执行该参数所给出的shell命令,在执行每一个命令之前,都会给出提示,让用户来确定是否执行。 (2) 选项 -name:按照文件名查找文件 -mtime -n +n:按照文件的更改时间来查找文件, – n表示文件更改时间距现在n天以内,+ n表示文件更改时间距现在n天以前 -newer file1 ! file2:查找更改时间比文件file1新但比文件file2旧的文件 -type:查找某一类型的文件,诸如:b – 块设备文件,d – 目录,c – 字符设备文件,p – 管道文件,l – 符号链接文件,f – 普通文件 -depth:在查找文件时,首先查找当前目录中的文件,然后再在其子目录中查找 (3) xargs find命令的-exec选项处理匹配到的文件时, find命令将所有匹配到的文件一起传递给exec执行。但有些系统对能够传递给exec的命令长度有限制,这样在find命令运行几分钟之后,就会出现溢出错误。错误信息通常是“参数列太长”或“参数列溢出”。这就是xargs命令的用处所在,特别是与find命令一起使用. (4) 举例 用ls -l命令列出所匹配到的文件,可以把ls -l命令放在find命令的-exec选项中 find . -type f -exec ls -l { } \; 查找系统中所有文件长度为0的普通文件,并列出它们的完整路径; find / -type f -size 0 -exec ls -l { } \; 用grep命令在所有的普通文件中搜索hostname这个词: find . -type f -print | xargs grep “hostname” 2.10.数学计算 (1) expr expr可用于计算各种表达式的值,可包含字符串,逻辑运算和函数(但没有指数或者对数函数)。 加法:answer=`expr $c + $d` 减法:answer=`expr $c – $d` 乘法:answer=`expr $c \* $d` 减法:answer=`expr $c / $d` (2)shell内部运算 shell自身可进行数学计算,使用shell内部函数的好处是,不用调用外部程序,从而减少外部内存使用量。 加法:answer=$(($c + $d)) 减法:answer=$(($c – $d)) 乘法:answer=$(($c * $d)) 减法:answer=$(($c / $d)) (3)bc计算器 前面两种数学计算方法均是集中在整数计算上,而bc是一个实现任意精度的计算器,可通过设置scale(小数点后十进制位数),达到想要的精确度的结果。 加法:answer=`echo “$c + $d”|bc` 减法:answer=`echo “$c – $d”|bc` 乘法:answer=`echo “$c * $d”|bc` 减法:answer=`echo “$c / $d”|bc` 使用scale设置精度: answer=`echo “scale=5;$c * $d”|bc` 若想使用三角函数,需要用-l选项激活bc,启动数学函数库: answer=`echo “scale=5; c($d)”|bc -l`
内核驱动中常见的miscdevice、platform_device、platform_drive 最近在看驱动模型,是越看越糊涂,以前接触比较多的都是一些字符驱动,对字符驱动的框架有一定的了解。后来因为想在驱动中实现设备文件的创建,又了解了一下,sysfs文件系统和udev设备文件系统,必然就涉及到了驱动模型。可是发现驱动模型和以前接触的字符驱动没什么联系。 比如,以前写字符驱动,主要的内容就是实现file_operations结构体里的函数,然后就是申请设备号,注册字符设备,根本就没有涉及到设备驱动模型。而驱动模型里,device_driver根本没有涉及到设备操作的函数、file_operations等,只有一些电源管理,热插拔相关的函数。platform_device里也主要是resource的管理,所以感觉两者根本就没关系,也很奇怪为什么要弄两套东西来实现,而且两者也对应不起来。 后来看了一些内核中的驱动源码,发现很多都是用miscdevice、platform_device、platform_driver实现的,而且流程很相似: 1、在系统初始化阶段注册platform_device,主要是添加设备对应的resource进链表,以便系统对设备占用的资源统一管理; 2、实现platform_driver并注册,在这部分,需要实现的主要有platform_driver结构体中的probe,还有remove、shutdown等一些关于热插拔、电源管理方面的函数。 3、然后在模块初始化函数(xx_init)里注册platform_driver(platform_driver_register) 其中设备资源的获取(platform_get_resource),如IO内存、IO端口、中断号,申请(request),物理地址到虚拟地址的映射(ioremap),misc_device的注册(misc_register),时钟的获取(clk_get)及使能(clk_enable)都是在probe函数里实现的,probe函数是在platform_driver注册,或者新设备添加时,platform_device和platform_driver匹配(通过名字)成功后执行的,有别于以往接触的字符驱动里的注册流程。 对于misc_device对于设备操作函数的实现和字符设备一样,都是填充file_operations结构体,然后在模块初始化函数里注册(misc_register)。 对于platform驱动模型,似乎就是platform_device负责设备资源,platform_driver负责电源管理以及资源的申请,中断的注册等设备初始化及启动有关的操作,然后就是设备操作方法(file_operations)的注册(misc_register或者cdev_add),cdev或者misc_device就负责file_operations。 但是目前还有不少疑问: 1、设备号的申请在哪里,它是怎么放到驱动模型里的device结构体中的? 2、platform_driver结构体和其中的device_driver结构体中都有probe、remove、shutdown等,为什么要在外层放重复的东西,二者有什么关系和区别嘛? 3、misc_register实现里最终和platform_device_register一样都会调用device_add,这样在设备驱动模型里不是有两个device和device_driver对应,而实际的物理设备只有一个嘛? 4、看起来好像驱动模型是对实际的设备及驱动的抽象,提取它们的信息包装成内核对象kobject,然后按照它们之间的关系对其进行分类、分层次管理(建立一棵树),借由这些对象,由系统管理设备资源的注册申请、释放以及实际驱动(file_operations)的注册时机(由此可以实现热插拔,即插即用)和电源管理(系统可以根据设备树来决定设备关闭的顺序,device->device_driver->shutdown)。 所以设备驱动模型中,device只是用来建立设备树,最终会根据结构体中的device_driver中的电源管理函数实现合理的电源开关顺序? 而对于热插拔有关的功能,和device与device_driver的匹配过程有关,而与设备树层次关系无关? (以上是目前想到的不明白的地方,遗漏的地方想起会再添加。改天找老师好好问问,太复杂了!)
ok6410 GPIO S3C6410的GPIO引脚相对来说比较多,而且大部分引脚都具有多重复用功能,如何在linux上用最简单的方式来控制GPIO这需要我们好好研究一下底层的代码了,其实方法有很多种,鉴于在操作系统端控制GPIO并不像控制传统的单片机那样。 这里我将提及一种方法来讲述,这种方法也是我至今看到最简单的方法 首先我们打开linux-3.0.1\arch\arm\plat-samsung\include\plat下gpio-cfg.h这个头文件,仔细浏览后发现,我们可以使用的函数: 1.设置单一io口 int s3c_gpio_cfgpin(unsigned int pin, unsigned int to); 里面有两个参数,第一个pin是选择哪个引脚,第二个参数有三种定义 设置成输出模式 #define S3C_GPIO_INPUT (S3C_GPIO_SPECIAL(0)) 设置成输入模式 #define S3C_GPIO_OUTPUT (S3C_GPIO_SPECIAL(1)) 复用功能选择 #define S3C_GPIO_SFN(x) (S3C_GPIO_SPECIAL(x)) 其实根据我使用的情况来说第1,2两个定义根本就是鸡肋,只有第3个S3C_GPIO_SFN(x)才是最有用的,举个例子: Ok6410的开发板的DS18B20的接口,器件被接在GPE0上,而GPE有如下复用功能其中的参数x就是对应上表的复用功能,当x=0时是输入功能,x=1时是输出功能......下面我想不用我说大家也明白了吧。 这个例子s3c_gpio_cfgpin(S3C64XX_GPE(0), S3C_GPIO_SFN(1));说明GPE0口配置为输出模式。 1.获取io口的配置 unsigned s3c_gpio_getcfg(unsigned int pin);这个函数跟上面讲到的刚好相反,是读取当前一个io口的配置,pin参数是要获得的引脚配置,函数会返回一个相应的值 2.设置一组io int s3c_gpio_cfgpin_range(unsigned int start, unsigned int nr, unsigned int cfg); 第一个参数start是开始的引脚,第二个nr是从start开始到第一个,注意配置的io必须是同一组的io,第三个cfg是配置状态 3.设置单一io的上拉电阻 int s3c_gpio_setpull(unsigned int pin, s3c_gpio_pull_t pull); 设置单个io为不同的上拉模式,模式分别为 S3C_GPIO_PULL_NONE S3C_GPIO_PULL_DOWN S3C_GPIO_PULL_UP 5.获取io口的上拉电阻配置 s3c_gpio_pull_t s3c_gpio_getpull(unsigned int pin); 获取单个io的上拉配置状态,会返回一个配置模式 6.设置一组io(包括上拉电阻) int s3c_gpio_cfgall_range(unsigned int start, unsigned int nr, unsigned int cfg, s3c_gpio_pull_t pull); 讲了这么多看到最后一个函数不讲也应该能看出到底是如何配置了吧 讲了这么多io口的配置方法,来看看如何来配置输出的电平状态。 打开linux-3.0.1\include\linux下的gpio.h的头文件,发现里面有好多的引脚函数其中最重要的也就这么几句 1.设置一个引脚的电平状态 static inline void gpio_set_value(unsigned gpio, int value) 第一个参数gpio为指定的引脚,第二个参数value为要设置的高低电平 2.获得一个引脚的电平状态 static inline int gpio_get_value(unsigned gpio) 第一个参数为gpio为指定的引脚,会返回一个电平状态 讲了上面这些我们基本能控制一个io了,现在我在介绍一种方法,这种方法只能进行输入和输出不能进行io的复用配置 1.io输出 static inline int gpio_direction_output(unsigned gpio, int value) 第一个参数gpio为指定的引脚,第二个参数为电平状态 2.io输入 static inline int gpio_direction_input(unsigned gpio) 第一个参数gpio为指定的引脚,会返回一个电平状态 出了上面方法外我们还可以直接对gpio的地址访问,linux已经为我们准备了这样的接口函数 #define __raw_readl(a) (__chk_io_ptr(a), *(volatile unsigned int __force *)(a)) #define __raw_writel(v,a) (__chk_io_ptr(a), *(volatile unsigned int __force *)(a) = (v)) 其中的a值为 S3C64XX_GPMCON S3C64XX_GPMPUD S3C64XX_GPMDAT 在reg-gpio.h中已经有了以上的定义 V为具体的数值。
linux中设备与驱动关联 1、对于Linux驱动开发来说,设备模型的理解是根本,顾名思义设备模型是关于设备的模型,设备的概念就是总线和与其相连的各种设备了。 总线、设备、驱动,也就是bus、device、driver,在内核里都会有它们自己专属的结构,在include/linux/device.h里定义。 首先是总线,bus_type. struct bus_type { constchar *name; --总线名 structbus_attribute *bus_attrs; --总线属性 structdevice_attribute *dev_attrs; --总线设备属性 structdriver_attribute *drv_attrs; --总线驱动属性 以下的函数会在设备注册或驱动注册的时候调用。 int (*match)(struct device *dev, structdevice_driver*drv); --实现设备与驱动的匹配。不同的总线实现匹配的方法不同,如platform总线采用name匹配,而usb_bus采用id匹配 int (*uevent)(struct device *dev, structkobj_uevent_env *env); int (*probe)(structdevice*dev); --在2.6的内核中实现一个设备与驱动的探测。主要是因为热插拔的设备的增多。 int (*remove)(structdevice*dev); --移除设备 void (*shutdown)(structdevice*dev); --关闭设备 int (*suspend)(struct device *dev,pm_message_tstate); int (*suspend_late)(struct device *dev,pm_message_t state); int (*resume_early)(structdevice*dev); int (*resume)(struct device *dev); structdev_pm_ops*pm; --电源管理 struct bus_type_private*p; --bus_type私有成员,这个结构体中主要包括了kset以及klist,用于管理其挂载其总线下的设备和驱动 }; 下面是设备device的定义: struct device{ struct device* parent; //父设备,一般一个bus也对应一个设备。 structkobject kobj;//代表自身 charbus_id[BUS_ID_SIZE]; structbus_type * bus; structdevice_driver *driver; void*driver_data; void*platform_data; ///更多字段忽略了 }; 下面是设备驱动定义: structdevice_driver { const char *name; structbus_type * bus;//所属总线 structcompletion unloaded; structkobject kobj;//代表自身 struct klistklist_devices;//设备列表 structklist_node knode_bus; struct module* owner; int (*probe)(struct device * dev); int (*remove)(struct device * dev); void(*shutdown) (struct device * dev); int(*suspend) (struct device * dev, pm_message_tstate); int (*resume)(struct device * dev); };
字符设备驱动之I2C设备驱动 一、概述 谈到在linux系统下编写I2C驱动,目前主要有两种方式,一种是把I2C设备当作一个普通的字符设备来处理,另一种是利用linux I2C驱动体系结构来完成。下面比较下这两种驱动。 第一种方法的好处(对应第二种方法的劣势)有: ● 思路比较直接,不需要花时间去了解linux内核中复杂的I2C子系统的操作方法。 第一种方法问题(对应第二种方法的好处)有: ● 要求工程师不仅要对I2C设备的操作熟悉,而且要熟悉I2C的适配器操作; ● 要求工程师对I2C的设备器及I2C的设备操作方法都比较熟悉,最重要的是写出的程序可移植性差; ● 对内核的资源无法直接使用。因为内核提供的所有I2C设备器及设备驱动都是基于I2C子系统的格式。I2C适配器的操作简单还好,如果遇到复杂的I2C适配器(如:基于PCI的I2C适配器),工作量就会大很多。 本文针对的对象是熟悉I2C协议,并且想使用linux内核子系统的开发人员。 网络和一些书籍上有介绍I2C子系统的源码结构。但发现很多开发人员看了这些文章后,还是不清楚自己究竟该做些什么。究其原因还是没弄清楚I2C子系统为我们做了些什么,以及我们怎样利用I2C子系统。本文首先要解决是如何利用现有内核支持的I2C适配器,完成对I2C设备的操作,然后再过度到适配器代码的编写。本文主要从解决问题的角度去写,不会涉及特别详细的代码跟踪。 二、I2C设备驱动程序编写 首先要明确适配器驱动的作用是让我们能够通过它发出符合I2C标准协议的时序。 在Linux内核源代码中的drivers/i2c/busses目录下包含着一些适配器的驱动。如S3C2410的驱动i2c-s3c2410.c。当适配器加载到内核后,接下来的工作就要针对具体的设备编写设备驱动了。 编写I2C设备驱动也有两种方法。一种是利用系统给我们提供的i2c-dev.c来实现一个i2c适配器的设备文件。然后通过在应用层操作i2c适配器来控制i2c设备。另一种是为i2c设备,独立编写一个设备驱动。注意:在后一种情况下,是不需要使用i2c-dev.c的。 1、利用i2c-dev.c操作适配器,进而控制i2c设备 i2c-dev.c并没有针对特定的设备而设计,只是提供了通用的read()、write()和ioctl()等接口,应用层可以借用这些接口访问挂接在适配器上的i2c设备的存储空间或寄存器,并控制I2C设备的工作方式。 需要特别注意的是:i2c-dev.c的read()、write()方法都只适合于如下方式的数据格式(可查看内核相关源码)图1 单开始信号时序 所以不具有太强的通用性,如下面这种情况就不适用(通常出现在读目标时)。图2 多开始信号时序 而且read()、write()方法只适用用于适配器支持i2c算法的情况,如: static const struct i2c_algorithm s3c24xx_i2c_algorithm = { .master_xfer = s3c24xx_i2c_xfer, .functionality = s3c24xx_i2c_func, }; 而不适合适配器只支持smbus算法的情况,如: static const struct i2c_algorithm smbus_algorithm = { .smbus_xfer = i801_access, .functionality = i801_func, }; 基于上面几个原因,所以一般都不会使用i2c-dev.c的read()、write()方法。最常用的是ioctl()方法。ioctl()方法可以实现上面所有的情况(两种数据格式、以及I2C算法和smbus算法)。 针对i2c的算法,需要熟悉struct i2c_rdwr_ioctl_data 、struct i2c_msg。使用的命令是I2C_RDWR。 struct i2c_rdwr_ioctl_data { struct i2c_msg __user *msgs; /* pointers to i2c_msgs */ __u32 nmsgs; /* number of i2c_msgs */ }; struct i2c_msg { _ _u16 addr; /* slave address */ _ _u16 flags; /* 标志(读、写) */ _ _u16 len; /* msg length */ _ _u8 *buf; /* pointer to msg data */ }; 针对smbus算法,需要熟悉struct i2c_smbus_ioctl_data。使用的命令是I2C_SMBUS。对于smbus算法,不需要考虑“多开始信号时序”问题。 struct i2c_smbus_ioctl_data { __u8 read_write; //读、写 __u8 command; //命令 __u32 size; //数据长度标识 union i2c_smbus_data __user *data; //数据 }; 下面以一个实例讲解操作的具体过程。通过S3C2410操作AT24C02 e2prom。实现在AT24C02中任意位置的读、写功能。 首先在内核中已经包含了对s3c2410 中的i2c控制器驱动的支持。提供了i2c算法(非smbus类型的,所以后面的ioctl的命令是I2C_RDWR) static const struct i2c_algorithm s3c24xx_i2c_algorithm = { .master_xfer = s3c24xx_i2c_xfer, .functionality = s3c24xx_i2c_func, }; 另外一方面需要确定为了实现对AT24C02 e2prom的操作,需要确定AT24C02的地址及读写访问时序。 ● AT24C02地址的确定原理图上将A2、A1、A0都接地了,所以地址是0x50。 ● AT24C02任意地址字节写的时序可见此时序符合前面提到的“单开始信号时序” ● AT24C02任意地址字节读的时序可见此时序符合前面提到的“多开始信号时序” 下面开始具体代码的分析(代码在2.6.22内核上测试通过): /*i2c_test.c * hongtao_liu <
[email protected]
> */ #include <stdio.h> #include <linux/types.h> #include <stdlib.h> #include <fcntl.h> #include <unistd.h> #include <sys/types.h> #include <sys/ioctl.h> #include <errno.h> #define I2C_RETRIES 0x0701 #define I2C_TIMEOUT 0x0702 #define I2C_RDWR 0x0707 /*********定义struct i2c_rdwr_ioctl_data和struct i2c_msg,要和内核一致*******/ struct i2c_msg { unsigned short addr; unsigned short flags; #define I2C_M_TEN 0x0010 #define I2C_M_RD 0x0001 unsigned short len; unsigned char *buf; }; struct i2c_rdwr_ioctl_data { struct i2c_msg *msgs; int nmsgs; /* nmsgs这个数量决定了有多少开始信号,对于“单开始时序”,取1*/ }; /***********主程序***********/ int main() { int fd,ret; struct i2c_rdwr_ioctl_data e2prom_data; fd=open("/dev/i2c-0",O_RDWR); /* */dev/i2c-0是在注册i2c-dev.c后产生的,代表一个可操作的适配器。如果不使用i2c-dev.c *的方式,就没有,也不需要这个节点。 */ if(fd<0) { perror("open error"); } e2prom_data.nmsgs=2; /* *因为操作时序中,最多是用到2个开始信号(字节读操作中),所以此将 *e2prom_data.nmsgs配置为2 */ e2prom_data.msgs=(struct i2c_msg*)malloc(e2prom_data.nmsgs*sizeof(struct i2c_msg)); if(!e2prom_data.msgs) { perror("malloc error"); exit(1); } ioctl(fd,I2C_TIMEOUT,1);/*超时时间*/ ioctl(fd,I2C_RETRIES,2);/*重复次数*/ /***write data to e2prom**/ e2prom_data.nmsgs=1; (e2prom_data.msgs[0]).len=2; //1个 e2prom 写入目标的地址和1个数据 (e2prom_data.msgs[0]).addr=0x50;//e2prom 设备地址 (e2prom_data.msgs[0]).flags=0; //write (e2prom_data.msgs[0]).buf=(unsigned char*)malloc(2); (e2prom_data.msgs[0]).buf[0]=0x10;// e2prom 写入目标的地址 (e2prom_data.msgs[0]).buf[1]=0x58;//the data to write ret=ioctl(fd,I2C_RDWR,(unsigned long)&e2prom_data); if(ret<0) { perror("ioctl error1"); } sleep(1); /******read data from e2prom*******/ e2prom_data.nmsgs=2; (e2prom_data.msgs[0]).len=1; //e2prom 目标数据的地址 (e2prom_data.msgs[0]).addr=0x50; // e2prom 设备地址 (e2prom_data.msgs[0]).flags=0;//write (e2prom_data.msgs[0]).buf[0]=0x10;//e2prom数据地址 (e2prom_data.msgs[1]).len=1;//读出的数据 (e2prom_data.msgs[1]).addr=0x50;// e2prom 设备地址 (e2prom_data.msgs[1]).flags=I2C_M_RD;//read (e2prom_data.msgs[1]).buf=(unsigned char*)malloc(1);//存放返回值的地址。 (e2prom_data.msgs[1]).buf[0]=0;//初始化读缓冲 ret=ioctl(fd,I2C_RDWR,(unsigned long)&e2prom_data); if(ret<0) { perror("ioctl error2"); } printf("buff[0]=%x/n",(e2prom_data.msgs[1]).buf[0]); /***打印读出的值,没错的话,就应该是前面写的0x58了***/ close(fd); return 0; } 以上讲述了一种比较常用的利用i2c-dev.c操作i2c设备的方法,这种方法可以说是在应用层完成了对具体i2c设备的驱动工作。
STM32时钟系统小结 在STM32中,有五个时钟源,为HSI、HSE、LSI、LSE、PLL。 、HSI是高速内部时钟,RC振荡器,频率为8MHz。 ②、HSE是高速外部时钟,可接石英/陶瓷谐振器,或者接外部时钟源,频率范围为4MHz~16MHz。 ③、LSI是低速内部时钟,RC振荡器,频率为40kHz。 ④、LSE是低速外部时钟,接频率为32.768kHz的石英晶体。 ⑤、PLL为锁相环倍频输出,其时钟输入源可选择为HSI/2、HSE或者HSE/2。倍频可选择为2~16倍,但是其输出频率最大不得超过72MHz。 其中40kHz的LSI供独立看门狗IWDG使用,另外它还可以被选择为实时时钟RTC的时钟源。另外, 实时时钟RTC的时钟源还可以选择LSE,或者是HSE的128分频。RTC的时钟源通过RTCSEL[1:0]来选择。 STM32中有一个全速功能的USB模块,其串行接口引擎需要一个频率为48MHz的时钟源。该时钟源只能从PLL输出端获取,可以选择为1.5分频或者1分频,也就是,当需要使用USB模块时,PLL必须使能,并且时钟频率配置为48MHz或72MHz。 另外,STM32还可以选择一个时钟信号输出到MCO脚(PA8)上,可以选择为PLL输出的2分频、HSI、HSE、或者系统时钟。 系统时钟SYSCLK,它是供STM32中绝大部分部件工作的时钟源。系统时钟可选择为PLL输出、HSI或者HSE。系统时钟最大频率为72MHz,它通过AHB分频器分频后送给各模块使用,AHB分频器可选择1、2、4、8、16、64、128、256、512分频。其中AHB分频器输出的时钟送给5大模块使用: ①、送给AHB总线、内核、内存和DMA使用的HCLK时钟。 ②、通过8分频后送给Cortex的系统定时器时钟。 ③、直接送给Cortex的空闲运行时钟FCLK。 ④、送给APB1分频器。APB1分频器可选择1、2、4、8、16分频,其输出一路供APB1外设使用(PCLK1,最大频率36MHz),另一路送给定时器(Timer)2、3、4倍频器使用。该倍频器可选择1或者2倍频,时钟输出供定时器2、3、4使用。 ⑤、送给APB2分频器。APB2分频器可选择1、2、4、8、16分频,其输出一路供APB2外设使用(PCLK2,最大频率72MHz),另一路送给定时器(Timer)1倍频器使用。该倍频器可选择1或者2倍频,时钟输出供定时器1使用。另外,APB2分频器还有一路输出供ADC分频器使用,分频后送给ADC模块使用。ADC分频器可选择为2、4、6、8分频。 在以上的时钟输出中,有很多是带使能控制的,例如AHB总线时钟、内核时钟、各种APB1外设、APB2外设等等。当需要使用某模块时,记得一定要先使能对应的时钟。 需要注意的是定时器的倍频器,当APB的分频为1时,它的倍频值为1,否则它的倍频值就为2。 连接在APB1(低速外设)上的设备有:电源接口、备份接口、CAN、USB、I2C1、I2C2、UART2、UART3、SPI2、窗口看门狗、Timer2、Timer3、Timer4。注意USB模块虽然需要一个单独的48MHz时钟信号,但它应该不是供USB模块工作的时钟,而只是提供给串行接口引擎(SIE)使用的时钟。USB模块工作的时钟应该是由APB1提供的。 连接在APB2(高速外设)上的设备有:UART1、SPI1、Timer1、ADC1、ADC2、所有普通IO口(PA~PE)、第二功能IO口。深圳专业STM32技术学习郭老师QQ754634522 使用HSE时钟,程序设置时钟参数流程: 1、将RCC寄存器重新设置为默认值 RCC_DeInit; 2、打开外部高速时钟晶振HSE RCC_HSEConfig(RCC_HSE_ON); 3、等待外部高速时钟晶振工作 HSEStartUpStatus = RCC_WaitForHSEStartUp(); 4、设置AHB时钟 RCC_HCLKConfig; 5、设置高速AHB时钟 RCC_PCLK2Config; 6、设置低速速AHB时钟 RCC_PCLK1Config; 7、设置PLL RCC_PLLConfig; 8、打开PLL RCC_PLLCmd(ENABLE); 9、等待PLL工作 while(RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET) 10、设置系统时钟 RCC_SYSCLKConfig; 11、判断是否PLL是系统时钟 while(RCC_GetSYSCLKSource() != 0x08) 12、打开要使用的外设时钟 RCC_APB2PeriphClockCmd()/RCC_APB1PeriphClockCmd()
基于Linux操作系统的 智能家居的设计 智能家居是当前社会一个新颖的话题,也是电子技术发展的方面,把电子技术充分应用在生活中。然而到目前为止,智能家居才做到智能小区这个地步,因为实现智能家居的费用比较高。智能家居也是物联网的一个分支,把家庭中的相关家居通过一个控制平台,连接到网络上,在联网的条件下,可以随时查看家里的情况和控制。 物联网的兴起,为智能家居提供了条件。如何通过简化物联网终端设备,最终开发出一套经济实用的支持多终端应用的智能家居物联网平台是非常有意义、有价值的工作。本系统基于Linux操作系统,开发了一套融合无线移动网络、射频识别装置的智能家居控制系统。此系统具有智能抄表、远程开启电器、射频识别和远程遥控等功能。 1 系统概述 本系统采用S3C2410芯片作为主控制器,操作界面为3.2寸TFT触摸显示屏。上电后,显示屏显示整个家居系统网络内各设备,每个设备分别对应一个图标,包括灯光控制、风扇、冰箱、空调、温度、电饭煲、烟雾、燃气流量采集等。点击图标进入该设备相应的详细信息栏。进入灯光控制界面,可以方便地查看家中各房间亮灯情况,也可远程通过短信方式控制各房间的灯的亮灭;燃气使用情况可方便地发送至相关采集部门;坐在办公室,一条短信可以将电饭煲电源接通。家居中各设备与主控平台间选择了2.4 GHz的无线射频收发芯片nRF2401,通过它可以实现各家居设备与主控平台间的无线通信。主控平台与外网的通信,采用的是ATK-SIM900A GSM/GPRS终端无线模块。烟雾传感器采用MQ-2传感器来采集室内烟雾情况。智能家居控制系统结构框图如图1所示。 图1 智能家居控制系统结构框图 嵌入式ARM2410系统开发平台是整个智能家居系统的监控与管理中心,它主要集成了无线通信模块、射频识别模块、红外感应模块、触摸显示屏。该总控平台一方面可以通过无线模块接收到外部命令(例如:手机短信),并通过射频识别,控制对应的家居设备按指示工作,例如,打开电饭煲、空调或洗衣机。另一方面,各家居设备运行信息可以通过射频模块接收采集(例如电表读数等),处理后,可以将数据发送到嵌入式ARM2410系统开发平台,该平台将数据分类处理后,可选择有用数据发送至对应公司服务器(例如供电局、水厂等),实现自动抄表。 智能家居控制系统的中的每一个家居设备,都需要分别安装一个射频识别模块,通过该模块可以与嵌入式ARM2410系统开发平台实现短距离无线通信。 2 射频识别模块 nRF2401是一款工作在2.4~2.5 GHz世界通用ISM频段的单片射频收发器件。该射频识别模块可以实现多机通信,多机通信采用频分多址的方法,只需要在接收端对不同的通道配置地址即可。发送端使用相应的地址作为本机地址。接收数据时通过读取STATUS中相关位即可得知接收的是哪个通道的数据。射频识别模块内包括:频率发生器、增强型 SchockBurstTM模式控制器、功率放大器、晶体振荡器、调制器和解调器。输出功率频道的选择和协议可以通过对应的SPI接口进行设置。射频识别模块功耗低,当工作在发射模式下发射功率为-6 dBm时,电流消耗为9.0 mA;接收模式时为12.3 mA,掉电模式和待机模式下电流消耗更低。 nRF2401在接收模式下可以接收6路不同通道的数据,nRF2401在星形网络中的结构如图2所示。每一个数据通道使用不同的地址,但是共用相同的频道,也就是说6个不同的nRF2401设置为发送模式后可以与同一个设置为接收模式的nRF2401进行通信,而设置为接收模式的 nRF2401可以对这6个发射端进行识别。同一时刻,所有的数据通道都被搜索,但只能接 图2 nRF24L01在星形网络中的结构 收一路数据通道的数据。nRF2401在确认收到数据后记录地址,并以此地址为目标地址发送应答信号,在发送端数据通道0被用作接收应答信号,因此数据通道0的接收地址要与发送端地址相等以确保接收到正确的应答信号。 3 烟雾的检测 烟雾检测采用MQ-2 传感器模块,模块能检测多种气体,当气体浓度超过程序中设定值的时候,模块检测出来并在相应引脚上产生信号,供单片机读取。模块有一下参数: 1、可以用于家庭和工厂的气体泄漏监测装置,适宜于液化气,丁烷,丙烷, 甲烷,酒精,烟雾等的探测; 2、灵敏度可调; 3、工作电压 5V 使用前,供电至少预热 2 分钟以上,传感器稍微发烫属于 正常现象; 4、输出形式 :a)模拟量电压输出 b)数字开关量输出(0 和 1) 5、串口通信 主控芯片采用8051系列单片机芯片,单片机通过串口与传感器通信,可以方便地采集到瞬时流量和累积流量,可记录自上电以来瞬时流量的最大值和最小值,具有超量程指示功能,程序模拟SPI接口,实现与NRF2401的通信。 4 Linux移植及Qt应用程序开发 本系统在Friednly2410开发板上移植了Linux操作系统,并在此嵌入式操作系统平台上进行了简易家居智能控制平台的开发。Linux移植及Qt应用程序开发步骤如图4所示。 图4 Linux移植及Qt应用程序开发步骤 首先是配置开发板所需要的环境软件。在开发板环境建立中,要注意的是对于没有串口的机器,一定要先安装USB转串口的驱动,而在安装时务必注意将电脑与板载串口的波特率设置为一致。这个没设置好,串口通信会有问题。除此之外,还需安装好串口调试工具及程序烧录下载工具。 其次是搭建Linux交叉编译环境。一般的电脑上都是Windows操作系统,要开发Linux嵌入式操作系统,需要安装虚拟机(例如VMware 等)、基于Linux内核的相关操作系统(例如Fedora)和交叉编译器。虚拟机是用来承载Linux操作系统在Windows机器上运行而设置的,就像虚拟光盘一样,是个虚拟的。交叉编译器,是用来编译和产生系统开发过程中各种镜像文件。深圳、广州、郑州想系统学习嵌入式的朋友可联系郭老师QQ754634522 接着进入移植过程。移植时一般顺序为:编译Uboot→编译内核→构建文件系统。 最后是Qt应用程序开发。本系统是基于图形界面开发的。Qt程序开发需要先建立Qtopia开发平台,然后进入编译目录,执行编译脚本,无误退出后,再编译应用程序。编译应用程序时,只需进入每个程序目录,执行make命令。然后,将编译好的程序的可执行文件拷贝到文件系统的镜像目录中,最后将编译生成的.bin文件烧录到开发板中即可。 结语 本文描述的是一种简易可行的智能家居联网方案,其具有成本低、易于实现、组网容易等优点,但对于更复杂的互动功能还存在一定的欠缺。但基于上述主控平台,只需要调整智能家居的内部局部通信网络就可以实现更加完善的智能家居功能。
完美无敌的vim配置 欢迎来到小码哥的博客博客搬家啦http://tieba.baidu.com/mo/q/checkurl?url=http%3A%2F%2Ft.cn%2FRvFZs2c&urlrefer=013d5565d05d70829871a0c4853a355a强大的vim配置文件,让编程更随意 花了很长时间整理的,感觉用起来很方便,共享一下。 我的vim配置主要有以下优点: 1.按F5可以直接编译并执行C、C++、java代码以及执行shell脚本,按“F8”可进行C、C++代码的调试 2.自动插入文件头 ,新建C、C++源文件时自动插入表头:包括文件名、作者、联系方式、建立时间等,读者可根据需求自行更改 3.映射“Ctrl + A”为全选并复制快捷键,方便复制代码 4.按“F2”可以直接消除代码中的空行 5.“F3”可列出当前目录文件,打开树状文件目录 6. 支持鼠标选择、方向键移动 7. 代码高亮,自动缩进,显示行号,显示状态行 8.按“Ctrl + P”可自动补全 9.[]、{}、()、""、' '等都自动补全 10.其他功能读者可以研究以下文件 vim本来就是很强大,很方便的编辑器,加上我的代码后肯定会如虎添翼,或许读者使用其他编程语言,可以根据自己的需要进行修改,配置文件里面已经加上注释。 读者感兴趣的话直接复制下面的代码到文本文件,然后把文件改名为“ .vimrc” (不要忘记前面的“.”),然后把文件放到用户文件夹的根目录下面即可。重新打开vim即可看到效果。 为方便管理,源码托管到了github,后期增加了好多新功能, 具体详见:http://tieba.baidu.com/mo/q/checkurl?url=https%3A%2F%2Fgithub.com%2Fma6174%2Fvim&urlrefer=0b10fa9bc9bf12b749aaace5e6935d3d 这是在github上的vim配置的截图:http://tieba.baidu.com/mo/q/checkurl?url=https%3A%2F%2Fgithub.com%2Fma6174%2Fvim&urlrefer=0b10fa9bc9bf12b749aaace5e6935d3d http://tieba.baidu.com/mo/q/checkurl?url=http%3A%2F%2Fwww.cnblogs.com%2Fma6174%2Farchive%2F2011%2F12%2F10%2F2283393.html&urlrefer=5654510c2272be67f9c0bafe7e2d06c8
按键驱动 #include <linux/module.h> #include <linux/kernel.h> #include <linux/fs.h> #include <linux/init.h> #include <linux/delay.h> #include <linux/poll.h> #include <linux/irq.h> #include <asm/irq.h> #include <linux/interrupt.h> #include <asm/uaccess.h> #include <mach/regs-gpio.h> #include <mach/hardware.h> #include <linux/platform_device.h> #include <linux/cdev.h> #include <linux/miscdevice.h> #include <linux/sched.h> #include <linux/gpio.h> #define DEVICE_NAME "buttons" /*定义中断所用的结构体*/ struct button_irq_desc { int irq; //按键对应的中断号 int pin; //按键所对应的 GPIO 端口 int pin_setting; //按键对应的引脚描述,实际并未用到,保留 int number; //定义键值,以传递给应用层/用户态 char *name; //每个按键的名称 }; /*结构体实体定义*/ static struct button_irq_desc button_irqs [] = { {IRQ_EINT8 , S3C2410_GPG(0) , S3C2410_GPG0_EINT8 , 0, "KEY0"}, {IRQ_EINT11, S3C2410_GPG(3) , S3C2410_GPG3_EINT11 , 1, "KEY1"}, {IRQ_EINT13, S3C2410_GPG(5) , S3C2410_GPG5_EINT13 , 2, "KEY2"}, {IRQ_EINT14, S3C2410_GPG(6) , S3C2410_GPG6_EINT14 , 3, "KEY3"}, {IRQ_EINT15, S3C2410_GPG(7) , S3C2410_GPG7_EINT15 , 4, "KEY4"}, {IRQ_EINT19, S3C2410_GPG(11), S3C2410_GPG11_EINT19, 5, "KEY5"}, }; /*开发板上按键的状态变量,注意这里是’0’,对应的 ASCII 码为 30*/ static volatile char key_values [] = {'0', '0', '0', '0', '0', '0'}; /*因为本驱动是基于中断方式的,在此创建一个等待队列,以配合中断函数使用;当有按键按下并读取到键 值时,将会唤醒此队列,并设置中断标志,以便能通过 read 函数判断和读取键值传递到用户态;当没有按 键按下时,系统并不会轮询按键状态,以节省时钟资源*/ static DECLARE_WAIT_QUEUE_HEAD(button_waitq); /*中断标识变量,配合上面的队列使用,中断服务程序会把它设置为 1,read 函数会把它清零*/ static volatile int ev_press = 0; /*本按键驱动的中断服务程序*/ static irqreturn_t buttons_interrupt(int irq, void *dev_id) { struct button_irq_desc *button_irqs = (struct button_irq_desc *)dev_id; int down; /*获取被按下的按键状态*/ down = !s3c2410_gpio_getpin(button_irqs->pin); /*状态改变,按键被按下,从这句可以看出,当按键没有被按下的时候,寄存器的值为 1(上拉),但按 键被按下的时候,寄存器对应的值为 0*/ if (down != (key_values[button_irqs->number] & 1)) { // Changed /*如果 key1 被按下,则 key_value[0]就变为’1’,对应的 ASCII 码为 31*/ key_values[button_irqs->number] = '0' + down; ev_press = 1; /*设置中断标志为 1*/ wake_up_interruptible(&button_waitq); /*唤醒等待队列*/ } return IRQ_RETVAL(IRQ_HANDLED); } /* *在应用程序执行 open(“/dev/buttons”,...)时会调用到此函数,在这里,它的作用主要是注册 6 个按键的中断。 *所用的中断类型是 IRQ_TYPE_EDGE_BOTH,也就是双沿触发,在上升沿和下降沿均会产生中断,这样做是为了更加有 *效地判断按键状态 */ static int s3c24xx_buttons_open(struct inode *inode, struct file *file) { int i; int err = 0; for (i = 0; i < sizeof(button_irqs)/sizeof(button_irqs[0]); i++) { if (button_irqs[i].irq < 0) { continue; } /*注册中断函数*/ err = request_irq(button_irqs[i].irq, buttons_interrupt, IRQ_TYPE_EDGE_BOTH, button_irqs[i].name, (void *)&button_irqs[i]); if (err) break; } if (err) { /*如果出错,释放已经注册的中断,并返回*/ i--; for (; i >= 0; i--) { if (button_irqs[i].irq < 0) { continue; } disable_irq(button_irqs[i].irq); free_irq(button_irqs[i].irq, (void *)&button_irqs[i]); } return -EBUSY; } /*注册成功,则中断队列标记为 1,表示可以通过 read 读取*/ ev_press = 1; /*正常返回*/ return 0; } /* *此函数对应应用程序的系统调用 close(fd)函数,在此,它的主要作用是当关闭设备时释放 6 个按键的中断处理函数 */ static int s3c24xx_buttons_close(struct inode *inode, struct file *file) { int i; for (i = 0; i < sizeof(button_irqs)/sizeof(button_irqs[0]); i++) { if (button_irqs[i].irq < 0) { continue; } /*释放中断号,并注销中断处理函数*/ free_irq(button_irqs[i].irq, (void *)&button_irqs[i]); } return 0; } /* *对应应用程序的 read(fd,...)函数,主要用来向用户空间传递键值 */ static int s3c24xx_buttons_read(struct file *filp, char __user *buff, size_t count, loff_t *offp) { unsigned long err; if (!ev_press) { if (filp->f_flags & O_NONBLOCK) /*当中断标识为 0 时,并且该设备是以非阻塞方式打开时,返回*/ return -EAGAIN; else /*当中断标识为 0 时,并且该设备是以阻塞方式打开时,进入休眠状态,等待被唤醒*/ wait_event_interruptible(button_waitq, ev_press); } /*把中断标识清零*/ ev_press = 0; /*一组键值被传递到用户空间*/ err = copy_to_user(buff, (const void *)key_values, min(sizeof(key_values), count)); return err ? -EFAULT : min(sizeof(key_values), count); } static unsigned int s3c24xx_buttons_poll( struct file *file, struct poll_table_struct *wait) { unsigned int mask = 0; /*把调用 poll 或者 select 的进程挂入队列,以便被驱动程序唤醒*/ poll_wait(file, &button_waitq, wait); if (ev_press) mask |= POLLIN | POLLRDNORM; return mask; } /*设备操作集*/ static struct file_operations dev_fops = { .owner = THIS_MODULE, .open = s3c24xx_buttons_open, .release = s3c24xx_buttons_close, .read = s3c24xx_buttons_read, .poll = s3c24xx_buttons_poll, }; static struct miscdevice misc = { .minor = MISC_DYNAMIC_MINOR, .name = DEVICE_NAME, .fops = &dev_fops, }; /*设备初始化,主要是注册设备*/ static int __init dev_init(void) { int ret; /*把按键设备注册为 misc 设备,其设备号是自动分配的*/ ret = misc_register(&misc); printk (DEVICE_NAME"\tinitialized\n"); return ret; }
rmmod : chdir(/lib/modules): No such file or directory ? 使用rmmod会出现 rmmod : chdir(/lib/modules): No such file or directory ? 现在的内核模块在插入卸载时都会要转到 “/lib/modules/内核版本号/ ” 这个目录里。所以只要建立这个目录就行了。 在目标板执行 #mkdir -p /lib/modules/$(uname -r) 较新版本的busybox 1.13.1+ 要卸载模块必须要 “完全匹配模块名”才行,原来在老标本的使用模块文件名就能卸载,现在发现不行了。 # lsmod mmc_block 9668 0 - Live 0xbf03a000 mmc_core 48000 1 mmc_block, Live 0xbf029000 这里面mmc_block和mmc_core为“完全匹配模块名”, #rmmod mmc_block #rmmod mmc_core 而不是mmc_block.ko mmc_core.ko 这样的模块文件名
块驱动开发 第1章 +---------------------------------------------------+ | 写一个块设备驱动 | +---------------------------------------------------+ 同样是读书,读小说可以行云流水,读完后心情舒畅,意犹未尽;读电脑书却举步艰难,读完后目光呆滞,也是意犹未尽,只不过未尽的是痛苦的回忆。 研究证明,痛苦的记忆比快乐的更难忘记,因此电脑书中的内容比小说记得持久。 而这套教程的目的是要打破这种状况,以至于读者在忘记小说内容忘记本文。 在这套教程中,我们通过写一个建立在内存中的块设备驱动,来学习linux内核和相关设备驱动知识。 选择写块设备驱动的原因是: 1:容易上手 2:可以牵连出更多的内核知识 3:像本文这样的块设备驱动教程不多,所以需要一个 好吧,扯淡到此结束,我们开始写了。 本章的目的用尽可能最简单的方法写出一个能用的块设备驱动。 所谓的能用,是指我们可以对这个驱动生成的块设备进行mkfs,mount和读写文件。 为了尽可能简单,这个驱动的规模不是1000行,也不是500行,而是100行以内。 这里插一句,我们不打算在这里介绍如何写模块,理由是介绍的文章已经满天飞舞了。 如果你能看得懂、并且成功地编译、运行了这段代码,我们认为你已经达到了本教程的入学资格, 当然,如果你不幸的卡在这段代码中,那么请等到搞定它以后再往下看: mod.c: #include <linux/module.h> static int __init init_base(void) { printk("----Hello. World----\n"); return 0; } static void __exit exit_base(void) { printk("----Bye----\n"); } module_init(init_base); module_exit(exit_base); MODULE_LICENSE ("GPL"); MODULE_AUTHOR("Zhao Lei"); MODULE_DESCRIPTION("For test"); Makefile: obj-m := mod.o KDIR := /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) default: $(MAKE) -C $(KDIR) SUBDIRS=$(PWD) modules clean: $(MAKE) -C $(KDIR) SUBDIRS=$(PWD) clean rm -rf Module.markers modules.order Module.symvers 好了,这里我们假定你已经搞定上面的最简单的模块了,懂得什么是看模块,以及简单模块的编写、编译、加载和卸载。 还有就是,什么是块设备,什么是块设备驱动,这个也请自行google吧,因为我们已经迫不及待要写完程序下课。 为了建立一个可用的块设备,我们需要做......1件事情: 1:用add_disk()函数向系统中添加这个块设备 添加一个全局的 static struct gendisk *simp_blkdev_disk; 然后申明模块的入口和出口: module_init(simp_blkdev_init); module_exit(simp_blkdev_exit); 然后在入口处添加这个设备、出口处私房这个设备: static int __init simp_blkdev_init(void) { add_disk(simp_blkdev_disk); return 0; } static void __exit simp_blkdev_exit(void) { del_gendisk(simp_blkdev_disk); } 当然,在添加设备之前我们需要申请这个设备的资源,这用到了alloc_disk()函数,因此模块入口函数simp_blkdev_init(void)应该是: static int __init simp_blkdev_init(void) { simp_blkdev_disk = alloc_disk(1); if (!simp_blkdev_disk) { ret = -ENOMEM; goto err_alloc_disk; } add_disk(simp_blkdev_disk); return 0; err_alloc_disk: return ret; } 还有别忘了在卸载模块的代码中也加一个行清理函数: put_disk(simp_blkdev_disk); 还有就是,设备有关的属性也是需要设置的,因此在alloc_disk()和add_disk()之间我们需要: strcpy(simp_blkdev_disk->disk_name, SIMP_BLKDEV_DISKNAME); simp_blkdev_disk->major = ?1; simp_blkdev_disk->first_minor = 0; simp_blkdev_disk->fops = ?2; simp_blkdev_disk->queue = ?3; set_capacity(simp_blkdev_disk, ?4); SIMP_BLKDEV_DISKNAME其实是这个块设备的名称,为了绅士一些,我们把它定义成宏了: #define SIMP_BLKDEV_DISKNAME "simp_blkdev" 这里又引出了4个问号。(天哪,是不是有种受骗的感觉,像是陪老婆去做头发) 第1个问号: 每个设备需要对应的主、从驱动号。 我们的设备当然也需要,但很明显我不是脑科医生,因此跟写linux的那帮疯子不熟,得不到预先为我保留的设备号。 还有一种方法是使用动态分配的设备号,但在这
vim与cscope整合中的两个问题 1. 如果遇到duplicate database 信息如下 line 42: E568: duplicate cscope database not added Press ENTER or type command to continue 那么就是vim的全局配置中也有cscope add cscope.out 和cscope_maps.vim或者用户的vim配置文件中的cscope add cscope.out冲突了
关键字const 我只要一听到被面试者说:"const意味着常数"(不是常数,可以是变量,只是你不能修改它),我就知道我正在和一个业余者打交道。去年Dan Saks已经在他的文章里完全概括了const的所有用法,因此ESP(译者:Embedded Systems Programming)的每一位读者应该非常熟悉const能做什么和不能做什么.如果你从没有读到那篇文章,只要能说出const意味着"只读"就可以了。尽管这个答案不是完全的答案,但我接受它作为一个正确的答案。(如果你想知道更详细的答案,仔细读一下Saks的文章吧。) 如果应试者能正确回答这个问题,我将问他一个附加的问题:下面的声明都是什么意思? Const只是一个修饰符,不管怎么样a仍然是一个int型的变量 const int a; int const a; const int *a; int * const a; int const * a const; 本质:const在谁后面谁就不可修改,const在最前面则将其后移一位即可,二者等效 前两个的作用是一样,a是一个常整型数。第三个意味着a是一个指向常整型数的指针(也就是,指向的整型数是不可修改的,但指针可以,此最常见于函数的参数,当你只引用传进来指针所指向的值时应该加上const修饰符,程序中修改编译就不通过,可以减少程序的bug)。 第四个意思a是一个指向整型数的常指针(也就是说,指针指向的整型数是可以修改的,但指针是不可修改的)。最后一个意味着a是一个指向常整型数的常指针(也就是说,指针指向的整型数是不可修改的,同时指针也是不可修改的)。 如果应试者能正确回答这些问题,那么他就给我留下了一个好印象。顺带提一句,也许你可能会问,即使不用关键字 ,也还是能很容易写出功能正确的程序,那么我为什么还要如此看重关键字const呢?我也如下的几下理由: 1) 关键字const的作用是为给读你代码的人传达非常有用的信息,实际上,声明一个参数为常量是为了告诉了用户这个参数的应用目的。如果你曾花很多时间清理其它人留下的垃圾,你就会很快学会感谢这点多余的信息。(当然,懂得用const的程序员很少会留下的垃圾让别人来清理的。) 2) 通过给优化器一些附加的信息,使用关键字const也许能产生更紧凑的代码。 3) 合理地使用关键字const可以使编译器很自然地保护那些不希望被改变的参数,防止其被无意的代码修改。简而言之,这样可以减少bug的出现。 const关键字至少有下列n个作用: (1)欲阻止一个变量被改变,可以使用const关键字。在定义该const变量时,通常需要对它进行初始化,因为以后就没有机会再去改变它了; (2)对指针来说,可以指定指针本身为const,也可以指定指针所指的数据为const,或二者同时指定为const; (3)在一个函数声明中,const可以修饰形参,表明它是一个输入参数,在函数内部不能改变其值; (4)对于类的成员函数,若指定其为const类型,则表明其是一个常函数,不能修改类的成员变量; (5)对于类的成员函数,有时候必须指定其返回值为const类型,以使得其返回值不为“左值”。例如: const classA operator*(const classA& a1,const classA& a2); operator*的返回结果必须是一个const对象。如果不是,这样的变态代码也不会编译出错: classA a, b, c; (a * b) = c; // 对a*b的结果赋值 操作(a * b) = c显然不符合编程者的初衷,也没有任何意义。
Linux系统调用的运行过程 在Linux中,系统调用是用户空间访问内核的唯一手段,它们是内核唯一的合法入口。 一般情况下,应用程序通过应用编程接口(API)而不是直接通过系统调用来编程,而且这种编程接口实际上并不需要和内核提供的系统调用对应。一个API定义了一组应用程序使用的编程接口。它们可以实现成一个系统调用,也可以通过调用多个系统调用来实现,即使不使用任何系统调用也不存在问题。实际上,API可以在各种不同的操作系统上实现,给应用程序提供完全相同的接口,而它们本身在这些系统上的实现却可能迥异。 在Unix世界中,最流行的应用编程接口是基于POSIX标准的,Linux是与POSIX兼容的。 从程序员的角度看,他们只需要给API打交道就可以了,而内核只跟系统调用打交道;库函数及应用程序是怎么使用系统调用不是内核关心的。 系统调用(在linux中常称作syscalls)通常通过函数进行调用。它们通常都需要定义一个或几个参数(输入)而且可能产生一些副作用。这些副作用通过一个long类型的返回值来表示成功(0值)或者错误(负值)。在系统调用出现错误的时候会把错误码写入errno全局变量。通过调用perror()函数,可以把该变量翻译成用户可以理解的错误字符串。 系统调用的实现有两个特别之处: 1)函数声明中都有asmlinkage限定词,用于通知编译器仅从栈中提取该函数的参数。 2)系统调用getXXX()在内核中被定义为sys_getXXX()。这是Linux中所有系统调用都应该遵守的命名规则。 系统调用号:在linux中,每个系统调用都赋予一个系统调用号,通过这个独一无二的号就可以关联系统调用。当用户空间的进程执行一个系统调用的时候,这个系统调用号就被用来指明到底要执行哪个系统调用;进程不会提及系统调用的名称。系统调用号一旦分配就不能再有任何变更(否则编译好的应用程序就会崩溃),如果一个系统调用被删除,它所占用的系统调用号也不允许被回收利用。Linux有一个"未使用"系统调用sys_ni_syscall(),它除了返回-ENOSYS外不做任何其他工作,这个错误号就是专门针对无效的系统调用而设的。虽然很罕见,但如果有一个系统调用被删除,这个函数就要负责“填补空位”。 内核记录了系统调用表中所有已注册过的系统调用的列表,存储在sys_call_table中。它与体系结构有关,一般在entry.s中定义。这个表中为每一个有效的系统调用指定了唯一的系统调用号。 用户空间的程序无法直接执行内核代码。它们不能直接调用内核空间的函数,因为内核驻留在受保护的地址空间上,应用程序应该以某种方式通知系统,告诉内核自己需要执行一个系统调用,系统系统切换到内核态,这样内核就可以代表应用程序来执行该系统调用了。这种通知内核的机制是通过软中断实现的。x86系统上的软中断由int$0x80指令产生。这条指令会触发一个异常导致系统切换到内核态并执行第128号异常处理程序,而该程序正是系统调用处理程序,名字叫system_call().它与硬件体系结构紧密相关,通常在entry.s文件中通过汇编语言编写。 所有的系统调用陷入内核的方式都是一样的,所以仅仅是陷入内核空间是不够的。因此必须把系统调用号一并传给内核。在x86上,这个传递动作是通过在触发软中断前把调用号装入eax寄存器实现的。这样系统调用处理程序一旦运行,就可以从eax中得到数据。上述所说的system_call()通过将给定的系统调用号与NR_syscalls做比较来检查其有效性。如果它大于或者等于NR_syscalls,该函数就返回-ENOSYS.否则,就执行相应的系统调用:call *sys_call_table(, %eax, 4); 由于系统调用表中的表项是以32位(4字节)类型存放的,所以内核需要将给定的系统调用号乘以4,然后用所得到的结果在该表中查询器位置。如图图一所示:上面已经提到,除了系统调用号以外,还需要一些外部的参数输入。最简单的办法就是像传递系统调用号一样把这些参数也存放在寄存器里。在x86系统上ebx,ecx,edx,esi和edi按照顺序存放前5个参数。需要六个或六个以上参数的情况不多见,此时,应该用一个单独的寄存器存放指向所有这些参数在用户空间地址的指针。给用户空间的返回值也通过寄存器传递。在x86系统上,它存放在eax寄存器中。 系统调用必须仔细检查它们所有的参数是否合法有效。系统调用在内核空间执行。如果任由用户将不合法的输入传递给内核,那么系统的安全和稳定将面临极大的考验。最重要的一种检查就是检查用户提供的指针是否有效,内核在接收一个用户空间的指针之前,内核必须要保证: 1)指针指向的内存区域属于用户空间2)指针指向的内存区域在进程的地址空间里3)如果是读,读内存应该标记为可读。如果是写,该内存应该标记为可写。 内核提供了两种方法来完成必须的检查和内核空间与用户空间之间数据的来回拷贝。这两个方法必须有一个被调用。 copy_to_user():向用户空间写入数据,需要3个参数。第一个参数是进程空间中的目的内存地址。第二个是内核空间内的源地址。第三个是需要拷贝的数据长度(字节数)。copy_from_user():向用户空间读取数据,需要3个参数。第一个参数是进程空间中的目的内存地址。第二个是内核空间内的源地址.第三个是需要拷贝的数据长度(字节数)。注意:这两个都有可能引起阻塞。当包含用户数据的页被换出到硬盘上而不是在物理内存上的时候,这种情况就会发生。此时,进程就会休眠,直到缺页处理程序将该页从硬盘重新换回到物理内存。 内核在执行系统调用的时候处于进程上下文,current指针指向当前任务,即引发系统调用的那个进程。在进程上下文中,内核可以休眠(比如在系统调用阻塞或显式调用schedule()的时候)并且可以被抢占。当系统调用返回的时候,控制权仍然在system_call()中,它最终会负责切换到用户空间并让用户进程继续执行下去。 给linux添加一个系统调用时间很简单的事情,怎么设计和实现一个系统调用是难题所在。实现系统调用的第一步是决定它的用途,这个用途是明确且唯一的,不要尝试编写多用途的系统调用。ioctl则是一个反面教材。新系统调用的参数,返回值和错误码该是什么,这些都很关键。一旦一个系统调用编写完成后,把它注册成为一个正式的系统调用是件琐碎的工作,一般下面几步: 1)在系统调用表(一般位于entry.s)的最后加入一个表项。从0开始算起,系统表项在该表中的位置就是它的系统调用号。如第10个系统调用分配到系统调用号为9。2)任何体系结构,系统调用号都必须定义于include/asm/unistd.h中3)系统调用必须被编译进内核映像(不能编译成模块)。这只要把它放进kernel/下的一个相关文件就可以。 用户的程序无法直接执行内核代码。他们不能直接调用内核的函数,因为内核驻留在受保护的地址空间。所以应用程序应该通过某种方式通知内核,告诉内核自己需要执行一个系统调用,希望系统切换到内核态,这样内核就可以代表应用程序来执行该系统调用了。 通知内核的机制是通过软中断的机制实现的:通过引发一个异常来促使系统切换到内核态去执行异常处理程序。此时的异常处理程序实际上就是系统调用处理程序。 通常,系统调用靠C库支持,用户程序通过包含标准头文件并和C库链接,就可以使用系统调用(或者使用库函数,再由库函数实际调用)。庆幸的是linux本身提供了一组宏用于直接对系统调用进行访问。它会设置好寄存器并调用int $0x80指令。这些宏是_syscalln(),其中n的范围是从0到6.代表需要传递给系统调用的参数个数。这是由于该宏必须了解到底有多少参数按照什么次序压入寄存器。以open系统调用为例: open()系统调用定义如下是:long open(const char *filename, int flags, int mode)直接调用此系统调用的宏的形式为:#define NR_open 5_syscall3(long, open, const char *, filename, int , flags, int, mode) 这样,应用程序就可以直接使用open().调用open()系统调用直接把上面的宏放置在应用程序中就可以了。对于每个宏来说,都有2+2*n个参数。每个参数的意义简单明了,这里就不详细说明了。
ubuntu下的tftp配置 tftp,我用的是 ubuntu9.04,在网上看到了很多文章,但是发现很多文章讲的都很模糊,或者 根本就是 错的,只好自己琢磨,走了很多弯路,费牛劲终于弄出来了,为了避免同行和我 一样走弯路,特把我的一些总结贴出来和大家共享,有什么不对的地方希望大家指 正,大家 有什么问题也可以一起探讨一下。 ubuntu 9.10 下 tftp 设置方法: 1:sudo apt-get install tftp tftpd openbsd-inetd 特别指出很多文章里用的是 netkit-inetd,但是实际下载时发现 这个软件是下不到的,特改用 openbsd-inetd,实验效果不错。 2:在根目录下创建文件夹 tftpboot cd / sudo mkdir tftpboot 建立文件夹 sudo chmod 777 tftpboot 更改文件夹权限 3: sudo gedit /etc/inetd.conf 修改成如下样子 tftp dgram udp wait nobody /usr/sbin/in.tftpd /tftpboot /usr/sbin/tcpd 4: sudo gedit /etc/xinetd.d/tftp 修改成如下样子(如果没有 tftp 文件就创建它) service tftp { disable =no socket_type =dgram protocol =udp wait =yes user =root server =/usr/sbin/in.tftpd server_args =-s /tftpboot -c source = 11 cps = 100 2 }5: sudo gedit /etc/default/tftpd-hpa 修改成如下样子(如果没有 tftpd-hpa 文件就创建 它) RUN_DAEMON="no" OPTIONS="-s /tftpboot -c -p -U tftpd" 6:sudo /etc/init.d/openbsd-inetd reload sudo in.tftpd -l /tftpboot 7: 在 tftpboot 文件夹下新建测试文件 aaa cd /tftpboot sudo touch aaa sudo chmod 777 aaa 9: 开始测试 tftp 服务 cd /home tftp 192.168.1.111 get /tftpboot/aaa 如果没有出现错误代码且在 home 目录下出现 aaa 文件则证明 tftp 服务建立成功 注意: 1:如果出现 permission denied 错误 则是操作者权限不够, 需要提升权限 su root 输入密码后就可以正常进行 tftp 传输操作了 2:如果出现 Access violation 错误 则是文件权限没有解开, 将要操作的文件操作权限全解开就可以了 chmod 777 文件名
常用命令 查看软件xxx安装内容:dpkg -L xxx 查找软件库中的软件:apt-cache search 正则表达式 查找软件库中的软件:aptitude search 软件包 查找文件属于哪个包:dpkg -S filename 查找文件属于哪个包:apt-file search filename 查询软件xxx依赖哪些包:apt-cache depends xxx 查询软件xxx被哪些包依赖:apt-cache rdepends xxx 增加一个光盘源:sudo apt-cdrom add 系统升级:sudo apt-get update;sudo apt-get dist-upgrade 清除已删除包的残馀配置文件:dpkg -l |grep ^rc|awk ‘{print $2}’ |sudo xargs dpkg -P 编译时缺少h文件的自动处理:sudo auto-apt run ./configure 查看安装软件时下载包的临时存放目录:ls /var/cache/apt/archives 备份当前系统安装的所有包的列表:dpkg –get-selections | grep -v deinstall > ~/somefile 从备份的安装包的列表文件恢复所有包:dpkg –set-selections < ~/somefile;sudo dselect
考研(为学弟学妹能做的,加油!!) 这个是去年我的复习的时候的一些冲刺阶段的计划安排(数学) 经过5,6个月的复习,相信你已经有了一定的基础知识,考研就是,吧原本零碎的知识点串起来,单个分析并不难(大家都是考试高手,也可以说是停留在记忆做题,模板做题),在最后的不到两个月的时间里,应该主要突袭真题,数学的重点放在真题上,最好是成套的做,自己记一下时间,为什么现在做?因为是在最后的临近考试的一个星期到两个星期是主要的英语作文和政治的时间,基本上都在背这个,并大部分是在背政治,和写英语作文上,我们已经没有太阔余的时间在来专门练习数学了, 所以在现阶段,主要的侧重点是:数学的练习,最好的是成套的真题的练习,理由:1,真题的重要性是不言而喻的,直接说是就是“考研数学”的考试大纲,一点不为过。2,每年的真题的的题型变换不大,但绝不会重题,所以练习真题,总结做该类型的模板,思路,多练习一下,把这种思路模板固定下来。这样在考场的时候,高度集中的情况下也会得心应手,不会没有思路拿到题时。3.数学是在上午考试,所以在做成套真题时候,尽量安排到上午。 最后:做真题的时候,不要去记答案,有的说用不几天就作完了。其实不然,按年份来,一套一套的去做,在你做到第十套的时候,相信你已经吧你做的第一套给忘的差不多了。记住做题的目的不是让你记答案,那没有用,我们要学着去练习这种类型的“做题思路,做题模板”,这个才是精髓. 数学的公式,这个是经常忘的,除了背就是大量联系,没有好的方法。太难的题型(在真题上,我不认为会有)不要太较劲了,记住考试是为了拿高分,拿满分的人不多。与其看个别难题,不如去提升自己的做题准确率,保证正确率。 在最后的时间里,最好做一个周计划,一个周做一次,严格去执行,最后的日子里,过的“充实点”。 以上是我自己原来考研的时候,准备的,每个人的情况,都不一样,你可以拿着当做一次参考,自己的计划安排还是以自己为主,根据自己的情况,自己去安排具体的计划。 时间安排: 5:30 起床(我们要占位子去图书馆) ----- 背政治 7:20 吃早餐 ----- 背英语 9:00 ——11:30 做数学(试卷成套的为主) 11:30 吃饭 12:00 对数学答案(自己分析一下错的) 12:40 午休 1:20 背英语(读的阅读理解,背作文) 2:00 做阅读理解, 4:30-5:30 写一篇作文(大小都行,找人去给你改) 5:30 吃饭 6:00 背英语和政治 8:00(7:30)专业课(政治)(我的专业课不难,所以时间分配的也不是太多) 10:30 回宿舍 11:30 看一会英语作文 12:00 睡觉 注:英语很差,所以花的时间有点多,以上是我最后两个月的计划安排,仅供参考,大部分时间都是严格执行的,错过的时间,找其他的时间补过来。考研的时候也没有忘记锻炼,一个星期打两次篮球,玩嘛就好好玩。
毕业了,还上一次院榜 http://tieba.baidu.com/mo/q/checkurl?url=http%3A%2F%2Fdqx.tlu.edu.cn%2Fs%2F17%2Ft%2F37%2F92%2F96%2Finfo37526.htm&urlrefer=5ef18f3f170f2bdcea12840a22853c7c,最后一次上榜,希望学弟学妹好好加油啊
求助:求大神指点 现在要选择Java和算法方向,两个选一个,导师不一样,不能全选,哪个好点,就业方面,待遇,
都走了,就像每个暑假那样,拉着行李走了,唯一不同的是,你们不会再回来了,
水壶宿舍有四个大的,,两个小的,,懒人桌,三个,,插排还几个,,,买了就送,,,18756289868,,,16705宿舍,,
再强,也有温柔的一面[呲牙]
21号 有没有想去方特的,,求组团
1
下一页