note_2017_3.c

Thumb指令与ARM指令的区别?
    Thumb指令是16位代码密度,ARM是32位代码密度,所以Thumb指令的代码密度要高于ARM指令,Thumb指令只是ARM指令的一个子集,不是完整的体系结构

对51或ARM内核应该掌握哪些东西?
    指令系统、中断处理系统、寄存器分配

解释下列定义的含义?
    int **a;//定义一个指向整型变量指针的指针a
    int *a[10];//一个有10个指针的数组,该指针指向一个int型,对比**argc和*argc[]可知道其应该是指针构成的数组,[]优先于*
    int (*a)[10];//一个数组的指针,该指针指向一个有10个int型的数组
    int (*a[10])(int);//一个有10个函数指针的数组,该指针指向一个返回值是int,且有一个int

指针与数组的区别?
    1.进行取地址&运算的时候,指针表示的是指针的指针,数组表示数组的首个元素地址;2.进行sizeof计算的时候,指针指的是指针变量本身的大小,4个字节(32bit系统)或2字节(16bit dos系统),数组表示的是整个数的大小

malloc动态分配内存再free释放后,那块内存数据是否保留?
    释放只是断开指针的指向关系,内存数据仍然保留

什么是内存泄露?
    在进程运行的时候,不断地分配堆内存,产生不够分配的情况就会出现内存不够,退出程序的问题。所以内存泄露主要是动态分配内存但没有及时进行释放引起的。

void *是什么意思?
    void *表示未确定类型的指针,C,C++规定void *类型可以转换为任何其他类型的指针

代码区什么时候销毁?
    进程结束就销毁

你知道哪些排序方法?
    插入,希尔,交换,冒泡,快速,选择,堆排序,归并,基数,二分,二分插入

什么时候用枚举变量,枚举变量有什么好处?
    功能相近的一组常量最好用枚举来定义,枚举不占内存。

定义结构体的时候,往往将大的数据写在前面是为什么?
    为了内存字对齐

用过什么单片机?
    瑞萨V850ES-SJ3 MCU,144pin,ROM有768K,RAM有60K,软件采用实习公司的简易OS及定周期循环,任务之间采用消息队列(先进先出)进行通讯,其结构如下
typedef struct msg{
    uchar id;    /* Task ID */
    uchar cmd;    /* Msg Command */
    uchar byte[4];    /* Parameter */
}MSG;
每个任务有他的Main处理函数xx_main()和定周期处理函数xx_10ms()、xx_100ms()、xx_1s()以及他的中断处理函数,包括中断处理xx_ixx()和定周期中断xx_i1ms();
三大部分:Task的main处理函数和Task的定周期处理和中断处理都是通过MSG结构来通讯。

什么是注册表?
    注册表是一个数据库,用于存储系统和应用程序的设置信息。在wince里,增加新驱动的时候,需要去修改注册表文件platform.reg

Wince中增加驱动的方法?
    1.新建驱动文件夹NewDrv(WINCE500/PLATFORM/Cetus/SRC/DRIVERS目录下)
    2.修改dirs编译引导文件,加载文件夹名字
    3.修改platform.bib配置文件(WINCE500/PALTFORM/Cetus/FILES目录下)
        追加
        ; <New Driver>注释
        NewDrv.dll    $(_PLATRELEASEDIR)\NewDrv.dll NK SH
    4.新增NewDriver.def文件,定义给上层的函数接口定义:
        (1)New_Init()
        (2)New_DeInit()
        (3)New_Open()
        (4)New_Close()
        (5)New_Read()
        (6)New_Write()
        (7)New_Seek()
        (8)New_PowerUp()
        (9)New_PowerDown()
        (10)New_IOControl()
    5.新增Makefile
        只需要!INCLUDE $(_MAKEENVROOT)\makefile.def
    6.新增Sources引导.cpp
        TARGETNAME = led 生成目标的文件名
        TARGETTYPE = PLATFORM 生成的文件受平台控制
        RELEASETYPE = LOCAL 生成的文件放置到当前路径
        TARGETTYPE = DYLINK 生成的目标类型是dll
                 LIBRARY 生成的目标类型是lib库
                 PROGRAM 生成的目标类型是exe文件
        TARGETLIBS 指示连接需要的库的名字
        SOURCELIBS 指示将与某一个lib一起连接    
    7.新增NewDrvIF.cpp
    8.修改platform.reg(WINCE500/PLATFORM/Cetus/FILES目录下)
        追加
        [HKEY_LOCAL_MACHINE\Drivers\$(LAUNCHTYPE)\NewDrv] //LAUNCHTYPE可以是BuiltIn
    `    "Dll" = "NewDrv.dll"
        "Prefix" = "NEW"  表示驱动的前缀
        "Index" = dword:1 前缀序号,不是必须的
        "Order" = dword:0
    9.追加ID在PfmAcmId.h(WINCE500/PLATFORM/Cetus/SRC/INC目录下)
        #define ACM_ID_DRIVER_NEW xx //[xx]NEW DRIVER

    10.在LaunchTable.cpp追加启动项(WINCE500/PLATFORM/Cetus/SRC/DRIVERS/AppLch/LaunchModule目录下)
        追加(使用buildin时候,LauncherTable不用追加)
        {
            LT_DRIVER_A,    //Launcher Type
            LM_ALLMODE,    //Launcher Mode
            NT_ALL,        //机种
            MOD_WORLDWIDE,    //对应地区
            DEV_CHK_NODE,    //Device Check
            TEXT("NEW1:"),    //Module Name
            TEXT("\\Drivers\\Launch\\NewDrv"),    //Registry
            60,                     //Priority
            1,                    //待机模块数
            {    //ID,TIMEOUT值,追加Sleep,Reserve
                {ACM_ID_DRIVER_NEW,WAIT_DEFAULT_MIN_LAUNCH_TIME,0,0},
                {0          ,0                ,0,0},
                ...
                {0          ,0                ,0,0},
            },
            ACM_ID_DRIVER_NEW,
            0,
            0,
        },

LRESULT、WPARAM、LPARAM、LPCSTR在wince中都是什么数据类型?
    LRESULT:在非WIN64就是long
    WPARAM:32bit,就是unsigned int
    LPARAM:32bit,在非WIN64就是long
    LPCSTR:指向一个8bit的常量字符串

WINCE目录分别放着什么文件夹,有什么作用?
    PLATFORM:存放BSP
    PRIVATE:WINCE共享源代码,不希望开发者修改
    PUBLIC:Windows平台开发工具
    SDK:按照平台的系统结构存放各种开发工具,例如编译器
    OTHERS:MFC,ATL的共享程序源代码、链接库档案,.NET的共享函数库

WINCE加载应用程序的步骤?
    1.将Greeting.exe编译成功后拷贝到WINCE500/PLATFORM/Argo/FILES目录下
    2.修改NK所在目录下的PLATFORM.bib
        追加
        Greeting.exe $(_TARGETPLATROOT)\FILES\Greeting.exe
    3.修改NK所在目录下的PLATFORM.reg
        追加
        [HKEY_LOCAL_MACHIN\init]
            "Launch60" = "Greeting.exe" //60表示启动时候的顺序,可以设置成100,确保其他程序都运行起来了
            "Depend60" = hex:14,00,1e,00
    执行makeimg

关于RFID的知识,你懂多少?
    1.接收天线和发射天线平行才能收的到信号
    2.天线的阻抗匹配(相等)才能获得最大功率
    3.为什么读写器的发射功率2W是用33dBm表示
        2W=2000mW=10lg(2*1000)
            =10*(lg2+lg1000)
            =10*(0.3+3)
            =33dBm(功率绝对值,是mw的几倍)

C++的构造函数是什么?
    在C++类中,构造函数用于在建立一个类对象时初始化各成员变量。一个类的构造函数的名字要与其类名同名,且不能有返回值,void也不行。

C++的析构函数是什么?
    析构函数是在类对象被删除时调用的函数,命名规则与构造函数是一样的,只不过需要在其函数名前紧加一个~(波浪号),且不能有参数。其作用就是释放构造函数申请的内存,防止内存泄漏。

C++对象的建立方法有哪些?
    (1)像变量一样建立起来对象,如Aves bird;
    (2)用new语句来建立对象
    Aves *bird;
    bird = new Aves();//new后面跟一个类的构造函数
    这样建立起来的对象没有名域空间的限制,如果要删除掉必须手动使用delete语句。
    delete bird;//delete后面跟要删除对象的指针变量

C++中向类添加成员函数的方法有哪些?
    (1)在类的声明体里面,像构造函数的声明方法
    (2)在类的声明体外面,外部的表现写法为:
    返回值 类名::函数名(参数列表)
    void Aves::run()
    {
        可以直接使用其他成员的变量名称
    }

C++中struct和class的关系?
    基本是同义词,在class中如果一开始没有指定权限关键字(public或private),那么默认权限为private,而在struct中默认权限是public,this代表当前实例对象的指针。

C++中怎么实现类的继承?
    class 派生类名:权限关键字 基类名
    class Ostrich:public Aves
    {

    }
    派生类就可以使用其父类的方法了。但如果父类的成员是私有的(private),子类是不能直接调用其方法的,唯一的办法是将private关键字改为protect,对派生类可见,对外部仍然不可见。

C++中基类的指针可以指向派生类对象吗?
    可以

说说你对于CWinApp类的理解?
    CWinApp是应用程序对象,有且只有一个从CWinApp派生的类的对象theApp对象,CWinApp又是从CWinThread派生的,即表示可能具有一个或多个线程的应用程序的主执行线程。

WinMain通过调用应用程序对象CWinApp的哪些成员函数来控制应用程序?
    初始化应用程序的时候,WinMain调用CWinApp的InitApplication和InitInstance成员函数
    运行应用程序的消息循环的时候,WinMain调用CWinApp的Run成员函数
    终止应用程序的时候,WinMain调用CWinApp的ExitInstance成员函数

dll与lib在编译上面的区别是什么?
    dll是一个编译好的程序,调用时可以直接调用其中的函数,不参加工程的编译。而lib应该说是一个程序集,只是把一些相应的函数总结在一起,如果调用lib中的函数,在工程编译时,这些调用的函数都将参加编译

为什么在C++下定义"C"函数需要加extern "C"关键字?
    因为在编译为OBJ文件的时候,对文件名、变量名进行重命名文件的规则不一样

RLM100读写器的通讯格式是怎么样的,有什么特点?
命令数据包格式:
数据段        SOF     LEN     CMD     PAYLOAD     EOF
长度(字节)    1       -    1       -           1
举例        0xAA    0x02    0x01            0x55 (读取功率)

响应数据包格式
数据段        SOF     LEN     CMD     STATUS        PAYLOAD     EOF(多了一个STATUS)
长度(字节)    1       -    1       1          -             1
举例        0xAA    0x04    0x01    0x00        0x14        0x55(输出功率0x14=20dBm)

特点:无校验和,若数据中出现了开始或结束字符(0xAA,0x55)则在它前面插入一个0xFF,相应的在解析响应数据的时候,遇到0xFF应该将0xFF去掉,隐藏,其不是有效数据位。

位段、位域结构体一般什么时候用的上?
    一般网络传输用的上,更好的利用内存,省空间

无源RFID卡片如何获得电压的?
    RFID卡片内的谐振电路的频率与读写器的载波频率相同,共振产生电荷积累,为RFID卡内的电路提供电压

RFID是怎么做防冲突算法的?
    防冲突算法是基于TDMA(时分多路法)的特定算法,读写器把可供使用的通道容量按时间分配给多个用户的技术。读写器在同一个时间只能建立一个通信关系,先选中一个标签,通信完成后,解除通信关系后,继续与下一个标签通信。

简单介绍一下IIC总线?
    IIC在CPU与IC,IC与IC之间双向传输,高速可达到400kbps,有两条线:SCL时钟线,SDA数据线
    开始信号:时钟为高电平时,数据由高电平向低电平跳变开始传输数据
    写数据时:主设备发送完开始信号后,从设备就知道有数据来了,从设备在时钟高电平时检测数据引脚高低。8个时钟周期后主设备将数据送到数据线上。
    结束信号:时钟为高电平时,数据由低电平向高电平跳变结束传输数据
    应答信号:接收数据的从设备在接收到8bit数据后,在第9个时钟周期,主设备释放数据线(因为有上拉,变为高),从设备向发送数据的主设备发送特定的低电平脉冲,将数据拉低。

简单介绍一下SPI总线?
    SPI总线有四条线:CLK、MOSI、MISO、CS,主控的CS可以通过不同GPIO选不同的SPI芯片,主控发送的CLK时钟一定要满足SPI芯片的速度,2440输出可以到25MHz

简单介绍一下NandFlash的控制?
    IIC、SPI都是通过先发地址再读写数据,Nand也是一样,Nand有8位并行数据线,CLE命令使能,ALE地址使能,发数据的时候CLE和ALE都为低,CE片选,RE读使能,WE写使能,R/B(Ready/Busy)。NandFlash有8个bank,
    每个bank有64页,每页2M,所以每个bank是128M,8个bank的容量就是一个G

如何访问内存?
    1.发出地址信号2.传输数据(6410用DDR内存,接法会与2410、2440不一样)
    
简单介绍一下LCD的控制?
    LCD一般与主控的LCD控制器相连,有VCLK信号(电子枪移动的时钟)、HSYNC水平同步信号(打完一行产生一个信号)、VSYNC垂直同步信号、24位的数据线、DE数据使能信号、背光控制信号以及电源。
    一般步骤为1.打开背光2.时序设置3.在FrameBuffer里写数据。但是清屏幕时,将颜色写入TPAL寄存器,使能该寄存器即可,不再通过FrameBuffer到2440的LCD控制器,这样就很快

简单介绍一下RAM-like接口的控制(比如SDRAM、NOR Flash、网卡)?
    类内存接口一般与主控的内存控制器相连,一般从设备共用主控的地址线和数据线,nRE读使能、nWE写使能信号,同一时间只能有一个工作,通过片选CS来选择(nGCS6、nGCS0、nGCS4),但是不需要手工设置
    片选信号,CPU访问不同地址的时候,将地址信号给内存控制器,就会自动选中相应的片选的信号。比如当地址范围Addr大于等于0小于等于128M(0x0800 0000 -1)的时候,选中NorFlash;
    当地址范围大于512M(0x2000 0000)小于等于640M(0x2800 0000 - 1),选中网卡;当地址范围大于等于768M(0x3000 0000)小于等于896M(0x3800 0000 -1),选中SDRAM。
    硬件决定每个片选脚对应的地址范围是一个bank,128M=2的27次方,所以需要27跟地址线。网卡设备比较特殊是因为网卡设备不需要这么多地址线,它只有一个地址线LADDR2(1的时候访问命令寄存器,
    0的时候访问数据寄存器)

简单介绍一下I2S接口?
    I2S接口只能发送声音数据,AC97声卡除了传声音数据还能传送控制指令。


简单介绍一下红外编码?
    红外编码采用的是NEC协议的PWM或者Philips RC-5的PPM(脉冲位置调制),PWM的话以发射红外线载波的占空比不同来代表“0”和“1”,载波频率是38KHz,占空比为50%(560us高,560us低)表示0,占空比为75%(1680us高,560us低)表示1。NEC遥控指令的数据格式:同步码头(4.5ms高,9ms低)+地址码+地址反码+控制码+控制反码+连发码(用来标记按下的次数),一般来说地址码和地址反码也叫客户码,控制码为按键码值。

简单介绍一下PS2接口?
    PS2的接口6根线,除了电源、地就是时钟和数据线,另外两根线保留,PS2设备到主机的数据在时钟信号的下降沿被主机读取,而主机到设备的数据在时钟的上升沿被设备读取,时钟一般10-20KHZ,
    一个数据包有11bit,bit0是起始位(总是为0),bit8-bit1是8位数据位,bit9是校验位,bit10是停止位(总是为1),bit11为应答位(仅仅在主机到设备的通信才有)

如何将一个Flash芯片制作成一个汉字字库芯片?
    采用GBK内码,它比GB2312(仅有几千个汉字),支持繁体字,总字数有2万多个。制作一个GBK字库放在SD卡里,然后复制写到FLASH芯片W25X16使得其成为字库芯片

SD数据信息分为哪5个部分?
    1.MBR(主引导记录区):分区表位于扇区0
    2.DBR(操作系统引导记录区):每个扇区占多少字节数
    3.FAT(文件分配表):记录每个文件的位置和区域,文件表头8字节(F8FFFF0FFFFFFFFF)文件结束4字节(FFFFFF0F)
    4.FDT(文件根目录表):32个扇区,每个短文件名占用32字节
    5.数据区

怎么从NandFlash启动,怎么从NorFlash启动?
    从NandFlash启动的时候,1.CPU将NandFlash前4K的内容拷贝到CPU片内4K的SRAM位置2.SRAM的0地址称为Stepping Stone,CPU 就从0地址开始取址执行,关开门狗,初始化存储器管理器
    3.把代码(包括前4K)拷贝到SDRAM,继续执行,考到的位置是0x3000 0000。若NandFlash程序容量小于4K,由硬件自动拷贝到4K RAM执行,若NandFlash程序容量大于4K,除了拷贝4K,
    更大任务就是把其他代码都考出来放到SDRAM
    从NorFlash启动的时候,1.NorFlash可以像内存读数据不能像内存一样写数据,NorFlash直接就是0地址,CPU从0地址开始执行

Android ADB调试工具
1.执行shell命令
2.
./adb kill-server 杀死ADB
./adb start-server 启动ADB
./adb devices 查看是否连接ADB
adb shell reboot(与linux命令一样)
adb logcat查看log

volatile的作用是什么?
    告诉编译器不要优化,不要以为没用到就擅自删除。

crt0.s启动文件的软件初始化和硬件初始化分别做了什么?
    crt0.S的功能是:通过它转入C程序
    软件初始化包括:设置栈,设置返回地址,调用main()函数,清理工作。硬件初始化包括:关开门狗、初始化时钟、初始化SDRAM(SRAM就不需要初始化直接用)

程序编译的过程包括哪几步?
    1.预编译2.编译,将.c编译为.s文件3.汇编,将.s汇编为.o文件4.链接 将多个.o合并成一个可执行文件,反汇编工具:arm-linux-objdump -D(表示所有段) -m(表示机器) arm leds_elf > leds.dis
    MIPS下的反汇编工具mipsisa32-elf-objdump -d ../atsc_kappa/obj/cmd.o

SRAM与SDRAM的区别?
    SRAM可以直接访问地址,发地址,读写就可以了,价格高,速度快,NorFlash接口与SRAM一样,访问简单
    SDRAM需要先初始化,先配置存储管理器(设置片选信号,bank选择,设置列地址,行地址,数据位宽),还需要一个不断刷新的刷新时钟,但是价格便宜
    DDR SDRAM是在SDRAM的基础上使用了DDL技术实现上下行双数据率传输,上升沿和下降沿都可以传输数据(Double Data Rate),而SDRAM是(Single Data Rate),只有在下降沿传输数据
    DDR2 可以并行存取,提供相当于DDR内存两倍的带宽。

PC值是什么?
    实际上是正在读取的指令的地址,PC=当前指令地址+8

为什么要有内存管理单元MMU?
    1.为了进行权限管理:不同程序地址空间互不相同,不能越出权限访问其他程序的地址空间。
    2.为了进行地址映射
    在硬件上,CPU里是CPU核连着MMU,MMU连着存储管理器;然后存储管理器才是连着外部的SDRAM的

虚拟地址与物理地址如何转换?
    对于MIPS架构,VA=0xA000 0000 + PA
    对于ARM架构,使用表格,页表(分为段,大页,小页,极小项),段映射的最小单位是1M,4G的话就分为4096个表项,以1M来对齐

调LCD的时候,8bpp与16bpp的区别?
    8bpp:8bit模式,放的是索引值不是颜色值,到调色板(调色板是一块2440的内存,不处于SDRAM,在2440里有一个256*2byte的调色板,也就是数组)取出来颜色发给LCD
    16bpp:16bit模式(如5:6:5),16bit颜色原原本本发送给LCD

U-Boot是什么概念,它有什么作用?
    U-Boot就是一个复杂一点的单片机程序,它的最终目的就是从Flash中读出内核,放到SDRAM后启动内核。另外它也常常用来添加一些和开发有关或和生产有关的一些方便的程序:
    如读写Flash操作,初始化时钟,初始化串口,初始化SDRAM,网卡和USB的相关操作等等。

如何解压U-Boot?
    tar xjf u-boot-1.1.6.tar.bz2 (-j, --bzip2                filter the archive through bzip2)

如何给U-Boot打补丁?
    cd u-boot-1.1.6/ 
    将补丁文件u-boot-1.1.6_jz2440.patch放在上层目录
    patch -p1 < ../u-boot-1.1.6_jz2440.patch (-p1表示忽略第一个斜杠/目录之前的信息)
修改过的文件包括:

如何将打了补丁的U-Boot打成一个压缩包?
    tar cjf u-boot-1.1.6_jz2440.tar.bz2 u-boot-1.1.6 (-c, --create               create a new archive)

编译U-Boot包括几个步骤?
    两个1.配置 make 100ask24x0_config 2.编译make (生成u-boot.bin)

查看JZ2440 U-Boot的Makefile并分析?
    100ask24x0_config    :    unconfig
        @$(MKCONFIG) $(@:_config=) arm arm920t 100ask24x0 NULL s3c24x0
    等同于执行以下命令
        mkconfig    100ask24x0 arm arm920t 100ask24x0 NULL s3c24x0
    可以搜到:MKCONFIG    := $(SRCTREE)/mkconfig
    说明源文件目录下有个mkconfig脚本
    脚本里的ln -s asm-arm asm表示建立一个链接文件asm,指向asm-arm
    如果执行:cd include && ls -l asm
    可以看到:asm->asm-arm

    具体操作在:
    ln -s ${SRCTREE}/include/asm-$2 asm
    那么
    #include <asm-arm/type.h>
    写成
    #include <asm/type.h>
    就可以

    ls asm-arm/arch -l
    asm-arm/arch -> arch-s3c24x0
    ls asm-arm/proc -l
    asm-arm/proc -> proc-armv

    mkconfig里
    echo "ARCH   = $2" >  config.mk
    echo "CPU    = $3" >> config.mk
    echo "BOARD  = $4" >> config.mk

    [ "$5" ] && [ "$5" != "NULL" ] && echo "VENDOR = $5" >> config.mk

    [ "$6" ] && [ "$6" != "NULL" ] && echo "SOC    = $6" >> config.mk
    所以当执行mkconfig    100ask24x0 arm arm920t 100ask24x0 NULL s3c24x0
    ARCH = arm
    CPU  = arm920t
    BOARD= 100ask24x0
    VENDOR = NULL
    SOC = s3c24x0

    LIBS += net/libnet.a是什么意思?
    把net下的文件编译好了打包成一个库

分析U-Boot的编译过程?
    由board/100ask24x0/u-boot.lds可以看出链接地址,编译的第一个文件是cpu/arm920t/start.S
    .text      :
    {
      cpu/arm920t/start.o    (.text)
          board/100ask24x0/boot_init.o (.text)
      *(.text)
    }
    执行grep "33F80000" * -nR(打印出行号)
    结果:
    board/100ask24x0/config.mk:25:TEXT_BASE = 33F80000
    这个是U-Boot在内存中的位置,内存的地址范围是3000 0000到3400 0000,也就是64M,而U-Boot所在地址到栈顶3400 0000的空间是512K,当然如果U-Boot大小超出512K,就需要修改U-Boot在内存的位置
    start.S执行的是什么操作呢?
    1.关看门狗,初始化时钟(跳到clock_init()函数),初始化SDRAM(跳到lowlevel_init.S初始化存储控制器,内存才可以使用)
    2.重定位:把程序从NandFlash拷贝到SDRAM(跳到CopyCode2Ram()函数)
    3.设置栈指针SP(Stack_setup()函数),设置好了才能去调用C函数(_start_armboot()函数进入第二个阶段)

    进入第二阶段lib_arm/board.c里的start_armboot后,主要执行flash_init(),nand_init(),main_loop()函数
    common/main.c里的main_loop()函数主要是等待命令输入,使用readline()函数读入串口的数据,然后执行比如从flash读出内核命令:s=getenv("bootcmd");run_command(s);
    bootcmd环境变量一般写在flash上,bootcmd  = nand read.jffs2 0x30007FC0 kernel;(jffs2的好处是长度不需要页对齐或块对齐,可以随便写)
    再比如启动内核的命令:bootm 0x30007FC0也是通过这个main_loop()函数来解析的。真正执行的时候是执行do_bootm()函数
    其命令相关的结构体是:cmd_tbl_t *cmdtp;

U-Boot怎么执行两条命令?
    通过分号隔开:print;md.w 0(打印内存0地址内容)
为什么内核是放在30007FC0?
    因为内核文件uImage其实是:头部+真正的内核
    而从30007FC0到30008000的64个字节放的就是头部的信息,头部的ih_load信息会指定真正内核的加载地址是30008000

如何解压内核,并打补丁?
    tar xjf linux-2.6.22.6.tar.bz2
    cd linux-2.6.22.6/
    将补丁文件linux-2.6.22.6_jz2440.patch放在上层目录
    patch -pl < ../linux-2.6.22.6_jz2440.patch

如何编译配置并编译内核?
    cp config_ok .config(内核配置的结果是生成.config文件,include/linux/autoconf.h是根据.config自动生成的,然后这个文件又会被顶层Makefile包含,这就是内核配置的原理,另外.config还生成了一个auto.conf文件)
    配置项CONFIG_DM9000 = y/m具体又是在哪里体现呢,是在子目录的Makefile里,如drivers/net/Makefile:
    obj-$(CONFIG_DM9000) += dm9dev9000c.o就会把这个模块的驱动加进来,编译进内核或者模块,根据$(CONFIG_DM9000)的值是y或者m
    make uImage
    最好将内核根目录的Documentation/kbuild/makefile.txt看一遍
    顶层Makefile:
    all:vmlinux(真正内核,第一个目标)
    vmlinux: $(vmlinux-lds) $(vmlinux-init) $(vmlinux-main) $(kallsyms.o) FORCE
    vmlinux-lds  := arch/$(ARCH)/kernel/vmlinux.lds链接脚本
    顶层目录下执行:rm vmlinux && make uImage V=1可以查看详细编译结果,了解具体编译过程
    vmlinux.lds里的:
    .text.head : {
        _stext = .;
        _sinittext = .;
        *(.text.head)
    }
    第一个执行文件是arch/arm/kernel/head.S

简要分析内核启动的过程?
    bootm做了两件事:1.读取头部,移动内核到合适的地方
    2.启动,do_bootm_linux函数又做了两件事(1)设置uboot死前告诉内核的启动参数和机器ID(#define MACH_TYPE_S3C2440 362)(2)跳到入口地址:30000000启动内核kernel,控制权交回内核,没uboot什么事了

怎么去编译一个文件系统?
    1.编译busybox生成的first_yaffs文件系统有bin、linuxrc、sbin、usr;
    2.新建dev文件夹,创建console、null设备
    3.新建etc目录(配置文件,这个是当时秦工给的,从编译器libc拉出来的),创建inittab文件(配置文件)
    4.不指定应用程序
    5.安装C库lib(这个也是当时秦工给的,也是从编译器libc拉出来的)

如何构造根文件系统?
    命令行输入mkfs出来很多制作文件系统的命令:mkfs.bfs;mkfs.cramfs;mkfs.ext2;mkfs.ext3;mkfs.ext4;mkfs.ext4dev;mkfs.fat;mkfs.minix;mkfs.msdos;mkfs.ntfs;mkfs.vfat,首先拿我们已经做好的文件系统来操作fs_mini_mdev.tar.bz2
    cd /work/nfs_root
    mkdir tmp
    cp fs_mini_mdev.tar.bz2 tmp/
    cd tmp/
    sudo tar xjf fs_mini_mdev.tar.bz2
    ls 解压出fs_mini_mdev
    
    对于YAFFS2文件系统,我们需要用到mkyaffs2image工具,光盘的linux/tools下有提供,将其拷贝到/work/tools下,再拷贝到系统bin目录下:sudo cp mkyaffs2image /bin/ 。并且为其增加可执行权限:sudo chmod +x /bin/mkyaffs2image

    cd /work/nfs_root/tmp/
    mkyaffs2image fs_mini_mdev fs.yaffs2

如何动态加载驱动?
    在开发板目录下执行:insmod xx.ko就将驱动加载进来,然后再执行相应的应用测试程序./**
    
linux文件系统目录分别是什么作用?
/bin:存放常用命令
/boot:存放启动程序
/dev:存放设备文件
/etc:存放启动、关闭,配置程序与文件
/home:用户工作根目录
/lib:存放共享链接库
/root:超级用户的工作目录
/sbin:系统管理员的常用管理程序
/tmp:存放临时文件
/lost+found:系统出现异常时,用于保存部分资料
/misc:一些实用工具
/mnt:光驱、硬盘等的挂载点
/media:光驱的自动挂载点
/proc:操作系统的实时信息
/sys:系统中的硬件设备信息
/srv:服务启动后需要提取的信息
/var:主要存放系统日志
/usr:存放用户程序
/tftpboot:tftp服务器的服务目录
/selinux:redhat提供的selinux安全程序

linux桌面环境下如何实现多任务操作:按住Ctrl+alt+F1(F1-F6)不放进入字符型控制台,Ctrl+alt+F1进入图形化窗口

如何添加用户,删除用户,修改用户密码:useradd whl,userdel whl,passwd whl
如何拷贝目录:cp -r test/ -r表示递归
如何创建多层目录:mkdir -p dir/dir2/dir3
如何查看目录下所有文件,包括隐藏文件:ls -a
如何隐藏文件,加一个点:mv c1.c .c1.c
如何打包并压缩成.tar: tar cvf tmp.tar /home/tmp
如何打包并压缩成.tar.gz:tar cvzf tmp.tar.gz /home/tmp 
如何解压缩.tar:tar xvf tmp.tar
如何解压缩zip:unzip abc.zip
如何解压缩.tar.bz2:tar -xjvf abc.tar.bz2

用户分为哪3类?:文件所有者u,和文件所有者同组的用户g,其他用户o
如何为其他用户增加对hello.c的写权限:chmod o+w hello.c
如何查看磁盘的使用情况:df -k(以字节为单位)
如何查看目录大小:du -b(以字节为单位) test
如何查看网络监听端口:netstat -a
如何打开任务管理器,查看CPU使用情况:top
如何查看系统所有进程:ps -aux

常用vi命令有哪些?:yy复制,[n]yy复制n行,p粘贴,dd删除,[n]dd删除n行,/查找,G移动到文件尾巴,u取消
o移动到行最前面,$移动到行最后面,n-向上移动n行,n+向下移动n行,nG移动到第n行

如何显示行号:set nu
如何取消行号:set nonu

如何查找命令所在目录?
查找命令所在目录:which ls,/bin/ls

什么是shell?:将用户输入的指令转换为相应的机器能够运行的程序种类
什么是shell脚本?:包含一系列命令序列的文本文件,类似批处理文件
如何运行脚本:sh hello.sh,.s1
shell脚本第一行为什么必须是#!/bin/sh ?因为#!是指定一个shell命令解析器为sh,其他还有类似ksh,bash,csh,tcsh
编辑好脚本还必须使其具有可执行属性,

shell脚本的变量与C语言的变量有什么区别?
编辑的时候,#开头表示注释,变量不需要声明,变量没有类型,但是使用时要加“$”符号,表示引用变量,如a="hello world",调用时echo $a,
默认的一些变量有哪些?$#传入脚本的命令行参数个数,$*所有命令行参数值,不包括$0,也就是说不包括文件名的参数列表。在各个参数值之间留有空格,$0命令行本身(shell文件名),
$1第一个命令行参数,$2第二个命令行参数。
如何声明一个局部变量?在变量名首次被赋值时加上local关键字可以声明一个局部变量,如local hello="var2",注意,变量赋值时,“=”左右两端都不能有空格,BASH中语句结尾不需要分号。

if语句
if [expression]
then
    #code block
fi
--------------------------
if [expression]
then
    #code block
else
    #code block
fi
-------------------------
else if语句
if [expression];then
    #code block
elif [expression]
    then
        #code block
    else
        #code block
    fi
fi

比较操作
相等 -eq =
不相等 -ne !=
大于 -gt >
小于 -lt <
大于等于 -ge
小于等于 -le
为空 -z
不为空 -n
&& 表示前面为真的时候执行后面
|| 表示前面不为真的时候执行后面
举例:
while [ $# -gt 0 ] while(传入命令行参数个数>0),注意:在"["和"]"符号的左右都留有空格
if [ $a = $b ] 如果a和b相等,注意:"="左右都有空格
if [ -z $a ] 如果字符串a为空
[ $# -lt 4] && exit,如果传入命令行参数个数小于4就退出
[ "$(BOARD_NAME)" ] || BOAED_NAME = $1,如果BOAED_NAME不存在,BOARD_NAME = 传入脚本的命令行参数的第一个参数

编辑的时候,以下这些各代表什么意思?
-e表示文件已经存在
-f表示文件是普通文件
-s表示文件大小不为零
-d表示文件是一个目录
-r表示文件对当前用户可以读取
-w表示文件对当前用户可以写入
-x表示文件对当前用户可以执行

例:
#!/bin/sh
folder=/home
[ -r "$folder" ] && echo "Can read $folder"
[ -f "$folder" ] || echo "this is not file"
执行结果,两条语句都能被打出来

for循环的基本结构
for var in [list]
do 
    #code block
done
其中$var是循环控制变量,[list]是var需要遍历的一个集合,do/done相当于C语言中的大括号
注意:如果for和do被写在同一行,必须在do前加上";",如:for $var in [list];do
例:
#!/bin/bash
#for day in Sun Mon Tue Wed Thu Fri Sat
for day in "Sun Mon Tue Wed Thu Fri Sat" 如果列表被包含在一对括号中,则被认为是一个元素
do
    echo $day
done
注意:for所在那行day不加"$"符号,而在循环体内,echo所在行变量$day是必须加上"$"符号的

while循环的基本结构
while [ condition ]
do 
    #code block
done
例子:
#!/bin/bash
while [ $# -gt 0 ]
do 
    echo helloworld
done
注意:while和until的区别在于while为真时执行,until是为假时执行

case语句结构
case "$var" in
    condition 1);;
    condition 2);;
    *)default statments;;
esac
例:
#!/bin/bash
echo "Hit a key,then hit return"
read Keypress
case "$Keypress" in
    [A-Z])    echo "Uppercase letter";;
    [a-z])    echo "Lowercase letter";;
    [0-9])    echo "Digit";;
    *)    echo "Punctuation,whitespace, or other";;
esac
注意:Read这里是一个函数,等待键盘按下,;;就是break的意思

linux系统怎么区分可执行文件?
在linux系统中,可执行文件没有统一的后缀,系统从文件的属性来区分可执行文件和不可执行文件

敲命令echo $?表示上一条命令的返回结果

Gcc通过后缀来区别输入文件的类别
.c:C语言源代码文件
.a:由目标文件构成的库文件gcc
.C,.cc或.cxx:C++源代码文件
.h:头文件
.i:已经预处理过的C源代码文件
.ii:已经预处理过的C++源代码文件
.o:编译后的目标文件
.s:汇编语言源代码文件
.S:经过预编译的汇编语言源代码文件

gcc [options编译器所需要的编译选项] [filenames要编译的文件名]
gcc -Wall hello.c -o hello
一般编译选项:
-Wall 打开所有类型语法警告,类似还有-ansi ACC标准语法所要求的告警,-pedantic ANSI标准所列所有告警
-w 不生成任何警告信息
-c只编译生成.o选项,自动生成与程序名一样的文件名,只编译不优化
-g带有调试信息,产生调试工具(GNU的GDB)所必要的符号信息
-O代码优化,优化编译,效率更高,主要进行线程跳转和延迟退栈两种优化 -O2比-O更优化 -O3与CPU相关

-I dirname 告诉预处理程序,如果在系统预设的头文件目录(/usr/include)中没有找到需要的头文件,就从指定的dirname 目录中去查找头文件,例如
gcc foo.c -I /home/include -o foo,就会多一个可以寻找文件的位置

-L dirname 告诉链接程序,首先在-L指定的目录中去寻找,然后再到系统默认的预设路径中(usr/lib)寻找,也是多了一个可以寻找头文件的位置

-lname 指明我们要用到的非C的库,装载名字为“libname.a”的函数库,函数库的命名规则都是“lib”开头的,比如-lm表示链接名为“libm.a”的数学函数库,例如:gcc foo.c -L /home/lib -lfoo -o foo

-static 静态链接库文件,例如gcc -static hello.c -o hello
库有动态和静态,动态库也叫共享库,用.so为后缀,静态用.a为后缀,库文件都在/usr/lib2和lib目录下

静态链接:和每个程序包包含在一起运行,每个程序就一个拷贝,是一个整体
动态链接:程序运行时才去载入这个库,多个程序共用一个拷贝
由于动态库节省空间,linux下进行链接的缺省操作是首先链接动态库

-DMACRO 定义MACRO宏,等效于在程序中使用#define MACRO
====================================================================
Makefile工程管理器,构建和管理自己的软件工程,Makefile文件描述整个工程的编译,链接等规则,用于说明如何生成一个或多个目标文件,能自动识别更新的代码文件并实现整个工程的自动编译工具。

规则格式如下
目标targets:依赖prerequisites(dependency_file)
    命令command(注意命令应以“TAB”符开始,否则运行会出错)

main.o:main.c
    gcc -c main.c

再比如多个依赖的情况
hello:main.o func1.o func2.o
    gcc main.o func1.o func2.o -o hello
main.o:main.c
    gcc -c main.c
func1.o:func1.c
    gcc -c func1.c
func2.o:func2.c
    gcc -c func2.c
.PHONY:clean
clean:
    rm -f hello main.o func1.o func2.o

Makefile被执行的条件:1.目标文件不存在2.依赖被修改
这里的.PHONY为伪目标(phony targets):只有命令,没有任何依赖的目标,".PHONY"将“clean”目标声明为伪目标
make命令默认会去找makefile或Makefile工程文件,否则的话就要用如下方式指定文件名:Make -f 文件名

添加依赖文件的时候如何使用变量使得添加依赖文件更加方便?
不使用变量:
hello:main.o func1.o func2.o func3.o
    gcc main.o func1.o func2.o func3.o -o hello
使用变量:
obj=main.o func1.o func2.o func3.o
hello:$(obj)
    gcc $(obj) -o hello
makefile中存在系统默认的自动化变量
$^:代表所有的依赖文件
$@:代表目标(的完整名称)
$<:代表第一个依赖文件

如:
hello:main.o func1.o func2.o func3.o
    gcc $^ -o $@

makefile的@echo表示取消echo本身的回显。还可以使用echo 1等字样查看执行到哪一步

==========================================================================================================
linux文件系统的两层结构:VFS和各种不同的具体的FS,cat /proc/filesystems可查看系统支持的文件系统

什么是进程:由代码段(text)、用户数据段(user segment)以及系统数据段(system segment)共同组成的一个动态执行环境,
是一个具有一定独立功能的程序的一次运行,他的特点是:动态性,并发性,独立性,异步性。

常用进程有哪些:1.交互进程,由shell启动;2.批处理进程;3.守护进程,后台运行(不因为终端关闭而关闭)

进程有哪些状态?
就绪,执行,阻塞

什么是进程的互斥?
进程的互斥主要指的是他的唯一性和排他性,当有若干进程都要使用某一共享资源时,任何时刻最多允许一个进程使用,其他要使用该资源的进程必须等待,直到
占用该资源的进程释放了该资源为止。

什么是临界资源?
操作系统中将一次只允许一个进程访问的资源称为临界资源。

什么是临界区?
进程中访问临界资源的那段程序代码称为临界区,或者说访问临界资源的代码

什么是进程的同步?
系统中的一些进程需要相互合作共同完成一项任务,一组并发进程按一定的顺序执行的过程称为进程的同步。

什么是合作进程?
具有同步关系的一组并发进程称为合作进程,他们之间相互发送的信号称为消息或事件

什么是进程的调度?
指的是按一定的算法,从一组待运行的进程中选出一个来占有CPU运行

调度的方式有哪些?
抢占式和非抢占式

调度算法有哪些?
先来先服务算法
短进程优先调度算法
高优先级优先调度算法
时间片轮转法

什么是死锁?
多个进程因竞争资源而形成一种僵局,若无外力作用,这些进程都将永远不能向前推进。

如何防止死锁?
各进程按照相同的顺序获取互斥资源可以防止死锁

父子进程的代码段和数据段有什么关系?
父子进程共享代码段,但是数据是彼此独立的,子进程的数据空间、堆栈空间都会从父进程那里得到一个拷贝,而不是共享。子进程对同一个数据的操作并不会影响父进程的同名数据

fork与vfork的区别
1.fork是子进程拷贝父进程的数据段,vfork是子进程与父进程共享数据段
2.fork是父子进程执行次序不确定,vfork是子进程先运行,父进程后运行

什么是exec函数族?
exec是用被执行的程序替换调用他的程序,改变当前进程的行为,替换为另一个进程。更新代码段,创建新的数据段和堆栈

fork和exec在创建PID上面有什么不同?
fork创建一个新的进程,产生一个新的PID。exec启动一个新的程序,替换原有的进程,因此进程的ID不会改变。

system函数的功能是什么?(int system(const char* string))
调用fork产生子进程,由子进程来调用/bin/sh -c string来执行参数string所代表的命令,举例如:system("ls -al /etc/passwd");

wait函数的功能是什么?
阻塞该进程,直到某个子进程退出

进程间的通信包括哪些内容?
1.数据传输,一个进程需要将它的数据发送给另一个进程
2.多个进程间共享同样的数据
3.通知时间:一个进程需要向另一个或一组进程发送信息,通知他们发生了某种事件
4.进程控制:有些进程需要完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有操作,并能够及时知道它的状态改变

linux进程间通信(IPC)由UNIX进程间通信,基于System V 进程间通信,POSIX进程间通信发展而来,他们的发展轨迹如下所示:
           AT&T
         ----------> System V IPC----->
        |                \
最初的Unix IPC    |----------------------------->     \
        |  BSD                  \    Linux IPC
        |----------> 基于Socket IPC--->      /
                         /
                        /
                Posix IPC-----> 

什么是Posix?
Portable Operating System Interface表示可移植操作系统接口,不局限于Unix,Windows都支持POSIX标准

什么是System V?
也被称为AT&T System V,是Unix操作系统众多版本中的一支

Linux进程间通信方式有哪些?用途是什么?
1.管道(pipe)和有名管道(FIFO)
    无名管道(pipe)是单向的,它把一个进程的输出和另一个进程的输入连接在一起,左边是管道头,右边是管道尾,写进程在管道尾写入数据,读进程在管道头读数据。数据被读出后,将从管道中删除。
其他读进程不能再读这些数据。主要用于父进程和子进程之间的通信,管道有个简单的留空机制,试图读空和向已满进程写操作的进程都将被阻塞。int pipe(int fieldis[2]);filedis[0]用于读,filedis[1]用于写
管道只能传送无格式的字节流。
    有名管道(FIFO)和无名管道的区别在于:无名管道只能由父子进程使用,(而且必须要在调用fork()之前调用pipe(),否则将创建两个管道),而有名管道,不相关的进程也能交换数据。它实质上是一个
文件,类似普通文件,存在文件实体。创建FIFO的函数是int mkfifo(const char)
2.信号(signal)
    信号是一种异步机制,是软件层次上对中断的一种模拟,比较古老的一种Unix进程间通信。产生信号的条件有以下几种:1.按键可以产生信号2.硬件异常产生信号(除数为0、无效的存储访问),硬件通知内核,内核知道后
产生信号通知进程,比如内核对一个正在访问无效存储区的进程产生一个SIGSEGV信号。3.进程使用kill函数发送信号给另一个进程4.用户可以用kill命令将信号发送给其他进程。信号类型有大概30种,常用的有以下几种:
SIGHUP、SIGINT、SIGKILL、SIGTERM、SIGCHLD、SIGSTOP,可以看出信号能够传送的信息量还是比较有限的,对于这些信号操作通常有三种处理方式1.忽略SIG_IGN2.执行用户希望的操作3.执行系统默认的操作SIG_DFL。信号发送函数有kill(向自身进程和其他进程发送信号)
和raise(向进程自身发送信号)、alarm(定时时间到产生SIGALRM,若不处理,默认操作是终止该进程,每个进程只能有一个闹钟,设置新的旧的即使没有超时也将被替换,当参数为0表示取消闹钟)。另外我们还要记得的是pause函数,
它不发送信号,他是等待,使调用进程挂起直至捕捉到一个信号并且执行了一个信号处理函数后,挂起才结束。信号处理的主要方法有两种:1.使用简单的signal函数
void (*signal(int signo,void (*func)(int)))(int) 2.使用信号集函数组(signal set)
kill -l可以查看系统所支持的所有信号列表

3.消息队列
    消息队列也叫报文队列,它就是一个消息的链表,可以把消息看作一个记录,具有特定的格式。一些进程可以向消息队列按照一定的格式添加消息,另一些进程则可以从消息队列中读走这些消息。他主要
有两种类型:POSIX消息队列和System V消息队列,后者被大量使用,System V消息队列有一个持续性,只有在内核重启或人工删除时,该消息队列才会被删除。每个消息队列在系统范围内都有一个唯一键值,或者说ID,
要获得一个消息队列的描述字,必须提供该消息队列的键值。键值由ftok函数获得,msgget再根据这个键值获得其对应的消息队列描述字,即消息队列ID:msqid,以实现对消息队列的各种不同操作。接着再由msgsnd函数向消息队列发送一条消息。
接收的时候,使用msgrcv()函数从msqid代表的消息队列读走消息并存储起来,成功读取后,该条消息将被删除。

4.共享内存
5.信号量(信号灯)
    信号量也叫信号灯,主要用于访问控制,根据信号量判定是否能够访问某些共享资源,主要也就是保护临界资源;另外还可以用于进程间同步。信号量分为二值信号灯和计数信号灯。二值信号灯只能取0和1,类似互斥锁,二值信号灯
只能有一个进程访问,强调共享资源,只要共享资源可用,其他进程同样可以修改信号灯的值;而互斥锁更强调进程,占用资源的进程使用完资源,必须由进程本身来解锁。计数信号灯可以多个进程进行访问。
使用的时候,也是由ftok获得键值,然后semget获得信号量,semop进行信号量控制
6.套接字(Socket)

什么是线程?他和进程有什么区别?
线程是进程的子集,由线程库调度,是程序执行的最小单位。进程是资源分配和管理的的最小单位,是内核调度的基本单位

为什么要用多线程?
第一,linux启动一个进程需要分配独立的地址空间,建立众多的数据表来维护他的代码段、堆栈段和数据段;而运行于一个进程中的多个线程,他们之间使用相同的地址空间,线程间切换上下文所需的时间开销也大大减小了。第二,进程间传递数据只能通过进程间通信的方式进行,这种方式费时且不方便,
而同一个进程下的多个线程之间共享数据空间,一个线程的数据可以直接被其他线程所用,这样更快捷方便。第三。使得多CPU系统更加有效。操作系统会保证当前线程不大于CPU数目时,不同的线程运行于不同的CPU上。第四,可以改善程序结构,一个又长又复杂的进程可以考虑分为多个线程,成为几个独立或
半独立的运行部分,这样程序更利于理解和修改,更简介明了。

多线程对编程有什么要求?
每个线程服务一个客户端。优势是开销小,通信机制简单,可共享内存。但共享地址空间,可靠性低,一个服务器出现问题时可能导致系统崩溃,同时全局共享可能带来竞争,共享资源需要互斥,对变成要求高。
编写linux下的多线程程序,需要使用头文件pthread.h,连接时需要使用库libpthread.a,因为pthread的库不是linux系统的库,所以在进行编译的时候要加上-lpthread,线程和进程是共享数据部分的。使用的时候
pthread_create()用于进程创建,pthread_exit()用于线程主动调用退出(或者从线程函数return都将正常退出,不正常退出是比如线程访问非法地址,自身运行错误导致的不可预见的错误)
pthread_join()用来等待一个线程的结束,将当前线程挂起,等待线程结束

===================================================================================================================================================================================================
linux网络参考模型与传统OSI参考模型有什么区别?
OSI参考模型从底层到顶层分别为:物理层、数据链路层、网络层、传输层、会话层、表示层、应用层;Linux的TCP/IP参考模型将网络层和数据链路层统称为网络接口层,网络层和传输层和OSI一样,没有会话层和表示层,直接最后一层就是应用层。
网络层的主要协议包括:
IP:Internet协议(在源主机和目的主机之间传输数据报文的数据块,包括数据传送、寻址、路由选择、数据报文的分段)
ICMP:网际控制报文协议(报告出错情况)
ARP:地址解析协议(转换32位IP地址和48位物理地址)

传输层的主要协议包括:
TCP:传输层控制协议(建立网络上用户进程之间的对话,确保进程间的可靠通信。提供功能包括1监听输入对话请求2.请求另一网络站点对话3.可靠发送和接收数据4.适度的关闭对话)
UDP:用户数据报文协议(提供不可靠的,不用建立对话的非连接型传输层服务,如视频点播)

socket是一种文件描述符,基于套接字的编程,利用TCP/IP协议栈,可以实现任意类型的通信,消息可以自由定义。
套接字有三种类型:流式套接字(SOCK_STREAM使用TCP协议)、数据报套接字(SOCK_DGRAM使用UDP协议)、原始套接字(SOCK_RAW使用IP协议)

在socket程序设计中,struct sockaddr用于记录网络地址:
struct sockaddr
{
    u_short sa_family;
    char sa_data[14];
}
sa_family:协议族,采用“AF_xxx”的形式,如:AF_INET(IP协议族)
sa_data:14字节的特定协议地址

在socket程序设计中,struct sockaddr_in同样用于记录网络地址:
struct sockaddr_in
{
    short int sin_family;/*协议族*/
    unsigned short int sin_port;/**端口号/
    struct in_addr sin_addr;/*协议特定地址*/
    unsigned char sin_zero[8];/*填0*/
}
编程中一般使用与sockaddr等价的sockaddr_in数据结构,操作更简单,可读性更好。
typedef struct in_addr{
    union{
        struct{
            unsigned char s_b1,
            s_b2,
            s_b3,
            s_b4;
            }S_un_b;
        struct{
            unsigned short s_w1,
            s_w2;
            }S_un_w;
        unsigned long S_addr;
        }
    }S_un;
}IN_ADDR;

inet_aton()函数和inet_ntoa()函数的作用是什么?
用于地址转换,n代表network,to就是to,a代表ascii,
inet_aton()函数是将192.168.0.1形式的IP转换为32位整数的IP,存储在参数inp指针
inet_ntoa()函数是将32位整数IP转换为192.168.0.1的格式

为什么要进行字节序转换?
因为网络传输的数据顺序是统一的,是big endian大端模式,如果和CPU的字节顺序不同则需要进行转换

发送的时候怎么转换字节序?
htons()把unsigned short类型从主机序转换到网络序,htonl()把unsigned long类型从主机序转换到网络序
接收的时候怎么转换字节序?
ntohs()把unsigned shrot类型从网络序转换到主机序,ntohl()把unsigned long类型从网络序转换到主机序

在网络中标识一台主机可以用IP地址,也可以用主机名。
struct hostent *gethostbyname(const char *hostname)
struct hostent
{
    char *h_name;/*主机的正式名称*/
    char *h_aliases;/*主机的别名*/
    int h_addrtype;/*主机的地址类型 AF_INET*/
    int h_length;/*主机的地址长度*/
    char **h_addr_list;/*主机的IP地址列表*/
}

#define h_addr h_addr_list[0]/*主机的第一个IP地址*/
常用的Socket编程函数:
socket创建一个socket
bind用于绑定IP地址和端口号到socket
connect用于与服务器建立连接
listen用于设置服务器能处理的最大连接要求
accept用来等待来自客户端的socket连接请求
send发送数据
recv接收数据

基于TCP的服务器的通讯过程
1.创建一个socket,用函数socket()
2.绑定IP地址、端口等信息到socket上,用函数bind()
3.设置允许的最大连接数,用函数listen()
4.等待来自客户端的连接请求,用函数accept()。(注意:这时候在等待客户机调用connect()建立连接)
5.收发数据,用函数send()和recv(),或者read()和write()
6.关闭网络连接

基于TCP的客户端通讯过程
1.创建一个socket,用函数socket()
2.设置要连接的服务器的IP地址和端口等属性
3.连接服务器,用函数connect()(注意:这时候服务器端正在执行accept()阻塞等待客户连接)
4.收发数据,用函数send()和recv(),或者read()和write()
5.关闭网络连接
------------------------------------------------------------------------------------
基于UDP的服务器的通讯过程
1.创建一个socket,用函数socket()
2.绑定IP地址,端口等信息到socket上,用函数bind()
3.循环接收函数,用函数recvfrom()(注意:这时候在等待客户机的服务请求)
4.关闭网络连接

基于UDP的客户端的通讯过程
1.创建一个socket,用函数socket()
2.绑定IP地址、端口等信息到socket上,用函数bind()
3.设置对方的IP地址和端口等属性
4.发送数据,用函数sendto()(注意:这时候服务器正在用recvfrom()阻塞等待客户机的服务请求)
5.关闭网络连接
-----------------------------------------------------------------------------------
在网络程序里,一般来说都是许多客户端对应一个服务器,常用服务器模型有:循环服务器(同一时刻只能响应一个客户端的请求)、并发服务器(同一时刻能响应多个客户端请求)

UDP循环服务器的实现方法:UDP服务器每次从套接字上读取一个客户端的请求->处理->然后将结果返回给客户机    
    socket(...);
    bind(...);
    while(1)
    {
        recvfrom(...);
        process(...);
        sendto(...);
    }
可以同时服务于多个客户端

TCP循环服务器为什么很少用?
因为TCP服务器一次只能处理一个客户端的请求,只有在这个客户端的所有请求都满足后,服务器才能继续后面的请求。这样,如果有一个客户占住服务器不放时,其他客户机都不能工作。

TCP并发服务器
并发服务器的思想是每一个客户机的请求并不由服务器直接处理,而是由服务器创建一个子进程来处理
    socket(...);
    bind(...);
    listen(...);
    while(1){
        accept(...);
        if(fork(...)==0){
            process(...);
            close(...);
            exit(...);
        }
        close(...);
    }

==========================================================================================================================
常量后面的“L”和"u"表示什么意思
unsigned short [int] 16bit,unsigned long [int] 32bit,"[]"部分可省略
unsigned long a = 0x0000FFF0L;//整形常量后缀“L”指明该常量0x0000FFF0是长整形,另外还有“u”表示无符号整形

指针占几个字节?内存单元的编号叫做地址/指针,单片机指针占三个字节,在32位机中地址长度都是32bit,因此无论哪种变量类型的指针都占4个字节

指针常量指的是什么?指针常量只有唯一的一个:NULL(空地址),通常用“\0”表示

字符串常量“A”和字符常量‘A’有什么区别?字符'A'只占用一个字节,“A”在内存中占两个字节,还有NULL字符

指针的运算符“*”写在哪边?指针定义建议把“*”写在靠近变量名一侧,如int *a;

局部变量和全局变量的作用域有什么区别?
1.局部变量在函数内部定义,仅被该模块内部的语句访问,在进入模块时生成,压入栈,在退出模块时消亡,弹出栈,释放内存单元。
2.全局变量贯穿整个程序,作用域为源文件,定义在所有函数之外且只能定义一次。在整个程序执行过程中都要占用内存空间。全局变量声明不能再赋值,只是表明函数内要用到某个外部变量,从模块化程序设计
的观点看,尽量不适用全局变量。

若局部变量和全局变量同名将会是什么情况?在局部变量作用域内,全局变量不起作用

static有什么作用?
1.声明为static的(局部)变量,在函数体内时,这一函数在被调用过程中维持其值不变(若未赋初值,静态局部变量初始化为0)
2.声明为static的(全局)变量,在函数体外,可以被模块内所有函数访问,但不能被模块外其他函数(同一源程序的其他源文件)访问,它是一个本地/静态的全局变量
3.声明为static的函数,只可以被模块内的其他函数调用

typedef有什么作用?
为一种数据类型定义一个新名字,目的有两点:1.给变量一个易记且意义明确的新名字2.简化一些比较复杂的类型声明

size_t strlen(const char *s);//不包括结束符“\0”

const的作用?保护那些不希望改变的参数,减小bug的出现
const int a = 10;等价于int const a = 10;
const int *a;//常量指针,const修饰指针a所指向的对象,所以不能通过指针a来修改指向的对象的值,但a可以指向其他对象,因为指针本身的值(地址)是可以改变的
int * const a;//指针常量,const修饰指针a,因此指针本身(地址)的值是不可以改变的,a不可以指向其他对象,而其指向的对象的值是可以改变的。
        C语言的指针常量只有NULL一个,其值为0,表明一个指针目前未指向任何对象,NULL指针
int const *a const;//a是指向常整数的常指针

赋值运算符的左边只能是一个变量名
自增自减运算符只用于变量
逗号运算符:表达式1,表达式2;//分别对两个表达式求值,并以第二个表达式的值作为整个符号表达式的值
如y = (x=x+b),(b+c);//y=b+c

*cp++:相当于*(cp++)同一优先级,结合性右向左

在数组中,a和&a[0]是等价的,都是指数组的地址。*(array+i)和array[i]是等价的,一个是指针操作,一个是数组操作,建议采用指针操作,利于后期维护和修改

当char *p = "hello world";指针所指向对象定义为只读的,不可以通过指针修改这个字符串的值,比如p[1] = 'c';就是错的

二维数组a[i][j],如何表示第1行第2列元素的地址和值?
地址:    &a[1][2]    a[1]+2        *(a+1)+2        可以看出a[1]等价于*(a+1)
元素值:a[1][2]        *(a[1]+2)    *(*(a+1)+2)

结构体指针的用法
struct stu{
...
...
}* student,s1;
结构体指针对成员的访问:student->name等价于(*student).name

__attribute__的作用?可以设置函数属性、变量属性、类型属性

预定义包括C语言自带的预定义符号和用户自己编写的宏定义
(1)预定义符号
__FILE__文件名
__LINE__行号
__FUNCTION__函数名
__DATE__日期
__TIME__时间
__STDC__若编译器遵循ANSIC ,则值为1

(2)宏定义又分为无参宏定义和带参宏定义,无参宏定义比较简单,一般对输出格式做宏定义,可以减小编写麻烦。
[1]带参宏定义要注意宏名和形参表之间不能有空格出现:比如:#define MAX(a,b) (a>b)?a:b如果写成#define MAX (a,b) (a>b)?a:b则宏名MAX将代表字符串“(a,b) (a>b)?a:b”
[2]带参宏定义中,形参不分配内存单元,不必做类型定义。
[3]宏调用的实参可以是表达式
    #define SQ(y) (y)*(y)
    sq = SQ(a+1);展开时用“a+1”代替y
[4]形参通常用括号括起来以避免出错,y肯定要加括号,否则展开时会出错,最好在整个符号串外也加括号#define SQ(y) ((y)*(y))

头文件包含的“”和<>有什么区别,双引号表示首先在当前源文件目录中查找再到系统查找,尖括号表示在系统头文件目录中查找

C语言中的内存分配
1.C语言程序所占内存有哪些分类
[1]栈(stack):函数参数值、局部变量、返回地址,由系统自动分配,压入栈分配,弹出栈释放。
[2]堆(heap):由程序员动态分配(malloc)并指定大小和释放(free),如char *p1;p1 = (char *)malloc(10);
[3]数据段(data):全局变量、静态变量、常数
它分为普通数据段和BSS数据段,普通数据段指的是包括可读可写/只读数据,静态初始化的全局变量或常量,BSS数据段指的是未初始化的全局变量
[4]代码段

堆和栈有什么区别?
(1)申请方式的区别,栈由系统自动分配,堆是程序员动态分配的
(2)申请后系统的响应不同,堆在操作系统中有一个记录空闲内存地址的链表。当收到程序申请时,系统遍历链表,寻找第一个空间大于所申请空间的堆节点,然后将该节点从空闲节点链表中删除,
并将该节点的空间分配给程序员。一般系统会在这块内存空间首地址处记录本次分配的大小。这样,代码中删除语句才能正确地释放本内存空间。如果找到的堆节点的大小与申请的大小不相同,系统
会自动地将多余的那部分重新放入空间链表中。而对于栈来说,只有栈的空间大于所申请的空间,系统才为程序提供内存,否则将报异常,提示栈溢出。
(3)申请大小的限制
堆是向高地址拓展的数据结构,是不连续的内存区域。这是由于系统用链表来存储的空闲内存地址,地址是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。因此堆获得的空间比较灵活也比较大。
栈是向低地址拓展的数据结构,是一块连续的内存区域。因此栈顶的地址和栈的最大容量是系统预先规定好的,如果申请的空间超过栈的剩余空间时,将提示栈溢出,因此,能从栈获得的空间较小。
(4)申请速度的限制
堆是由malloc等语句分配的内存,一般速度较慢而且容易产生内存碎片,不过用起来很方便
栈由系统自动分配,速度较快,但程序员一般无法控制
(5)堆和栈的存储内容
堆一般在堆的头部用一个字节存放堆的大小,堆中的具体内容由程序员安排。在调用函数时,第一个进栈的是函数调用语句的下一条可执行语句的地址,然后是函数的各个参数,大多C语言编译器中参数是
由右往左入栈的,然后是函数中的局部变量。当本次函数调用结束后,局部变量先出栈,然后是参数,最后是栈顶指针指向的最开始的存储地址。也就是调用该函数处的下一条指令,由该点继续运行。也就
对应了栈的先进后出的原则。

什么是字长?能够由计算机一次完成处理的数据称为字,不同体系结构的字长一般不同,现在通用的处理器字长为32位
linux给出的数据类型有什么特点?linux给出了两种数据类型:1.不透明的数据类型,利用typedef声明,如保存进程标识符pid_t(实际是int)2.长度明确的数据类型:内核“asm/type.h”

什么是数据对齐?对齐是内存数据与内存中的相对位置相关的问题。如果一个变量的内存地址正好是它长度的整数倍,它就被称作是自然对齐的。
例如,对于一个32位(4个字节)类型的数据,如果它在内存中的地址刚好可以被4整除(最低两位是0),那么它就是自然对齐的。0x04(0000 0100),0x1c(0001 1100)

什么是字节顺序?一个字中各个字节的顺序,由大端模式,有小端模式
大端模式:字数据的高字节存储在低地址中,低字节存储在高地址
小端模式:与大端模式相反(ARM9)

__asm__ 后面使用_volatile_是什么作用?向gcc声明不允许对内联汇编优化

C语言关键字volatile表明某个变量的值可能随时被外部改变,因此对这些变量的存取不能缓存到寄存器,每次使用时需要重新读取。在多线程环境经常使用,因为同一个变量可能被多个线程修改(共享内存)

运算优先级口诀
括号成员第一:[]、()、->
全体单目第二:++、--、+(正)、-(负)
乘除余三,加减四:余是取余%
移位五,关系六:<<、>>、>、<、>=、<=
等于与不等排第七:==、!=
位与、异或、位或“三分天下”八九十:&、^、|
逻辑与十一,逻辑或十二
条件高于赋值:?:
逗号运算级最低:,

什么是内存泄漏问题?malloc分配的内存无法由程序自动释放,因此在使用完后必须调用free将所分配的内存释放掉,否则将出现内存泄漏的问题
================================================================================================================================
linux内核常见数据结构有哪些?

1.链表:可以动态进行存储分配,根据需要开辟内存单元,还可以方便地实现数据的增加和删除。
链表元素包括数据域(存储数据元素信息)和指针域(存储该元素的直接后继元素地址),所以它的整体结构就形成了一个用指针链连起来的线性表
 -------     -------------------      --------------         ---------------
|    |    |    |        |     |     |    |        |    |    |
| Head    |------>| Data    |    -------->| Data  |   -------> ... ----->| Data    | NULL    |
|    |    |    |        |     |     |    |        |    |    |
 -------     --------------------      --------------         ---------------
 头指针

2.单向链表:由头指针唯一确定,可以用头指针的名字来命名,头指针指向单向链表的第一个节点。
typedef struct _link_node
{
    element_type data;//element_type为有效数据类型,可以一个或多个
    struct _link_node *next;//只有一个指针成员,类型为指向相同结构类型的指针,通过指针引用自身这种类型的结构
}link_node;
typedef link_node* link_list;

3.(1)节点初始化
int init_link(link_list *list)
{
    *list = (link_list)malloc(sizeof(link_node));//用malloc分配函数节点
    if(!list)//若分配失败,返回
    {
        return -1;
    }
    memset(&((*list)->data), 0, sizeof(element-type));//初始化链表节点的数据域
    (*list)->next = NULL;
    return 0;
}

(2)数据查询
int get_element(link_list list, int i, element_type *elem)
{
    /*当第i个元素存在时,其值赋给elem并返回*/
    link_list p = NULL;//没有指向任何地址
    int j = 0;
    p = list->next;//初始化,指向链表的第一个节点,j为计数器
    while((j++ < i) && (p = p->next));//为防止i过大,通过判断p是否为空来确定是否到达链表的尾部
    if(!p || (j <= i))//若第i个元素不存在
    {
        return -1;
    }
    *elem = p->data;//取得第i个数据
    return 0;
}
(3)链表的插入与删除:链表最常见的操作,灵活性的体现
int link_insert(link_list list, int i, element_type elem)
{
    link_list p = list,new_node;
    int j = 0;
    while((j++ < i) && (p = p->next));//找到i位,要插入的位置
    if(!p || (j<=i))
    {
        return 0;
    }
    new_node = (link_list)malloc(sizeof(link_node));//初始化节点
    new_node->data = elem;//初始化要插入的元素的数据
    new_node->next = p->next;//将新节点插入链表
    p->next = new_node;
    return 1;
}

(4)将几个单向链表合并也是链表操作中的常见操作之一

双向链表的每个节点中包含两个指针域,分别指向该节点的前一个节点和后一个节点。所以双向链表可以反向搜索。较为灵活。
struct link_node
{
    element_type data;
    struct link_node *next;
    struct link_node *priv;
};

单向循环列表:最后一个节点指向第一个节点,p->next 等于头指针。

linux内核的链表数据结构来自文件“include/linux/list.h”
struct list_head{
    struct list_head *next, *prev;//双向链表,没有数据域
}
在linux内核链表中,不是在链表结构中包含数据,而是在数据结构中包含链表节点。需要用链表组织起来的数据会包含一个上面提到的struct list_head成员
linux内核链表接口
实际上Linux只定义了链表节点,并没有专门定义链表头,
#define LIST_HEAD_INIT(name)      { &(name), &(name) }
#define LIST_HEAD(name)      struct list_head name = LIST_HEAD_INIT(name)
其中的name是struct list_head结构的变量的地址,而不是包含struct list_head的数据结构的变量的地址
a)插入
对链表的插入操作有两种:在表头插入和在表尾插入。Linux为此提供了两个接口:
static inline void list_add(struct list_head *new, struct list_head *head)
static inline void list_add_tail(struct list_head *new, struct list_head *head)
b) 删除
static inline void list_del(struct list_head *entry);
c) 搬移
Linux提供了将原本属于一个链表的节点移动到另一个链表的操作,并根据插入到新链表的位置分为两类:
static inline void list_move(struct list_head *list, struct list_head *head);
static inline void list_move_tail(struct list_head *list, struct list_head *head);
d) 合并
除了针对节点的插入、删除操作,Linux链表还提供了整个链表的插入功能:
static inline void list_splice(struct list_head *list, struct list_head *head);

什么是树?树的定义:由n(n>=0)个节点组成的有限集合。(n=0称为空树)
什么是节点的度?节点的分支数
什么是树的度?树中所有节点度的最大值
什么是树的深度?树中所有节点层次的最大值
什么是终端节点(叶子)?度为0的节点

什么是有序树?每棵子树从左向右的顺序拥有一定的顺序,不得互换
什么是森林?是m棵互不相交的集合

什么是二叉树?每个节点的度《=2,它的子树有左右之分,是有序树

什么是满二叉树?
深度k且有(2的k次方-1)个节点
如当k=4,有(2的4次方-1)个节点

什么是完全二叉树?
若设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边,这就是完全二叉树。

二叉树的存储方式有哪两种?顺序存储和链式存储结构,顺序存储结构使用于完全二叉树,存储形式为用一组连续的存储单元按照完全二叉树的每个节点编号的顺序存放节点内容。
除最后一层外,每一层上的节点数均达到最大值;在最后一层上只缺少右边的若干结点。

#define MAX_TREE_NODE_SIZE 100
typedef struct
{
    entry_type item[MAX_TREE_NODE_SIZE];/* 根存储在下标为1的数组单元中 */
    int n;    /* 当前完全二叉树的节点个数 */
}qb_tree;
优点:空间利用率高,缺点:插入和删除节点不方便,需要整体移动数组

链式存储:二叉树节点结构,拥有一个左孩子指针lchild,数据元素的内容item,rchild右孩子指针
类型定义:
typedef struct _bt_node
{
    entry_type item;
    struct bt_node *lchild,*rchild;
}bt_node, *b_tree;
特点:寻找孩子节点容易,寻找双亲节点比较困难。当需要频繁地寻找双亲,可以给每个节点添加一个指向双亲节点的指针域parent:lchild,item,rchild,parent

什么是遍历二叉树?按照某种顺序访问二叉树中的每个节点一次且仅一次的过程。分为TLR(根左右)的先序遍历,LTR(左根右)的中序遍历,LRT(左右根)的后序遍历

如何统计二叉树叶子节点?只需要将访问操作变成判断该节点是否为叶子节点,如果是叶子节点,将累加器加1

如何统计二叉树中的高度(使用后序遍历)
首先分别求出左右子树的高度,在此基础上得出该棵树的高度即左右子树较大的高度值加1

平衡树:B树(用于文件系统数据库)、AVL树和红黑树(用于检索领域)

红黑树:在linux内核,STL源码中广为使用,平衡机制比较灵活,因此能取得最好的统计性能。它的每个节点都被着色为红色或黑色。

红黑树是指满足下列条件的二叉搜索树
1.每个节点要么是红色,要么是黑色
2.所有的叶节点都是空节点,并且是黑色的
3.如果一个节点是红色的,那么它的两个子节点都是黑色的。
4.节点到其子孙节点的每条简单路径都包含相同数目的黑色节点
5.根节点永远是黑色
树的黑色高度:从根节点到叶节点的黑色节点数
红黑树的性质保证了从根节点到叶节点的路径长度不会超过任何其他路径的两倍。
所以当黑色高度为n时,从根到叶节点的简单路径的最短长度为n-1,最大长度为2*(n-1)

(1)2种常见的数据结构:链表和树。记录在结构中的相对位置是随机的,其相对位置和记录的关键字(索引)不存在确定关系。
哈希表:在记录的存储位置和它的关键字之间建立一个确定的对应关系f,使得每个关键字和结构中一个唯一的存储位置相对应。

(2)哈希表的构造方法.
1.直接地址法:Hash(key) = a * key + b,取关键字(key)的某个线性函数值Hash(key)作为哈希地址。特点:一对一的映射,要求哈希地址空间的大小与关键字集合的大小相同。
2.数字分析法:分析已有的数据,尽量选取能够减少冲突的数字来构建哈希函数
3.除留余数法:设哈希表允许的地址数为m(比如是25),取一个不大于m,但最接近或等于m的质数p,或选取一个不含有小于20的质因数的合数作为除数(比如23),利用以下公式把关键字转换成哈希地址。哈希函数为
hash(key)=key&p p<=m,比如有一个关键码key=962148,哈希地址为hash(962148)=962148%23=12
4.乘余取整法:先让关键字key乘上一个常数A(0<A<1),提取乘积的小数部分。然后,再用乘数n乘以这个值,对结果向下取整,把它作为哈希表地址
5.平方取中法
6.折叠法
7.随机数法

为什么不能返回指向栈内存的指针或引用?因为栈内存在函数结束时会释放掉
int *abc(void)
{
    int a = 100;
    int *p = &a;
    return p;
}
printf("%s",abc());
打印结果:无,因为变量a在函数执行完就释放掉了,除非你的变量是在常量区或者动态分配了且还没释放掉
什么是“野指针”?野指针不是NULL指针,是指向“垃圾”内存(不可用内存)的指针
为什么会产生“野指针”?1.指针变量没有被初始化,乱指(本应该指向NULL或合法内存)2.指针P被free或delete之后,没有置为NULL。3.指针操作超越了变量的作用范围,就是说指针指向的这个变量可能已经消失了

如何将一个单向链表转换为一个双向链表?

排序算法有哪些?交换排序(冒泡排序和快速排序)、插入排序(直接插入排序、折半/二分插入排序、2路插入排序、希尔排序)

交换排序的基本思想是:两两比较待排序记录的关键字,发现两个记录的次序相反时即进行交换,直到没有反序的记录为止。交换排序有冒泡排序和快速排序。
<1>冒泡算法:若有n个待排序的记录,首先比较第一个记录和第二个记录,关键字若逆序(从小到大排序)则交换位置,然后比较第二个记录和第三个记录的关键字...直到第n-1个记录和第n个记录的关键字
进行比较和交换。第一趟冒泡结果:关键字最大的记录移动到最后位置,适用于局部有序,文件较小。
#include<stdio.h>
#define SIZE 8

void BubbleSort(int a[],int n)
{
    int i,j,temp;
    for(j = 0;j < n-1;j++)//最多做n-1次交换
        for(i = 0; i < n-1-j; i++)
        {
            if(a[i] > a[i+1])//按照升序排列
            {
                temp = a[i];
                a[i] = a[i+1];
                a[i+1] = temp;
            }
        }
}

int main()
{
    int number[SIZE]={95,45,15,78,84,51,24,12};
    int i;
    BubbleSort(number,SIZE);
    for(i = 0; i < SIZE; i++)
    {
        printf("%d",number[i]);
    }
    printf("\n");
}
----------------------------------------------------------------------
<2>快速排序的基本思想:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归
进行,以此达到整个数据变成有序序列。假设要排序的数组是A[1]...A[N],首先任意选取一个数据,通常选用第一个数据作为关键数据,然后所有比它小的数据都放到它前面,所有比它大的数都放到它后面,
这个过程称为一趟快速排序。

1)设置两个变量I、J,排序开始的时候 I=0,J=N-1;
2)以第一个数组元素作为关键数据,赋值给temp,即temp=A[0];
3)从J开始向前搜索,即由后开始向前搜索,找到第一个小于X的值,两者交换;
4)从I开始向后搜索,即由前开始向后搜索,找到第一个大于X的值,两者交换;
5)重复第3、4步,直到I=J;

#include "stdio.h"
void QuickSort(int a[],int low,int high)
{
    int i = low;
    int j = high;
    int temp = a[low];

    if(low < high)
    {
        while(i < j)//仅当区间长度大于1时才需要排序,从区间两端交替向中间扫描,直到i=j为止
        {
            while(i < j && a[j] >= temp)
            {
                j--;//从右向左扫描,查找第一个关键字小于temp的记录e[j]
            }
            a[i] = a[j];//两者交换
            while(i < j && a[i] <= temp)
            {
                i++;//从左向右扫描,查找第一个关键字大于temp的记录e[i]
            }
            a[j] = a[i];//两者交换        
        }
        a[i] = temp;//基准记录也被最后定位
        if(low < i-1)
            QuickSort(a,low,i-1);//对左区间递归排序
        if(high > i+1)
            QuickSort(a,i+1,high);//对右区间递归排序
    }
    else
    {
        return;
    }
}

void main()
{
    int arry[]={49,38,65,97,76,13,27,49};
    int len = 8;
    int i;
    printf("before sort \n");
    for(i = 0; i < len;i++)
        printf("%d,",arry[i]);
    printf("\n");
    QuickSort(arry,0,len-1);
    for(i = 0; i < len;i++)
        printf("%d,",arry[i]);
    printf("\n");
}
-----------------------------------------------------------------------------------------------------------------------------------
插入排序的基本思想是在一个已经在排好序的有序表中插入一个新的记录,得到一个新的记录增加1的新的有序表。因此插入排序中的两个基本操作是:1.与有序表中所有元素比较,得到插入位置 
移动有序表中的元素,将新记录插入到正确位置,
<1>直接插入排序:顺序和有序表中的元素进行比较,边比较边移动元素
设数组为a[0…n-1]。
1.      初始时,a[0]自成1个有序区,无序区为a[1..n-1]。令i=1
2.      将a[i]并入当前的有序区a[0…i-1]中形成a[0…i]的有序区间。
3.      i++并重复第二步直到i==n-1。排序完成。
#include <stdio.h>
void InsertSort(int a[],int n)
{
    int i,j,temp;
    for(i = 1;i < n;i++)//选择需要n-1次
    {
        if(a[i] < a[i - 1])
        {
            temp = a[i];//暂存下标为1的数,下标从1开始,因为开始时下标为0的数,前面没有任何数,此时认为它是排好顺序的
            for(j = i-1;j>=0 && a[j]>temp;j--)
            {
                a[j+1] = a[j];
            }
            a[j+1] = temp;
        }
    }
}

void main()
{
    int a[]={7,8,3,4,6,9,5,1,2};
    int i;
    printf("before sort \n");
    for(i = 0; i < 9;i++)
        printf("%d,",a[i]);
    printf("\n");
    InsertSort(a,9);
    for(i = 0; i < 9;i++)
        printf("%d,",a[i]);
    printf("\n");
}

------------------------------------------------------------------------------------------------------------------------------------------------------------------
<2>折半插入排序/二分插入排序:折半地和有序表中的元素进行比较,找到插入位置,然后移动相应位置的元素。作为一个有序序列你如果和中间比,
如果比中间大就和后面那一部分比,然后后面又找中间部分。在平均的情况下比直接插比要快
#include <stdio.h>
void BinaryInsertSort(int a[],int n)
{
    int middle = 0;
    for(int i = 1;i < n;i++)
    {
        int low = 0;
        int high = i -1;
        int temp = a[i];
        while(low <= high)
        {
            middle =  (low + high)/2;
            if(temp < a[middle])
                high = middle - 1;
            else
                low = middle + 1;
        }
        for(int j = i-1;j >= low;j--)
            a[j+1] = a[j];
        a[low] = temp;//a[high+1] = temp;也可以
    }
}

int main()
{
    int TestData[5] = {34,15,6,89,67};
    int i = 0;
    printf("before sort\n");
    for(i = 0;i<5;i++)
    printf("|%d|",TestData[i]);
    int retData = BinaryInsertSort(TestData,5);
    printf("after sort:\n");
    for(i = 0;i<5;i++)
    printf("|%d|",TestData[i]);
    return 0; 
}
-----------------------------------------------------------------------------------------
<3>二路插入排序的基本思想是用二分查找法在有序表中找到正确的插入位置,然后移动记录,空出插入位置,再进行插入。它是在折半插入排序的基础上再改进,其目的是减少排序过程中移动记录的次数,但为此需要n个记录的辅助空间。
具体做法:另设一个和L.r同类型的数组temp,首先将L.r[1]赋值给temp[1],并将temp[1]看成是在排好序列中处于中间位置的记录,然后从L.r中第2个记录起依次插入到temp[1]之前或之后的有序序列中。
先将待记录的关键字和temp[1]的关键字进行比较,若L.r[i].key < temp[1].key,则将L.r[i]插入到temp[1]之后的有序表中。在实现算法时,可将temp看成是一个循环变量,并设两个指针first和final
分别指示排序过程中得到的有序序列中的第一个记录和最后一个记录在temp中的位置。
void TwoInsertSort(int a[],int n)
{
    int temp[100] = {0};
    int i;
    int j;
    int first = 0;//first、final分别指示temp中排好序的记录的第1个和最后1个记录的位置。
    int final = 0;
    temp[first] = a[0];
    for(i = 1;i < n;i++)
    {
        if(a[i] < temp[first])//待插入记录小于temp中最小值,插入到temp[first]之前(不需要移动d数组的元素)
        {
            first = (first-1+n)%n;
            temp[first] = a[i];
        }
        else if(a[i] >= temp[final])//待插入记录大于temp中最大值,插入到temp[final]之后(不需要移动temp数组的元素)
        {
            final=final+1;
            temp[final] = a[i];
        }
        else//待插入记录大于temp中最小值,小于temp中最大值,插入到temp的中间(需要移动temp数组的元素)
        {
            j=final;
            final++;
            while(a[i]<temp[j])
            {    
                temp[(j+1+n)%n]=temp[j];
                j=(j-1+n)%n;

            }
            temp[j+1]=a[i];

        }
    }
    for(int i = 0;i < n;i++)
    {
        a[i] = temp[(first+i)%n];
    }
}
void main()
{
    int a[]={7,8,3,4,6,9,5,1,2};
    int i;
    printf("before sort \n");
    for(i = 0; i < 9;i++)
        printf("%d,",a[i]);
    printf("\n");
    TwoInsertSort(a,9);
    for(i = 0; i < 9;i++)
        printf("%d,",a[i]);
    printf("\n");
}
<4>希尔排序/缩小增量排序:直接插入排序的一种优化,它基于以下两点进行优化。a.当待排序序列基本有序时,直接插入排序的时间复杂度可提高至O(n);
b.当n值比较小时,直接插入会比较快

#include <stdio.h>
#include <math.h>
 
#define MAXNUM 10
 
void main()
{
    void shellSort(int array[],int n,int t);//t为排序趟数
    //int array[MAXNUM],i;
    //for(i=0;i<MAXNUM;i++)
        //scanf("%d",&array[i]);
    int i;
    int array[MAXNUM] = {25,41,2,98,64,74,36,29,46,38};
    shellSort(array,MAXNUM,(int)(log(MAXNUM+1)/log(2)));//排序趟数应为log2(n+1)的整数部分
    for(i=0;i<MAXNUM;i++)
        printf("%d ",array[i]);
    printf("\n");
}
 
//根据当前增量进行插入排序
void shellInsert(int array[],int n,int dk)
{
    int i,j,temp;
    for(i=dk;i<n;i++)//分别向每组的有序区域插入
    {
        temp=array[i];
        for(j=i-dk;(j>=i%dk)&&array[j]>temp;j-=dk)//比较与记录后移同时进行
            array[j+dk]=array[j];
        if(j!=i-dk)
            array[j+dk]=temp;//插入
    }
}
 
//计算Hibbard增量
int dkHibbard(int t,int k)
{
    return (int)(pow(2,(t-k+1))-1);//gcc ShellSort.c -o ShellSort -lm
}
 
//希尔排序
void shellSort(int array[],int n,int t)
{
    void shellInsert(int array[],int n,int dk);
    int i;
    for(i=1;i<=t;i++)
        shellInsert(array,n,dkHibbard(t,i));
}
 
//此写法便于理解,实际应用时应将上述三个函数写成一个函数。

=============================================================
linux的export命令用于将本地数据区中的变量转移到用户环境区
shell脚本中,[]表示条件测试
输出重定向有两种方式,一种是直接输出,使用一个大于号“>”实现;另一种是以附加的方式输出,使用两个大于号“>>”实现。前者会覆盖原始的输出内容,而后者会添加到文件最后。
http://blog.163.com/bical@126/blog/static/479354942013411114118416/
链接器并不管函数所在的源文件,只管函数的中间目标文件(Object File),在大多数时候,由于源文件太多,编译生成的中间目标文件太多,而在链接时需要明显地指出中间目标文件名,这对于编译很不方便,
所以,我们要给中间目标文件打个包,在Windows下这种包叫“库文件”(Library File),也就是 .lib 文件,在UNIX下,是Archive File,也就是 .a 文件。
Makefile的规则:
target也就是一个目标文件,可以是Object File,也可以是执行文件。还可以是一个标签(Label)/伪目标
prerequisites就是,要生成那个target所需要的文件或是目标。
command也就是make需要执行的命令。(任意的Shell命令)

prerequisites中如果有一个以上的文件比target文件要新的话,command所定义的命令就会被执行。这就是Makefile的规则。也就是Makefile中最核心的内容。
make会比较targets文件和prerequisites文件的修改日期,如果prerequisites文件的日期要比targets文件的日期要新,或者target不存在的话,那么,make就会执行后续定义的命令。
clean不是一个文件,它只不过是一个动作名字,有点像C语言中的lable一样,其冒号后什么也没有,那么,make就不会自动去找文件的依赖性,也就不会自动执行其后所定义的命令。要执行其后的命令,就要在make命令
后明显得指出这个lable的名字。这样的方法非常有用,我们可以在一个makefile中定义不用的编译或是和编译无关的命令,比如程序的打包,程序的备份,等等。

在默认的方式下,也就是我们只输入make命令。那么,

1、make会在当前目录下找名字叫“Makefile”或“makefile”、“GNUmakefile”的文件。最好使用“Makefile”这个文件名,因为,这个文件名第一个字符为大写,这样有一种显目的感觉。最好不要用“GNUmakefile”,
这个文件是GNU的make识别的。有另外一些make只对全小写的“makefile”文件名敏感,但是基本上来说,大多数的make都支持“makefile”和“Makefile”这两种默认文件名。当然,你可以使用别的文件名来书写Makefile,
比如:“Make.Linux”,“Make.Solaris”,“Make.AIX”等,如果要指定特定的Makefile,你可以使用make的“-f”和“--file”参数,如:make -f Make.Linux或make --file Make.AIX。
2、如果找到,它会找文件中的第一个目标文件(target),在上面的例子中,他会找到“edit”这个文件,并把这个文件作为最终的目标文件。
3、如果edit文件不存在,或是edit所依赖的后面的 .o 文件的文件修改时间要比edit这个文件新,那么,他就会执行后面所定义的命令来生成edit这个文件。
4、如果edit所依赖的.o文件也不存在,那么make会在当前文件中找目标为.o文件的依赖性,如果找到则再根据那一个规则生成.o文件。(这有点像一个堆栈的过程)
5、当然,你的C文件和H文件是存在的啦,于是make会生成 .o 文件,然后再用 .o 文件生命make的终极任务,也就是执行文件edit了。

make会一层又一层地去找文件的依赖关系,直到最终编译出第一个目标文件。在找寻的过程中,如果出现错误,比如最后被依赖的文件找不到,那么make就会直接退出,并报错,而对于所定义的命令的错误,或是编译不成功,make根本不理。make只管文件的依赖性,即,如果在我找了依赖关系之后,冒号后面的文件还是不在,那么对不起,我就不工作啦。

在makefile中我们可以使用变量。makefile的变量也就是一个字符串,理解成C语言中的宏可能会更好。
比如,我们声明一个变量,叫objects, OBJECTS, objs, OBJS, obj, 或是 OBJ,反正不管什么啦,只要能够表示obj文件就行了。于是,我们就可以很方便地在我们的makefile中以“$(objects)”的方式来使用这个变量了

= 是最基本的赋值
:= 是覆盖之前的值
?= 是如果没有被赋值过就赋予等号后面的值
+= 是添加等号后面的值
至于makefile中“=”和“:=”的区别到底有什么区别,
1、“=”
      make会将整个makefile展开后,再决定变量的值。也就是说,变量的值将会是整个makefile中最后被指定的值。看例子:

            x = foo
            y = $(x) bar
            x = xyz

      在上例中,y的值将会是 xyz bar ,而不是 foo bar 。

2、“:=”

      “:=”表示变量的值决定于它在makefile中的位置,只会到这句语句之前去找,而不是整个makefile展开后的最终值。

            x := foo
            y := $(x) bar
            x := xyz

      在上例中,y的值将会是 foo bar ,而不是 xyz bar 了。

清空目标文件(.o和执行文件)的规则更为稳健的做法是:

.PHONY : clean
clean :
-rm edit $(objects)

隐晦规则:GNU的make很强大,它可以自动推导文件以及文件依赖关系后面的命令,于是我们就没必要去在每一个[.o]文件后都写上类似的命令,因为,我们的make会自动识别,并自己推导命令。
只要make看到一个[.o]文件,它就会自动的把[.c]文件加在依赖关系中,如果make找到一个whatever.o,那么whatever.c,就会是whatever.o的依赖文件。并且 cc -c whatever.c 也会被推导出来

前面说过,.PHONY意思表示clean是一个“伪目标”,为了避免和文件重名的这种情况。而在rm命令前面加了一个小减号的意思就是,也许某些文件出现问题,但不要管,继续做后面的事。当然,clean的规则不要放在文件的开头,
不然,这就会变成make的默认目标,相信谁也不愿意这样。不成文的规矩是——“clean从来都是放在文件的最后”。

Makefile里主要包含了五个东西:显式规则、隐晦规则、变量定义、文件指示和注释。
文件指示。其包括了三个部分,一个是在一个Makefile中引用另一个Makefile,就像C语言中的include一样;另一个是指根据某些情况指定Makefile中的有效部分,就像C语言中的预编译#if一样;
还有就是定义一个多行的命令。还值得一提的是,在Makefile中的命令,必须要以[Tab]键开始。

在Makefile使用include关键字可以把别的Makefile包含进来,这很像C语言的#include,被包含的文件会原模原样的放在当前文件的包含位置。include的语法是:include <filename>
filename可以是当前操作系统Shell的文件模式(可以保含路径和通配符)举个例子,你有这样几个Makefile:a.mk、b.mk、c.mk,还有一个文件叫foo.make,以及一个变量$(bar),其包含了e.mk和f.mk,
那么,下面的语句:include foo.make *.mk $(bar)等价于:include foo.make a.mk b.mk c.mk e.mk f.mk
如果include所指出的其它Makefile文件都没有指定绝对路径或是相对路径的话,make会在当前目录下首先寻找,如果当前目录下没有找到,那么,make还会在下面的几个目录下找:
1、如果make执行时,有“-I”或“--include-dir”参数,那么make就会在这个参数所指定的目录下去寻找。
2、如果目录<prefix>/include(一般是:/usr/local/bin或/usr/include)存在的话,make也会去找。

如果你的当前环境中定义了环境变量MAKEFILES,那么,make会把这个变量中的值做一个类似于include的动作。这个变量中的值是其它的Makefile,用空格分隔。也许有时候你的Makefile出现了怪事,
那么你可以看看当前环境中有没有定义这个变量。建议不要使用这个环境变量,因为只要这个变量一被定义,那么当你使用make时,所有的Makefile都会受到它的影响

GNU的make工作时的执行步骤入下:(想来其它的make也是类似)

1、读入所有的Makefile。
2、读入被include的其它Makefile。
3、初始化文件中的变量。
4、推导隐晦规则,并分析所有规则。
5、为所有的目标文件创建依赖关系链。
6、根据依赖关系,决定哪些目标要重新生成。
7、执行生成命令。

规则的语法:
targets : prerequisites ; command
command
...
command是命令行,如果其不与“target prerequisites”在一行,那么,必须以[Tab键]开头,如果和prerequisites在一行,那么可以用分号做为分隔。如果命令太长,你可以使用反斜框(‘/’)作为换行符。
make对一行上有多少个字符没有限制。一般来说,make会以UNIX的标准Shell,也就是/bin/sh来执行命令。

objects = *.o

上面这个例子,表示了,通符同样可以用在变量中。并不是说[*.o]会展开,不!objects的值就是“*.o”。Makefile中的变量其实就是C/C++中的宏。如果你要让通配符在变量中展开,也就是让objects的值是所有[.o]的
文件名的集合,那么,你可以这样:objects := $(wildcard *.o)这种用法由关键字“wildcard”指出,

https://www.stallman.org/

如果没有指明这个特殊变量“VPATH”,make只会在当前的目录中去找寻依赖文件和目标文件。如果定义了这个变量,那么,make就会在当当前目录找不到的情况下,到所指定的目录中去找寻文件了。VPATH = src:../headers

上面的的定义指定两个目录,“src”和“../headers”,make会按照这个顺序进行搜索。目录由“冒号”分隔。(当然,当前目录永远是最高优先搜索的地方)另一个设置文件搜索路径的方法是使用make的“vpath”关键字
(注意,它是全小写的),这不是变量,这是一个make的关键字,这和上面提到的那个VPATH变量很类似,但是它更为灵活。它可以指定不同的文件在不同的搜索目录中。
vpath %.h ../headers该语句表示,要求make在“../headers”目录下搜索所有以“.h”结尾的文件。(如果某文件在当前目录没有找到的话)

Makefile的规则中的目标可以不止一个,其支持多目标,可以使用一个自动化变量“$@”,这个变量表示着目前规则中所有的目标的集合,
bigoutput littleoutput : text.g
generate text.g -$(subst output,,$@) > $@
上述规则等价于:
bigoutput : text.g
generate text.g -big > bigoutput
littleoutput : text.g
generate text.g -little > littleoutput
其中,-$(subst output,,$@)中的“$”表示执行一个Makefile的函数,函数名为subst,后面的为参数。关于函数,将在后面讲述。这里的这个函数是截取字符串的意思,“$@”表示目标的集合,就像一个数组,“$@”依次取出目标,并执于命令。


# Makefile to compare sorting routines

BASE = /home/blufox/base
CC           =   gcc
CFLAGS  =   -O –Wall
EFILE      =   $(BASE)/bin/compare_sorts
INCLS     =   -I$(LOC)/include
LIBS        =   $(LOC) b/g_lib.a \
                     $(LOC) b/h_lib.a
LOC        =   /usr/local

OBJS = main.o    another_qsort.o    chk_order.o \
             compare.o    quicksort.o

$(EFILE): $(OBJS)
@echo “linking …”
@$(CC) $(CFLAGS) –o $@ $(OBJS) $(LIBS)

$(OBJS): compare_sorts.h
$(CC) $(CFLAGS) $(INCLS) –c $*.c

# Clean intermediate files
clean:
rm *~ $(OBJS)
在上面的例子中,CC 和 CCFLAGS 就是 make 的变量。但值得注意的是,如果变量名的长度超过一个字符,在引用时就必须加圆括号()。$(CFLAGS)变量就是传给编译器的编译参数。
如果没有定义就是空的。比如CFLAGS = -O2 -fno-strict-alising -Wall,可以考虑用,自己定义编译需要的选项。也可以不用。

静态模式可以更加容易地定义多目标的规则,可以让我们的规则变得更加的有弹性和灵活。
<targets ...>: <target-pattern>: <prereq-patterns ...>
<commands>
...
target-parrtern是指明了targets的模式,也就是的目标集模式。
prereq-parrterns是目标的依赖模式,它对target-parrtern形成的模式再进行一次依赖目标的定义。如果我们的<target-parrtern>定义成“%.o”,意思是我们的<target>集合中都是以“.o”结尾的,而如果我们的<prereq-parrterns>定义成“%.c”,意思是对<target-parrtern>所形成的目标集进行二次定义,其计算方法是,取<target-parrtern>模式中的“%”(也就是去掉了[.o]这个结尾),并为其加上[.c]这个结尾,形成的新集合。所以,我们的“目标模式”或是“依赖模式”中都应该有“%”这个字符
objects = foo.o bar.o
all: $(objects)
$(objects): %.o: %.c
$(CC) -c $(CFLAGS) $< -o $@
上面的例子中,指明了我们的目标从$object中获取,“%.o”表明要所有以“.o”结尾的目标,也就是“foo.o bar.o”,也就是变量$object集合的模式,而依赖模式“%.c”则取模式“%.o”的“%”,也就是“foo bar”,并为其加下“.c”的后缀,于是,我们的依赖目标就是“foo.c bar.c”。而命令中的“$<”和“$@”则是自动化变量,“$<”表示所有的依赖目标集(也就是“foo.c bar.c”),“$@”表示目标集
(也就是“foo.o bar.o”)。于是,上面的规则展开后等价于下面的规则:

foo.o : foo.c
$(CC) -c $(CFLAGS) foo.c -o foo.o
bar.o : bar.c
$(CC) -c $(CFLAGS) bar.c -o bar.o

再看一个例子:
files = foo.elc bar.o lose.o
$(filter %.o,$(files)): %.o: %.c
$(CC) -c $(CFLAGS) $< -o $@
$(filter %.elc,$(files)): %.elc: %.el
emacs -f batch-byte-compile $<
$(filter %.o,$(files))表示调用Makefile的filter函数,过滤“$filter”集,只要其中模式为“%.o”的内容。

大多数的C/C++编译器都支持一个“-M”的选项,即自动找寻源文件中包含的头文件,并自动生成一个依赖关系。例如,如果我们执行下面的命令:
cc -M main.c
其输出是:
main.o : main.c defs.h
GNU的C/C++编译器,你得用“-MM”参数,不然,“-M”参数会把一些标准库的头文件也包含进来。GNU组织建议把编译器为每一个源文件的自动生成的依赖关系放到一个文件中,为每一个“name.c”的文件都生成一个“name.d”的Makefile文件,[.d]文件中就存放对应[.c]文件的依赖关系。

接着,我们可以使用Makefile的“include”命令,来引入别的Makefile文件,例如:
sources = foo.c bar.c
include $(sources:.c=.d)
上述语句中的“$(sources:.c=.d)”中的“.c=.d”的意思是做一个替换,把变量$(sources)所有[.c]的字串都替换成[.d]

make的命令默认是被“/bin/sh”——UNIX的标准Shell解释执行的。

通常,make会把其要执行的命令行在命令执行前输出到屏幕上。当我们用“@”字符在命令行前,那么,这个命令将不被make显示出来,最具代表性的例子是,我们用这个功能来像屏幕显示一些信息。如:
@echo 正在编译XXX模块......
当make执行时,会输出“正在编译XXX模块......”字串,但不会输出命令,如果没有“@”,那么,make将输出:
echo 正在编译XXX模块......
正在编译XXX模块......

make若带参数“-n”或“--just-print”,那么其只是显示命令,但不会执行命令,用于调试Makefile,查看命令执行的顺序

如果你要让上一条命令的结果应用在下一条命令时,你应该使用分号分隔这两条命令。比如你的第一条命令是cd命令,你希望第二条命令得在cd之后的基础上运行,那么你就不能把这两条命令写在两行上,而应该把这两条命令写在一行上,用分号分隔。

给make加上“-i”或是“--ignore-errors”参数,那么,Makefile中所有命令都会忽略错误。
make的参数的是“-k”或是“--keep-going”,这个参数的意思是,如果某规则中的命令出错了,那么就终止该规则的执行,但继续执行其它规则。

在一些大的工程中,我们会把我们不同模块或是不同功能的源文件放在不同的目录中,我们可以在每个目录中都书写一个该目录的Makefile,这有利于让我们的Makefile变得更加地简洁,而不至于把所有的东西全部写在一个Makefile中,这样会很难维护我们的Makefile,这个技术对于我们模块编译和分段编译有着非常大的好处。例如,我们有一个子目录叫subdir,这个目录下有个Makefile文件,来指明了这个目录下文件的编译规则。那么我们总控的Makefile可以这样书写:
subsystem:
cd subdir && $(MAKE)
其等价于:
subsystem:
$(MAKE) -C subdir
定义$(MAKE)宏变量的意思是,也许我们的make需要一些参数,所以定义成一个变量比较利于维护。这两个例子的意思都是先进入“subdir”目录,然后执行make命令。
还有一个在“嵌套执行”中比较有用的参数,“-w”或是“--print-directory”会在make的过程中输出一些信息,让你看到目前的工作目录。比如,如果我们的下级make目录是“/home/hchen/gnu/make”,如果我们使用“make -w”来执行,那么当进入该目录时,我们会看到:make: Entering directory `/home/hchen/gnu/make'.而在完成下层make后离开目录时,我们会看到:make: Leaving directory `/home/hchen/gnu/make'
当你使用“-C”参数来指定make下层Makefile时,“-w”会被自动打开的。如果参数中有“-s”(“--slient”)或是“--no-print-directory”,那么,“-w”总是失效的。

变量在声明时需要给予初值,而在使用时,需要给在变量名前加上“$”符号,但最好用小括号“()”或是大括号“{}”把变量给包括起来。如果你要使用真实的“$”字符,那么你需要用“$$”来表示。Makefile中的变量在使用处展开的真实样子。可见其就是一个“替代”的原理。更像是宏,在Makefile中执行的时候其会自动原模原样地展开在所使用的地方。变量是大小写敏感的,“foo”、“Foo”和“FOO”是三个不同的变量名。

对于系统变量“MAKELEVEL”,其意思是,如果我们的make有一个嵌套执行的动作(参见前面的“嵌套使用make”),那么,这个变量会记录了我们的当前Makefile的调用层数。

如果我们要定义一个变量,其值是一个空格,那么我们可以这样来:
nullstring :=
space := $(nullstring) # end of the line
nullstring是一个Empty变量,其中什么也没有,而我们的space的值是一个空格。因为在操作符的右边是很难描述一个空格的,这里采用的技术很管用,先用一个Empty变量来标明变量的值开始了,而后面采用“#”注释符来表示变量定义的终止,这样,我们可以定义出其值是一个空格的变量。

两种变量的高级使用方法,第一种是变量值的替换。
我们可以替换变量中的共有的部分,其格式是“$(var:a=b)”或是“${var:a=b}”,其意思是,把变量“var”中所有以“a”字串“结尾”的“a”替换成“b”字串。这里的“结尾”意思是“空格”或是“结束符”。
还是看一个示例吧:
foo := a.o b.o c.o
bar := $(foo:.o=.c)
这个示例中,我们先定义了一个“$(foo)”变量,而第二行的意思是把“$(foo)”中所有以“.o”字串“结尾”全部替换成“.c”,所以我们的“$(bar)”的值就是“a.c b.c c.c”。
另外一种变量替换的技术是以“静态模式”(参见前面章节)定义的,如:
foo := a.o b.o c.o
bar := $(foo:%.o=%.c)
这依赖于被替换字串中的有相同的模式,模式中必须包含一个“%”字符,这个例子同样让$(bar)变量的值为“a.c b.c c.c”。 

第二种高级用法是——“把变量的值再当成变量”。先看一个例子:
x = y
y = z
a := $($(x))
在这个例子中,$(x)的值是“y”,所以$($(x))就是$(y),于是$(a)的值就是“z”。

再复杂一点,我们再加上函数:
x = variable1
variable2 := Hello
y = $(subst 1,2,$(x))
z = y
a := $($($(z)))
这个例子中,“$($($(z)))”扩展为“$($(y))”,而其再次被扩展为“$($(subst 1,2,$(x)))”。$(x)的值是“variable1”,subst函数把“variable1”中的所有“1”字串替换成“2”字串,于是,“variable1”变成“variable2”,再取其值,所以,最终,$(a)的值就是$(variable2)的值——“Hello”。

使用多个变量来组成一个变量的名字,然后再取其值:
first_second = Hello
a = first
b = second
all = $($a_$b)
这里的“$a_$b”组成了“first_second”,于是,$(all)的值就是“Hello”。

多行变量,还有一种设置变量值的方法是使用define关键字。使用define关键字设置变量的值可以有换行,这有利于定义一系列的命令
define指示符后面跟的是变量的名字,而重起一行定义变量的值,定义是以endef关键字结束。因为命令需要以[Tab]键开头,所以如果你用define定义的命令变量中没有以[Tab]键开头,那么make就不会把其认为是命令。
下面的这个示例展示了define的用法:
define two-lines
echo foo
echo $(bar)
endef

prog : CFLAGS = -g目标变量,当我们设置了这样一个变量,这个变量会作用到由这个目标所引发的所有的规则中去。
其语法是:
<target ...> : <variable-assignment>
<target ...> : overide <variable-assignment>
通过上面的目标变量中,我们知道,变量可以定义在某个目标上。

模式变量的好处就是,我们可以给定一种“模式”,可以把变量定义在符合这种模式的所有目标上。make的“模式”一般是至少含有一个“%”的,所以,我们可以以如下方式给所有以[.o]结尾的目标定义目标变量:
%.o : CFLAGS = -O
同样,模式变量的语法和“目标变量”一样:
<pattern ...> : <variable-assignment>
<pattern ...> : override <variable-assignment>
override同样是针对于系统环境传入的变量,或是make命令行指定的变量。

语法

使用条件判断
libs_for_gcc = -lgnu
normal_libs =
ifeq ($(CC),gcc)
libs=$(libs_for_gcc)
else
libs=$(normal_libs)
endif
foo: $(objects)
$(CC) -o foo $(objects) $(libs)
ifeq、else和endif。ifeq的意思表示条件语句的开始,并指定一个条件表达式,表达式包含两个参数,以逗号分隔,表达式以圆括号括起。else表示条件表达式为假的情况。endif表示一个条件语句的结束,任何一个条件表达式都应该以endif结束。

条件表达式的语法为:
<conditional-directive>
<text-if-true>
endif
以及:
<conditional-directive>
<text-if-true>
else
<text-if-false>
endif

其中<conditional-directive>表示条件关键字,如“ifeq”。这个关键字有四个:ifeq、ifneq、ifdef、ifndef
在<conditional-directive>这一行上,多余的空格是被允许的,但是不能以[Tab]键做为开始(不然就被认为是命令)。而注释符“#”同样也是安全的。“else”和“endif”也一样,只要不是以[Tab]键开始就行了。

第一个是我们前面所见过的“ifeq”

ifeq (<arg1>, <arg2> ) 
ifeq '<arg1>' '<arg2>' 
ifeq "<arg1>" "<arg2>" 
ifeq "<arg1>" '<arg2>' 
ifeq '<arg1>' "<arg2>" 

比较参数“arg1”和“arg2”的值是否相同。当然,参数中我们还可以使用make的函数。如:
ifeq ($(strip $(foo)),)
<text-if-empty>
endif
这个示例中使用了“strip”函数,如果这个函数的返回值是空(Empty),那么<text-if-empty>就生效。

第二个条件关键字是“ifneq”。语法是:
ifneq (<arg1>, <arg2> ) 
ifneq '<arg1>' '<arg2>' 
ifneq "<arg1>" "<arg2>" 
ifneq "<arg1>" '<arg2>' 
ifneq '<arg1>' "<arg2>" 
其比较参数“arg1”和“arg2”的值是否相同,如果不同,则为真。和“ifeq”类似。

第三个条件关键字是“ifdef”。语法是:ifdef <variable-name> 
如果变量<variable-name>的值非空,那到表达式为真。否则,表达式为假。当然,<variable-name>同样可以是一个函数的返回值。注意,ifdef只是测试一个变量是否有值,其并不会把变量扩展到当前位置。还是来看两个例子:
示例一:
bar =
foo = $(bar)
ifdef foo
frobozz = yes
else
frobozz = no
endif
示例二:
foo =
ifdef foo
frobozz = yes
else
frobozz = no
endif
第一个例子中,“$(frobozz)”值是“yes”,第二个则是“no”。

第四个条件关键字是“ifndef”。其语法是:ifndef <variable-name>,和“ifdef”是相反的意思。

函数的调用语法
函数调用,很像变量的使用,也是以“$”来标识的,其语法如下:
$(<function> <arguments> )或是${<function> <arguments>}
这里,<function>就是函数名,make支持的函数不多。<arguments>是函数的参数,参数间以逗号“,”分隔,而函数名和参数之间以“空格”分隔。函数调用以“$”开头,以圆括号或花括号把函数名和参数括起。感觉很像一个变量,是不是?函数中的参数可以使用变量,为了风格的统一,函数和变量的括号最好一样,如使用“$(subst a,b,$(x))”这样的形式,而不是“$(subst a,b,${x})”的形式。因为统一会更清楚,也会减少一些不必要的麻烦。

comma:= ,
empty:=
space:= $(empty) $(empty)
foo:= a b c
bar:= $(subst $(space),$(comma),$(foo))
在这个示例中,$(comma)的值是一个逗号。$(space)使用了$(empty)定义了一个空格,$(foo)的值是“a b c”,$(bar)的定义用,调用了函数“subst”,这是一个替换函数,这个函数有三个参数,第一个参数是被替换字串,第二个参数是替换字串,第三个参数是替换操作作用的字串。这个函数也就是把$(foo)中的空格替换成逗号,所以$(bar)的值是“a,b,c”。

字符串处理函数

$(subst <from>,<to>,<text> ) 
名称:字符串替换函数——subst。
功能:把字串<text>中的<from>字符串替换成<to>。
返回:函数返回被替换过后的字符串。
示例:
$(subst ee,EE,feet on the street),
把“feet on the street”中的“ee”替换成“EE”,返回结果是“fEEt on the strEEt”。

$(patsubst <pattern>,<replacement>,<text> ) 
名称:模式字符串替换函数——patsubst。
功能:查找<text>中的单词(单词以“空格”、“Tab”或“回车”“换行”分隔)是否符合模式<pattern>,如果匹配的话,则以<replacement>替换。这里,<pattern>可以包括通配符“%”,表示任意长度的字串。如果<replacement>中也包含“%”,那么,<replacement>中的这个“%”将是<pattern>中的那个“%”所代表的字串。(可以用“/”来转义,以“/%”来表示真实含义的“%”字符)
返回:函数返回被替换过后的字符串。
示例:
$(patsubst %.c,%.o,x.c.c bar.c)
把字串“x.c.c bar.c”符合模式[%.c]的单词替换成[%.o],返回结果是“x.c.o bar.o”

“$(var:<pattern>=<replacement> )”
相当于“$(patsubst <pattern>,<replacement>,$(var))”,

而“$(var: <suffix>=<replacement> )”
则相当于“$(patsubst %<suffix>,%<replacement>,$(var))”。

例如有:objects = foo.o bar.o baz.o,
那么,“$(objects:.o=.c)”和“$(patsubst %.o,%.c,$(objects))”是一样的。

$(strip <string> )
名称:去空格函数——strip。
功能:去掉<string>字串中开头和结尾的空字符。
返回:返回被去掉空格的字符串值。
示例:
$(strip a b c )
把字串“a b c ”去到开头和结尾的空格,结果是“a b c”。

$(findstring <find>,<in> )
名称:查找字符串函数——findstring。
功能:在字串<in>中查找<find>字串。
返回:如果找到,那么返回<find>,否则返回空字符串。
示例:
$(findstring a,a b c)
$(findstring a,b c)
第一个函数返回“a”字符串,第二个返回“”字符串(空字符串)

$(filter <pattern...>,<text> )
名称:过滤函数——filter。
功能:以<pattern>模式过滤<text>字符串中的单词,保留符合模式<pattern>的单词。可以有多个模式。
返回:返回符合模式<pattern>的字串。
示例:
sources := foo.c bar.c baz.s ugh.h
foo: $(sources)
cc $(filter %.c %.s,$(sources)) -o foo
$(filter %.c %.s,$(sources))返回的值是“foo.c bar.c baz.s”。

$(filter-out <pattern...>,<text> )
名称:反过滤函数——filter-out。
功能:以<pattern>模式过滤<text>字符串中的单词,去除符合模式<pattern>的单词。可以有多个模式。
返回:返回不符合模式<pattern>的字串。
示例:
objects=main1.o foo.o main2.o bar.o
mains=main1.o main2.o
$(filter-out $(mains),$(objects)) 返回值是“foo.o bar.o”。

$(sort <list> )
名称:排序函数——sort。
功能:给字符串<list>中的单词排序(升序)。
返回:返回排序后的字符串。
示例:$(sort foo bar lose)返回“bar foo lose” 。
备注:sort函数会去掉<list>中相同的单词。

$(word <n>,<text> )
名称:取单词函数——word。
功能:取字符串<text>中第<n>个单词。(从一开始)
返回:返回字符串<text>中第<n>个单词。如果<n>比<text>中的单词数要大,那么返回空字符串。
示例:$(word 2, foo bar baz)返回值是“bar”。

$(wordlist <s>,<e>,<text> ) 
名称:取单词串函数——wordlist。
功能:从字符串<text>中取从<s>开始到<e>的单词串。<s>和<e>是一个数字。
返回:返回字符串<text>中从<s>到<e>的单词字串。如果<s>比<text>中的单词数要大,那么返回空字符串。如果<e>大于<text>的单词数,那么返回从<s>开始,到<text>结束的单词串。
示例: $(wordlist 2, 3, foo bar baz)返回值是“bar baz”。

$(words <text> )
名称:单词个数统计函数——words。
功能:统计<text>中字符串中的单词个数。
返回:返回<text>中的单词数。
示例:$(words, foo bar baz)返回值是“3”。
备注:如果我们要取<text>中最后的一个单词,我们可以这样:$(word $(words <text> ),<text> )。

$(firstword <text> )
名称:首单词函数——firstword。
功能:取字符串<text>中的第一个单词。
返回:返回字符串<text>的第一个单词。
示例:$(firstword foo bar)返回值是“foo”。
备注:这个函数可以用word函数来实现:$(word 1,<text> )。
以上,是所有的字符串操作函数,如果搭配混合使用,可以完成比较复杂的功能。这里,举一个现实中应用的例子。我们知道,make使用“VPATH”变量来指定“依赖文件”的搜索路径。于是,我们可以利用这个搜索路径来指定编译器对头文件的搜索路径参数CFLAGS,如:

override CFLAGS += $(patsubst %,-I%,$(subst :, ,$(VPATH)))

如果我们的“$(VPATH)”值是“src:../headers”,那么“$(patsubst %,-I%,$(subst :, ,$(VPATH)))”将返回“-Isrc -I../headers”,这正是cc或gcc搜索头文件路径的参数。

文件名操作函数
$(dir <names...> ) 
名称:取目录函数——dir。
功能:从文件名序列<names>中取出目录部分。目录部分是指最后一个反斜杠(“/”)之前的部分。如果没有反斜杠,那么返回“./”。
返回:返回文件名序列<names>的目录部分。
示例: $(dir src/foo.c hacks)返回值是“src/ ./”。

$(notdir <names...> ) 
名称:取文件函数——notdir。
功能:从文件名序列<names>中取出非目录部分。非目录部分是指最后一个反斜杠(“/”)之后的部分。
返回:返回文件名序列<names>的非目录部分。
示例: $(notdir src/foo.c hacks)返回值是“foo.c hacks”。

$(suffix <names...> ) 
名称:取后缀函数——suffix。
功能:从文件名序列<names>中取出各个文件名的后缀。
返回:返回文件名序列<names>的后缀序列,如果文件没有后缀,则返回空字串。
示例:$(suffix src/foo.c src-1.0/bar.c hacks)返回值是“.c .c”。

$(basename <names...> )
名称:取前缀函数——basename。
功能:从文件名序列<names>中取出各个文件名的前缀部分。
返回:返回文件名序列<names>的前缀序列,如果文件没有前缀,则返回空字串。
示例:$(basename src/foo.c src-1.0/bar.c hacks)返回值是“src/foo src-1.0/bar hacks”。

$(addsuffix <suffix>,<names...> ) 
名称:加后缀函数——addsuffix。
功能:把后缀<suffix>加到<names>中的每个单词后面。
返回:返回加过后缀的文件名序列。
示例:$(addsuffix .c,foo bar)返回值是“foo.c bar.c”。

$(addprefix <prefix>,<names...> ) 
名称:加前缀函数——addprefix。
功能:把前缀<prefix>加到<names>中的每个单词后面。
返回:返回加过前缀的文件名序列。
示例:$(addprefix src/,foo bar)返回值是“src/foo src/bar”。

$(join <list1>,<list2> )
名称:连接函数——join。
功能:把<list2>中的单词对应地加到<list1>的单词后面。如果<list1>的单词个数要比<list2>的多,那么,<list1>中的多出来的单词将保持原样。如果<list2>的单词个数要比<list1>多,那么,<list2>多出来的单词将被复制到<list2>中。
返回:返回连接过后的字符串。
示例:$(join aaa bbb , 111 222 333)返回值是“aaa111 bbb222 333”。

foreach函数和别的函数非常的不一样。因为这个函数是用来做循环用的,Makefile中的foreach函数几乎是仿照于Unix标准Shell(/bin/sh)中的for语句,或是C-Shell(/bin/csh)中的foreach语句而构建的。它的语法是:
$(foreach <var>,<list>,<text> )
这个函数的意思是,把参数<list>中的单词逐一取出放到参数<var>所指定的变量中,然后再执行<text>所包含的表达式。每一次<text>会返回一个字符串,循环过程中,<text>的所返回的每个字符串会以空格分隔,最后当整个循环结束时,<text>所返回的每个字符串所组成的整个字符串(以空格分隔)将会是foreach函数的返回值。
所以,<var>最好是一个变量名,<list>可以是一个表达式,而<text>中一般会使用<var>这个参数来依次枚举<list>中的单词。举个例子:
names := a b c d
files := $(foreach n,$(names),$(n).o)
上面的例子中,$(name)中的单词会被挨个取出,并存到变量“n”中,“$(n).o”每次根据“$(n)”计算出一个值,这些值以空格分隔,最后作为foreach函数的返回,所以,$(files)的值是“a.o b.o c.o d.o”。
注意,foreach中的<var>参数是一个临时的局部变量,foreach函数执行完后,参数<var>的变量将不在作用,其作用域只在foreach函数当中。

if函数很像GNU的make所支持的条件语句——ifeq(参见前面所述的章节),if函数的语法是:
$(if <condition>,<then-part> ) 或是
$(if <condition>,<then-part>,<else-part> )

call函数是唯一一个可以用来创建新的参数化的函数。你可以写一个非常复杂的表达式,这个表达式中,你可以定义许多参数,然后你可以用call函数来向这个表达式传递参数。其语法是:
$(call <expression>,<parm1>,<parm2>,<parm3>...)
当make执行这个函数时,<expression>参数中的变量,如$(1),$(2),$(3)等,会被参数<parm1>,<parm2>,<parm3>依次取代。而<expression>的返回值就是call函数的返回值。例如:
reverse = $(1) $(2)
foo = $(call reverse,a,b)
那么,foo的值就是“a b”。当然,参数的次序是可以自定义的,不一定是顺序的,如:
reverse = $(2) $(1)
foo = $(call reverse,a,b)
此时的foo的值就是“b a”。

origin函数不像其它的函数,他并不操作变量的值,他只是告诉你你的这个变量是哪里来的?其语法是:
$(origin <variable> )
注意,<variable>是变量的名字,不应该是引用。所以你最好不要在<variable>中使用“$”字符。Origin函数会以其返回值来告诉你这个变量的“出生情况”,下面,是origin函数的返回值:
“undefined”
如果<variable>从来没有定义过,origin函数返回这个值“undefined”。
“default”
如果<variable>是一个默认的定义,比如“CC”这个变量,
“environment”
如果<variable>是一个环境变量,并且当Makefile被执行时,“-e”参数没有被打开。
“file”
如果<variable>这个变量被定义在Makefile中。
“command line”
如果<variable>这个变量是被命令行定义的。
“override”
如果<variable>是被override指示符重新定义的。
“automatic”
如果<variable>是一个命令运行中的自动化变量。

shell函数也不像其它的函数。顾名思义,它的参数应该就是操作系统Shell的命令。它和反引号“`”是相同的功能。这就是说,shell函数把执行操作系统命令后的输出作为函数返回。于是,我们可以用操作系统命令以及字符串处理命令awk,sed等等命令来生成一个变量,如:
contents := $(shell cat foo)
files := $(shell echo *.c)
注意,这个函数会新生成一个Shell程序来执行命令,所以你要注意其运行性能,如果你的Makefile中有一些比较复杂的规则,并大量使用了这个函数,那么对于你的系统性能是有害的。特别是Makefile的隐晦的规则可能会让你的shell函数执行的次数比你想像的多得多。

make提供了一些函数来控制make的运行。通常,你需要检测一些运行Makefile时的运行时信息,并且根据这些信息来决定,你是让make继续执行,还是停止。
$(error <text ...> )
产生一个致命的错误,<text ...>是错误信息。注意,error函数不会在一被使用就会产生错误信息,所以如果你把其定义在某个变量中,并在后续的脚本中使用这个变量,那么也是可以的。例如:
示例一:
ifdef ERROR_001
$(error error is $(ERROR_001))
endif
示例二:
ERR = $(error found an error!)
.PHONY: err
err: ; $(ERR)
示例一会在变量ERROR_001定义了后执行时产生error调用,而示例二则在目录err被执行时才发生error调用。

$(warning <text ...> )
这个函数很像error函数,只是它并不会让make退出,只是输出一段警告信息,而make继续执行

make命令执行后有三个退出码:
0 —— 表示成功执行。
1 —— 如果make运行时出现任何错误,其返回1。
2 —— 如果你使用了make的“-q”选项,并且make使得一些目标不需要更新,那么返回2。

在Unix世界中,软件发布时,特别是GNU这种开源软件的发布时,其makefile都包含了编译、安装、打包等功能。我们可以参照这种规则来书写我们的makefile中的目标。
“all”
这个伪目标是所有目标的目标,其功能一般是编译所有的目标。
“clean”
这个伪目标功能是删除所有被make创建的文件。
“install”
这个伪目标功能是安装已编译好的程序,其实就是把目标执行文件拷贝到指定的目标中去。
“print”
这个伪目标的功能是例出改变过的源文件。
“tar”
这个伪目标功能是把源程序打包备份。也就是一个tar文件。
“dist”
这个伪目标功能是创建一个压缩文件,一般是把tar文件压成Z文件。或是gz文件。
“TAGS”
这个伪目标功能是更新所有的目标,以备完整地重编译使用。
“check”和“test”
这两个伪目标一般用来测试makefile的流程。
如果你要书写这种功能,最好使用这种名字命名你的目标,这样规范一些,规范的好处就是——不用解释,大家都明白。而且如果你的makefile中有这些功能,一是很实用,二是可以显得你的makefile很专业(不是那种初学者的作品)。

检查规则
有时候,我们不想让我们的makefile中的规则执行起来,我们只想检查一下我们的命令,或是执行的序列。于是我们可以使用make命令的下述参数:
“-n”
“--just-print”
“--dry-run”
“--recon”
不执行参数,这些参数只是打印命令,不管目标是否更新,把规则和连带规则下的命令打印出来,但不执行,这些参数对于我们调试makefile很有用处。

“-t”
“--touch”
这个参数的意思就是把目标文件的时间更新,但不更改目标文件。也就是说,make假装编译目标,但不是真正的编译目标,只是把目标变成已编译过的状态。

“-q”
“--question”
这个参数的行为是找目标的意思,也就是说,如果目标存在,那么其什么也不会输出,当然也不会执行编译,如果目标不存在,其会打印出一条出错信息。

“-W <file>”
“--what-if=<file>”
“--assume-new=<file>”
“--new-file=<file>”
这个参数需要指定一个文件。一般是是源文件(或依赖文件),Make会根据规则推导来运行依赖于这个文件的命令,一般来说,可以和“-n”参数一同使用,来查看这个依赖文件所发生的规则命令。

下面列举了所有GNU make 3.80版的参数定义。其它版本和产商的make大同小异,不过其它产商的make的具体参数还是请参考各自的产品文档。
“-b”
“-m”
这两个参数的作用是忽略和其它版本make的兼容性。

“-B”
“--always-make”
认为所有的目标都需要更新(重编译)。

“-C <dir>”
“--directory=<dir>”
指定读取makefile的目录。如果有多个“-C”参数,make的解释是后面的路径以前面的作为相对路径,并以最后的目录作为被指定目录。如:“make –C ~hchen/test –C prog”等价于“make –C ~hchen/test/prog”。

“—debug[=<options>]”
输出make的调试信息。它有几种不同的级别可供选择,如果没有参数,那就是输出最简单的调试信息。下面是<options>的取值:
a —— 也就是all,输出所有的调试信息。(会非常的多)
b —— 也就是basic,只输出简单的调试信息。即输出不需要重编译的目标。
v —— 也就是verbose,在b选项的级别之上。输出的信息包括哪个makefile被解析,不需要被重编译的依赖文件(或是依赖目标)等。
i —— 也就是implicit,输出所以的隐含规则。
j —— 也就是jobs,输出执行规则中命令的详细信息,如命令的PID、返回码等。
m —— 也就是makefile,输出make读取makefile,更新makefile,执行makefile的信息。

“-d”
相当于“--debug=a”。

“-e”
“--environment-overrides”
指明环境变量的值覆盖makefile中定义的变量的值。

“-f=<file>”
“--file=<file>”
“--makefile=<file>”
指定需要执行的makefile。

“-h”
“--help”
显示帮助信息。

“-i”
“--ignore-errors”
在执行时忽略所有的错误。

“-I <dir>”
“--include-dir=<dir>”
指定一个被包含makefile的搜索目标。可以使用多个“-I”参数来指定多个目录。

“-j [<jobsnum>]”
“--jobs[=<jobsnum>]”
指同时运行命令的个数。如果没有这个参数,make运行命令时能运行多少就运行多少。如果有一个以上的“-j”参数,那么仅最后一个“-j”才是有效的。(注意这个参数在MS-DOS中是无用的)

“-k”
“--keep-going”
出错也不停止运行。如果生成一个目标失败了,那么依赖于其上的目标就不会被执行了。

“-l <load>”
“--load-average[=<load]”
“—max-load[=<load>]”
指定make运行命令的负载。

“-n”
“--just-print”
“--dry-run”
“--recon”
仅输出执行过程中的命令序列,但并不执行。

“-o <file>”
“--old-file=<file>”
“--assume-old=<file>”
不重新生成的指定的<file>,即使这个目标的依赖文件新于它。

“-p”
“--print-data-base”
输出makefile中的所有数据,包括所有的规则和变量。这个参数会让一个简单的makefile都会输出一堆信息。如果你只是想输出信息而不想执行makefile,你可以使用“make -qp”命令。如果你想查看执行makefile前的预设变量和规则,你可以使用“make –p –f /dev/null”。这个参数输出的信息会包含着你的makefile文件的文件名和行号,所以,用这个参数来调试你的makefile会是很有用的,特别是当你的环境变量很复杂的时候。

“-q”
“--question”
不运行命令,也不输出。仅仅是检查所指定的目标是否需要更新。如果是0则说明要更新,如果是2则说明有错误发生。

“-r”
“--no-builtin-rules”
禁止make使用任何隐含规则。

“-R”
“--no-builtin-variabes”
禁止make使用任何作用于变量上的隐含规则。

“-s”
“--silent”
“--quiet”
在命令运行时不输出命令的输出。

“-S”
“--no-keep-going”
“--stop”
取消“-k”选项的作用。因为有些时候,make的选项是从环境变量“MAKEFLAGS”中继承下来的。所以你可以在命令行中使用这个参数来让环境变量中的“-k”选项失效。

“-t”
“--touch”
相当于UNIX的touch命令,只是把目标的修改日期变成最新的,也就是阻止生成目标的命令运行。

“-v”
“--version”
输出make程序的版本、版权等关于make的信息。

“-w”
“--print-directory”
输出运行makefile之前和之后的信息。这个参数对于跟踪嵌套式调用make时很有用。

“--no-print-directory”
禁止“-w”选项。

“-W <file>”
“--what-if=<file>”
“--new-file=<file>”
“--assume-file=<file>”
假定目标<file>需要更新,如果和“-n”选项使用,那么这个参数会输出该目标更新时的运行动作。如果没有“-n”那么就像运行UNIX的“touch”命令一样,使得<file>的修改时间为当前时间。

“--warn-undefined-variables”
只要make发现有未定义的变量,那么就输出警告信息。

“隐含规则”也就是一种惯例,make会按照这种“惯例”心照不喧地来运行,那怕我们的Makefile中没有书写这样的规则。例如,把[.c]文件编译成[.o]文件这一规则,你根本就不用写出来,make会自动推导出这种规则,并生成我们需要的[.o]文件。
“隐含规则”会使用一些我们的系统变量,我们可以改变这些系统变量的值来定制隐含规则的运行时的参数。如系统变量“CFLAGS”可以控制编译时的编译器参数。
Makefile:
foo : foo.o bar.o
cc –o foo foo.o bar.o $(CFLAGS) $(LDFLAGS)
我们可以注意到,这个Makefile中并没有写下如何生成foo.o和bar.o这两目标的规则和命令。因为make的“隐含规则”功能会自动为我们自动去推导这两个目标的依赖目标和生成命令。
make会在自己的“隐含规则”库中寻找可以用的规则,如果找到,那么就会使用。如果找不到,那么就会报错。在上面的那个例子中,make调用的隐含规则是,把[.o]的目标的依赖文件置成[.c],并使用C的编译命令“cc –c $(CFLAGS) [.c]”来生成[.o]的目标。也就是说,我们完全没有必要写下下面的两条规则:
foo.o : foo.c
cc –c foo.c $(CFLAGS)
bar.o : bar.c
cc –c bar.c $(CFLAGS)
因为,这已经是“约定”好了的事了,make和我们约定好了用C编译器“cc”生成[.o]文件的规则,这就是隐含规则。

CC 是一个全局变量,它指定你的Makefile所用的编译器,一般默认是gcc。在unix下生成.o文件的过程叫编译(compile),将无数.o文件集合生成可执行文件的过程叫链接(link);有时会在unix界面下看到.a文件,那是Archive File,相当于windows下的库文件Library File,.a文件作用是:由于源文件太多(上例是指.c和.h文件过多),编译生成的中间目标文件(.o文件)太多,而在链接时需要明显地指出中间目标文件名,这对于编译很不方便,所以,我们要给中间目标文件打个包,这个包就是.a文件。

隐含规则一览
1、编译C程序的隐含规则。
“<n>.o”的目标的依赖目标会自动推导为“<n>.c”,并且其生成命令是“$(CC) –c $(CPPFLAGS) $(CFLAGS)”

2、编译C++程序的隐含规则。
“<n>.o”的目标的依赖目标会自动推导为“<n>.cc”或是“<n>.C”,并且其生成命令是“$(CXX) –c $(CPPFLAGS) $(CFLAGS)”。(建议使用“.cc”作为C++源文件的后缀,而不是“.C”)

3、编译Pascal程序的隐含规则。
“<n>.o”的目标的依赖目标会自动推导为“<n>.p”,并且其生成命令是“$(PC) –c $(PFLAGS)”。

4、编译Fortran/Ratfor程序的隐含规则。
“<n>.o”的目标的依赖目标会自动推导为“<n>.r”或“<n>.F”或“<n>.f”,并且其生成命令是:
“.f” “$(FC) –c $(FFLAGS)”
“.F” “$(FC) –c $(FFLAGS) $(CPPFLAGS)”
“.f” “$(FC) –c $(FFLAGS) $(RFLAGS)”

5、预处理Fortran/Ratfor程序的隐含规则。
“<n>.f”的目标的依赖目标会自动推导为“<n>.r”或“<n>.F”。这个规则只是转换Ratfor或有预处理的Fortran程序到一个标准的Fortran程序。其使用的命令是:
“.F” “$(FC) –F $(CPPFLAGS) $(FFLAGS)”
“.r” “$(FC) –F $(FFLAGS) $(RFLAGS)”

6、编译Modula-2程序的隐含规则。
“<n>.sym”的目标的依赖目标会自动推导为“<n>.def”,并且其生成命令是:“$(M2C) $(M2FLAGS) $(DEFFLAGS)”。“<n.o>” 的目标的依赖目标会自动推导为“<n>.mod”,并且其生成命令是:“$(M2C) $(M2FLAGS) $(MODFLAGS)”。

7、汇编和汇编预处理的隐含规则。
“<n>.o” 的目标的依赖目标会自动推导为“<n>.s”,默认使用编译器“as”,并且其生成命令是:“$(AS) $(ASFLAGS)”。“<n>.s” 的目标的依赖目标会自动推导为“<n>.S”,默认使用C预编译器“cpp”,并且其生成命令是:“$(AS) $(ASFLAGS)”。

8、链接Object文件的隐含规则。
“<n>”目标依赖于“<n>.o”,通过运行C的编译器来运行链接程序生成(一般是“ld”),其生成命令是:“$(CC) $(LDFLAGS) <n>.o $(LOADLIBES) $(LDLIBS)”。这个规则对于只有一个源文件的工程有效,同时也对多个Object文件(由不同的源文件生成)的也有效。例如如下规则:

x : y.o z.o

并且“x.c”、“y.c”和“z.c”都存在时,隐含规则将执行如下命令:

cc -c x.c -o x.o
cc -c y.c -o y.o
cc -c z.c -o z.o
cc x.o y.o z.o -o x
rm -f x.o
rm -f y.o
rm -f z.o

如果没有一个源文件(如上例中的x.c)和你的目标名字(如上例中的x)相关联,那么,你最好写出自己的生成规则,不然,隐含规则会报错的。

9、Yacc C程序时的隐含规则。
“<n>.c”的依赖文件被自动推导为“n.y”(Yacc生成的文件),其生成命令是:“$(YACC) $(YFALGS)”。(“Yacc”是一个语法分析器,关于其细节请查看相关资料)

10、Lex C程序时的隐含规则。
“<n>.c”的依赖文件被自动推导为“n.l”(Lex生成的文件),其生成命令是:“$(LEX) $(LFALGS)”。(关于“Lex”的细节请查看相关资料)

11、Lex Ratfor程序时的隐含规则。
“<n>.r”的依赖文件被自动推导为“n.l”(Lex生成的文件),其生成命令是:“$(LEX) $(LFALGS)”。

12、从C程序、Yacc文件或Lex文件创建Lint库的隐含规则。
“<n>.ln” (lint生成的文件)的依赖文件被自动推导为“n.c”,其生成命令是:“$(LINT) $(LINTFALGS) $(CPPFLAGS) -i”。对于“<n>.y”和“<n>.l”也是同样的规则。

下面是所有隐含规则中会用到的变量:
1、关于命令的变量。
AR 
函数库打包程序。默认命令是“ar”。 
AS 
汇编语言编译程序。默认命令是“as”。
CC 
C语言编译程序。默认命令是“cc”。
CXX 
C++语言编译程序。默认命令是“g++”。
CO 
从 RCS文件中扩展文件程序。默认命令是“co”。
CPP 
C程序的预处理器(输出是标准输出设备)。默认命令是“$(CC) –E”。
FC 
Fortran 和 Ratfor 的编译器和预处理程序。默认命令是“f77”。
GET 
从SCCS文件中扩展文件的程序。默认命令是“get”。 
LEX 
Lex方法分析器程序(针对于C或Ratfor)。默认命令是“lex”。
PC 
Pascal语言编译程序。默认命令是“pc”。
YACC 
Yacc文法分析器(针对于C程序)。默认命令是“yacc”。
YACCR 
Yacc文法分析器(针对于Ratfor程序)。默认命令是“yacc –r”。
MAKEINFO 
转换Texinfo源文件(.texi)到Info文件程序。默认命令是“makeinfo”。
TEX 
从TeX源文件创建TeX DVI文件的程序。默认命令是“tex”。
TEXI2DVI 
从Texinfo源文件创建军TeX DVI 文件的程序。默认命令是“texi2dvi”。
WEAVE 
转换Web到TeX的程序。默认命令是“weave”。
CWEAVE 
转换C Web 到 TeX的程序。默认命令是“cweave”。
TANGLE 
转换Web到Pascal语言的程序。默认命令是“tangle”。
CTANGLE 
转换C Web 到 C。默认命令是“ctangle”。
RM 
删除文件命令。默认命令是“rm –f”。

2、关于命令参数的变量

下面的这些变量都是相关上面的命令的参数。如果没有指明其默认值,那么其默认值都是空。

ARFLAGS 
函数库打包程序AR命令的参数。默认值是“rv”。
ASFLAGS 
汇编语言编译器参数。(当明显地调用“.s”或“.S”文件时)。 
CFLAGS 
C语言编译器参数。
CXXFLAGS 
C++语言编译器参数。
COFLAGS 
RCS命令参数。 
CPPFLAGS 
C预处理器参数。( C 和 Fortran 编译器也会用到)。
FFLAGS 
Fortran语言编译器参数。
GFLAGS 
SCCS “get”程序参数。
LDFLAGS 
链接器参数。(如:“ld”)
LFLAGS 
Lex文法分析器参数。
PFLAGS 
Pascal语言编译器参数。
RFLAGS 
Ratfor 程序的Fortran 编译器参数。
YFLAGS 
Yacc文法分析器参数。 

有些时候,一个目标可能被一系列的隐含规则所作用。例如,一个[.o]的文件生成,可能会是先被Yacc的[.y]文件先成[.c],然后再被C的编译器生成。我们把这一系列的隐含规则叫做“隐含规则链”。在上面的例子中,如果文件[.c]存在,那么就直接调用C的编译器的隐含规则,如果没有[.c]文件,但有一个[.y]文件,那么Yacc的隐含规则会被调用,生成[.c]文件,然后,再调用C编译的隐含规则最终由[.c]生成[.o]文件,达到目标。我们把这种[.c]的文件(或是目标),叫做中间目标。不管怎么样,make会努力自动推导生成目标的一切方法,不管中间目标有多少,其都会执着地把所有的隐含规则和你书写的规则全部合起来分析,努力达到目标。在默认情况下,对于中间目标,它和一般的目标有两个地方所不同:第一个不同是除非中间的目标不存在,才会引发中间规则。第二个不同的是,只要目标成功产生,那么,产生最终目标过程中,所产生的中间目标文件会被以“rm -f”删除。Make会优化一些特殊的隐含规则,而不生成中间文件。如,从文件“foo.c”生成目标程序“foo”,按道理,make会编译生成中间文件“foo.o”,然后链接成“foo”,但在实际情况下,这一动作可以被一条“cc”的命令完成(cc –o foo foo.c),于是优化过的规则就不会生成中间文件。

模式规则中,至少在规则的目标定义中要包含"%",否则,就是一般的规则。目标中的"%"定义表示对文件名的匹配,"%"表示长度任意的非空字符串。例如:"%.c"表示以".c"结尾的文件名(文件名的长度至少为3),而"s.%.c"则表示以"s."开头,".c"结尾的文件名(文件名的长度至少为5)。

下面是所有的自动化变量及其说明:
$@
表示规则中的目标文件集。在模式规则中,如果有多个目标,那么,"$@"就是匹配于目标中模式定义的集合。

$%
仅当目标是函数库文件中,表示规则中的目标成员名。例如,如果一个目标是"foo.a(bar.o)",那么,"$%"就是"bar.o","$@"就是"foo.a"。如果目标不是函数库文件(Unix下是[.a],Windows下是[.lib]),那么,其值为空。

$<
依赖目标中的第一个目标名字。如果依赖目标是以模式(即"%")定义的,那么"$<"将是符合模式的一系列的文件集。注意,其是一个一个取出来的。

$?
所有比目标新的依赖目标的集合。以空格分隔。

$^
所有的依赖目标的集合。以空格分隔。如果在依赖目标中有多个重复的,那个这个变量会去除重复的依赖目标,只保留一份。

$+
这个变量很像"$^",也是所有依赖目标的集合。只是它不去除重复的依赖目标。

$* 
这个变量表示目标模式中"%"及其之前的部分。如果目标是"dir/a.foo.b",并且目标的模式是"a.%.b",那么,"$*"的值就是"dir/a.foo"。这个变量对于构造有关联的文件名是比较有较。如果目标中没有模式的定义,
那么"$*"也就不能被推导出,但是,如果目标文件的后缀是make所识别的,那么"$*"就是除了后缀的那一部分。例如:如果目标是"foo.c",因为".c"是make所能识别的后缀名,所以,"$*"的值就是"foo"。这个特性
是GNU make的,很有可能不兼容于其它版本的make,所以,你应该尽量避免使用"$*",除非是在隐含规则或是静态模式中。如果目标中的后缀是make所不能识别的,那么"$*"就是空值。

在上述所列出来的自动量变量中。四个变量($@、$<、$%、$*)在扩展时只会有一个文件,而另三个的值是一个文件列表。这七个自动化变量还可以取得文件的目录名或是在当前目录下的符合模式的文件名,只需要搭配上"D"或"F"字样。这是GNU make中老版本的特性,在新版本中,我们使用函数"dir"或"notdir"就可以做到了。"D"的含义就是Directory,就是目录,"F"的含义就是File,就是文件。

下面是对于上面的七个变量分别加上"D"或是"F"的含义:
$(@D)
表示"$@"的目录部分(不以斜杠作为结尾),如果"$@"值是"dir/foo.o",那么"$(@D)"就是"dir",而如果"$@"中没有包含斜杠的话,其值就是"."(当前目录)。

$(@F)
表示"$@"的文件部分,如果"$@"值是"dir/foo.o",那么"$(@F)"就是"foo.o","$(@F)"相当于函数"$(notdir $@)"。

"$(*D)"
"$(*F)"
和上面所述的同理,也是取文件的目录部分和文件部分。对于上面的那个例子,"$(*D)"返回"dir",而"$(*F)"返回"foo"

"$(%D)"
"$(%F)"
分别表示了函数包文件成员的目录部分和文件部分。这对于形同"archive(member)"形式的目标中的"member"中包含了不同的目录很有用。

"$(<D)"
"$(<F)"
分别表示依赖文件的目录部分和文件部分。

"$(^D)"
"$(^F)"
分别表示所有依赖文件的目录部分和文件部分。(无相同的)

"$(+D)"
"$(+F)"
分别表示所有依赖文件的目录部分和文件部分。(可以有相同的)

"$(?D)"
"$(?F)"
分别表示被更新的依赖文件的目录部分和文件部分。

一般来说,一个目标的模式有一个有前缀或是后缀的"%",或是没有前后缀,直接就是一个"%"。因为"%"代表一个或多个字符,所以在定义好了的模式中,我们把"%"所匹配的内容叫做"茎",例如"%.c"所匹配的文件"test.c"
中"test"就是"茎"。因为在目标和依赖目标中同时有"%"时,依赖目标的"茎"会传给目标,当做目标中的"茎"。例如有一个模式"e%t",文件"src/eat"匹配于该模式,于是"src/a"就是其"茎"

而要让make知道一些特定的后缀,我们可以使用伪目标".SUFFIXES"来定义或是删除,如:
.SUFFIXES: .hack .win
把后缀.hack和.win加入后缀列表中的末尾。
.SUFFIXES: # 删除默认的后缀
.SUFFIXES: .c .o .h # 定义自己的后缀

//以下关于隐含规则搜索算法内容还没仔细看过
隐含规则搜索算法
比如我们有一个目标叫 T。下面是搜索目标T的规则的算法。请注意,在下面,我们没有提到后缀规则,原因是,所有的后缀规则在Makefile被载入内存时,会被转换成模式规则。如果目标是"archive(member)"的函数库文件模式,那么这个算法会被运行两次,第一次是找目标T,如果没有找到的话,那么进入第二次,第二次会把"member"当作T来搜索。
1、把T的目录部分分离出来。叫D,而剩余部分叫N。(如:如果T是"src/foo.o",那么,D就是"src/",N就是"foo.o")
2、创建所有匹配于T或是N的模式规则列表。
3、如果在模式规则列表中有匹配所有文件的模式,如"%",那么从列表中移除其它的模式。
4、移除列表中没有命令的规则。
5、对于第一个在列表中的模式规则:
1)推导其"茎"S,S应该是T或是N匹配于模式中"%"非空的部分。
2)计算依赖文件。把依赖文件中的"%"都替换成"茎"S。如果目标模式中没有包含斜框字符,而把D加在第一个依赖文件的开头。
3)测试是否所有的依赖文件都存在或是理当存在。(如果有一个文件被定义成另外一个规则的目标文件,或者是一个显式规则的依赖文件,那么这个文件就叫"理当存在")
4)如果所有的依赖文件存在或是理当存在,或是就没有依赖文件。那么这条规则将被采用,退出该算法。
6、如果经过第5步,没有模式规则被找到,那么就做更进一步的搜索。对于存在于列表中的第一个模式规则:
1)如果规则是终止规则,那就忽略它,继续下一条模式规则。
2)计算依赖文件。(同第5步)
3)测试所有的依赖文件是否存在或是理当存在。
4)对于不存在的依赖文件,递归调用这个算法查找他是否可以被隐含规则找到。
5)如果所有的依赖文件存在或是理当存在,或是就根本没有依赖文件。那么这条规则被采用,退出该算法。
7、如果没有隐含规则可以使用,查看".DEFAULT"规则,如果有,采用,把".DEFAULT"的命令给T使用。
一旦规则被找到,就会执行其相当的命令,而此时,我们的自动化变量的值才会生成。

函数库文件也就是对Object文件(程序编译的中间文件)的打包文件。在Unix下,一般是由命令"ar"来完成打包工作。
一、函数库文件的成员
一个函数库文件由多个文件组成。你可以以如下格式指定函数库文件及其组成:
archive(member)
这个不是一个命令,而一个目标和依赖的定义。一般来说,这种用法基本上就是为了"ar"命令来服务的。如:
foolib(hack.o) : hack.o
ar cr foolib hack.o
如果要指定多个member,那就以空格分开,如:
foolib(hack.o kludge.o)
其等价于:
foolib(hack.o) foolib(kludge.o)
你还可以使用Shell的文件通配符来定义,如:
foolib(*.o)
二、函数库成员的隐含规则
当make搜索一个目标的隐含规则时,一个特殊的特性是,如果这个目标是"a(m)"形式的,其会把目标变成"(m)"。于是,如果我们的成员是"%.o"的模式定义,并且如果我们使用"make foo.a(bar.o)"的形式调用Makefile时,隐含规则会去找"bar.o"的规则,如果没有定义bar.o的规则,那么内建隐含规则生效,make会去找bar.c文件来生成bar.o,如果找得到的话,make执行的命令大致如下:
cc -c bar.c -o bar.o
ar r foo.a bar.o
rm -f bar.o
还有一个变量要注意的是"$%",这是专属函数库文件的自动化变量,有关其说明请参见"自动化变量"一节。
三、函数库文件的后缀规则
你可以使用"后缀规则"和"隐含规则"来生成函数库打包文件,如:
.c.a:
$(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $*.o
$(AR) r $@ $*.o
$(RM) $*.o
其等效于:
(%.o) : %.c
$(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $*.o
$(AR) r $@ $*.o
$(RM) $*.o

四、注意事项
在进行函数库打包文件生成时,请小心使用make的并行机制("-j"参数)。如果多个ar命令在同一时间运行在同一个函数库打包文件上,就很有可以损坏这个函数库文件。所以,在make未来的版本中,应该提供一种机制来避免并行操作发生在函数打包文件上。
但就目前而言,你还是应该不要尽量不要使用"-j"参数。

在Unix下的make,无论是哪种平台,几乎都使用了Richard Stallman开发的make和cc/gcc的编译器,而且,基本上都是GNU的make。我们不但可以利用make这个工具来编译我们的程序,还可以利用make来完成其它的工作,
因为规则中的命令可以是任何Shell之下的命令,所以,在Unix下,你不一定只是使用程序语言的编译器,你还可以在Makefile中书写其它的命令,如:tar、awk、mail、sed、cvs、compress、ls、rm、yacc、rpm、ftp……等等,
等等,来完成诸如"程序打包"、"程序备份"、"制作程序安装包"、"提交代码"、"使用程序模板"、"合并文件"等等五花八门的功能,文件操作,文件管理,编程开发设计,或是其它一些异想天开的东西。
==================================================================================================================================
linux内核架构包括7个部分:系统调用接口SCI、进程管理PM、虚拟文件系统VFS、内存管理MM、网络协议栈NetworkStack、体系结构相关代码Arch(和CPU相关代码)、设备驱动DD
虚拟文件系统为各种具体的文件系统,如Ext2、FAT、NFS、设备文件devpts文件操作提供统一的接口。linux内核源代码在www.kernel.org
arch目录下是各种CPU对应子目录,每个CPU子目录下又进一步分解为boot、mm、kernel等子目录,分别包含控制系统引导、内存管理、系统调用,这个也是移植时改动最多的。当然还有驱动driver目录
fs目录下的devpts目录也是一种文件系统的实现代码,它是linux的/dev/pts虚拟文件系统,fs目录下的其他公用代码则是用于实现虚拟文件系统VFS
include目录是内核所需要的头文件,include/linux子目录是与平台无关的头文件、与平台相关的头文件则放在相应的子目录中。
mm目录中的文件是用于实现内存管理中与体系结构无关的部分,与体系有关的是arch目录下对应CPU的mm目录下
net目录是网络协议栈的实现代码,其中包括802目录(802无线通讯协议核心支持代码)、appletalk目录(与苹果系统联网的协议)、ax25目录(AX25无线Internet协议)、bridge目录(桥接设备)、ipv4目录(IP协议族V4版本32位寻址模式)、ipv6目录(IP协议族V6版本)、其他还有bluetooth目录、can目录、ethernet目录、irda目录等等也在这里面
sound目录是音频设备的驱动程序,专门分出一个目录出来在根目录下
usr目录下是cpio命令的实现,利用cpio 可将文件或目录从文件库获取出来或将散列文件拷贝到文件库。
virt目录是内核虚拟机
-------------------------
make clean:移除除了config配置文件之外的其他生成文件
make mrproper:移除所有生成文件和配置文件(请正式彻底先生来处理)
make distclean:在make mrproper基础上把编辑器备份文件和补丁文件也删除了

make oldconfig:使用已有的配置文件(.config),但是会询问新增的配置选项
make menuconfig选择M表示编译成模块,只编译不链接,*表示不仅编译还要链接产生内核映像zImage

编译内核make zImage与make bzImage的区别:在X86平台上,zImage只能用于小于512K的内核。

编译的时候如何获取详细编译信息?make zImage V=1
编译好的内核在哪里?arch/<CPU>/boot/目录下
怎么编译内核模块?make modules
如何安装内核模块?make modules_install,将编译好的内核模块从内核源代码目录拷贝到/lib/modules目录下
内核提供的可参考的内核配置文件在哪里?arch/$CPU/configs下的各种xxx_defconfig文件
一般在哪里找到引导程序文件?etc/grub.conf或etc/lilo.conf
什么是内核模块机制?内核模块机制就是让内核的一些组件在需要被使用的时候才动态地添加到正在运行的内核中,而不是让内核文件zImage本身就包含某个组件,并不把模块本身编译进内核文件。在内核通过module_init()和module_exit()宏来实现动态加载和卸载,它和一般应用程序的区别是它没有main函数,打印的时候必须使用内核打印函数printk函数,直到卸载函数被调用,模块才从内核消失。

加载insmod(insmod hello.ko)
卸载rmmod(rmmod hello)
查看lsmod
加载modprobe(modprobe hello)它与insmod的区别是它会根据文件/lib/modules/<$version>/modules.dep来查看(探测)要加载的模块,看它是否还依赖于其他模块,如果是,modprobe会首先找到这些模块,把他们先加载到内核。

在模块驱动程序中通过宏module_param指定模块参数,它会在加载模块时传递给模块,module_param(name,type,perm),name是模块参数名称、type是这个参数的类型、perm是模块参数的访问权限
如static char *name = "David Xie";
static int age=30;
module_param(age, int, S_IRUGO);
module_param(name, charp, S_IRUGO);

在module_init(hello_init);时,hello_init()函数打印时候就可以调用这两个参数,printk(KERN_EMERG" Name:%s Age:%d \n",name,age);

所谓内核符号表就是在内核内部函数或变量中可供外部引用的函数和变量的符号表。在 2.6 内核下,使用以下命令可以看到内核符号表:
# cat /proc/kallsyms | more
c0100000 T _text
c0100000 T startup_32
c0100054 t default_entry
...
在内核符号表中,左边一列是符号地址,右边一列是函数和变量。在一个模块加载后,任何一个被模块导出的符号都会成为内核符号表的一部分。一般情况下,你设计的一个模块实现了自己所需的功能,这里也并不一定需要要所有的符号都导出来。但是,如果希望别人也能用到自己的模块,那就需要将其导出。使用以下宏可以导出符号到内核符号表:
EXPORT_SYMBOL(符号名);
EXPORT_SYMBOL_GPL(符号名);
使用 EXPORT_SYMBOL_GPL() 只用于包含 GPL 许可权的模块。
示例代码:
#include <linux/init.h>
#include <linux/module.h>

MODULE_LICENSE("Dual BSD/GPL");

int add_int(int a, int b)
{
    return (a + b);
}

int sub_int(int a, int b)
{
    return (a - b);
}

EXPORT_SYMBOL(add_int);
EXPORT_SYMBOL(sub_int);

编译后用 insmod 加载到内核中,然后执行以下命令:
# cat /proc/kallsyms |grep add_int
f9d70034 r __ksymtab_add_int    [modadd]
f9d7004c r __kstrtab_add_int    [modadd]
f9d70040 r __kcrctab_add_int    [modadd]
6ce40a9f a __crc_add_int    [modadd]
f9d70000 T add_int    [modadd]
(滤掉一些多余的输出)

当insmod hello.ko出现版本不匹配的错误时怎么办?1.使用modprobe --force-modversion hello.ko强行插入2.通过uname -r查看当前运行的内核版本,确保它与所依赖的内核代码版本一致。

-------------------------------
printk允许根据严重程度,通过附加不同的优先级来对消息分类。include/linux/kernel.h定义了8种记录级别。由高到低:
#define    KERN_EMERG    "<0>"    /* system is unusable            */
#define    KERN_ALERT    "<1>"    /* action must be taken immediately    */
#define    KERN_CRIT    "<2>"    /* critical conditions            */
#define    KERN_ERR    "<3>"    /* error conditions            */
#define    KERN_WARNING    "<4>"    /* warning conditions            */
#define    KERN_NOTICE    "<5>"    /* normal but significant condition    */
#define    KERN_INFO    "<6>"    /* informational            */
#define    KERN_DEBUG    "<7>"    /* debug-level messages            */
/* Use the default kernel loglevel */没有指定优先级的printk默认使用
#define KERN_DEFAULT    "<d>"

---------------------------
反汇编工具是把编译好的转换为汇编程序,如arm-linux-objdump,arm-linux-objdump -D -S hello
ELF文件查看工具:arm-linux-readelf,arm-linux-readelf -a hello
arm-linux-readelf -d hello 查看hello使用的动态库
----------------------------------------------------
一个同时装有bootloader、内核启动参数、内核映像和根文件系统映像的固态存储设备(如nand flash\spi flash)的典型空间分配结构为(从地址0x0000 0000开始):
bootloader、boot parameters、kernel、root filesystem

PC机一般是怎么加载操作系统的?PC机中的引导加载程序由BIOS(其本质是一段固件程序)和GRUB或LILO一起组成。BIOS在完成硬件检测和资源分配之后,将硬盘中的引导程序读到系统内存中然后将控制权交给引导程序。引导程序的主要内务就是将内核从硬盘上读到内存中,然后跳转到内核的入口点去运行,即启动操作系统。

嵌入式系统中,通常没有像BIOS那样的固件程序,因此整个系统的加载启动任务就完全由Bootloader来完成,这个程序一般都安排在地址0x0000 0000(也可能是CPU制造商预先安排好的地址,MIPS 处理器默认的程序入口是0xBFC00000,这个地址在硬件上已经确定为FLASH的位置)。通过bootloader这段小程序可以初始化硬件设备,从而将系统的软硬件环境带到一个合适的状态(设置好C语言的运行环境,比如堆栈),以便为最终调用操作系统做好准备。

Bootloader大多采用两个阶段:
stage 1:(1)初始化硬件(2)为stage2准备内存空间,复制stage 2到内存(3)设置堆栈,跳转到stage 2的C入口
stage 2:(1)初始化本阶段硬件(2)将内核和根文件系统从Flash读到内存(3)调用内核,跳到内核入口
拷贝到内存是因为在内存中运行比在flash中快。

uboot源码下载地址:ftp://ftp.denx.de/pub/u-boot
uboot目录的common目录实现uboot支持的命令
include目录是uboot使用的头文件,该目录下configs目录有与开发板相关的配置头文件,移植需要修改的文件之一
net目录是与网络协议栈相关的代码,如TFTP协议,RARP协议的实现
tools目录是生成uboot的工具,如mkimage,crc

uboot的makefile从功能上可以分为两个部分:
1.执行每种board相关的配置(如make mini2440_config或自行make menuconfig)
2.编译生成uboot.bin(如make CROSS_COMPILE=arm-linux-)

uboot提供了丰富的命令集,用help命令可以查看当前单板所支持的命令:bdinfo、bootm、date、echo、fatload、help、md、nand、nfs、ping、printenv、reset、saveenv、setenv、sleep、tftpboot、usb、usbboot、version

MT7620 # printenv
bootcmd=tftp
bootdelay=1
baudrate=57600
ethaddr="00:AA:BB:CC:DD:10"
filesize=3bc429
fileaddr=80A00000
ipaddr=10.10.10.123
serverip=10.10.10.3
autostart=no
bootfile=alan_uImage
stdin=serial
stdout=serial
stderr=serial

Environment size: 217/4092 bytes

mini2440通过设置环境参数tftp自启动的方法:
#setenv bootcmd tftp 31000000 uImage \; bootm 31000000
#saveenv
各项之间用\;隔开,且前后有空格。

在uboot目录下的board目录下新建一个目录,目录名和开发板名字一致,如mini2440或rt2880等等
--------------------------
创建一个新的文件系统,一般需要新建哪些目录:
bin dev etc lib proc sbin sys usr mnt tmp var
usr/bin usr/lib usr/sbin lib/modules

怎么创建设备文件
cd /dev
mknod -m 666 console c 5 1
mknod -m 666 null c 1 3

make menuconfig busybox的时候一般是选中“Build busybox as a static binary”,静态链接

内核文件uImage=Uboot header + zImage(Decompress code + Compressed vmlinux)
内核的启动大致分为三个阶段:解压缩、初始化、启动应用程序

MTD(memory technology device内存技术设备)是用于访问memory设备(ROM、flash)的Linux的子系统。MTD的主要目的是为了使新的memory设备的驱动更加简单,为此它在硬件和上层之间提供了一个抽象的接口。MTD的所有源代码在/drivers/mtd子目录下。CFI接口的MTD设备分为四层(从设备节点直到底层硬件驱动),这四层从上到下依次是:设备节点、MTD设备层、MTD原始设备层和硬件驱动层。设备节点:通过mknod在/dev子目录下建立MTD字符设备节点(主设备号为90)和MTD块设备节点(主设备号为31),通过访问此设备节点即可访问MTD字符设备和块设备。我们要对flash读取的时候就是要给它完成底层MTD原始设备(本例中的m25p40芯片)的加入(包括配置内核kconfig、makefile及davinci_spi_platform.c 改写),然后这个底层设备就会通过probe函数注册自己的mtd结构体,(struct mtd_file_info *mfi = file->private_data;struct mtd_info *mtd = mfi->mtd;)。然后中间的mtd设备层才能够调用这个底层设备的数据,诸如:mtdchar.c里的read、write调用。最终完成擦写具体flash的目的。

romfs是一个只读文件系统,主要用在 mainly for initial RAM disks of installation disks.使用romfs文件系统可以构造出一个最小的内核,并且很节省内存

linux内存管理子系统中的地址类型分为
1.物理地址
2.线性地址(虚拟地址):32位CPU架构可以表示4G的地址空间,0x0000 0000到0xffff fff
3.逻辑地址:程序代码经过编译后,出现在汇编程序中的地址

它们之间怎么转化?逻辑地址通过段式内存管理单元(Segmented Unit)转化为线性地址,线性地址再通过页式内存管理单元(Physical Address)转化为物理地址

段寄存器是为了对内存进行分段管理而增加的,16位CPU有四个段寄存器,程序可以同时访问四个不同含义的段:代码段、堆栈段、数据段、附加段

每一个32位的线性地址被划分为三部分:页目录索引(10位,这是进行地址转换的开始点)、页表索引(10位)、偏移(12位)

在linux中所提到的逻辑地址和线性地址(虚拟地址)可以认为是一致的,所以它主要靠分页机制。Linux 2.6.29内核采用了四级页管理架构:页全局目录、页上级目录、页中间目录、页表

虚拟内存管理技术有什么作用?(1)保护OS,用户无法看到实际的物理地址(2)使用户程序可使用比物理内存更大的地址空间(用户空间从0x0000 0000到0xbfff ffff共3G空间,内核空间从3G到4G)

用户怎么访问内核空间?用户进程通过系统调用访问内核空间。

进程申请和获得的是虚拟地址,当进程真的去访问虚拟地址时,才会由“请页机制”告诉内核去为进程分配物理页,并建立对应的页表,映射到物理地址上。

内核空间分布(3G到4G空间):
直接映射区(有896M)+8M+动态映射区(用内核函数vmalloc分配)+永久内存分配区(使用alloc_page或kmap函数来分配)+固定映射区+4K
----------------------------------------
内核链表在include/linux/list.h
---------------------------------
时钟中断由系统的定时硬件以周期性的时间间隔产生,这个间隔(即频率)由内核根据HZ来确定,HZ是一个与体系结构无关的常数,可配置(50-1200),在x86平台,默认值为1000.表示每秒钟产生1000次时钟中断。每当时钟中断发生时,全局变量jiffies(unsigned long)就加1,因此jiffies记录了自linux启动后时钟中断发生的次数。驱动程序常利用jiffies来计算不同事件的时间间隔。

延迟执行:如果对延迟的精度要求不高,最简单的实现方法如下--忙等待:
unsigned long j=jiffies+jit_delay*HZ;
while(jiffies<j)
{
    /* do nothing */
}
延时了jit_delay秒
-----------------
内核定时器注册的处理函数只执行一次,不是循环执行。内核定时器被组织成双向链表,并使用struct timer_list结构描述,在include/linux/Timer.h
struct timer_list {
    /*
     * All fields that change during normal runtime grouped to the
     * same cacheline
     */
    struct list_head entry;/*内核使用*/
    unsigned long expires;/*超时的jiffies*/
    struct tvec_base *base;/*内核使用*/

    void (*function)(unsigned long);/*超时处理函数*/
    unsigned long data;/*超时处理函数参数*/

    int slack;

#ifdef CONFIG_TIMER_STATS
    void *start_site;
    char start_comm[16];
    int start_pid;
#endif
#ifdef CONFIG_LOCKDEP
    struct lockdep_map lockdep_map;
#endif
};

初始化定时器队列结构
#define init_timer(timer)\
    init_timer_key((timer), NULL, NULL)

启动定时器
extern void add_timer(struct timer_list *timer);

在定时器超时前将它删除,超时后系统会自动删除。
extern int del_timer(struct timer_list * timer);
----------------------------------------------------
进程4要素:
1.有一段程序供其执行。这段程序不一定是某个进程专有,可以与其他进程共用
2.有进程专用的内核空间堆栈
3.在内核中有一个task_struct数据结构(include/linux/sched.h),即通常所说的“进程控制块”(PCB process control block)。有了这个数据结构,进程才能成为内核调度的一个基本单位接受内核的调度。进程是资源分配的最小单位,线程是调度的最小单位。
4.有独立的用户空间

struct task_struct结构里有一个成员struct sched_rt_entity rt;
rt->time_slice时间片,内核将int static_prio为100-139的优先级映射到200ms-10ms的时间片上去,优先级越大,则分配的时间片越小。

进程的结束都要借助对内核函数do_exit的调用。

linux进程调度是指从就绪的进程中选出最适合的一个来执行。

调度策略include/linux/sched.h

/*
 * Scheduling policies
 */
#define SCHED_NORMAL        0//普通的分时进程
#define SCHED_FIFO        1//先入先出的实时进程
#define SCHED_RR        2//时间片轮转的实时进程
#define SCHED_BATCH        3//批处理进程
/* SCHED_ISO: reserved but not implemented yet */
#define SCHED_IDLE        5//只在系统空闲时才能够被调度执行的进程

什么是调度类?调度类的引入增强了内核调度程序的可拓展性,这些类封装了调度策略,并将调度策略模块化,所以这些类其实可以理解为调度程序模块。

全公平调度器CFS(completely fair schedule)的设计思想是:在一个真实的硬件上模型化一个理想的、精确的多任务CPU。该理想CPU模型运行在100%的负荷、在精确 平等速度下并行运行每个任务,每个任务运行在1/n速度下,即理想CPU有n个任务运行,每个任务的速度为CPU整个负荷的1/n。由于真实硬件上,每次只能运行一个任务,这就得引入"虚拟运行时间"(virtual runtime)的概念,虚拟运行时间为一个任务在理想CPU模型上执行的下一个时间片(timeslice)。它从RSDL/SD中吸取了完全公平的思想,不再跟踪进程的睡眠时间,也不再企图区分交互式进程。它将所有的进程都统一对待,这就是公平的含义。 CFS 背后的主要想法是维护为任务提供处理器时间方面的平衡(公平性)。CFS为了体现的公平表现在2个方面
(1)进程的运行时间相等(2)睡眠的进程进行补偿

CFS调度类(在kernel/sched_fair.c中实现)用于以下调度策略:SCHED_NORMAL、SCHED_BATCH、SCHED_IDLE
实时调度类(在kernel/sched_rt.c中实现)用于SCHED_FIFO、SCHED_RR

kernel/sched.c
/*
 * schedule() is the main scheduler function.
 */
asmlinkage void __sched schedule(void){}
该函数被调用就是表示调度的发生了!它的调度步骤是:
1).清理当前运行中的进程
2).选择下一个要运行的进程;(pick_next_task)
3).设置新进程的运行环境
4).进程上下文切换

调度分为主动式和被动式
1.主动式:主动放弃CPU,让出CPU
    current->state = TASK_INTERRUPTIBLE;
    schedule();
2.被动式:
用户抢占(linux2.4 linux2.6):发生在从系统调用返回用户空间、从中断处理程序返回用户空间的时候
内核抢占(linux2.6):更高优先级的进程/线程可以抢占正在内核空间运行的低优先级进程/线程。当然有些特殊情况下是不允许内核抢占的:
(1)内核正在进行中断处理
(2)内核正在进行中断上下文的Bottom half(中断的底半部)处理。硬件中断返回前会执行软中断,此时依然处于中断上下文中。
(3)进程正在持有spinlock自旋锁、wirtelock、readlock读写锁等,当持有这些锁的时,不应该被抢占,否则由于抢占导致其他CPU长期不能获得锁而死等
(4)内核正在执行调度程序schedule。抢占的原因就是为了执行新的调度,没有理由将调度程序抢占掉再运行调度程序。

进程的thread_info结构中有一个叫做内核抢占计数的变量preempt_count有什么作用?
它是为了保证linux内核在以上情况下不会被抢占,当内核要进入以上几种状态时,preempt_count就会加1,指示内核不允许抢占。每当内核从以上几种状态退出的时候,preempt_count就减1,同时进行可抢占的判断与调度。比如中断处理完成,解锁,使能软中断等。

调度标志:TIF_NEED_RESCHED
作用:内核提供了一个need_resched标志来表明是否需要重新执行一次调度。
设置:当某个进程耗尽它的时间片会设置这个标识;当一个优先级更高的进程进入可执行状态的时候,也会设置这个标志。
--------------------------------------------------------
在linux2.6.36.x版内核中,共有系统调用369个,可以在arch/arm/include/asm/unistd.h中找到他们。应用程序首先用适当的值填充寄存器,然后调用一个特殊的指令跳转到内核某一个固定位置,内核根据应用程序所填充的固定值来找到相应的函数执行。
1.适当的值:在文件include/asm/unistd.h中为每个系统调用规定了唯一的编号,这个号称为系统调用号
/*
 * This file contains the system call numbers.
 */

#define __NR_restart_syscall        (__NR_SYSCALL_BASE+  0)
#define __NR_exit            (__NR_SYSCALL_BASE+  1)
#define __NR_fork            (__NR_SYSCALL_BASE+  2)
#define __NR_read            (__NR_SYSCALL_BASE+  3)
#define __NR_write            (__NR_SYSCALL_BASE+  4)
#define __NR_open            (__NR_SYSCALL_BASE+  5)
#define __NR_close            (__NR_SYSCALL_BASE+  6)
2.特殊的指令
在intel CPU中,这个中断由0x80实现
在ARM中,这个指令是SWI(已经重新命名为SVC指令)
3.固定的位置
在ARM体系中,应用程序跳转到的固定内核位置是ENTRY(vector_swi)<arch/arm/kernel/entry-common.S>
4.相应的函数
内核根据应用程序传递来的系统调用号,从系统调用表sys_call_table找到相应的内核函数
/arch/arm/kernel/calls.S
/* 0 */        CALL(sys_restart_syscall)
        CALL(sys_exit)
        CALL(sys_fork_wrapper)
        CALL(sys_read)
        CALL(sys_write)
/* 5 */        CALL(sys_open)
        CALL(sys_close)
        CALL(sys_ni_syscall)        /* was sys_waitpid */
        CALL(sys_creat)
        CALL(sys_link)
/* 10 */    CALL(sys_unlink)

如何自己实现系统调用?向内核添加新的系统调用,需要执行3个步骤:
1.添加新的内核函数
2.更新头文件unistd.h
3.针对这个新函数更新系统调用表calls.S
实例演示:
1.在arch/arm/kernel/sys.c(linux2.6.36.x版本是sys_arm.c)添加函数
asmlinkage int sys_add(int a,int b)
{
    return a+b;
}
/*asmlinkage 使用栈传递参数*/
2.在arch/arm/include/asm/unistd.h中添加如下代码:
#define __NR_add            (__NR_SYSCALL_BASE+370)//linux2.6.36.x
3.在arch/arm/kernel/calls.S中添加代码,指向新实现的系统调用函数:
/* 370 */    CALL(sys_add)
应用程序:
#include <stdio.h>
#include <linux/unistd.h>
main()
{
    int result;
    result = syscall(370,1,2);
    printf("result=%d\n",result);        
}
--------------------------------------
proc文件系统:
实例:通过/proc/meminfo,查询当前内存使用情况。
结论:proc文件系统是一种在用户态检查内核状态的机制
查看我们路由器MT7620的proc文件:
# ls proc/
6080          25            2             mtd           cpuinfo
1555          24            1             execdomains   devices
1550          14            self          ioports       interrupts
1547          13            mounts        iomem         loadavg
1259          12            net           timer_list    meminfo
938           11            sysvipc       modules       stat
500           10            fs            buddyinfo     uptime
499           9             driver        pagetypeinfo  version
200           8             tty           vmstat        softirqs
112           7             bus           zoneinfo      kmsg
29            6             sys           vmallocinfo   crypto
28            5             irq           slabinfo      diskstats
27            4             misc          filesystems   partitions
26            3             scsi          cmdline       mt7620
常见的有:
子目录/文件名            内容描述
apm            高级电源管理信息
bus            总线以及总线上的设备
devices            可用的设备信息
driver            已经启用的驱动程序
interrupts        中断信息
ioports            端口使用信息
version            内核版本

# cat /proc/version 
Linux version 2.6.36 (alan@alan-ThinkPad-T420) (gcc version 3.4.2) #167 Sun May 24 13:26:50 CST 2015    

# cat /proc/devices 查看内核目前支持的设备,可看到主设备号和设备名字
Character devices:
  1 mem
  2 pty
  3 ttyp
  4 ttyS
  5 /dev/tty
  5 /dev/console
  5 /dev/ptmx
 10 misc
 21 sg
 90 mtd
128 ptm
136 pts
180 usb
189 usb_device
217 spiS0
251 nvram
252 gpio
253 rdm0
254 bsg

Block devices:
  1 ramdisk
259 blkext
  8 sd
 31 mtdblock
 65 sd
 66 sd
 67 sd
 68 sd
 69 sd
 70 sd
 71 sd
128 sd
129 sd
130 sd
131 sd
132 sd
133 sd
134 sd
135 sd
179 mmc

# cat /proc/interrupts 
           CPU0       
  3:       5360          Ralink  eth2
  4:    1528328          Ralink  ra0
  5:   13008345          Ralink  timer
  6:          0          Ralink  ralink_gpio
  7:          0          Ralink  Ralink_DMA
 12:       2130          Ralink  serial
 17:          0          Ralink  Ralink_ESW
 18:          1          Ralink  ehci_hcd:usb1, ohci_hcd:usb2
 33:          0          Ralink  rt2880_timer0
 34:          0          Ralink  rt2880_timer1

ERR:          0

它的特点是
1).每个文件都规定了严格的权限
2).可以用文本编辑程序读取(more命令,cat命令,vi程序等等)
3).不仅可以有文件,还可以有子目录
4).可以自己编写程序添加一个/proc目录下的文件
5).文件的内容都是动态创建的,并不存在于磁盘上。

内核描述:include/linux/proc_fs.h
struct proc_dir_entry {
    unsigned int low_ino;
    unsigned short namelen;
    const char *name;
    mode_t mode;
    nlink_t nlink;
    uid_t uid;
    gid_t gid;
    loff_t size;
    const struct inode_operations *proc_iops;
    /*
     * NULL ->proc_fops means "PDE is going away RSN" or
     * "PDE is just created". In either case, e.g. ->read_proc won't be
     * called because it's too late or too early, respectively.
     *
     * If you're allocating ->proc_fops dynamically, save a pointer
     * somewhere.
     */
    const struct file_operations *proc_fops;
    struct proc_dir_entry *next, *parent, *subdir;
    void *data;
    read_proc_t *read_proc;
    write_proc_t *write_proc;
    atomic_t count;        /* use count */
    int pde_users;    /* number of callers into module in progress */
    spinlock_t pde_unload_lock; /* proc_fops checks and pde_users bumps */
    struct completion *pde_unload_completion;
    struct list_head pde_openers;    /* who did ->open, but not ->release */
};

实现一个proc文件的流程
(1)调用create_proc_entry创建一个struct proc_dir_entry
(2)对创建的struct proc_dir_entry进行赋值:read_proc,write_proc(为了能让用户读写添加proc文件,需要挂接读写回调函数),mode,owner,size

------------------------------------------------
Linux内核异常,内核级的程序,总有死机的时候,Oops(惊讶的感叹词)可以看成是内核级的Segmentation Fault。应用程序如果进行了非法内存访问或执行了非法指令,会得到Segfault信号,一般的行为是coredump,应用程序也可以自己截获Segfault信号,自行处理。如果内核自己犯了这样的错误,则会打出Oops信息。像空指针、非法指针都会产生Oops非法指针:小于0xc000 0000(3G)的地址,在内核中访问3G以下的地址都会产生异常。
分析步骤:1.错误原因提示2.调用栈(对照反汇编代码,利用堆栈信息找到调用关系)3.寄存器(比如看PC指针)
------------
块设备:访问的最小数据量是一个块,块的大小是512字节或512字节的整数倍,这是unix的要求,而Linux则允许块设备传送任意数目的字节。字符设备可以随机地访问,而块设备不能随机地访问。字符设备和块设备都会有设备文件,而网络接口设备没有设备文件。设备文件放在/dev目录下。
任何网络事务都通过一个接口(网络接口)来进行,一个接口通常是一个硬件设备(eth0),但是它也可以是一个纯粹的软件设备,比如回环接口(lo,自己发自己收)。一个网络接口负责发送和接收数据报文。

字符设备文件和字符设备驱动通过设备号建立联系。设备号作用:主设备号用来标识与设备文件相连的驱动程序,次编号被驱动程序用来辨别操作的是哪个设备。主设备号用来反映设备类型,而次设备号用来区分同类型的设备。

主次设备号:字符设备通过字符设备文件来存取。通过主设备号来找设备文件和连接驱动程序。字符设备文件由使用ls -l的输出的第一列的“c”标识。如果使用ls -l命令,会看到在设备文件项中有2个数(由一个逗号分隔)这些数字就是设备文件的主次设备号。查看/dev,ls -l /dev。次设备号区分同一类设备的具体哪一个设备。
# ls -l /dev/
drwxr-xr-x    2 0        0               0 pts
crw-r--r--    1 0        0         81,   0 video0
crw-r--r--    1 0        0        108,   0 ppp
crw-r--r--    1 0        0        217,   0 spiS0
crw-r--r--    1 0        0        218,   0 i2cM0
crw-r--r--    1 0        0        219,   0 mt6605
crw-r--r--    1 0        0        200,   0 flash0
crw-r--r--    1 0        0        210,   0 swnat0
crw-r--r--    1 0        0        220,   0 hwnat0
crw-r--r--    1 0        0        230,   0 acl0
crw-r--r--    1 0        0        240,   0 ac0
crw-r--r--    1 0        0        250,   0 mtr0
crw-r--r--    1 0        0        251,   0 nvram
crw-r--r--    1 0        0        252,   0 gpio
crw-r--r--    1 0        0        253,   0 rdm0
crw-r--r--    1 0        0        233,   0 pcm0
crw-r--r--    1 0        0        234,   0 i2s0
crw-r--r--    1 0        0        235,   0 cls0
crw-r--r--    1 0        0        236,   0 spdif0
crw-r--r--    1 0        0        245,   0 vdsp
crw-r--r--    1 0        0        225,   0 slic
brw-rw----    1 0        0         90,   8 device
crw-rw----    1 0        0          2,   0 ptyp0
crw-rw----    1 0        0          2,   1 ptyp1
crw-rw----    1 0        0          2,   2 ptyp2
crw-rw----    1 0        0          2,   3 ptyp3
crw-rw----    1 0        0          2,   4 ptyp4
crw-rw----    1 0        0          2,   5 ptyp5

内核中如何描述设备号?
dev_t其实质为unsigned int 32位整数,其中高12位为主设备号,低20位为次设备号
如何从dev_t中分解出主设备号?
MAJOR(dev_t dev)
如何从dev_t中分离出次设备号?
MINOR(dev_t dev)

分配主设备号采用(1)静态申请、(2)动态分配的方法
静态申请
1.根据Documentation/devices.txt,确定一个没有使用的主设备号
2.使用register_chrdev_region函数注册设备号
优点是简单,缺点是一旦驱动被广泛使用,这个随机选定的主设备号可能会导致设备号冲突,而使驱动程序无法注册。
fs/char_dev.c

/**
 * register_chrdev_region() - register a range of device numbers
 * @from: the first in the desired range of device numbers; must include
 *        the major number.希望申请使用的设备号
 * @count: the number of consecutive device numbers required希望申请使用的设备号数目
 * @name: the name of the device or driver.设备名,体现在/proc/devices
 *
 * Return value is zero on success, a negative error code on failure.
 */
int register_chrdev_region(dev_t from, unsigned count, const char *name)
{
...
}
功能:申请使用从from开始的count个设备号(主设备号不变,次设备号增减)

动态分配:使用alloc_chrdev_region分配设备号,缺点是无法在安装驱动前创建设备文件,因为安装前还没有分配到主设备号,解决方法是安装驱动后,从/proc/devices中查询设备号

/**
 * alloc_chrdev_region() - register a range of char device numbers
 * @dev: output parameter for first assigned number 分配到的设备号
 * @baseminor: first of the requested range of minor numbers 起始次设备号
 * @count: the number of minor numbers required 需要分配的设备号数目
 * @name: the name of the associated device or driver 设备名(体现在/proc/devices)
 *
 * Allocates a range of char device numbers.  The major number will be
 * chosen dynamically, and returned (along with the first minor number)
 * in @dev.  Returns zero or a negative error code.
 */
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
            const char *name)
{
...
}
功能:请求内核动态分配count个设备号,且次设备号从baseminor开始

什么是注销号?
不论使用哪种方法分配设备号,都应该在不再使用它们时释放这些设备号。
void unregister_chrdev_region(dev_t from, unsigned count)释放从from开始的count个设备号

怎么创建设备文件?
1.使用mknod命令手工创建2.自动创建
1.手工创建,mknod filename type major minor
filename:设备文件名
type:设备文件类型
major:主设备号(1-255选择空缺的)
minor:次设备号
如:mknod serial0 c 100 0
mknod /dev/xxx c 111 0 
--------------------------------
Linux字符设备驱动程序设计中有三个重要的数据结构
struct file 由内核在打开一个文件时创建,在文件关闭时释放。
struct inode 用来记录文件的物理上的信息,一个文件可以对应多个file结构,但是只有一个inode结构
struct file_operations 一个函数指针的集合,定义能在设备上进行的操作。结构中的成员指向驱动中的函数,这些函数实现一个特别的操作,对于不支持的操作保留位NULL。

struct file
{
    重要成员:
    loff_t            f_pos;/*文件读写位置*/
    const struct file_operations    *f_op;
}

struct inode {
    重要成员:
    dev_t            i_rdev;    /*设备号*/
}

struct file_operations { 
    struct module *owner;
    loff_t (*llseek) (struct file *, loff_t, int);//修改文件的当前读写位置,并将新位置作为返回值
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); //从设备中读取数据
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); //向设备发送数据
    ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);     
    ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);     
    int (*readdir) (struct file *, void *, filldir_t);
    unsigned int (*poll) (struct file *, struct poll_table_struct *);//此函数在系统调用select内部被使用,作用是把当前的文件指针挂到设备内部定义的等待队列中。
    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); 
    long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
    int (*mmap) (struct file *, struct vm_area_struct *);//将设备映射到进程虚拟地址空间中
    int (*open) (struct inode *, struct file *);//在设备文件上的第一个操作,并不要求驱动程序一定要实现这个方法。如果该项为NULL,设备的打开操作永远成功。初始化设备、表明次设备号
    int (*flush) (struct file *);
    int (*release) (struct inode *, struct file *);//当设备文件被关闭时调用这个操作。与open相仿,release也可以没有。有时也成为close,关闭设备。
    int (*fsync) (struct file *, int datasync);
    int (*aio_fsync) (struct kiocb *, int datasync);
    int (*fasync) (int, struct file *, int);
    int (*lock) (struct file *, int, struct file_lock *);
    ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);     
    ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *);     
    ssize_t (*sendfile) (struct file *, loff_t *, size_t, read_actor_t, void *); 
    ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);     
    unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long,    unsigned long, unsigned long); 
    int (*check_flags)(int);
    int (*flock) (struct file *, int, struct file_lock *);
    ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, size_t, unsigned int); 
    ssize_t (*splice_read)(struct file *, struct pipe_inode_info *, size_t, unsigned int); };
read 和 write方法的buff参数是用户空间指针。因此,它不能被内核代码直接引用,理由是用户空间指针在内核空间时可能根本是无效的,没有那个地址的映射。因此,内核提供了专门的函数用于访问用户空间的指针,例如:arch/arm/include/asm/Uaccess.h
static inline unsigned long __must_check copy_from_user(void *to, const void __user *from, unsigned long n)
static inline unsigned long __must_check copy_to_user(void __user *to, const void *from, unsigned long n)

-------------------
linux2.6内核中,字符设备使用struct cdev来描述。字符设备的注册可分为三个步骤:1.分配cdev 2.初始化cdev 3.添加cdev 
1.分配可用cdev_alloc函数来完成。原型为fs/char_dev.c里的
/**
 * cdev_alloc() - allocate a cdev structure
 *
 * Allocates and returns a cdev structure, or NULL on failure.
 */
struct cdev *cdev_alloc(void)
{
    ...
}
2.初始化使用cdev_init函数来完成。
/**
 * cdev_init() - initialize a cdev structure
 * @cdev: the structure to initialize 待初始化的cdev结构
 * @fops: the file_operations for this device 设备对应的操作函数集
 *
 * Initializes @cdev, remembering @fops, making it ready to add to the
 * system with cdev_add().
 */
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
     ...
}
3.添加使用cdev_add函数来完成
/**
 * cdev_add() - add a char device to the system
 * @p: the cdev structure for the device待添加到内核的字符设备结构
 * @dev: the first device number for which this device is responsible设备号
 * @count: the number of consecutive minor numbers corresponding to this
 *         device 添加的设备个数
 *
 * cdev_add() adds the device represented by @p to the system, making it
 * live immediately.  A negative error code is returned on failure.
 */
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
    p->dev = dev;
    p->count = count;
    return kobj_map(cdev_map, dev, count, NULL, exact_match, exact_lock, p);
}
另外:字符设备的注销使用cdev_del函数来完成
/**
 * cdev_del() - remove a cdev from the system
 * @p: the cdev structure to be removed
 *
 * cdev_del() removes @p from the system, possibly freeing the structure
 * itself.
 */
void cdev_del(struct cdev *p)
--------------------------
驱动调试技术:1).打印调试(printk,正式发布驱动应当去掉,所以最好用宏全局地打开或关闭printk)、调试器调试(kgdb)、查询调试(通过proc文件系统查询内核状态)
------------------
并发与竞争
什么是并发?多个执行单元同时被执行
什么是竞态?并发的执行单元对共享资源(硬件资源和软件上的全局变量等)的访问导致的竞争状态
怎么处理并发?常用技术是加锁或者互斥,即确保在任何时间只有一个执行单元可以操作共享资源。在Linux内核中主要通过semaphore机制和spin_lock机制实现。

Linux内核的信号量在概念和原理上与用户态的信号量是一样的,但是它不能在内核之外使用,它是一种睡眠锁。如果有一个任务想要获得已经被占用的信号量时,信号量会将这个任务进程放入到一个等待队列,然后让其睡眠。当持有信号量的进程将信号释放后,处于等待队列中的任务将被唤醒,并让其获得信号量。

include/linux/semaphore.h的相关结构与操作
/* Please don't access any members of this structure directly */
struct semaphore {
    spinlock_t        lock;
    unsigned int        count;
    struct list_head    wait_list;
};

static inline void sema_init(struct semaphore *sem, int val)
该函数用于初始化设置信号量的初值

#define init_MUTEX(sem)        sema_init(sem, 1)//用于初始化一个互斥锁,即它把信号量sem的值设置为1
#define init_MUTEX_LOCKED(sem)    sema_init(sem, 0)//用于初始化一个互斥锁,但它把信号量sem的值设置为0,即一开始就处在已经锁住的状态


extern void down(struct semaphore *sem);//获取信号量sem,可能会导致进程睡眠,因此不能在中断上下文使用该函数,该函数是将sem的值减去1,如果信号量sem的值为非负,就直接返回,否则调用者将被挂起,直到别的任务释放该信号量才能继续运行。down已经不建议使用
extern int __must_check down_interruptible(struct semaphore *sem);//获取信号量sem,如果信号量不可用,进程将被置为TASK_INTERRUPTIBLE类型的睡眠状态。该函数由返回值来区分是否正常返回还是被信号中断返回,如果返回0,表示获得信号量正常返回,如果被信号打断,返回-EINTR
extern int __must_check down_killable(struct semaphore *sem);//获取信号量sem,如果信号量不可用,进程将被置为TASK_KILLABLE类型的睡眠状态
extern int __must_check down_trylock(struct semaphore *sem);
extern int __must_check down_timeout(struct semaphore *sem, long jiffies);
extern void up(struct semaphore *sem);//释放信号量,释放信号量通过把信号量的值sem加1实现。如果sem的值为非正数,表明有任务等待该信号量,因此唤醒这些等待者。

自旋锁:
自旋锁最多只能被一个可执行单元持有。自旋锁不会引起调用者睡眠,如果一个执行线程试图获得一个已经被持有的自旋锁,那么线程就会一直进行忙循环,一直等待下去,在那里看是否该自旋锁的保持者已经释放了锁,自旋就是这个意思。
include/linux/spinlock.h
/* 该宏用于初始化自旋锁x,自旋锁在使用前必须先初始化 */
#define spin_lock_init(_lock)                \
do {                            \
    spinlock_check(_lock);                \
    raw_spin_lock_init(&(_lock)->rlock);        \
} while (0)

/* 获取自旋锁lock,如果成功,立即获得锁,并马上返回,否则它将一直自旋在那里,直到该自旋锁的保持者释放 */
static inline void spin_lock(spinlock_t *lock)
{
    raw_spin_lock(&lock->rlock);
}

/* 试图获取自旋锁lock,如果能立即获得锁并返回真,否则立即返回假,它不会一直等待被释放 */
static inline int spin_trylock(spinlock_t *lock)
{
    return raw_spin_trylock(&lock->rlock);
}

/* 释放自旋锁lock,它与spin_trylock或spin_lock配对使用 */
static inline void spin_unlock(spinlock_t *lock)
{
    raw_spin_unlock(&lock->rlock);
}

信号量PK自旋锁
信号量可能允许有多个持有者,而自旋锁在任何时候只能允许一个持有者。当然也有信号量叫互斥信号量(只能一个持有者),允许有多个持有者的信号量叫计数信号量。

信号量适合于保持时间较长的情况;而自旋锁适合于保持时间非常短的情况,在实际应用中自旋锁控制的代码只有几行,而持有自旋锁的时间也一般不会超过两次上下文切换的时间,因为线程一旦要进行切换,就至少花费切出切入两次,自旋锁的占用时间如果远远长于两次上下文切换,我们就应该选择信号量。
------------------------------------------------------------------------------------
ioctl设备控制
除了读写串口还得设置波特率,就是对硬件设备的控制功能。
用户空间的ioctl原型:
int ioctl(int fd,unsigned long cmd,...)
...表示一个可选参数,是否存在取决于cmd参数是否涉及与设备的数据交互,就是看这个命令需不需要带参数,设置波特率肯定要带参数,关机命令就不需要带参数

驱动的ioctl原型
int (*ioctl)(struct inode *inode,struct file *filp,unsigned int cmd,unsignded long arg)
inode物理信息
filp和打开的文件相关的信息,如读写位置
cmd命令,从用户空间的ioctl传下来的
arg参数,如果cmd命令不涉及数据传输,则arg参数无意义

实现ioctl设备方法:
file_operations 里的函数就是设备方法。
1.定义命令(防止对错误设备使用正确的命令,命令号在系统范围内是唯一的)
ioctl命令编码/命令号就像电话号码一样是分段的:类型(幻数),序号,传送方向,参数的大小(查看include/asm/ioctl.h)
#define _IOC_DIR(nr)        (((nr) >> _IOC_DIRSHIFT) & _IOC_DIRMASK)
#define _IOC_TYPE(nr)        (((nr) >> _IOC_TYPESHIFT) & _IOC_TYPEMASK)
#define _IOC_NR(nr)        (((nr) >> _IOC_NRSHIFT) & _IOC_NRMASK)
#define _IOC_SIZE(nr)        (((nr) >> _IOC_SIZESHIFT) & _IOC_SIZEMASK)

Documentation/ioctl-number.txt文件罗列了在内核中已经使用了的幻数
0x00    00-1F    linux/fs.h        conflict!
0x00    00-1F    scsi/scsi_ioctl.h    conflict!
0x00    00-1F    linux/fb.h        conflict!
0x00    00-1F    linux/wavefront.h    conflict!
0x02    all    linux/fd.h
0x03    all    linux/hdreg.h
0x04    all    linux/umsdos_fs.h
0x06    all    linux/lp.h
0x09    all    linux/md.h
0x12    all    linux/fs.h
'#'    00-3F    IEEE 1394 Subsystem    Block for the entire subsystem
'1'    00-1F    <linux/timepps.h>    PPS kit from Ulrich Windl
                    <ftp://ftp.de.kernel.org/pub/linux/daemons/ntp/PPS/>
'8'    all                SNP8023 advanced NIC card
                    <mailto:mcr@solidum.com>
'A'    00-1F    linux/apm_bios.h
'B'    C0-FF                advanced bbus
                    <mailto:maassen@uni-freiburg.de>
'C'    all    linux/soundcard.h
'D'    all    asm-s390/dasd.h

Type幻数(类型)表明哪个设备的命令,8位宽
Number序号,表明在设备命令中的第几个,8位宽
Direction数据传送的方向,_IOC_NONE(没有数据传输)、_IOC_READ意思是应用程序从设备读
Size用户数据大小,13/14位宽,视处理器而定

内核用宏来定义命令
_IO(type,nr)没有参数的命令
_IOR(type,nr,datatype)从驱动中读数据
_IOW(type,nr,datatype)写数据到驱动
_IOWR(type,nr,datatype)双向传送

#define MEM_IOC_MAGIC 'm'//定义幻数,不能重复

#define MEM_IOCSET _IOW(MEM_IOC_MAGIC,0,int)
#define MEM_IOCGQSET _IOR(MEM_IOC_MAGIC,1,int)
2.实现命令
ioctl函数实现通常用switch,如果命令不支持返回-EINVAL
ioctl参数如果是整数可以直接使用,如果是指针,使用前需要检查,确保用户地址是有效的

不需要参数检测,函数已经有了检测:
copy_from_user
copy_to_user
get_user
put_user

需要检测:
__get_user
__put_user

怎么检测?用int access_ok(int type,const void *addr,unsigned long size)
type:VERIFY_READ或VERIFY_WRITE,用来表明是读用户内存还是写用户内存
addr:要操作的用户内存地址
size: 操作长度
返回1是成功,0是失败,存取有问题,则ioctl返回—EFAULT
ioctl函数怎么实现参数检测
if(_IOC_DIR(cmd)&_IOC_READ)
    err=!access_ok(VERIFY_WRITE,(void __user *)arg,_IOC_SIZE(cmd));
//why _IOC_READ 对应 VERIFY_WRITE???
_IOC_READ是从设备,从驱动去读用户空间,所以要检测的是写用户内存
用户空间的arg可能是个指针,但是内核空间的ioctl的arg是个unsigned long ,所以这里要强制转换回来(void __user *)arg

接下来实现switch(cmd)函数
int ioarg;
__get_user(ioarg,(int *)arg);
__put_user(ioarg,(int *)arg);
来与用户空间交互数据
------------------------------------------------
内核等待队列
用来实现进程的阻塞,可以看做是保存进程的容器,在阻塞进程时,将进程放入等待队列,当唤醒进程时,
从等待队列中取出进程。
2.6内核提供等待队列操作
1.定义等待队列
wait_queue_head_t my_queue
2.初始化等待队列
init_waitqueue_head(&my_queue)
3.定义并初始化等待队列
DECLARE_WAIT_QUEUE_HEAD(my_queue)
4.有条件睡眠
wait_event(queue,condition)
当condition为真立即返回,如果为假进入TASK_UNINTERRUPTIBLE模式的睡眠,并挂在queue指定的等待队列上

wait_event_interruptible(queue,condition)
可以进入TASK_INTERRUPTIBLE的睡眠

int wait_event_killable(queue,condition)
可以进入TASK_KILLABLE的睡眠
5.无条件睡眠(老版本,不建议使用)
sleep_on(wait_queue_head_t *q)
让进程进入不可中断的睡眠,并放入等待队列

interruptible_sleep_on(wait_queue_head_t *q)
让进程进入可中断的睡眠,并把它放入等待队列

6.从等待队列中唤醒进程
wake_up(wait_queue_t *q)
唤醒q中所有进程:TASK_UNINTERRUPTIBLE、TASK_INTERRUPTIBLE、TASK_KILLABLE

wake_up_interruptible(wait_queue_t *q)
唤醒q中唤醒状态为TASK_INTERRUPTIBLE的进程

-----------------------------------------------
阻塞型字符设备驱动
当一个设备无法满足用户的读写请求时应当如何处理?
驱动应该阻塞进程,使他进入睡眠,直到请求可以得到满足

文件读写默认是用阻塞方式,如果用非阻塞方式可以通过O_NONBLOCK非阻塞方式打开文件,进行读写操作,如果读写没有就绪,系统只会返回-EAGAIN,不会阻塞

驱动里:mem_read()函数,使用阻塞方式读
while(!have_data)//如果没有数据可读,因为是interruptible型的阻塞,所以有可能是中断或信号唤醒,所以可能还是没有数据,所以还得用while再回过头来检测,而不能用if
{
    if(file->f_flags & O_NONBLOCK)
        return -EAGAIN;
    wait_event_interruptible(dev->inq,have_data);//使得调用这个驱动的应用阻塞
}

因为没有数据,所以阻塞,那么什么时候唤醒,那必然是有数据,写完了数据后唤醒
mem_write()函数
//从用户空间写入数据
have_data = true;//有新的数据可读
//唤醒读进程
wake_up(&dev->inq);//这时候任何进程都可以来读

测试程序:1.能不能阻塞,2.能不能唤醒
两个程序,两个进程,一个读进程,一个写进程,
先运行读进程,让其阻塞,然后运行写进程,让其唤醒。
读进程fread(Buf,sizeof(Buf),1,fp);到这里阻塞了
直到写进程fwrite(Buf,sizeof(Buf),1,fp);往设备写入数据,读进程才被唤醒,fread();后面的程序才会被执行
----------------------------------------------------------
Poll设备操作
系统调用(用户空间)    驱动(内核空间)
Open            Open
Close            Release
Read            Read
Write            Write
Ioctl            Ioctl
lseek            llseek
Select            Poll

Select系统调用用于多路监控,当没有一个文件满足要求(文件是否可读或可写)时,select将阻塞调用进程。
int select(int maxfd,fd_set *readfds,fd_set *writefds,fe_set *exceptfds,
const struct timeval *timeout)
maxfd:文件描述符的范围,比待检测的最大文件描述符大1
readfds:被读监控的文件描述集合
writefds:被写监控的文件描述集合
exceptfds:被异常监控的文件描述符集合
timeout:定时器,
timeout取值0表示不管有没有文件满足要求都立即返回,没有文件满足返回0,有的话返回一个正值
timeout为NULL,select将阻塞进程知道某个文件满足要求
timeout为正整数,就是等待的最长时间,即select在timeout时间内阻塞进程,超时时间到就唤醒了。

select返回值:
1.正常情况下返回满足要求的文件描述符个数
2.经过了timeout等待后仍无文件满足要求,返回值为0
3.如果select被某个信号中断,它将返回-1并设置errno为EINTR
4.如果出错,返回-1并设置相应的errno

select系统调用方法:
1.将要监控的文件添加到文件描述符集(宏操作)
    #include <sys/select.h>
    void FD_SET(int fd,fd_set *fdset)将文件描述符fd添加到文件描述符集fdset中;
    void FD_CLR(int fd,fd_set *fdset)将文件描述符fd从文件描述符集fdset中清除
    void FD_ZERO(fd_set *fdset)清空文件描述符集fdset
    void FD_ISSET(int fd,fd_set *fdset)在调用select后使用FD_ISSET来检测文件描述符集fdset中的文件fd发生了变化

使用方法:
    FD_ZERO(&fds);//清空集合
    FD_SET(fd1,&fds);//设置描述符
    FD_SET(fd2,&fds);//设置描述符
    maxfdp = fd1 + 1;//描述符最大值加1,假设fd1>fd2
    switch(select(maxfdp,&fds,NULL,NULL,&timeout))//读监控
    {
    case -1:exit(-1);break;//select错误,退出程序
    case 0:break;
    default:
        if(FD_ISSET(fd1,&fds))//测试fd1是否可读
    }
2.调用select开始监控
3.判断文件是否发生变化

应用程序使用select系统调用,它可能会阻塞进程,这个调用由驱动poll方法实现,
原型为:
unsigned int (*poll)(struct file *filp,poll_table *wait)
poll设备方法负责完成:
1.使用poll_wait将等待队列添加到poll_table中。
2.返回描述设备是否可读或可写的掩码

位掩码
POLLIN设备可读
POLLRDNORM数据可读
POLLOUT设备可写
POLLWRNORM数据可写

设备可读通常返回(POLLIN|POLLRDNORM)
设备可写通常返回(POLLOUT|POLLWRNORM)

范例:
static unsigned int mem_poll(struct file *filp,poll_table *wait)
{
    struct scull_pipe *dev = filp->private_data;
    unsigned int mask = 0;

    /*把等待队列添加到poll_table*/
    poll_wait(filp,&dev->inq,wait);

    /*返回掩码*/
    if(有数据可读)
        mask = PLLIN|POLLRDNORM;/*设备可读*/
    return mask;
}
Poll方法只是做一个登记,真正的阻塞发生在select.c中的do_select内核函数
真正阻塞发生在do_select函数的poll_schedule_timeout(&table,TASK_INTERRUPTIBLE,to ,slack)
这个阻塞发不发生又是由前面的poll函数返回的掩码决定的,mask=(*f_op->poll)(file,retval?NULL:wait);
---------------------------------------------
自动创建设备文件
2.4内核,设备文件系统devfs
devfs_register(dir,name,flags,major,minor,mode,ops,info)
dir:目录名,为空表示在/dev/目录下创建
name:文件名
major:主设备号
minor:次设备号
mode:创建模式
ops:操作函数集
info:通常为空

2.6内核,devfs不存在了,udev称为devfs的替代。相比devfs,udev(mdev)存在于应用层。利用
udev(mdev)来实现设备文件的自动创建很简单,在驱动初始化的代码里调用class_create为该设备创建一个class,
再为每个设备调用device_create创建对应的设备。
例:
配置busybox选上mdev,
struct class *myclass = class_create(THIS_MODULE,"my_device_driver");
device_create(myclass,NULL,MKDEV(major_num,0),NULL,"my_device");
当驱动被加载时,udev(mdev)就会自动在/dev下创建my_device设备文件

======================================================================================================================================================================
点灯的第一个汇编程序led_on.S,我们来查看它的Makefile文件

led_on.bin : led_on.S
    arm-linux-gcc -g -c -o led_on.o led_on.S    #-c表示只编译生成.o文件
    arm-linux-ld -Ttext 0x0000000 -g led_on.o -o led_on_elf    #链接是指将.o文件连接起来,生成可以在特定平台上运行的可执行文件,-T表示指定text代码段到代码段起始地址0x0000 0000
    arm-linux-objcopy -O binary -S led_on_elf led_on.bin    #复制ELF格式可执行文件,转换为二进制文件 -O表示指定格式为二进制文件,-S表示不要复制重定位/符号信息
clean:
    rm -f   led_on.bin led_on_elf *.o

-----------------
链接脚本:nand.lds
SECTIONS { 
  firtst      0x00000000 : { head.o init.o nand.o}
  second     0x30000000 : AT(4096) { main.o }

链接脚本将程序分为两段,分别是不同文件的运行地址

4K意外的代码拷贝到SDRAM的时候,要涉及到1.从4096的地方开始读,即4096以外的main.o 2.读到SDRAM的0x3000 0000处,3.读多少,读2K(估计)
------------------------
2440的几个时钟分别是什么,大概多大?FCLK(CPU 400MHz)、HCLK(SDRAM 100-133MHz)、PCLK(IIC\UART\定时器 50MHz)
--------------------
内核启动分析?
init/main.c的start_kernel()函数的几个关键函数
setup_arch(&command_line);
如果u-boot没有传入内核启动参数"bootargs",内核会启用setup_arch函数的default_command_line默认的命令行启动参数
如果传进来了参数就用
setup_command_line(command_line);去解析这个参数
调用关系:(内核启动流程)
start_kernel
rest_init
kernel_init
prepare_namespace
mount_root(挂载根文件系统)
init_post(执行第一个应用程序:init进程/sbin/init,1.读取配置文件2.解析配置文件3.执行用户程序)
1.
sys_open("/dev/console");
sys_dup(0);//复制
sys_dup(0);//复制
这三个文件代表标准输入、标准输出、标准错误

2.run_init_process()处理
(1)命令行传入的init=/linuxrc
(2)"/sbin/init"
"/etc/init"
"/bin/init"
"/bin/sh"
----------------------------------------
利用busybox创建最小根文件系统
1.init进程(这个在busybox,所以得先编译busybox源码)
    busybox根目录下查看INSTALL文件,可以看到怎么编译
    The BusyBox build process is similar to the Linux kernel build:

      make menuconfig     # This creates a file called ".config"
      make                # This creates the "busybox" executable
      make install        # or make CONFIG_PREFIX=/path/from/root install
    make install默认安装的PC上,而我们是要编译成嵌入式开发板,所以我们指定编译目录,免得破坏文件系统
    make CONFIG_PREFIX=/path/from/root install

    修改Makefile
    ARCH            ?= arm
    CROSS_COMPILE   ?= arm-linux-
    新版本的busybox是可以进入make menuconfig选择交叉编译工具

    make menuconfig
    选上Tab补齐功能

    make

    mkdir -p /work/nfs_root/first_fs_alan
    make CONFIG_PREFIX=/work/nfs_root/first_fs_alan install
    装完到安装目录下查看
    cd /work/nfs_root/first_fs_alan/
    root@book-desktop:/work/nfs_root/first_fs_alan# ls -l
    total 12
    drwxr-xr-x 2 root root 4096 2015-07-18 14:05 bin
    lrwxrwxrwx 1 root root   11 2015-07-18 14:05 linuxrc -> bin/busybox
    drwxr-xr-x 2 root root 4096 2015-07-18 14:05 sbin
    drwxr-xr-x 4 root root 4096 2015-07-18 14:05 usr
    linuxrc是指向busybox的链接
    ls bin/ls -l
    lrwxrwxrwx 1 root root 7 2015-07-18 14:05 bin/ls -> busybox
    ls命令也是指向busybox的链接


2.创建两个设备文件:dev/console,dev/null
    仿造PC上的/dev目录,首先查看/dev目录
    root@book-desktop:/work/nfs_root/first_fs_alan# ls /dev/console /dev/null -l
    crw------- 1 root root 5, 1 2015-07-18 09:03 /dev/console
    crw-rw-rw- 1 root root 1, 3 2015-07-18 08:46 /dev/null
    完了,我们需要来创建console和null两个设备
    (1)cd /work/nfs_root/first_fs_alan
    mkdir dev
    cd dev
    sudo mknod console c 5 1(字符设备、主设备号、次设备号)
    sudo mknod null c 1 3

3./etc/inittab配置项
    mkdir etc
    vi etc/inittab
    inittab内容:
    console::askfirst:-/bin/sh
    这个表示执行/bin/sh程序,标准输入,标准输出,标准错误都定位到/dev/console
    这就创建了最简单的配置文件了,再比如我们MT7620路由器的inittab是:
    ::sysinit:/etc_ro/rcS
    ttyS1::respawn:/bin/sh
4.配置文件指定的程序
    做一个最小的根文件系统,不指定应用程序
5.C库(如果make menuconfig busybox的BusyboxSettings-->Build Options-->[]BuildBusyBox as a static binary(no shared libs)选中就不需要C库了,不过我们还是使用动态链接)
    在制作交叉编译工具链的时候,已经生成了glibc库,可以直接用来构建根文件系统。
    安装C库,在/work/tools/gcc-3.4.5-glibc-2.3.6/arm-linux/lib
    .a表示静态库,我们只要动态库,拷贝.so文件,后缀加上-d(当文件是链接文件时,拷贝链接文件而不是实际文件,否则文件很大)
    根文件系统下创建lib目录 mkdir /work/nfs_root/first_fs_alan/lib
    cp *.so* /work/nfs_root/first_fs_alan/lib -d
-------------------------
打包文件系统:
yaffs1映像文件用于小页的nandflash (512byte/页)
现在用的是2048byte/页
cd /work/nfs_root
mkyaffs2image first_fs_alan first_fs_alan.yaffs2
然后进行烧录,
烧录完毕之后,
proc是虚拟的文件系统,搜集的是系统运行的一些信息,运行ps、reboot、ifconfig命令的时候才能正确执行,所以我们还要完善我们的文件系统:
开发板上执行:mkdir proc
然后手动挂载:mount -t proc none /proc
ps命令可以看到各个进程(就像windows下的任务管理器一样,可以用kill关闭掉比如说我们自己写的user_app应用程序)
# ps
  PID  Uid        VSZ Stat Command
    1 0          3092 S   init     
    2 0               SW< [kthreadd]
    3 0               SWN [ksoftirqd/0]
    4 0               SW< [watchdog/0]
    5 0               SW< [events/0]
    6 0               SW< [khelper]
   55 0               SW< [kblockd/0]
   56 0               SW< [ksuspend_usbd]
   59 0               SW< [khubd]
   61 0               SW< [kseriod]
   73 0               SW  [pdflush]
   74 0               SW  [pdflush]
   75 0               SW< [kswapd0]
   76 0               SW< [aio/0]
  707 0               SW< [mtdblockd]
  742 0               SW< [kmmcd]
  760 0          3096 S   -sh 
  773 0          3096 R   ps

proc是内核提供的一个虚拟文件系统
cd proc
cd 1
ls -l fd
    0->/dev/console
    1->/dev/console
    2->/dev/console

cat /proc/mounts 查看当前已经挂载了哪些文件系统
# cat /proc/mounts
rootfs / rootfs rw 0 0
/dev/root / yaffs rw 0 0
none /proc proc rw 0 0

MT7620的路由器板子的挂载的文件系统
rootfs / rootfs rw 0 0
proc /proc proc rw,relatime 0 0
none /var ramfs rw,relatime 0 0
none /dev ramfs rw,relatime 0 0
none /etc ramfs rw,relatime 0 0
none /tmp ramfs rw,relatime 0 0
none /media ramfs rw,relatime 0 0
none /sys sysfs rw,relatime 0 0
none /proc/bus/usb usbfs rw,relatime 0 0
devpts /dev/pts devpts rw,relatime,mode=600 0 0
mdev /dev ramfs rw,relatime 0 0
devpts /dev/pts devpts rw,relatime,mode=600 0 0

如果你不想像上面那样通过mount -t proc none /proc来挂载的话,那就要修改这个配置文件/etc/inittab
root@book-desktop:/work/nfs_root/first_fs_alan# mkdir proc
root@book-desktop:/work/nfs_root/first_fs_alan/etc# vi inittab
追加一行:
::sysinit:/etc/init.d/rcS(注意:有次实验写成rcs,导致开机的时候找不到这个脚本)
创建etc/init.d/rcS脚本文件
root@book-desktop:/work/nfs_root/first_fs_alan# mkdir etc/init.d
root@book-desktop:/work/nfs_root/first_fs_alan# vi etc/init.d/rcS
内容就是我们刚刚挂载的命令:
mount -t proc none /proc
然后给这个脚本加上可执行的属性
root@book-desktop:/work/nfs_root/first_fs_alan# chmod +x etc/init.d/rcS 

或者有另外一种方法:
将rcS脚本里的mount -t proc none /proc
改为mount -a
mount -a 就是去读/etc/fstab文件,根据这个文件内容来挂载文件系统的,显然我们现在还没有/etc/fstab这个文件,所以我们要添加这个文件:
#device mount-point     type    options        dump    fsck    order
proc    /proc           proc    defaults        0       0
来启动各种各样的文件系统
继续往下完善根文件系统
-------------------------------------
udev :自动创建/dev设备节点
busybox下有个简化版本叫mdev,使用mdev创建设备文件
具体可以查看busybox-1.7.0/docs/mdev.txt
以下是我们需要做的步骤:
Here's a typical code snippet from the init script:
[1] mount -t sysfs sysfs /sys/* mdev 通过sysfs文件系统获得设备信息*/
[2] echo /bin/mdev > /proc/sys/kernel/hotplug /*设置内核,当有设备拔插时调用/bin/mdev程序 */
[3] mdev -s /* 在dev目录下生成内核支持的所有设备的节点*/

Of course, a more "full" setup would entail executing this before the previous
code snippet:
[4] mount -t tmpfs mdev /dev /* 使用内存文件系统,减少对Flash的读写 */
[5] mkdir /dev/pts /* devpts用来支持外部网络连接(telnet)的虚拟终端 */
[6] mount -t devpts devpts /dev/pts /* */

cd first_fs_alan
mkdir sys
1)先做第一步和第四步:
vi etc/fstab追加
sysfs    /sys    sysfs    default    0    0
tmpfs    /dev    tmpfs    default    0    0

2)再做第五六,二三步:
vi etc/init.d/rcS追加
mkdir /dev/pts
mount -t devpts /dev/pts
echo /sbin/mdev > /proc/sys/kernel/hotplug
mdev -s    
烧录新的文件系统
就会发现/dev目录下除了console、null多了很多东西
原来只有两个设备文件:
# ls dev/ -l
crw-r--r--    1 0        0          5,   1 Jan  1 00:41 console
crw-r--r--    1 0        0          1,   3 Jul 18  2015 null
现在就自动创建了非常多的设备文件:
# ls /dev/
console          ptys0            tty18            ttyq8
dsp              ptys1            tty19            ttyq9
event0           ptys2            tty2             ttyqa
full             ptys3            tty20            ttyqb
kmem             ptys4            tty21            ttyqc
kmsg             ptys5            tty22            ttyqd
loop0            ptys6            tty23            ttyqe
loop1            ptys7            tty24            ttyqf
loop2            ptys8            tty25            ttyr0
loop3            ptys9            tty26            ttyr1
loop4            ptysa            tty27            ttyr2
loop5            ptysb            tty28            ttyr3
loop6            ptysc            tty29            ttyr4
loop7            ptysd            tty3             ttyr5
mem              ptyse            tty30            ttyr6
mice             ptysf            tty31            ttyr7
mixer            ptyt0            tty32            ttyr8
mouse0           ptyt1            tty33            ttyr9
mtd0             ptyt2            tty34            ttyra
mtd0ro           ptyt3            tty35            ttyrb
mtd1             ptyt4            tty36            ttyrc
mtd1ro           ptyt5            tty37            ttyrd
mtd2             ptyt6            tty38            ttyre
mtd2ro           ptyt7            tty39            ttyrf
mtd3             ptyt8            tty4             ttys0
mtd3ro           ptyt9            tty40            ttys1
mtdblock0        ptyta            tty41            ttys2
mtdblock1        ptytb            tty42            ttys3
mtdblock2        ptytc            tty43            ttys4
mtdblock3        ptytd            tty44            ttys5
null             ptyte            tty45            ttys6
psaux            ptytf            tty46            ttys7
ptmx             ptyu0            tty47            ttys8
pts              ptyu1            tty48            ttys9
ptya0            ptyu2            tty49            ttysa
ptya1            ptyu3            tty5             ttysb
ptya2            ptyu4            tty50            ttysc
ptya3            ptyu5            tty51            ttysd
ptya4            ptyu6            tty52            ttyse
ptya5            ptyu7            tty53            ttysf
ptya6            ptyu8            tty54            ttyt0
ptya7            ptyu9            tty55            ttyt1
ptya8            ptyua            tty56            ttyt2
ptya9            ptyub            tty57            ttyt3
ptyaa            ptyuc            tty58            ttyt4
ptyab            ptyud            tty59            ttyt5
ptyac            ptyue            tty6             ttyt6
ptyad            ptyuf            tty60            ttyt7
ptyae            ptyv0            tty61            ttyt8
ptyaf            ptyv1            tty62            ttyt9
ptyb0            ptyv2            tty63            ttyta
ptyb1            ptyv3            tty7             ttytb
ptyb2            ptyv4            tty8             ttytc
ptyb3            ptyv5            tty9             ttytd
ptyb4            ptyv6            ttyS0            ttyte
ptyb5            ptyv7            ttyS1            ttytf
ptyb6            ptyv8            ttyS2            ttyu0
ptyb7            ptyv9            ttyS3            ttyu1
ptyb8            ptyva            ttya0            ttyu2
ptyb9            ptyvb            ttya1            ttyu3
ptyba            ptyvc            ttya2            ttyu4
ptybb            ptyvd            ttya3            ttyu5
ptybc            ptyve            ttya4            ttyu6
ptybd            ptyvf            ttya5            ttyu7
ptybe            ptyw0            ttya6            ttyu8
ptybf            ptyw1            ttya7            ttyu9
ptyc0            ptyw2            ttya8            ttyua
ptyc1            ptyw3            ttya9            ttyub
ptyc2            ptyw4            ttyaa            ttyuc
ptyc3            ptyw5            ttyab            ttyud
ptyc4            ptyw6            ttyac            ttyue
ptyc5            ptyw7            ttyad            ttyuf
ptyc6            ptyw8            ttyae            ttyv0
ptyc7            ptyw9            ttyaf            ttyv1
ptyc8            ptywa            ttyb0            ttyv2
ptyc9            ptywb            ttyb1            ttyv3
ptyca            ptywc            ttyb2            ttyv4
ptycb            ptywd            ttyb3            ttyv5
ptycc            ptywe            ttyb4            ttyv6
ptycd            ptywf            ttyb5            ttyv7
ptyce            ptyx0            ttyb6            ttyv8
ptycf            ptyx1            ttyb7            ttyv9
ptyd0            ptyx2            ttyb8            ttyva
ptyd1            ptyx3            ttyb9            ttyvb
ptyd2            ptyx4            ttyba            ttyvc
ptyd3            ptyx5            ttybb            ttyvd
ptyd4            ptyx6            ttybc            ttyve
ptyd5            ptyx7            ttybd            ttyvf
ptyd6            ptyx8            ttybe            ttyw0
ptyd7            ptyx9            ttybf            ttyw1
ptyd8            ptyxa            ttyc0            ttyw2
ptyd9            ptyxb            ttyc1            ttyw3
ptyda            ptyxc            ttyc2            ttyw4
ptydb            ptyxd            ttyc3            ttyw5
ptydc            ptyxe            ttyc4            ttyw6
ptydd            ptyxf            ttyc5            ttyw7
ptyde            ptyy0            ttyc6            ttyw8
ptydf            ptyy1            ttyc7            ttyw9
ptye0            ptyy2            ttyc8            ttywa
ptye1            ptyy3            ttyc9            ttywb
ptye2            ptyy4            ttyca            ttywc
ptye3            ptyy5            ttycb            ttywd
ptye4            ptyy6            ttycc            ttywe
ptye5            ptyy7            ttycd            ttywf
ptye6            ptyy8            ttyce            ttyx0
ptye7            ptyy9            ttycf            ttyx1
ptye8            ptyya            ttyd0            ttyx2
ptye9            ptyyb            ttyd1            ttyx3
ptyea            ptyyc            ttyd2            ttyx4
ptyeb            ptyyd            ttyd3            ttyx5
ptyec            ptyye            ttyd4            ttyx6
ptyed            ptyyf            ttyd5            ttyx7
ptyee            ptyz0            ttyd6            ttyx8
ptyef            ptyz1            ttyd7            ttyx9
ptyp0            ptyz2            ttyd8            ttyxa
ptyp1            ptyz3            ttyd9            ttyxb
ptyp2            ptyz4            ttyda            ttyxc
ptyp3            ptyz5            ttydb            ttyxd
ptyp4            ptyz6            ttydc            ttyxe
ptyp5            ptyz7            ttydd            ttyxf
ptyp6            ptyz8            ttyde            ttyy0
ptyp7            ptyz9            ttydf            ttyy1
ptyp8            ptyza            ttye0            ttyy2
ptyp9            ptyzb            ttye1            ttyy3
ptypa            ptyzc            ttye2            ttyy4
ptypb            ptyzd            ttye3            ttyy5
ptypc            ptyze            ttye4            ttyy6
ptypd            ptyzf            ttye5            ttyy7
ptype            ram0             ttye6            ttyy8
ptypf            ram1             ttye7            ttyy9
ptyq0            ram10            ttye8            ttyya
ptyq1            ram11            ttye9            ttyyb
ptyq2            ram12            ttyea            ttyyc
ptyq3            ram13            ttyeb            ttyyd
ptyq4            ram14            ttyec            ttyye
ptyq5            ram15            ttyed            ttyyf
ptyq6            ram2             ttyee            ttyz0
ptyq7            ram3             ttyef            ttyz1
ptyq8            ram4             ttyp0            ttyz2
ptyq9            ram5             ttyp1            ttyz3
ptyqa            ram6             ttyp2            ttyz4
ptyqb            ram7             ttyp3            ttyz5
ptyqc            ram8             ttyp4            ttyz6
ptyqd            ram9             ttyp5            ttyz7
ptyqe            random           ttyp6            ttyz8
ptyqf            root             ttyp7            ttyz9
ptyr0            s3c2410_serial0  ttyp8            ttyza
ptyr1            s3c2410_serial1  ttyp9            ttyzb
ptyr2            s3c2410_serial2  ttypa            ttyzc
ptyr3            timer            ttypb            ttyzd
ptyr4            ts0              ttypc            ttyze
ptyr5            tty              ttypd            ttyzf
ptyr6            tty0             ttype            urandom
ptyr7            tty1             ttypf            usbdev1.1_ep00
ptyr8            tty10            ttyq0            usbdev1.1_ep81
ptyr9            tty11            ttyq1            vcs
ptyra            tty12            ttyq2            vcsa
ptyrb            tty13            ttyq3            watchdog
ptyrc            tty14            ttyq4            zero
ptyrd            tty15            ttyq5
ptyre            tty16            ttyq6
ptyrf            tty17            ttyq7

并且挂载了多种文件系统
cat /proc/mounts
# cat /proc/mounts 
rootfs / rootfs rw 0 0
/dev/root / yaffs rw 0 0
proc /proc proc rw 0 0
sysfs /sys sysfs rw 0 0
tmpfs /dev tmpfs rw 0 0

--------------------------------
jffs2格式一般是用在norflash,不过也可以用在nand flash 
制作jffs2工具:
(1)安装zlib库
cd /work/GUI/xwindow/
cd X/deps/zlib-1.2.3
./configure --shared --prefix=/usr/#编译成动态库
make
sudo make install#安装到系统目录里面
(2)安装mtd-utils
cd /work/tools
tar xjf mtd-utils-05.07.23.tar.bz2
cd mtd-utils-05.07.23/util
make 
sudo make install

制作jffs2镜像:
cd /work/nfs_root
mkfs.jffs2 -n -S 2048 -e 128KiB -d first_fs -o first_fs.jffs2
-S 2048表示每页大小2048byte
-e 128KiB表示擦除块的大小是128KiB

它是一个压缩的文件系统,会比yaffs2小的多。

在U-Boot启动参数里,指定根文件系统的类型:
set bootargs noinitrd root=/dev/mtdblock3 rootfstype=jffs2 init=/linuxrc console=ttySAC0
save
boot
--------------------------
我们这个时候基本上已经创建了一个最小但完整的根文件系统,这时候
要挂接根文件系统,得先查看网卡信息,配置网卡IP,首先开发板执行
#ifconfig的时候没有反应,说明没有网卡,网卡的灯也是不亮的
这时候需要去启动网卡
#ifconfig eth0 up
执行完发现网卡的灯亮了,这时候才是和ubuntu连上了
#ifconfig eth0
eth0      Link encap:Ethernet  HWaddr 00:60:6E:33:44:55  
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:248 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000 
          RX bytes:28063 (27.4 KiB)  TX bytes:0 (0.0 B)
          Interrupt:51 Base address:0xa000
      发现没有IP信息
这时候怎么办呢?
配置网卡ip:
#ifconfig eth0 192.168.1.7
然后ping一下ubuntu
# ping 192.168.1.100
PING 192.168.1.100 (192.168.1.100): 56 data bytes
64 bytes from 192.168.1.100: seq=0 ttl=64 time=3.038 ms
64 bytes from 192.168.1.100: seq=1 ttl=64 time=1.947 ms
64 bytes from 192.168.1.100: seq=2 ttl=64 time=1.210 ms
64 bytes from 192.168.1.100: seq=3 ttl=64 time=0.986 ms
确实能够ping的通,这个时候再来挂接nfs服务器才是有意义的。注意,这个每次掉电都要重新设置ip

怎么挂接NFS文件系统?
首先你要从flash启动根文件系统,然后才能执行mount命令去挂接

(1)服务器允许那个目录可以被挂接,启动NFS服务,通过/etc/exports配置文件指定这个目录
追加:/work/nfs_root/first_fs *(rw,sync,no_root_squash)
重启服务器:sudo /etc/init.d/nfs-kernel-server restart
测试NFS有没有启动成功:将自己挂载到/mnt目录下
sudo mount -t nfs 192.168.1.100:/work/nfs_root/first_fs /mnt
    -----------------------------------------
    ubuntu怎么启动nfs服务器 
    1.$sudo apt-get install nfs-kernel-server

    2.vi /etc/exports
    /work/nfs_root *(rw,sync,no_root_squash)
    /work/nfs_root/my_yaffs *(rw,sync,no_root_squash)

    其中: 
    /work/nfs_root/my_yaffs是要共享的目录,
    *代表允许所有的网络段访问,
    rw是可读写权限,sync是资料同步写入内存和硬盘,
    no_root_squash是nfs客户端分享目录使用者的权限,
    如果客户端使用的是root用户,那么对于该共享目录而言,该客户端就具有root权限。

    3、重启服务 
    $sudo /etc/init.d/portmap restart 
    $sudo /etc/init.d/nfs-kernel-server restart

    netstat -tl
    tcp        0      0 *:nfs                   *:*                     LISTEN     
    tcp        0      0 *:37226                 *:*                     LISTEN     
    tcp        0      0 *:sunrpc                *:*                     LISTEN     
    tcp        0      0 *:33010                 *:*                     LISTEN     
    tcp        0      0 *:ftp                   *:*                     LISTEN     
    tcp        0      0 *:ssh                   *:*                     LISTEN     
    tcp        0      0 localhost:ipp           *:*                     LISTEN     
    tcp        0      0 *:56249                 *:*                     LISTEN     
    tcp6       0      0 [::]:ssh                [::]:*                  LISTEN     
    tcp6       0      0 localhost:ipp           [::]:*                  LISTEN

    nfs和sunrpc启动

    4、测试nfs 
    $showmount -e 

    Export list for book-desktop:
    /work/nfs_root/my_yaffs *
    /work/nfs_root 

    或者可以使用以下命令把它挂载在本地磁盘上,例如将/rootfs挂载到/mnt下: 
    $sudo mount -t nfs localhost:/work/nfs_root/my_yaffs /mnt

    可以运行df命令查看是否挂载成功。查看后可以使用以下命令卸载: 
    $ df
    Filesystem           1K-blocks      Used Available Use% Mounted on
    /dev/sda6             19560336   4040388  14526332  22% /
    udev                    254668       232    254436   1% /dev
    none                    254668       248    254420   1% /dev/shm
    none                    254668       116    254552   1% /var/run
    none                    254668         0    254668   0% /var/lock
    none                    254668         0    254668   0% /lib/init/rw
    /dev/sda1                93307     20219     68271  23% /boot
    /dev/sdb1             41279536   2148368  37034288   6% /work
    localhost:/work/nfs_root/my_yaffs
                  41279552   2148352  37034304   6% /mnt

    $ sudo umount /mnt
    ---------------------------------------------
(2)由开发板去挂接,手动挂载和开机自动挂载
1).手动设置IP并挂载
#ifconfig eth0 up
#ifconfig eth0 192.168.1.7
#ping 192.168.1.100(ubuntu)
# mkdir mnt
# mount -t nfs -o nolock,vers=2 192.168.1.100:/work/nfs_root/first_fs_alan /mnt
这种是手动挂载,从flash上启动根文件系统,再用命令挂载nfs
2).自动设置IP并挂载nfs文件系统
修改U-Boot命令行参数,参看doc/nfsroot.txt文档
         noinitrd root=/dev/nfs nfsroot=[<server-ip>:]<root-dir>[,<nfs-options>] ip=<client-ip>:<server-ip>:<gw-ip>:<netmask>:<hostname>:<device>:<autoconf> init=/linuxrc console=ttySAC
具体操作:
OpenJTAG> set bootargs noinitrd root=/dev/nfs nfsroot=192.168.1.100:/work/nfs_root/first_fs_alan ip=192.168.1.7:192.168.1.100:192.168.1.1:255.255.255.0::eth0:off init=/linuxrc console=ttySAC0
OpenJTAG> save
OpenJTAG> reset
重启后启动打印信息:
    IP-Config: Complete:
          device=eth0, addr=192.168.1.7, mask=255.255.255.0, gw=192.168.1.1,
         host=192.168.1.7, domain=, nis-domain=(none),
         bootserver=192.168.1.100, rootserver=192.168.1.100, rootpath=
    Looking up port of RPC 100003/2 on 192.168.1.100
    Looking up port of RPC 100005/1 on 192.168.1.100
    VFS: Mounted root (nfs filesystem).
    Freeing init memory: 136K
    init started: BusyBox v1.7.0 (2015-07-18 13:59:21 CST)
    starting pid 762, tty '': '/etc/init.d/rcS'
    mount: can't find /dev/pts in /etc/fstab

    Please press Enter to activate this console. 
    starting pid 767, tty '/dev/console': '/bin/sh'
    # ls
    bin             dev             linuxrc         usr
    cfbcopyarea.ko  etc             proc
    cfbfillrect.ko  lcd.ko          sbin
    cfbimgblt.ko    lib             sys
发现手动挂载的时候每次都要去配置IP,而设置默认成NFS启动根文件系统之后,默认就会去先设置好板子eth0网卡的IP,然后进行nfs挂载
改回来从nandflash启动根文件系统:
OpenJTAG> set bootargs noinitrd root=/dev/mtdblock3 init=/linuxrc console=ttySAC0
OpenJTAG> save
OpenJTAG> reset
 
-----------------------------------------
驱动框架:
1.写出led_open、led_read
2.怎么告诉内核这个驱动
    a.定义file_operations类型结构体,填充,应用程序有哪些接口,file_operations就有对应的接口(成员)
    b.把这个结构告诉内核:register_chrdev()
    c.谁来调用register_chrdev(),驱动的入口函数first_drv_init来调用它
    d.修饰(module_init里有一个结构体,结构体里有一个函数指针,指向入口函数)

    register_chrdev()的作用是以major为索引找到一项把file_operations填充进去
    register_chrdev(0,"first_drv",&first_drv_fops);//0代表自动分配主设备号,找空缺

# lsmod
Module                  Size  Used by    Tainted: P  
rt2860v2_ap 1770528 1 - Live 0xc0375000 (P)

在应用中的open("/dev/xxx");是怎么来的?
a.通过mknod /dev/xxx c 主 次
手工建立而来
b.自动创建的,通过udev、mdev,根据系统信息创建设备节点。在/sys/class有各种类,类下面有不同的设备,
cat /sys/class/firstdrv/xyz/dev
主设备号:次设备号
252:0
这些信息一更改,mdev根据这些设备信息来创建设备节点,是依赖于脚本文件/etc/init.d/rcS里有一句:
echo /sbin/mdev > /proc/sys/kernel/hotplug
内核里一旦有设备加载进去(热插拔)或卸载掉时,它就会根据该文件指示的/sbin/mdev去调用所指示的应用程序
------------------------------------------------------------------------------------------------------
驱动中,虚拟地址到物理地址的转换:
VA = ioremap(PA,size);
注意:写好的驱动second_drv.ko
因为权限不够,所以执行secondtest时没有效果,解决办法:chmod 777 second_drv.ko
这就是为什么做开发最好用root权限
./seconddrvtest & 后台运行,通过top指令可以看到扫描按键方式占用99%资源,因为死循环
---------
注册终端服务程序
request_irq(irq,handle,irqflags,devname,devid)
卸载终端处理程序
free_irq(irq,dev_id)
----------------------------
异步通知机制
1.查询方式:耗资源
2.中断:read()一直在等待
3.poll机制:指定超时时间
异步通知机制就是,不再是应用程序主动去读,而是由驱动通过异步通知来提醒应用程序,通过发送信号signal
目标:按下按键时,驱动通知应用程序
(1)应用程序注册信号处理函数
(2)谁来发信号:驱动
(3)发给谁:APP,(APP要告诉驱动APP的PID)
(4)怎么发:kill_fasync(驱动里)
kill_fasync(&button_async,SIGIO,POLL_IN)
&button_async这个结构包含有进程ID,即你要发给谁
SIGIO发的信号类型
POLL_IN表示有数据等待读取
---------------------------------
同步互斥阻塞
目的:同一时刻,只能有1个APP打开/dev/buttons
不同的APP针对同一个/dev/buttons,可能不是原子操作,分很多步的操作,所以可能一下子
执行不完,被其他任务插进来打断。
具体操作如下:
1).定义信号量 struct semaphore sem;
2).初始化信号量
void sema_init(struct semaphore *sem,int val);
void init_MUTEX(struct semaphore *sem);初始化为0
1)和2)也可以用一个宏代替
static DECLARE_MUTEX(button_lock);
down(&button_lock);//down无法获取到信号量就会进入休眠(阻塞),等待信号量释放才会往下执行

在入口函数sixth_drv_init初始化定时器,设置处理函数
按下按键后:
1)之前的做法是:进入按键中断处理,确定按键值,唤醒应用程序或发信号
2)现在的做法是:修改定时器的定时时间为10ms,定时时间到,定时处理函数被调用,确定按键值
唤醒应用程序,发信号

--------------------------------
input输入子系统,使用现成的驱动,把自己代码融合进去。
现成的,别人做好的输入子系统框架input.c核心层,它向上提供统一接口
下面就分离成两部分,buttons.c与硬件相关,
evdev.c是纯软件部分

需要自己写的是
1.major
2.file.operations
    .open
    .read
    .write
3.register_chrdev
4.入口函数
5.出口函数

硬件相关操作:有数据产生时,调用input_event上报
--------------------------------------------------
exec 0 < /dev/tty1
表示将0号文件(标准输入文件)改为/dev/tty1

关于bus、driver(比较稳定的代码)、device(硬件相关)模型
用bus里的match函数来比较driver能不能支持device,能支持就调用里面的probe函数,让硬件相关的
和比较稳定的代码建立联系
-------------------------------
测试LCD
insmod cfbcopyarea.ko
insmod cfbfillrect.ko
insmod cfbimgblt.ko
insmod lcd.ko
echo hello > /dev/tty1 //可以在LCD上看见hello
cat lcd.ko > /dev/fb0
这表示把lcd.ko扔到显存上,fb_info得到字模,描出文字
vi /etc/inittab追加
tty1::askfirst:-/bin/sh
shell输出LCD程序
-------------------
driver/video/fbmem.c只是一个抽象的分层操作,最终要依赖于底层硬件的驱动如
s3c2410fb.c提供的结构体fb_info
每次来一个VCLK,在同一行上打一个点,一行完了产生一个行同步信号HSYNC,
一页完了产生一个垂直同步信号VSYNC,从第一个点开始打。
打什么颜色呢,要分配显存(没那么高级,是从内存划分的),把地址告诉LCD控制器。
s3c_lcd = framebuffer_alloc(0, NULL);//0表示不需要额外的私有数据空间

Linux命令dmesg用来显示开机信息,kernel会将开机信息存储在ring buffer中。
您若是开机时来不及查看信息,可利用dmesg来查看。开机信息亦保存在/var/log目录中,名称为dmesg的文件里。

下载并启动新内核,不带原来的LCD,否则会与我们新写的LCD驱动冲突
OpenJTAG> nfs 30000000 192.168.1.5:/work/nfs_root/uImage_nolcd
bootm 30000000

挂接nfs网络文件系统
mount -t nfs -o nolock,vers=2 192.168.1.5:/work/nfs_root/first_fs /mnt
cd /mnt/

拷贝/work/system/linux-2.6.22.6/drivers/video/cfbcopyarea.ko、cfbfillrect.ko、cfbimgblt.ko三个文件到/work/nfs_root/first_fs根文件系统下
insmod cfbcopyarea.ko 
insmod cfbfillrect.ko 
insmod cfbimgblt.ko 

tty1对应driver/video/console/fbcon.c

-------------------------------------------
TS:触摸屏驱动
(1)编译出一个不带TS的内核:uImage_nots_alan,拷贝到cp arch/arm/boot/uImage /work/nfs_root/uImage_nots_alan,使用新内核启动

(2)使用s3c_ts.c和Makefile编译出s3c_ts.ko
内核原来可参考的TS驱动文件是linux-2.6.22.6\drivers\input\touchscreens3c2410_ts.c

(3)测试驱动上报事件

查看设备变化
(1. ls /dev/event* 
(2. insmod s3c_ts.ko
(3. ls /dev/event* 

观察设备的变化
(4. hexdump /dev/event0
            秒       微秒   type code    value
0000000 29a4 0000 8625 0008 0003 0000 0172 0000
0000010 29a4 0000 8631 0008 0003 0001 027c 0000
0000020 29a4 0000 8634 0008 0003 0018 0001 0000
0000030 29a4 0000 8638 0008 0001 014a 0001 0000
0000040 29a4 0000 863c 0008 0000 0000 0000 0000
0000050 29a4 0000 c85e 0008 0003 0000 0171 0000
0000060 29a4 0000 c874 0008 0003 0001 027d 0000
0000070 29a4 0000 c87b 0008 0000 0000 0000 0000
0000080 29a4 0000 ed37 0008 0003 0018 0000 0000
0000090 29a4 0000 ed48 0008 0001 014a 0000 0000
00000a0 29a4 0000 ed4a 0008 0000 0000 0000 0000

使用:通过tslib插件(应用程序)来测试我们的驱动程序

(4)编译安装tslib
编译完成后,把生成的安装目录tmp拷贝到开发板目录
cp tmp /work/nfs_root/first_fs_alan/ts_dir -rfd
d表示是链接文件,拷贝过去还是链接文件

进入开发板,将ts所有文件拷贝到开发板根目录下
cd /mnt/ts_dir
cp * / -rfd
开发板就有了所有的tslib需要的文件

先安装s3c_ts.ko, lcd.ko
如果
insmod cfbcopyarea.ko 
insmod cfbfillrect.ko 
insmod cfbimgblt.ko 
insmod lcd.ko
加载出现段错误,可以将lcd驱动重新编译一下,因为我们内核做了修改,去掉了ts驱动,所以重新在新内核编译下就会好。

1.
修改 /etc/ts.conf第1行(去掉#号和第一个空格):
# module_raw input
改为:
module_raw input

2.
修改环境变量,指定ts设备是/dev/event0,lcd设备是/dev/fb0,校准文件和配置文件,插件目录等等
export TSLIB_TSDEVICE=/dev/event0
export TSLIB_CALIBFILE=/etc/pointercal
export TSLIB_CONFFILE=/etc/ts.conf
export TSLIB_PLUGINDIR=/lib/ts
export TSLIB_CONSOLEDEVICE=none
export TSLIB_FBDEVICE=/dev/fb0

执行应用程序:

ts_calibrate
这时候会生成校验文件/etc/pointercal,这个文件原来是没有的
# ts_calibrate 
xres = 480, yres = 272
Took 10 samples...
Top left : X =  263 Y =  170
Took 10 samples...
Top right : X =  259 Y =  857
Took 13 samples...
Bot right : X =  740 Y =  859
Took 12 samples...
Bot left : X =  738 Y =  153
Took 14 samples...
Center : X =  502 Y =  504
-41.683289 0.008511 0.545465
-44.310059 0.359821 0.000502
Calibration constants: -2731756 557 35747 -2903904 23581 32 65536


ts_test
# ts_test
706.074514:    236    142      1
706.110585:    239    142      1
706.150608:    238    141      1
706.170603:    237    141      1
706.210593:    239    141      1
706.250586:    239    140      1
706.270603:    239    140      1
706.290601:    239    140      1

ts_print
打印坐标,LCD坐标
812.680589:    407    231      1
812.720589:    409    232      1
812.740597:    411    233      1
812.780589:    412    234      1
812.820603:    416    235      1
812.840598:    417    235      1
812.886045:    419    235      0


ts_print_raw
打印原始数据,ADC电压值
896.535596:    856    860      1
896.575591:    860    889      1
896.595598:    864    903      1
896.615596:    868    908      1
896.635599:    870    906      1
896.655597:    874    910      1
896.675596:    880    916      1
896.695599:    887    921      1
896.715597:    892    922      1
896.735596:    898    915      1
896.781049:      0      0      0
896.781051:    898    915      0
--------------------------------------
linux原来usb鼠标的驱动在linux-2.6.22.6\drivers\hid\usbhid\usbmouse.c
去掉原来的驱动,编译出一个不带hid的内核uImage_nohid_alan

怎么写USB设备驱动程序?
1. 分配/设置usb_driver结构体
        .id_table
        .probe
        .disconnect
2. 注册

测试1th/2th:
1. make menuconfig去掉原来的USB鼠标驱动
-> Device Drivers 
  -> HID Devices
  <> USB Human Interface Device (full HID) support 

2. make uImage 并使用新的内核启动

3. insmod usbmouse_as_key.ko
4. 在开发板上接入、拔出USB鼠标
1th:
    插入:
    拔出:

测试3th:
1. insmod usbmouse_as_key.ko
2. ls /dev/event*
3. 接上USB鼠标
4. ls /dev/event*
5. 操作鼠标观察数据

测试4th:
1. insmod usbmouse_as_key.ko
2. ls /dev/event*
3. 接上USB鼠标
4. ls /dev/event*
5. cat /dev/tty1    然后按鼠标键
6. hexdump /dev/event0
--------------------------------------------------
块设备:
insmod ramblock.ko
ls /dev/ramblock 
cat /proc/devices
可以看到设备号一致的ramblock设备
cat /dev/ramblock > /mnt/ramblock.bin把整个磁盘(块设备)的内容通通存到/mnt/ramblock.bin文件里
在PC上查看ramblock.bin,-o loop把一个普通文件当成一个块设备来挂接,回环设备
   sudo mount -o loop ramblock.bin /mnt

memcopy就实现了把内存当成块设备来也读写,如果是真正的硬盘或flash,这个读写操作就要更加复杂了,memcopy就得替换成具体硬件的操作方法

读写块设备(比如U盘)的时候的时候,可能他并不是直接马上就读写完成的,比如
cp /etc/init.d/rcS /tmp
发现不是马上写入,而是在执行了sync之后(系统调用,同步的意思)
才开始写入,打印:
do_ramblock_request write 11
do_ramblock_request write 12
do_ramblock_request write 13
do_ramblock_request write 14
do_ramblock_request write 15

或者是在umount /tmp/之后才开始写,要嘛先把读操作都做完了,要嘛把写操作都做完了,不会交叉执行,就像电梯一样,先上后下或先下后上。否则控制效率太低了。

fdisk是分区的老工具,为了兼容一些旧的硬盘,我们必须假装我们有多少个磁头,多少个柱面等信息,分区就是几个柱面合起来组成一个分区
fdisk /dev/ramblock


FS文件系统帮你把文件的读写转换成块设备扇区的读写,把“读写”放入队列,优化后再执行,提高读写效率。

----------------------------------------------------------
NandFlash:
注册一个平台driver,假设内核里面有同名的平台设备,就调用probe函数。

不要有分区,整个就一个区,那就是用add_mtd_device(s3c_mtd);
如果想要构建分区那就要用add_mtd_partitions(s3c_mtd, s3c_nand_parts, 4);


-----------------------------------------------------------
NorFlash:
NorFlash可以像内存一样读,但不能像内存一样写,否则不是很容易被破坏数据。
NorFlash无位反转坏块,NandFlash有位反转坏块

设置为Nand启动,0地址对应片内4K RAM;设置为Nor启动,0地址就是对应Nor的0地址,这也是为什么我们用J-LINK烧写U-Boot到NorFlash的时候填写的地址就是从0地址开始烧录。
因为Nor支持XIP(excute in place),代码可以直接取指执行,Nand不支持XIP,所以要把Nand的前4K内容拷贝到片内4K RAM去启动。

uboot可以读0地址数据:
MT7620 # md.b 0
00000000: 6a 80 1b 3c 00 40 1a 40 00 d0 7b 8f 82 d5 1a 00    j..<.@.@..{.....
00000010: 80 d0 1a 00 21 d8 7a 03 00 20 1a 40 00 00 7b 8f    ....!.z.. .@..{.
00000020: 42 d0 1a 00 f8 0f 5a 33 21 d8 7a 03 00 00 7a 8f    B.....Z3!.z...z.
00000030: 04 00 7b 8f 42 d1 1a 00 00 10 9a 40 42 d9 1b 00    ..{.B......@B...

UDP数据包帧格式是怎么样的?[与TCP/IP参考模型一致]
第一层(数据链路层)是Ethernet II包,记录源MAC地址是目的MAC地址
第二层(网络层)是IPV4包,记录源IP和目的IP
第三层(传输层)是UDP包,记录的是端口等信息
第四层(应用层)才是真正的数据,如“Hello World”字符串数据

关于驱动中的probe函数,请解释下它的作用?
static struct platform_driver pdrv ={
      .probe  = plat_probe,
      .remove = plat_remove,
      .driver = {
           .name = "plat-realtek",
      }
};
Linux中总线、设备、驱动这3者是非常重要的数据结构,它们互相之间都有联系一旦一个设备和一个驱动能够匹配上,就会执行驱动里的probe。
总之一句话,probe函数作为driver的最基本的函数指针,一旦你的device和driver匹配(match,由总(bus)来完成,匹配工作发生在device_register()和drvier_register()的时候),probe函数就肯定会被调用;而probe的参数,pci里是struct pci_dev *pdev ,这个是由linux内核启动时遍历pci总线后得到的pci设备的描述符。probe调用期间一般会完成device的初始化,注册中断等操作。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值