ARM内核版本号--------ARM SoC版本号--------芯片型号
ARMv1
…
ARMv6 ------------------ARM11--------------------S3C6410…
ARMv7-----Cortex-M(微控制器/单片机)/Cortex-A(应用级,手机、平板)/Cortex-R(实时的、工业航天…)…
ARMv8-------Cortex-A30、ARM Cortex-A50、ARM Cortex-A70
ARMv9
#############################################
内核版本号与SoC版本由ARM确定,分为3个系列,有64位架构
SoC:System on Chip
包含外设(Peripheral)内部的外部设备
CPU:中央处理器=运算器+控制器
用最少的指令完成任务,编译器容易设计。功耗高
提供基本指令集,编译器设计变难
RISC的访问方式。CPU地址空间有限故能访问的内存大小与IO也有限。CPU需先将内存数据加载至寄存器中处理
CISC访问方式。CPU设计复杂
冯诺伊曼结构:程序、数据均放在内存中,存在安全、稳定性问题
哈佛结构:程序放在ROM、FLASH中;数据放在RAM中
单片机、嵌入式大多采用;软件复杂,需要规划链接地址
寄存器是CPU外设的硬件的一部分,可编程开关
哈佛结构中,类似于访问内存,寄存器操作是位操作。单个寄存器位宽一般与CPU位宽一样,利于最佳效率
210属于A8架构,32位,有32根地址线和32根数据线。寻址空间为4G
内存:RAM CPU可直接访问(地址总线+数据总线)速度快
外存:ROM 由控制器接口连接访问
常见外部存储器:
FLASH:电存储
NorFlash: 接至SROM bank,可总线式访问(类似内存访问),价格贵可靠,一般用于启动
NANDFLASH:
NandFlash: (分SLC、MLC和TLC)价格便宜
eMMC/iNand(闪迪)/moviNand(三星):类似SD卡的芯片,均为eMMC
SD卡/TF卡/MMC卡:
eSSD:嵌入式MLC硬盘
SATA接口硬盘:磁存储
SRAM:无需初始化即可使用(容量小)
DRAM:需要软件初始化内存才能使用
NORFLASH:可总线式访问(启动介质)
NANDFLASH:不能总线式访问,需初始化使用
210启动方式:内置96K SRAM(IRAM),64K NORFLASH(IROM)
启动过程见IROM_API-P5
1.CPU读取IROM执行基本初始化(IROM代码BL0由SOC厂商内置的),硬件选择启动方式读取启动代码(第一部分BL1,16K)至SRAM并校验。
2.SRAM中运行读取到的启动代码(UBOOT-BL1)。搬运剩余UBOOT(BL2)至SRAM的BL2
3.SRAM中的BL2初始化SDRAM并加载OS至SDRAM
4.跳转至OS的地址运行
ARM32位约定:Byte=8bit Halfword=16bit=2Byte Word=32bit
ARM32位指令集:
ARM指令集32bit、Thumb2指令集
非特权模式:
User:大部分任务执行的模式
特权模式:
异常模式:
FIQ:快速中断
IRQ:普通中断
Supervisor:管理模式(复位、软中断进入,引导)
Abort:异常模式
Undef:未定义指令模式
System: 与User使用相同寄存器的特权模式
写CPSR寄存器切换或CPU自行切换
*如此设计由OS的安全要求有关
模式 ABORT User FIQ IRQ SVC UNDEF
r0 通用
r1 通用
r2 通用
r3 通用
r4 通用
r5 通用
r6 通用
r7 通用
r8 部分不通用 r8-F
r9 部分不通用 r9-F
r10 部分不通用 r10-F
r11 部分不通用 r11-F
r12 部分不通用 r12-F
(sp) r13-A r13-U r13-F r13-I r13-S r13-D
(lr) r14-A r14-U r14-F r14-I r14-S r14-D
r15(pc) 通用
cpsr 通用
spsr-A 无 spsr-F spsr-I spsr-S spsr-D
---------------------------------------------------------
总计:37
@:SYSTEM模式使用USER模式寄存器
sp:堆栈指针,保护现场使用
lr:存储当前模式的返回地址
pc:当前程序执行的指针,pc指向哪里,CPU就执行哪条指令。程序跳转就是将目标代码地址放在pc中
cpsr:程序状态寄存器,记录CPU状态
spsr:用于保存之前模式的CPSR
32-28 27 24 23-16 15-8 7 6 5 4-0
NZCV Q J Undef ined I F T mode
*条件位:发生时结果自动置1(ALU为运算器)
N:ALU Negative 运算结果为负
Z:ALU Zero 运算结果为0
C:ALU Carried out ALU运算结果有进位
V: ALU oVerflowed ALU运算结果溢出
--------------------------------------------
Q:指示饱和状态(仅ARMv5支持)
J:=1时JAVA加速使用(仅ARMv5支持)
中断禁止位:
I: =1时禁止IRQ
F: =1时禁止FIQ
--------------------------------------------
T:仅ARM xT架构支持,=0为ARM状态,=1为Thumb状态
mode:ARM的模式位
当发生异常时,CPU自动将PC跳转至异常处地址处理。这些地址由异常向量表(CPU预先设置的异常中断地址)决定。某些CPU提供二级向量表
VECTOR TABLE:
指令: 编译后得到机器码
伪指令:编译器提供,指导编译过程,不生成机器码
load 内存加载至寄存器 store 寄存器内容存入内存
常见ARM32位指令:
寻址方式:
1.寄存器寻址 mov r1,r2 r2值赋给r1
2.立即数寻址 mov r0,#0xffff
3.寄存器移位寻址 mov r0,r1,lsl #3 r1左移3位赋值给r0
4.寄存器间接寻址 ldr r1,[r2] 将r2为地址的内容赋值给r1
5.基址变址寻址 ldr r1,[r2,#4] r2地址+4的地址 的内容给r1
6.多寄存器寻址 ldmia r1!,{r2-r7,r12} 以r1为地址,依次将内容放入多个寄存器中(弹栈)
7.堆栈寻址(压栈)stmfd sp!,{r2-r7,lr} 将栈连续访问,放入大括号内的寄存器中
8.相对寻址 beq xxx 跳转到xxx标号。实质为pc指针相对偏移量
指令后缀:指令族
指令+B 功能不变,由操作32位变为操作8位
指令+H 功能不变,由操作32位变为操作16位
指令+S 功能不变,操作有符号数/影响CPSR标志条件位
条件执行后缀:条件的成立由上句代码结果,只影响本句代码的执行。(根据CPSR判断)
标志 标志位 含义
---------------------------------------
EQ Z=1 运算结果为0
NE Z=0 运算结果不为0(不相等)
---------------------------------------
CS/HS C=1 无符号大于等于
CC/LO C=0 无符号小于
MI N=1 负数
PL N=0 正数或0
VS V=1 溢出
VC V=0 未溢出
HI C=1,Z=0 无符号数大于
LS C=0,Z=1 无符号小于等于
GE N=V 有符号数大于等于
LT N!=V 有符号数小于
GT Z=0,N=V 有符号数大于
LE Z=1,N!=V有符号数小于等于
AL 无条件执行,指令默认条件
NV 从不执行
常见指令
数据传输
*mov 寄存器 mvn 按位取反后赋值
算术运算 add sub rsb adc sbc rsc
*逻辑指令 and orr eor(异或) bic(位清除)
*比较指令,自动影响CPSR: cmp(比较相等) cmn tst teq
乘法指令 mvl mla umull umlal smull smlal
前导0计数 clz
*cpsr访问指令:cpsr需要专门指令访问
mrs 读cpsr/spsr
msr 写cpsr/spsr
跳转指令:
*b:直接跳转
*bl:保存返回地址到lr然后跳转 //bx:跳转同时切换为ARM模式
访问内存指令:
*ldr/str:
*ldm/stm:多字访问
后缀 ia increase after 先传输,地址+
ib increase before 先地址+,传输
da decrease after 传输,地址-
db
---------------------------------
* fd 满减
ed 空减
fa 满增
ea 空增
swp:内存与寄存器互换指令
立即数(普通数字):带#。立即数有合法、非法。非0部分8位内即合法
软中断指令:swi软件模拟中断,实现OS系统调用
协处理器指令:一般SoC只实现CP15协处理器.协处理器和MMUC、cache、TLB等处理有关
mrc&mcr
^:目标寄存器有PC寄存器时,会同时将spsr写入到cpsr
*伪指令:不同编译环境的伪指令不同,以下为GNU环境
@ 注释
// 注释
/*注释 */
# 注释
:标号,标记指令地址
.点代表当前指令地址
--------------------------------------------------
.global xxx @声明为外部的链接属性,声明为全局变量,让别的文件可用
.section .text @将当前段指定为代码段
.ascii .byte .shrt .long .word .quad .float .string变量类型
.align X 以2的X次方对齐
.balignl x,0xnnnnn @对齐,填充:b表示位填充,l表示为long4字节为单位填充 ,以16字节对齐
.equ @类似宏定义
.end 文件结束 .include 包含头文件 .arm/.code32 声明代码为ARM指令 .thumb/.code16声明为thumb指令
* ldr 大范围地址加载指令(伪指令不用考虑合法立即数)不使用#而是=,绝对地址
* adr 小范围地址加载,以PC为基准表示地址(相对地址)
* adrl
* nop 空操作
ARM指令流水线(CPU自动处理,了解即可)
三级流水线: 取值-->解码-->执行
ARM PC PC-4 PC-8
PC指针实际指向被取址的指令,并非正在执行的指令
流水线越多速度越快,但跳转会重置流水线
使用串口做控制台
1.选择启动方式 0xd0020010(usb启动地址)下载BL1
2.下载、烧写 运行bootloader(uboot)
3.下载、烧写 OS(使用uboot中的fastboot,通过USB进行,需要驱动)
-1.分区fdisk -c 0
-2.fastboot flash bootloader分区 uboot镜像
-3.fastboot flash kernel分区 内核文件(linux kernel)
-4.fastboot flash system分区 文件系统(rom)
fastboot常用命令
fastboot devices 查看当前连接设备
fastboot flash xxx 用于烧录
fastboot reboot 重启设备
uboot的参数:bootcmd bootargs
set bootcmd 'movi read kernel 30008000;bootm 30008000'
set bootargs console=ttySAC2,115200 root=/dev/mmcblk0p2 rw init=/linuuxrc rootfsype=ext3
save
elf文件:OS下的 可执行文件,包含了整个文件的基本属性,如文件版本,目标机器型号,程序入口等等。是带格式的映象
hex文件:包含地址信息,因此无需指定地址信息
bin文件:只包含可执行二进制,可顺序执行,内部没有地址标记是直接的内存映象的表示。需要指定地址信息
(mkv210_image.c)用于给生成的bin文件添加ECC头校验。根据MCU特性,USB\UART启动无需使用校验,因此可直接使用生成的bin文件
BL0(内置的IROM代码)无需编写了,210内置
1.关看门狗
2.从初始化cache指令
3.初始化栈
4.初始化堆内存
5.初始化块设备的 复制函数
6.初始化PLL锁相环和系统时钟
7.复制BL1至SRAM中
8.校验BL1,如果失败尝试二次启动
9.是否安全启动&安全启动相关内容
10.跳转至BL1的起始地址
//1.关看门狗(210BL0已经做)
// 初始化时钟(210默认初始化为1G)
//2.设置栈(210BL0已做)(C语言环境:c语言的局部变量使用栈实现)IROM-API:P13memory map
:SVC栈0xD003_7780-0xD003_0xD003_7D80 ARM使用满减栈,故sp=0xD003_7D80
//3.开指令cache (210BL0已做):UM-P
icache是内部自动的,打开即可
4.重定位和链接脚本
5.初始化ddr
6.搬运uboot到ddr
7.从iram长跳转到ddr中继续执行uboot(使用ldr修改PC指针)
内存相关见NT5TU64M16GG-P10:BLOCK DIAGRAM 128Mb*8 UM-P598:memory
其中,要清楚内存是分Bank的,每个bank寻址由行地址和列地址组成
bl sdram_asm_init
1.链接脚本中指定目标地址
2.下载时运行在非目标地址
3.使用PIC将代码搬运至链接地址处
4.长跳转到目标地址中执行(以目标地址为起始,链接脚本为偏移的地址)
伪指令:adr短加载(加载运行时相对地址)
ldr长加载(加载相对链接脚本的地址)
adr r0, _start //_start为起始地址,短加载 运行时地址
ldr r1, =_start //长加载 链接地址,因此与上面的地址不一定相同
ldr r2, =bss_start //重定位代码的结束地址
cmp r0, r1 // 比较_start的运行时地址和链接地址是否相等,是否需要重定位
beq clean_bss //如果相等,不需要重定位,跳转至clean_bss函数,否则继续下面的代码
//搬运
copy_loop:
ldr r3,[r0],#4 //将r0地址的内容放至r3中,地址加4字节
str r3,[r1],#4 //将r3内容放至r1地址中
cmp r1,r2 //目标地址==重定位结束地址
bne copy_loop //如果不等,没有复制完成,继续
clean_bss:
ldr r0, =bss_start
ldr r1, =bss_end
cmp r0, r1 // 如果r0等于r1,说明bss段为空,直接下去
beq run_on_dram // 清除bss完之后的地址
mov r2, #0 //不等,,则清除
shell脚本语言:一类语言。可自动化执行shell命令,一般用于配置
无需编译链接,可直接解释运行。解析器逐行解释代码
常用shell语言:sh、bash、python...
执行方法:1: ./xx.sh 需要可执行权限
2: source xx.sh 无需可执行权限
3: bash xx.sh bash解释器执行
注意:应注意语法严格
变量引用:使用$xxx 有时仅可以${xxx}
#!/bin/sh #!开始,加上解释器的pathname
# #为行注释
echo "hello word"
shell中为弱变量类型,`xx`反引号的作用是调用命令的返回内容
if的参数[ -参数前后都要有空格 ]:
-f :判断文件是否存在
-d :判断目录是否存在
-eq 是否相等 -gt大于 -lt小于 -ge大于等于 -le小于等于
-z :字符串是否为空 -o 逻辑或 ||和&&用于简写的if表达式
echo中可使用>创建文件并添加echo的内容,>>用于已存在的文件添加内容
shell传参: $#调用传参个数 $0、$1、$2以此表示传参各参数。命令本身不算参数。$x可用shift移出一个
break作用与c语言不同,用于跳出循环
引用其他makefile: include(原地展开)
#用来做注释
@命令前加这个表示静默执行(不显示命令本身)
= 引用最后一次赋值
:= 此句之前的值
?= 如果已被定义,此句略过
+= 给已经赋值的变量接续赋值
export导出环境变量,给其他makefile使用
通配符:
* 匹配多个任意字符
? 匹配单个任意字符
[xy]将括号内字符挨个匹配
% 类似*,一般只用于规则中
常见自动变量:
$@ 规则的目标文件名
$< 规则的依赖文件名
$^ 依赖的文件集合
0.就是一个裸机程序
1.自身可开机直接启动-uboot-start.S :根据SOC启动介质
2.引导内核启动,给内核传参(用于引导系统)
3.拥有部署系统的能力。能够使用uboot完成OS的下载(uboot/kernel/rootfs...)
4.能够初始化并控制一部分硬件(如:NAND/LCD/USART)
5.提供人机交互,如命令行shell
生命周期:uboot本质是一个裸机程序(单线程),从start.S到OS内核启动结束
部署操作系统:fastboot方式/ftp方式
tftp:uboot作为客户端,pc的ftp作为服务器,需设置ip&severip(此处传到了内存中)
movi命令将内存中的镜像写到flash中
环境变量bootcmd:自动运行命令的设置movi read kernel 30008000;bootm 30008000
环境变量,内核约定好的参数:bootargs:console=ttySAC2,115200 root=/dev/mmcblk0p2 rw init=/linuxrc rootfstype=ext3
x210uboot发现,无相同命令时可省略后边的命令,如printenv可为pri/prin/...
有空格的单个参数使用单引号将参数作为一个参数
printenv- print environment variables
打印环境变量,可简化为print
setenv - set environment variables 设置环境变量
*saveenv - save environment variables to persistent storage整体覆盖保存内存的环境变量到flash
tftpboot- boot image TFTP下载镜像
tftpboot [loadAddress] [[hostIPaddr:]bootfilename]
nfs - boot image via network using NFS protocol
movi(mmc) SD/inand相关操作
movi init - Initialize moviNAND and show card info
movi read {u-boot | kernel|分区名|分区地址} {addr} - Read data from sd/mmc
movi write {fwbl1 | u-boot | kernel} {addr} - Write data to sd/mmc
movi read rootfs {addr} [bytes(hex)] - Read rootfs data from sd/mmc by size
movi write rootfs {addr} [bytes(hex)] - Write rootfs data to sd/mmc by size
movi read {扇区sector#} {bytes(hex)} {addr} - instead of this, you can use "mmc read"
movi write {扇区sector#} {bytes(hex)} {addr} - instead of this, you can use "mmc write"
内存操作:注意防止越界
mw - memory write (fill)写内存
md - memory display显示内存内容
mm - memory modify (auto-incrementing)修改内存
启动内核
bootm - boot application image from memory从内存某地址启动程序镜像同时给内核传参
go - start application at address 'addr'从某地址启动应用/裸机程序
-----------------------------------------------------------------
autoscr - run script from memory
base - print or set address offset
bdinfo - print Board Info structure
boot - boot default, i.e., run 'bootcmd'
bootd - boot default, i.e., run 'bootcmd'
bootelf - Boot from an ELF image in memory
bootp - boot image via network using BootP/TFTP protocol
bootvx - Boot vxWorks from an ELF image
cmp - memory compare
coninfo - print console devices and information
cp - memory copy
crc32 - checksum calculation
dcache - enable or disable data cache
dhcp - invoke DHCP client to obtain IP/boot params
dnw - initialize USB device and ready to receive for Windows server (specific)
echo - echo args to console
erase - erase FLASH memory
exit - exit script
ext2format - disk format by ext2
ext2load- load binary file from a Ext2 filesystem
ext2ls - list files in a directory (default /)
ext3format - disk format by ext3
fastboot- use USB Fastboot protocol
fatformat - disk format by FAT32
fatinfo - print information about filesystem
fatload - load binary file from a dos filesystem
fatls - list files in a directory (default /)
fdisk - fdisk for sd/mmc.
flinfo - print FLASH memory information
help - print online help
icache - enable or disable instruction cache
iminfo - print header information for application image
imls - list all images found in flash
imxtract- extract a part of a multi-image
itest - return true/false on integer compare
loadb - load binary file over serial line (kermit mode)
loads - load S-Record file over serial line
loady - load binary file over serial line (ymodem mode)
loop - infinite loop on address range
MMC sub systemprint MMC informationmovi - sd/mmc r/w sub system for SMDK board
mtest - simple RAM test
nm - memory modify (constant address)
ping - send ICMP ECHO_REQUEST to network host
protect - enable or disable FLASH write protection
rarpboot- boot image via network using RARP/TFTP protocol
reset - Perform RESET of the CPU
reginfo - print register information
run - run commands in an environment variable
sdfuse - read images from FAT partition of SD card and write them to booting device.
sleep - delay execution for some time
test - minimal test like /bin/sh
version - print monitor version
uboot-jiuding
文件:
CHANGELOG : 修改日志 记录每个版本的修改记录 ,git管理工具也可以生成
arm_config.mk : .mk类文件 makefile中调用的某个文件
config.mk
* rules.mk : uboot的makefile使用的规则
COPYING : GPL许可版权
CREDITS : 感谢目录
-image_split : 分割uboot的脚本
MAINTAINERS : 维护者名单
-MAKEALL : 可能是帮助uboot编译的脚本
* Makefile : 主makefile文件
-mk : 快速编译脚本
* mkconfig : uboot主要配置脚本,可移植性主要由此文件实现
-mkmovi : 和启动介质相关
README : 说明文档
目录:
api : 硬件无关的功能函数API,uboot本身使用的
-api_examples : API示例代码
+ arch : 新版uboot中的:架构相关
* board : 开发板相关信息,已移植的板/厂家
+ boot : 新版uboot中的:可能与启动顺序有关
+ cmd : 新版uboot中的:
* common : 与具体硬件无关的普遍适用的代码。主要是uboot的命令与env环境变量相关的
+ configs : 新版uboot中的:可能与soc相关的
* cpu- : 新uboot无,SOC相关代码,需要移植的。包括SOC初始化、控制代码、start.S
disk : 磁盘相关的
doc : 文档文件,uboot相关文档、资料
+ dts : ??
* drivers : 从linux中拿出的uboot必须使用的设备驱动
+ env : ??
examples : 示例代码
* fs : 文件系统,从linux中移植的
* include : 头文件目录集合
* lib_ xx- : 架构相关的库文件
lib_generic- : 架构通用库文件
libfdt- : 设备树相关的
+ lib : ?可能修改了以上的文件夹
-nand_spl : nand相关的
net : 网络相关代码,如tftp nfs sntp ...
-onenand_xxx : 三星的flash相关的
post : ??
* -sd_fusing : 烧录uboot镜像到sd卡
+ scripts : 脚本
+ test : ?测试代码
tools : 工具类代码
版本信息:
U_BOOT_VERSION = $(VERSION主版本).$(PATCHLEVEL补丁版本).$(SUBLEVEL次级补丁版本)$(EXTRAVERSION附加信息) "1.3.4xxx"
VERSION_FILE =$(obj)include/version_autogenerated.h 编译后生成的版本宏定义
环境变量:
HOSTARCH := i386/sparc64/arm/ppc/其他---- 主机架构:这里我的x86_64不会被替换
HOSTOS := linux/... 操作系统:linux,本来应该是Linux
export HOSTARCH HOSTOS
静默编译: findstring s XECHO=echo:如果MAKEFLAGS中有s则静默编译。可以只输出编译过程信息,没有makefile信息
build编译方法: 1.原地编译:默认将当前目录.c编译的.o放在同一文件夹。
2.输出文件夹编译方式:linux kernel也是如此。
-1. make O=/指定路径
-2. export BULLD DIR=/路径 然后make
配置脚本目录变量:NO.101 MKCONFIG := $(SRCTREE)/mkconfig
标志远程编译变量并导出环境变量
加载配置 include $(obj)include/config.mk 未编译的源码目录没有此文件,由配置过程生成。此文件的值和配置有关。
CROSS_COMPILE 编译器前缀全路径
导入其他配置文件:NO.185 include $(TOPDIR)/config.mk
U-Boot objects、LIB集合:NO.188
ALL目标:NO.286
x210_sd_config :NO.2589配置目标
@$(MKCONFIG) $(@:_config=) arm s5pc11x x210 samsung s5pc110 #执行$(SRCTREE)/mkconfig脚本并传参
将_config替换为空 $1=x210_sd $2=arm $3=s5pc11x $4=x210 $5=samsung $6=s5pc110 $#=6
主要是编译环境相关
包含自动配置生成的board configs sinclude $(OBJTREE)/include/autoconf.mk指导条件编译、可移植性的。
由include/configs/xx.h文件生成(宏定义)因此移植时需要修改此文件
根据环境ARCH、CPU...包含不同的编译配置信息
根据CONFIG_NAND_U_BOOT配置LDSCRIPT变量 $(TOPDIR)/board/$(BOARDDIR)/xxx.lds链接脚本的路径
CPPFLAGS:NO.157 += -DTEXT_BASE=$(TEXT_BASE) 用于指定链接地址0xc
TEXT_BASE在主makefile配置时编写,编译后在.obj/board/samsung/x210/config.mk编译前在主makefile配置目标中
这里TEXT_BASE=0xc3e0 0000 uboot使用了虚拟地址映射,实际地址在:0x23e0 0000
自动推导规则编译:NO.239
BOARD_NAME = x210_sd $#要为4-6
NO.33-181: 创建符号链接,选择需要移植代码创建符号链接
include/ 创建asm -> asm-arm
include/asm-arm/创建arch -> arch->s5pc110--------被后面删掉了
include/ 创建regs.h -> include/s5pc110.h
include/asm-arm/创建arch -> arch->s5pc11x
include/asm-arm 创建proc -> /include/proc-armv
创建/include/config.mk文件---》为了让主makefile使用
创建、追加配置头文件 /include/config.h 包含了x210_sd.h,用来生成autoconfig.mk,然后用于指导编译
ENTRY(_start) :程序入口,类似c中的main
-Ttext的链接地址和.lds链接脚本中都指定链接地址时,以-Ttext地址优先
代码段中,应注意排列顺序。BL1前16k的文件必须排列在前
BL1:汇编阶段、SRAM中,主要操作Soc内部
寻找第一个入口:lds链接脚本的ENTRY声明的地方的符号,为入口/cpu/s5pc11x/start.S
头文件包含
config.h 配置后生成的,实际指向x210_sd.h
version.h 指向version_autogenerated.h 定义了U_BOOT_VERSION值来自makefile的修改
如果定义CONFIG_ENABLE_MMU 包含
指向s5pc110.h
条件编译
CONFIG_EVT1=1 CONFIG_FUSED未定义 则定义校验头16字节占位
start.S:
复位跳转到reset,设置SVC32模式,并关闭普通中断与快速中断
构建异常向量表
16字节对齐,TEXT_BASE由配置makefile时指定的
reset
-cpu_init_crit:
disable_l2cache 禁止l2cache
set_l2cache_auxctrl_cycle 设置l2cache
enable_l2cache 使能l2cache
刷新L1 I / D cache
禁止MMU和缓存 disable MMU stuff and caches
Read booting information:
R2-->PRO_ID_BASE=0xE0000000 OMR_OFFSET=0x04 包含了OM_PIN信息
R3-->根据启动信息,加载启动介质方式MMC/NAND...
设置栈 0xd0036000--》sram中
lowlevel_init 进行底层初始化-->uboot-9d/board/samsung/x210/lowlevel_init.S
压栈,防止再次调用函数
-check reset status 检查复位状态:如冷启动、热启动、复位...
-IO Retention release IO恢复。。。
*关看门狗
-外部SRAM&SROM初始化
*PS_HOLD pin(GPH0_0) set to high 锁定开关
判断在SRAM还是DDR,是否需要重新初始化时钟、内存
如果SRAM中,
初始化时钟
初始化内存控制器(DDR)---cpu_init.S uboot中,256内存地址设置为0x3000..裸机中设为0x20...都可工作
uboot整体物理地址设置为DMCO(0X3000..-0X3FFF..)DMC1(0X4000..-0X4FFF) 512MB
初始化串口,打印‘O’
tzpc_init...打印‘K’
再次供电锁存,无意义,但不影响
再次设置栈,此时DDR已初始化,设置ddr中的栈。//33e0 0000
再次判断代码运行在SRAM还是DDR,如果是SRAM则重定位代码至DDR
判断启动介质,最终调用相应copy函数movi_bl2_copy--uboot-9d/cpu/s5pc11x/movi.c
将启动设备中完整的uboot复制到DDR中了
enable_mmu虚拟地址映射:MMU(内部外设,实现虚拟地址和物理地址转换),建立虚拟地址映射表
MMU在CP15协处理器控制:映射、访问控制、
1.设置cp15-c3,实现控制域访问
2.转换表基地址设置TTB:cp15-c2,即设置转换表:表索引对应虚拟地址,表项对应物理地址。
内存映射与管理以块为单位,块大小取决于设置和mmu支持。
虚拟地址映射实际就是建立映射表,放置时要求xx位对齐
-lowlevel_init.S
->mmu_table:粗表映射按1MB
3.使能mmu-cp15-c1:bit0控制
设置栈,将栈放置在较安全的地方,紧凑而不浪费内存
ldr清bss-start到end
ldr远跳转加载uboot/lib_arm/bord.c start_armboot:UBOOT的第二阶段(BL2,DDR中)
--------------------------------------------------------------------------------------------
BL2:C阶段、DRAM中、注重Soc外部
uboot/lib_arm/bord.c -start_armboot(418):外部硬件初始化、uboot本身初始化
init_fnc_t **init_fnc_ptr; //函数指针 数组
gd_base:
gd(全局变量,指向gd_t类型的指针),
gd_t(include/asm-arm/global_data.h,uboot使用的全局变量,包含一个bd_t硬件参数)
分配gd、bd的内存
内存间隔(内嵌汇编),防止gcc优化
init_sequence定义同时初始化。包含的board相关的初始化函数的指针
遍历以上的函数指针数组
uboot/cpu/s5pc11x.c -> cpu_init();
uboot/board/samsung/x210/x210.c -> board_init();
网卡初始化(不是驱动,硬件相关的)
配置开发板的机器码
cpu/s5pc11x/interrupts.c -> interrupt_init() 不是中断初始化,而是定时器4初始化(10ms)用来做delay
common/env_movi.c(有很多启动介质,选择相应版本) -> env_init 环境变量初始化
init_baudrate 波特率初始化
cpu/s5pc11x/serial.c serial_init //这里什么都没做,估计是uboot框架
common/console.c console_init_f 控制台初始化(_f表示first第一阶段初始化,r为第二部分初始化)
display_banner串口显示uboot第二部分,版本信息(此时串口未被初始化,使用putc,调用serial_putc操作寄存器发送)
控制台是软件虚拟的设备,最终映射到硬件实现。中间件可以实现缓冲
print_cpuinfo 打印cpu信息 时钟
checkboard //检查板信息
init_func_i2c //初始化软件/硬件IIC (X210中未使用)
dram_init //dram初始化,初始化dram软件信息
display_dram_config //打印显示dram信息
CFG_NO_FLASH相关 //NorFLASH的配置信息(实际中没有)
CONFIG_VFD //uboot自带虚拟显示(未开启)
CONFIG_LCD //uboot自带LCD显示相关(未开启)
CONFIG_MEMORY_UPPER_CODE //uboot堆管理器初始化,实现malloc/free
-Board Specific开发板独有的初始化
mmc_initialize...
--CONFIG_HAS_DATAFLASH 串行存储设备(这里没有)
env_relocate环境变量SD卡重定位到ddr。如果SD卡没有,则从代码中使用默认一套ENV
IP Address赋值
MAC Address赋值
devices_init 硬件驱动设备初始化
jumptable_init 跳转表(未使用)
console_init_r 控制台第二阶段初始化(纯软件初始化)
enable_interrupts 中断初始化(未使用中断、空的)
loadaddr、bootfile 环境变量初始化读出全局变量
board_late_init 板级最后的初始化。实际是空的
eth_initialize 网卡相关初始化(网卡本身的初始化)
*x210_preboot_init 启动前初始化(LCD相关初始化)
check_menu_update_from_sd 检查自动更新
main_loop----------最终到这里
作用:掉电保存全局变量,不用修改源码。环境变量优先级大于代码中的硬编码值。
工作方式: 1.刚烧录时,无环境变量,加载代码中的默认环境变量。-/common/env_common.c
2.SAVE时将DDR环境变量保存至SD卡中,下次启动时, \
uboot第二阶段env_relocate环境变量会被加载至DDR
getenv()UBOOT内部获取环境变量使用
uboot与linux驱动:
linux下MMU是开启的,驱动使用虚拟地址
linux驱动本身做了模块化设计
uboot/裸机,使用物理地址。uboot移植了linux的源代码。
uboot硬件驱动比linux简单
iNand/sd驱动解析:
从start_armboot:
/drivers/mmc/mmc.c -mmc_initialize
mmc初始化:Soc的mmc控制器初始化、时钟、GPIO的SFR初始化,芯片的初始化
1.cpu_mmc_init
/cpu/s5pc11x/setup_hsmmc.c 开启mmc时钟,
/cpu/s5pc11x/setup_hsmmc.c 初始化mmc相关gpio
smdk_s3c_hsmmc_init(); //三星开发板的mmc通道初始化
建立mmc结构体,调用mmc_register进行驱动注册,链表链接到mmc_devices
mmc_init()
2.mmc_init() mmc卡、芯片的初始化
由于函数以函数指针形式存在于结构体中,具体的操作函数是需要被绑定的
驱动框架思想:
每个驱动的关键数据结构(结构体),包含了成员属性、函数指针(成员函数)
即驱动就被抽象为了一个结构体。包含:驱动(构建一个结构体)初始化、驱动工作时操作函数和变量
分离思想:
驱动中将操作方法(函数)、数据(变量)分开:不同的位置来存储管理驱动的操作方法和变量
分层思想:分成不同的层次如soc层、bord层、软件层等
1.复制uboot,修改makefile中交叉编译工具,配置编译,可使用sd_fuing脚本烧录sd卡(注意参数)
2.看现象,失败,未打印"O",屏蔽start.s中的PMIC电源管理。uboot可工作
3.修改配置目标xxx_config,对应一个include/configs/xxx.h文件
修改xx.h的信息,lowleveinit.s-system_clock_init修改时钟配置(根据配置头文件)
修改ddr配置(配置文件):内存地址、内存大小
修改ddr为0x30000000 256MB+0x40000000 256MB
修改ddr寄存器 宏修改, ddr相关uboot宏修改,虚拟地址映射修改
SD(INAND驱动)问题:
问题现象-》定位错误代码-》错误需要修改的代码
网卡:DM9000-总线式,复用数据线地址线,接至SROMC1
配置网卡初始化函数,配置相关config宏
linux中网卡生成的设备名一般是eth x/wlan x
linux使用专用命令来操作网卡
linux应用程序通过socket接口实现上网,应用层->socket接口 和驱动层->网络驱动框架 是完全分离的。由框架承接上下层。
uboot中操作网络相关的命令实际是直接操作驱动的,上下层并不完全分离。
版本差异:
新版uboot使用了linux kernel配置体系(kbuild.kconfig.menuconfig)
工作中一般不需要从uboot移植,而是soc厂商配套开发板移植
1.uboot本身是一个裸机程序:
-1.从启动角度看,操作系统内核本身也是一个裸机程序,OS可以分为内核层、应用层,操作权限不同。
-2.嵌入式系统,bootloader、kernel、rootfs都以镜像形式存在于介质中。运行时在DDR内存中
-3.启动过程就是由ROM到RAM启动硬件、软件达到运行状态的过程
-4.分区:预先设定的存储位置,以便指导启动(uboot\kernel\实际分区表必须一致)
-5.内核启动需要放置在预定链接地址,内核需要bootloader重定位到内存运行。需要启动参数
2.启动内核
从哪里加载:Flash、服务器...kernel分区(raw分区)
怎么加载:使用uboot命令读取 如movi(读写inand) bootm启动内核--实际是do_bootm函数
tftp 内存地址 镜像名--远程tftp下载(一般用于开发)
3.do_bootm函数 /common/Cmd_bootm.c
-1.条件编译执行secureboot(签名认证)
-2.CONFIG_ZIMAGE_BOOT宏控制zImage格式内核启动
-3.uboot 的ELF可执行程序使用objcopy生成bin可烧录镜像文件
-4.kernel生成vmlinux/vmlinuz ELF可执行程序 objcopy生成bin可烧录镜像image
将image压缩+解压代码(自解压)=zImage
zImage加工(64字节头信息)=uImage
zImage不是uboot原生支持的方式
uImage方式因设备树原因成为了历史版本:校验uImage得到kernel启动地址
bootm.c lib_arm/bootm.c
调用检查机器码,配置传参tag数据结构,启动
4.main_loop:获取、解析、执行命令
获取:console_buffer -> lastcommand
命令结构体数组放在自定义段中(开始结束地址即段的开始结束地址)
解析:parse_line将命令解析为argv find_cmd在命令集合中搜索命令
执行:run_command(const char *cmd, int flag)
uboot来源:uboot官方、SoC厂商、开发板供应商
-1.from 开发板供应商:九鼎
1.配置config
make xxx_config
得到:Configuring for x210_sd board... 配置完成
2.编译得到bin文件
检查arm-linux-gcc是否合适正确
主makefile中编译工具链路径名称是否正确
make -j4 多线程编译
uboot :必须从flash起始地址(SoC的启动地址)存放。
一般设置512KB-1MB左右
var :uboot的环境变量表,紧挨uboot分区。32kB-1MB
kernel :大小一般3、5MB-N
rootfs
自由空间 :一般挂载到rootfs
分区在系统移植前应确定好,uboot与kernel使用同一个分区表,部署时按照系统代码的分区方法进行
DDR分区和FLASH分区不同。注意不要内存冲突即可
操作系统的核心功能:
内存管理、进程调度(多任务实现宏观并行)、硬件设备管理(OS控制硬件,APP不用管)、文件系统(管理存储设备)
操作系统的扩展功能:协议栈、有用的应用程序包
内核与发行版的区别:
内核仅实现核心功能,不包含应用程序:www.kernel.org
发行版(普通意义的操作系统):包含内核、协议栈、应用程序包…
------------------------------------------
应用0|应用2|…|应用层-用户态
…
kernel-内核态
…
------------------硬件---------------------
要有整体的认识,层次的作用清楚、层次关联互相调用要清楚。
驱动 (是内核的一部分、与硬件接轨)
内核中的硬件设备管理模块----驱动工作在内核态
内核权限高(无限制)、驱动故障可能导致内核崩溃、驱动漏洞会使内核不安全
应用程序不属于内核,工作在用户态,资源受限制,应用故障不会导致内核崩溃
应用程序-----调用----->内核API接口--内核驱动----->硬件
APP是最终目标,内核是为APP提供资源管理服务的
------------------------------------------
rootfs为操作系统提供根目录,进程1存放在根文件系统中
内核启动后会装载根文件系统,启动根目录的进程1
内核各功能的代码是彼此独立的,如调度系统和内存管理系统没有全局变量的互相引用,函数调用也是遵循接口规范的(低耦合)
模块化体现:配置时可裁剪(源码级,静态变化)、模块化编译和安装(无需修改系统,动态变化)、源码中条件编译
模块化好处:可裁剪、灵活、可扩展性、利于协作,是一种普遍性的系统设计原则。
版本:
linux0.01(初版)--0.11(适合阅读,较成熟)--2.4x(经典书籍多)--2.6早期(类似2.4晚期)--2.6晚期(驱动、头文件位置修改了)
linux3.x 4.x---linux5.16(2021.12最新版本)
.linux5.15
├── arch ***架构,如arm
├── block *块设备管理代码(逻辑代码),以块为整体,如sd卡、SSD、inand
├── certs ?证书处理?
├── COPYING -f -版权声明
├── CREDITS -f -贡献名单
├── crypto 校验、加密算法…
├── Documentation -文档
├── drivers ***驱动文件夹
| ├──firmware 固件,原来在根文件目录,固化到IC里的代码,像IROM代码
├── fs *filesystem,linux支持的文件系统的实现
├── include ***各种架构的cpu,共用文件,特有的在arch/arm/include目录下
├── init 内核的初始化
├── ipc linux进程间通信代码实现
├── Kbuild -f
├── Kconfig -f
├── kernel *linux内核本身的代码文件
├── lib *公用的库函数(内核编程中不能使用c语言标准库函数)
├── LICENSES
├── MAINTAINERS
├── Makefile -fmakfile编译
├── mm *内存管理相关代码
├── net *网络相关代码(如TCP/IP协议)
├── README
├── samples -示例代码
├── scripts 脚本文件,辅助linux内核配置编译生成的脚本文件
├── security 安全相关代码
├── sound *音频处理相关代码
├── tools linux用到的工具
├── usr *initramfs和linux内核启动有关
└── virt 内核虚拟机
1.make distclean检查makefile的CROSS_COMPILE和ARCH?=
2.第一步配置make 合适的.config make xxx_defconfig
3.第二步配置make menuconfig
4.make--make后的镜像在arch/arm/boot zImage
配置是为了得到.config文件(可看作一个脚本),类似与uboot/include/configs/xxx_xx.h
内核用此文件指导整个编译、链接
make 合适的_defconfig解决大部分的配置项-可以避开很多不懂的配置项
相当于cp arch/arm/configs/合适的_defconfig ./.config
make menuconfig调整开发板细节
linux内核编译方法:编入,直接编译链接到zImage,去除即不编译链接,模块化即只编译单独链接到.ko文件进行动态加载卸载
menuconfig本身是一套软件支持,ncuress库实现文字图像界面
menuconfig本身读取Kconfig文件
显示菜单内容是由源码树各目录文件下的Kconfig提供支持,即能看到哪些菜单
menuconfig读取/写入.config
每个菜单的选择结果是保存到.config中的,即哪个=y哪个为注释..
注释:#xxx
menuconfig (这是一个菜单)
config (菜单的项目)XXX (构成.config文件中的CONFIG_XXX=?)
tristate (三种选择状态)"菜单显示的名字"
bool (两种选择状态)"菜单显示的名字"
source (引用子Kconfig)"(path)/Kconfig"
depends on XXX (此配置项依赖于XXX配置项,也可进行逻辑运算)
select XXX(会自动选择的项目)
default x(默认配置值)
help 按下?后显示的帮助信息
=y =m =空 选项会导致CONFIG_XXX宏的值,最终导致makefile进行编译或内核makefile中将其链接为模块或不编译
1.内核MIKEFILE
版本号定义
内核支持O=xxx,编译到一个文件夹
ARC\CROSS_COMPILE变量
2.lds链接脚本分析-找到程序入口
kernel链接脚本提供一个arch/($arch)/kernel/vmlinux.lds.S,真正得到的是vmlinux.lds,因此是动态的(可以借用预处理器)。
程序入口,编译后的lds中找到了ENTRY(stext)
arch/arm/kernel/head-nommu.S
arch/arm/kernel/head.S
3.head.S文件(校验,建立粗页表,c运行环境,b start_kernel)
KERNEL_RAM_VADDR:虚拟内存地址=(KERNEL_OFFSET + TEXT_OFFSET)
(5.x版本中)arch/arm/include/asm/memory.h中KERNEL_OFFSET=内核映像开始的虚拟地址
=PAGE_OFFSET= lowmem开始的虚拟地址,内存在上面=CONFIG_PAGE_OFFSET
在.config中定义为CONFIG_PAGE_OFFSET=0xC0000000
$(textofs-y)控制偏移 应该是0x8000 0000 但目前没找到在哪里定义,lds也+了这个偏移量
KERNEL_RAM_PADDR(新版内核已取消这个定义)
XIP_KERNEL:原地执行
ENTRY(stext):kernel真正的入口,一般被解压代码调用的。需要MMU是关的,D-cache是关的。。UBOOT传递的参数:r0=0,r1=machine nr(机器码), r2 =atags pointer(参数所在的地址)
ARM的函数调用传参是通过寄存器(还有一种是栈内存传参)
MMU关的,没有页表,因此我们链接内核到了虚拟地址,但是又不可以用物理地址,因此内核开始运行时,没有虚拟地址,因此要用位置无关码。__pa(0x虚拟地址) 可以转换为物理地址。
linux kernel分配的mach id在/arch/arm/tools/mach-types文件
禁用中断,设置SVC模式
读出cp15,c0寄存器,读出processor id(cpu id),校验cpu id的合法性
machine id(机器码)的校验
_vet_atages uboot通过tage给内核传参的校验:内存分布memtag,uboot的bootargs…
create_page_tables:建立段式页表(开启虚拟地址映射)
-1-先建立一个段式页表(粗页表,以MB区分,每个页表用四个字节表示,总共需要16K).-2-再建立一个细页表(以4kb为单位),启动新的页表,废除段式页表
调用switch_data:存了一些地址和全局变量、函数
mmap_switched:复制数据段、清bss段(构建c运行环境),保存一些id号、atage…
b start_kernel:跳转到c运行阶段
4.start_kernel–kenel/init/main.c文件
处理smp相关的:对称多处理器(相同的多核cpu)
锁定依赖处理:内核调试模块,处理内核自旋锁死锁。
cgroup:control groups内核提供用来处理进程组的技术
printk(内核用来从console打印调试信息,不同于printf的是可在参数前加一个打印级别的宏):
linux控制台有消息过滤器,可根据设置来显示不同等级消息
linux_banner:字符串信息:版本、环境…
setup_arch (处理cmdline-uboot给kernel传的bootargs)cpu架构的初始化过程,确定当前的架构,硬件平台相关的定义…
default_command_line默认分配的全局命令 默认=CONFIGXXXX
定义一些描述硬件的结构体指针
setup_processor:查找cpu的类型,并打印cpu的相关信息
setup_machine:通过机器码查到这个板子machine_desc的描述符
__lookup_machine_type:内核链接时将各种cpu信息组成machine_desc结构体实例,使用.arch.info.init段属性链接在一起。
打印Machine:板的名字
如果传参atages不是空,则打印个信息
旧版风格的tage分析…并且将参数加载到内核的变量中
经过处理后,打印cmdline.(稍后会解析cmdline)
内存初始化相关
一些command_line处理:具体以后再说
多处理器相关:nr_cpu_ids、per_cpu_areas、smp_prepare_boot_cpu
打印kernel command line
parse_early_param&parse_args:解析cmdline传参和其他传参,到字符串数组。目前没执行
内核初始化相关
trap_init:设置异常向量表
mm_init:内存管理模块初始化
sched_init:调度系统初始化
禁止抢占
中断初始化、定时器、软中断、时间系统…
console_init:控制台初始化-在此前的打印信息是存在缓冲区的,控制台初始化后才能打印出
…等 构建内核环境、****加载驱动
rest_init:剩下的比较重要的初始化
rest_init:启动了两个kernel线程、cpu_idle进程
kernel_init线程(kernel_thread函数运行)进程1:最重要的进程,init进程
kthreadd线程(kernel_thread函数运行)进程2:内核守护进程,保证内核本身正常工作
schedule:开启内核的调度系统.
最终调用:cpu_idle-进程0(不返回):无任务时,执行空闲进程
进程和线程:应用层运行的程序构成一个进程/线程。内核运行一个函数,就是一个线程
进程:一个独立运行的程序任务(与其他程序独立,可被内核单独调用)用ps命令可以查看
进程0(空闲进程)是内核进程不是用户进程,故ubutu下看不到
线程:linux中,线程类似于进程,不同在于,线程是容易通讯
kernel_init-进程1:内核态向用户态转变。
这个进程有两个状态,init开始时是内核线程,运行了用户态程序后,将自己强转为用户态,后续进程都工作在用户态了。
内核态时:挂载根文件系统,试图找到用户态init程序。内核源码的所有函数都是内核态的,因此应用程序只能独立于内核源码。因此根文件系统是给内核提供用户态环境的。
用户态时:init大部分有意义的工作都在用户态下进行的。其他的用户进程都是由init进程派生的
如何从内核态转到用户态:init进程在内核态通过kernel_execve执行一个用户空间编译链接的init应用程序,就跳跃到了用户态。进程号未改变,跳跃过程是单向的。用户态要调用内核只能通过API。
init进程在用户态:构建用户交互功能。linux进程的创建是从父进程创建来的。
init启动了login进程(登录)、命令行进程、shell进程(解析执行命令行)
shell进程启动其他用户进程
init源码:
kenel_thead->kernel_init
…
打开控制台设备,得到文件描述符编号。(linux中,设备也以文件形式存在,如/dev/buzzer代表蜂鸣器设备【真实设备】,console代表控制台设备【虚拟设备】)
复制了2次文件描述符。得到了三个文件描述符,分别为0、1、2,即标准输入、标准输出、标准输出。因此后续进程都默认具有这三个文件描述符(子进程会继承父进程的文件描述符)。
prepare_namespace:->mount_block_root挂载根文件系统
根文件系统位置、文件类型:bootloader会通过传参告诉kernel根文件系统的信息
root=/dev/mmcblk0p2 rw ->根文件系统的位置
rootfstype=ext3 ->根文件系统的类型
挂载rootfs成功后会打印出:VFS:Mounted root (xxx) on device xxx
如果挂载失败:uboot bootargs参数错误、根文件系统烧录失败(fastboot烧录不容易出错,代码烧录方式容易出错)、根文件系统本身有问题。
init_post->run_init_process(调用kernel_execve执行用户态进程1)
进入rootfs寻址init程序并执行
init程序确定方法:uboot cmdline指定(init=/xxx)/代码预先指定的(如:/sbin/init|/etc/init|/bin/init|/bin/sh)
cmdline常用参数:
格式:xxx=yyyy【空格】->内核解析,影响内核启动
root=[根文件系统位置:/dev/mmcblk0p2(支持nfs网络文件系统)]->设备名可以在哪看?
rootfstype=[文件系统类型:ext3]
console=[控制台设备声明/dev/ttySAC0,115200->串口0,波特率115200]
mem=[指定内存有多少]
init=[进程1的pathname:/linuxrc]
产品出厂:rootfs会在物理存储器上
开发时:rootfs可以在nfs上
root=/dev/nfs nfsroot=[IP]:/pathname ip=[HOST IP]:[server IP]:[网关]:[子网]::[网卡]:[DHCP状态]
内核架构相关代码
arch:硬件架构有关代码
mach:machine architecture->一类machine定义,即相同soc。一个开发板对应一个.c文件
plat:平台设备驱动[可能类似于stm32的HAL],SOC(内部外设)硬件相关数据代码
include:架构相关头文件,不是通用的
头文件包含格式:xxx/yyy.h ->kernel/include/xxx/yyy.h
drivers:硬件驱动
OTHERS:硬件无关代码,一般不会修改的
构建移植环境:
设置makefile: CROSS_COMPILE ARCH
make xx_defconfig
make/make O=xxx -jxx
运行kernel:
1.解压部分代码没有运行:
可能原因:解压地址应==链接地址对应的物理地址
解压地址在哪看:(物理地址).config ->0x3000 8000
链接地址在哪看:(虚拟地址).config ->0xc000 8000
查看自解压部分代码:/mach/makefile.boot
zreladdr-y:自解压的地址
param_phys-y:uboot参数地址
2.解压后没有正确运行
可能原因:某些地址不正确
3.内核中,机器码的确定
arch/arm/mach-xxx/mach-xxx.c 具体的文件要看makefile和.config文件
MACHINE_START宏&MACHINE_END;定义了一个结构体变量,会被链接到一个特定段
MACHINE_START(type,name)
.nr->mach id:xxx,对应uboot定义的值,对应一个开发板
.init_machine = xxx ->机器硬件初始化函数->绑定了内核初始化过程中的硬件信息
4.内核启动过程中的错误信息:
Oops信息-》内核错误的致命信息
kernel panic:内核抱怨信息
5.设备驱动问题
6.分区
UBOOT的->fdisk -c 0对iNand分区.已经写死了。分区信息是由iNAND本身的MBR传递的
如果是NAND,则分区表要在内核中构建->要与uboot中构建的一致
7.网卡
.config开启网卡
硬件由mach文件的结构体中.init_machine调用初始化
SOC的硬件配置
设备本身的配置
驱动数据配置
内核调试方法:
第0阶段:没打印信息
1.自解压代码
2.在第一阶段汇编开头操作LED操作代码,注意r0-r2被用作uboot传参
用于判断内核有没有被正常运行
第一阶段:
校验(CPU ID、板的机器码、传参tag)
创建页表
start_kernel
根文件系统存在的原因:
init应用程序需要一个位置,存在根文件系统
根文件系统给内核提供了根目录/
内核启动后应用层配置在根文件系统(/etc/)
shell命令程序、cd程序…
只有内核没有配置文件、库文件、程序,是不能工作的。
根文件系统实质:
特殊用途的文件系统
根文件系统本身也属于某种文件系统格式
文件系统作用:存储设备是分 块/扇区 的,底层访问存贮设备是按块号/扇区号 访问。文件系统是一套对存储设备扇区管理的软件。将对扇区的访问变为对目录和文件名的访问。我们对pathname的访问会被文件系统转换为扇区号的访问。不同文件系统的差异在于对扇区的管理策略和方法不同。如坏块、碎片管理
根文件系统的形式:
镜像文件形式:(格式由制作工具决定)
使用专用工具制作,可供烧录的镜像文件
镜像文件包含了根文件系统的所有文件&文件夹
烧录此镜像类似于对相应分区格式化
镜像文件系统具有一定格式,由制作时决定
目的是将块设备烧录为一定格式,内核启动后会挂载它
文件夹形式:(没有格式)
文件夹形式的根文件系统,就是包含特定格式内容的文件夹
由一个空文件夹添加必要的文件构成。
镜像文件形式的根文件系统由文件夹形式的根文件系统使用专用工具制作而成
简单制作一个ext3格式的根文件系统:
mke2fs是一个应用程序,用于制作ext系列的根文件系统
制作根文件系统命令:
dd if=/dev/zero of=rootfs.ext2 bs=1024(1kb) count=2048(2MB) losetup /dev/loop1 rootfs.ext2 mke2fs -m 0 /dev/loop1 2048 mount -t ext2 /dev/loop1 ./rootfs
我们将空的镜像挂载到了./rootfs目录下
然后添加linuxrc 等有用的文件
添加完成后:卸载 umount /dev/loop1 losetup -d /dev/loop1
烧录rootfs
NFS格式挂载rootfs:一般做调试
NFS(net file system):网络通信协议,由服务端和客户端构成
linux kernel中要开启NFS客户端,需要有nfs服务器
设置NFS环境变量,修改rootfs的文件权限为777,exprtfs -r 更新,sudo showmount localhost -e
配置内核使用nfs
设置env环境变量 root=/dev/nfs nfsroot=xxxx…
配置文件中开启NFS DHCP
驱动中开启NFS->root file system on NFS
根文件系统的linuxrc:即init进程 PID=1
一个可执行的应用程序(应用层,目标内核系统下可执行,即是使用arm-linux-gcc编译的)
应用程序如果是静态编译链接则可直接运行,但如果是动态编译的,必须提供相应的库文件。
linuxrc由内核直接调用执行,则此程序一般都是静态的
负责引出用户界面
最终开启一个操作界面程序给用户提供一个cmdline或者GUI.
负责启动后的配置(运行时配置etc)
/linuxrc在嵌入式系统中一般就是busybox,是一个c项目,包含了很多c文件和h文件。一个可配置编译成各个平台下可运行的应用程序。busybox是为嵌入式环境下构建rootfs使用的,专门开发用来做linuxrc init程序的。
busybox为系统提供了一整套shell命令集。ls、pwd…,本来是独立的程序,busybox是一个集合程序。因此busybox需要被移植
rootfs必须有什么:
/dev 目录:存储设备对应的文件,一个文件表示一个硬件设备
/sys 目录:必须有,创建空的也可以。虚拟文件系统
/proc 目录:必须有,创建空的也可以。虚拟文件系统
/usr 目录:busybox安装会自动生成
/etc 关键目录:运行时配置文件,直接、间接被/linuxrc调用。制作根文件系统的关键
/lib 关键目录:系统库文件(动态、静态)
VFS:虚拟文件系统(内核的功能)
是内核设计机制,通过文件系统将难以管理的磁盘扇区转换为path+name.
将硬件设备的访问虚拟化为了对目录+文件的访问。
有VFS后可通过设备文件去访问硬件,访问设备文件和访问普通文件的方法是统一的。
屏蔽了APP对不同文件类型访问的细节。屏蔽了不同文件系统和设备驱动的细节
VFS和rootfs的挂载有关,/dev/设备文件和VFS有关,驱动与VFS有关
构建一个文件夹形式的根文件系统,后制作烧录镜像
1.下载busybox构建环境
常用配置:
set->build options->static binary:配置为静态链接
set->Library tuning 库相关
->vi-style line.. 支持vi风格行命令
->Fancy shell prompts 允许转义代码
linux module utilities 驱动模块相关
-> Simplified modutils=n 简化modu工具
-> 剩余除Blacklist support(blacklist黑名单支持)全选
linux system utilities
->mdev (udev嵌入式简化版本)
配置编译
glibc无法链接,尝试安装libc6-dev
make install安装:即执行目标install,源码方式安装软件。
修改config,set安装位置
尝试挂载,执行/linuxrc成功,提示没有etc/xx等busybox配置文件
2.添加inittab运行时配置文件,文本格式,放在/etc/
busybox工作时调用了/etc/inittab文件,并以行为单位进行解析
:表示一种格式符号是分隔符,#表示注释
格式【id[1-4字符,唯一标识]:runlevels[运行级别]:action[执行条件]:process[要执行的进程]】
busybox会不停的检查这个文件,解释执行。
action:respawn(复活进程)、wait(等待此项目结束再开始下一个)、once、boot(系统启动时)、sysinit(访问控制台前执行)、askfirst(类似respawn,打印Pleasxxx)...
3.busybox源码分析
main -> xxx_main(根据argv[0],调用具体功能)
inittable解析:在/init/init.c->init_main中解析(默认环境变量也在这里)
busybox体积优势:busy提供的shell是阉割版,有很多公用代码,符合嵌入式系统特点
4./etc/init.d/rcS文件,可以很复杂,可以很简单,<-inittab<-busybox(linuxrc)
导出PATH环境变量到系统
设置不同的运行级别,这里为单用户模式
设置系统umask值,决定创建文件、文件夹的操作权限(666-权限)
挂载所有可以挂载的文件系统(包括虚拟文件系统),busybox会查找/etc/fstab文件
mdev:是udev的简化版本,配合linux驱动生成设备文件.没有启动时/dev/是空的
rc.S->添加
ehcho /sbin/mdev > /proc/sys/kernel/hotplug
mdev -s
hostname:-》放在rc.S中
用来设置一个主机名称,或显示主机名
/bin/hostname -F /etc/sysconfig/HOSTNAME //设置主机名
ifconfig eth0 【ip】(注意如果NFS方式下,可能会出错)
总之,rcS文件可以放很多我们开机想执行的命令
5.profile文件&登录理论
profile是被init进程(busybox)自动调用的
用一个程序完成一个功能,即登录也是一个程序。如果产品需要综合功能,则先使用很多小程序完成功能,再集成起来做大功能。
登录:
我们要有登录系统,就不能直接出现/bin/sh,应该先执行一个负责管理用户和密码的程序。
busybox也集成了这个功能:/bin/login /sbin/gettty,用这替代-/bin/sh
用户名、密码设置,和登录程序有关,但常见linux系统的管理方式几乎一样
密码一般都是加密的文字,存放在一个文件中
实现登录:
1.修改inittab中-/bin/sh为/bin/login或/sbin/gettty
2.为root设置密码
描述用户名和密码的文件:/etc/shadow(加密后的密码) /etc/passwd(密码的设置)
3.常用的为/sbin/gettty,busybox中,两种是相同的
6.动态链接库的copy
未交叉编译会出现:syntax error:unexpected错误
编译默认是动态的,静态需要加 -static
没有动态库的动态程序会出现:not found
动态链接库会在交叉编译工具目录的/libc/lib/,复制时要加-rdf参数
arm-linux-strip 可以去掉动态链接库的符号信息
7.开机自启动:将要执行的脚本添加到rcS合适位置或者添加一个自启动脚本即可
前台运行:默认执行方式
后台运行:执行同时让出控制台,程序既能后台运行,但不影响控制台操作。方法是在程序执行时添加一个参数 &
8.sysinit执行rcS,shutdown执行rcK
9.制作rootfs镜像并烧录
确定文件夹格式rootfs可用,设置bootargs,创建镜像格式文件系统并挂载,copyfs文件
使用fastboot烧录,设置bootargs
bsp:一般是芯片厂家/板卡厂家提供
x210_qt_bsp:使用一个脚本来管理整个bsp
tslib:提供应用层的触摸屏支持
bootloader:uboot或其他的
kernel:内核源码
buildroot:用于构建根文件系统
mk脚本文件分析:source执行mk脚本时,可以没有可执行权限
shell脚本分为:变量定义、函数、代码。
shell没有main函数,脚本的代码就相当于主函数,脚本中的函数是不会被直接执行的。
buildroot:
一个集成包,集成了交叉编译工具的制作、rootfs配置编译的过程
暂停,去做uboot移植实验
广义:操作了硬件的代码
驱动分层:由系统调用(API),操作硬件设备。(驱动本身也分层)
模块化设计:
宏内核:单内核,将整个内核看作一个大裸机程序,运行在一个地址空间运行,相互间可直接调用函数,紧耦合
微内核:功能被独立,各功能间通过IPC通讯
linux:本质是宏内核,包括静态模块化(可裁剪编译),动态模块化(不重新编译、不关机重启即可安装协助)
驱动分类:分类是看硬件特性的而不是看接口看协议
字符设备驱动: 以字节处理的设备,如LCD、串口、LED、蜂鸣器、触摸屏、按键...
块设备驱动:以块为单位的(多个字节),块大小由硬件决定的。
网络设备驱动:专为网卡设计的。
驱动的安全性要求:稳定、高效、无漏洞
驱动是内核的重要组成部分,内核会以调用函数的方式调用驱动代码,驱动的动态安装和卸载都会“更改”内核
驱动对内核的影响:驱动程序崩溃可能会导致内核崩溃,驱动的效率会影响内核整体效率,驱动的漏洞会造成内核安全漏洞。
**常见驱动安全性问题:未初始化指针,恶意用户程序,缓冲区溢出,竞争状态(并发访问)
驱动所需的知识:C语言,硬件操作、应用层API
驱动学习注意:1.注重实践,一步一步写,2.建立框架思维,考虑整体和上下层,3.通过简单设备学linux驱动框架,4.学会总结,记录
1.前提:正常运行linux的开发板+内核源码树(经过配置编译之后的内核源码)+nfs挂载的rootfs+nfs服务器
2.步骤:-1-驱动源码编写,makefile编写,编译。-2-insmod装载模块,测试,rmmod卸载mod
注:驱动安装的环境和运行的内核要是相同的环境,否则驱动安装会出现版本校验错误
源码:
pintk在发行版中会被拦截,dmesg命令可以查看
模块安装时会将模块插入到链表中做记录,模块的vermagic和系统的内核相同,否则不能被安装
printk:内核源码中用来打印信息,类似printf(应用编程使用的C库),printk由内核封装的。
printk有打印级别设置,命令行也可以设置打印级别。
头文件:应用程序的头文件是编译器带的,在/usr/inlcude下
内核的头文件是在内核源码的/include目录下的头文件
makefile:
KERN_DIR = 一个正确的内核源码树目录
obj-m += 添加到模块编译目标中
make -C /进入到内核源码树 M=`pwd`(shell pwd命令,保存当前的路径) modules
原理:利用make -C进入指定内核源码树,借用源码中的编译规则编译这个模块,并拷贝回当前目录
文件:.ko是内核目标文件,即驱动
安装:
lsmod -列出驱动列表(已经安装的)
insmode -安装一个模块 insmod xxx.ko
modinfo -打印出模块的内置信息
rmmod -卸载一个已经安装的模块,卸载模块不能加.ko后缀
...
MODULE_XXX -用来描述模块信息
MODULE_LICENSE -模块的许可证
MODULE_AUTHOR -模块的作者
MODULE_DESCRIPTION -模块的描述
MODULE_ALIAS -模块的别名
...
__int & __exit 宏定义,在 将这个函数放入.xxx.text段
应用层-->API-->设备驱动-->硬件
API:open read write close (函数指针)
驱动中提供真正的open\read\write\close 操作硬件
file operations结构体:内核源码定义的结构体,用于实现API到设备驱动
元素主要是函数指针,用来挂接实体函数地址,每个设备驱动都需要定义一个该结构体类型变量
设备驱动向内核注册时提供该结构体变量。
注册设备驱动:若不注册内核查不到该设备驱动,由驱动自己向内核注册,
调用内核的函数(register_chrdev)进行注册,注册前内核查不到该驱动,注册后可以挂接
register_chrdev:向内核注册自己的file operations,一般来说返回值 成功为0 不成功为负数
参数:1.主设备号,由内核定义的,编号可以自己指定或内核分配 2.字符串指针:设备驱动名
3. file_operations结构体指针
内核如何管理字符设备驱动:内核有一个数组来存储注册的字符设备驱动 ,
register_chrdev内部将要注册的驱动信息存储在数组相应位置
查看已注册的设备驱动:cat /proc/devices 可以查看主设备号
实践:目的-给空模块添加驱动壳子–核心工作:file_operations及其元素填充、注册驱动
应该先有一个框架-知道自己在做什么,细节代码无需全部手敲,可以到内核中参考copy,所有代码不能似懂非懂
一个驱动框架:
驱动初始化:即将模块安装
驱动卸载:则将模块移除
对接API:实现file_operations结构体内的函数
注册驱动:将file_operations通过register_chrdev在驱动初始化时将驱动注册,/proc/devices可查看
注销驱动:将驱动注销
让内核自动分配主设备号:将MAJOR 传0,让内核自动分配,成功则会返回分配的主设备号,否则会返回负
应用调用驱动:
驱动设备文件:/dev/… 应用通过设备文件,用API操作驱动
设备文件信息的关键:主次设备号
生成设备文件:[mknod /dev/xxx c 主设备号 次设备号]
实验:安装test驱动后 mknod /dev/hehe c 250 0
即可在/dev/查看到此设备文件/dev/hehe
应用层的读写本质就是将应用层和内核驱动的数据交互。数据的传递是用复制方式的而不是指针直接读写。__copy_from_user __copy_to_user
驱动中操作硬件:
1.硬件原理不变,寄存器不变,操作代码不变
2.寄存器地址不同,由物理地址变为虚拟地址。寄存器物理地址由SOC决定的
编程习惯不同:寄存器直接操作变为通过kernel封装的IO读写函数操控
内核的虚拟地址映射:静态和动态映射是可以同时使用的
映射表实质是头文件的宏定义
静态映射:内核移植时,以硬编码 映射,更改则需要重新编译。【静态映射表】
不同版本/SOC的静态映射表、位置、文件名可能不同
动态映射:根据函数和需要,动态建立映射、使用、销毁映射。
静态映射LED:
静态映射表寻址:头文件、arch/arm/目录 平台 mapxxx文件
三星2.6.3-5.7 :
主映射表:所有静态映射:/arch/arm/plat-s5p/include/plat/map-s5p.h-按模块定义的
定义的都是寄存器基地址的虚拟地址
GPIO映射表:(以主映射表为基础)
/arch/arm/mach-s5pv210/include/mach/regs-gpio.h
GPIO的具体寄存器定义:/arch/arm/mach-s5pv210/include/mach/gpio-bank.h
动态映射LED:
建立动态映射:request_mem_region:向内核报告 需要映射的内存资源
ioremap:真正实现映射-传物理地址返回虚拟地址
销毁动态映射:iounmap
release_mem_region
新版本注册字符设备驱动:
老接口:register_chrdev +file_operations结构体+主次设备号
新接口:register_chardev_region注册设备号/alloc_chrdev_region自动分配设备号 + cdev(字符设备驱动注册)
cdev:内核结构体,相关函数:
cdev_alloc:定义cdev结构体指针,用cdev_alloc实例化。即cdev = cdev_alloc();
cdev_init:将cdev结构体与file_operaions结构体绑定
cdev_init的替代方法:cdev->owner = THIS_MODULE;
cdev->ops = &fops;
cdev_add:注册驱动
cdev_del:注销驱动,并释放cdev结构体堆
unregister_chrdev_region:注销主次设备号
设备号宏:MKDEV-主次设备号,MAJOR-主设备号,MINOR次设备号。dev_t类型
驱动操作出错时的倒影式错误处理方法
利用标号和goto处理,出错后将之前正确执行代码的残留清理干净
自动创建字符设备驱动的设备文件
原来: API->驱动: 申请设备号,注册驱动,查看设备号,mknode根据设备号创建设备文件,API打开设备文件
自动创建删除:udev(PC)==mdev(嵌入式),运行应用层的程序进行创建
insmod安装后,创建成功–内核通过netlink协议–》mdev/udev创建设备文件.
**应用自动创建设备驱动也是需要驱动支持的
内核驱动设备类相关函数:注册驱动完成后/卸载设备前 进行
class_create:创建一个设备类,创建后可在/sys/class查看
device_create:根据设备类、设备号、设备文件名、通知应用创建一个设备文件,创建后可在/proc/devices
device_destroy:根据设备类名、设备号 通知应用删除设备文件.操作sysfs,创建/sys/class/xxclass/文件
class_destroy:根据设备类名 删除设备类
设备类相关代码分析:
sys文件系统设计思想:内核和应用的接口,放在文件系统中
静态映射表建立的关键:
1.映射表描述 -即对应关系-物理地址、虚拟地址的值宏定义
2.映射函数建立 -由映射表建立linux内核的页表映射关系
kernel/arch/arm/mach-xxx/mach-xxx.c
xxxx_map_io(void)
->xx_init_io()
-> iotable_init()
本质上,页表是一个结构体数组,每个元素是一个映射,映射了一段内存关系
3.开机时调用映射表建立函数
start_kernel->setup_arch->paging_init->devicemaps_init
动态映射分析:
真实驱动中,用结构体封装方式进行单次多寄存器的动态映射
内核提供的寄存器读写接口
writel() write32()
readl() read32()
驱动框架:驱动由驱动开发工程师(产品的驱动或驱动的移植)、内核维护者
内核驱动维护者对驱动设计的一套成熟的、标准的、典型的驱动实现.不同厂家设备的相同部分自己实现号,不同部分给具体驱动开发工程师实现.
内核维护者管控系统资源体系,如内存\中断号...
内核提供的一些接口函数\特点数据结构
LED驱动框架: 创建一个led类
/drivers/leds/led-class.c led-core.c + 自己实现的led驱动
内核开发者做了什么:开发维护如led-class.c led-core.c的框架
驱动厂商工程师做了什么:根据框架和厂商Soc硬件,开发相应驱动
产品: 根据Soc厂商提供的源码做移植\调试
分析:led驱动主要分析led-class.c,分析时从后往前,看熟悉的部分
led驱动实现为模块,这个驱动框架是可以被装载\卸载的.不需要时可以完全去除.
subsys_initcall&module_init作用相关,前者启动时间更早,是一个宏,放在一个段内
led_class_attrs :让应用程序可以通过目录下的属性文件操作驱动,进而操作硬件设备
attribute对应 /sys/class/leds/目录的内容(文件\文件夹..)
是一种驱动实现方法,有区别于file_operaion的方法
内核中添加或去除某个驱动:
修改Kbuild、makefile、源文件,在配置.conf中打开或关闭模块
5.4.5--2.6.35.7
gpiolib:
实时查man手册:
man 1 xx查linux shell命令,man 2 xxx查API, man 3 xxx查库函数
输入型参数和输出型参数:
会使用errno和perror
如if(a= =11) 最好写为if(11==a)
避免野指针,判断指针是否野指针时,写成if (NULL != p)
注意:输入型参数与输出型参数
用宏定义来实现DEBUG和release
//学习方法:man手册、头文件、百度、博客、总结
写程序尽量避免使用全局变量,尤其是非static类型的全局变量。
全局变量应该定义在c文件中并且在头文件中声明,而不要定义在头文件中
alloc一定要有对应的free
变量占用的空间由数据类型决定,不同平台占用内存不同。存和取数据类型时必须相同
void类型代表的是任意类型,它的类型是未知的,没有指定的。
隐式转换:默认向精度更高的方向转换,转换后的变量是临时变量,赋值时会隐式转换为左值类型。
c语言原生类型无bool类型,不是关键字可用int代替
程序运行的目的:计算机程序 = 代码 + 数据(经过运行后) = 结果
冯诺依曼结构是:数据和代码放在一起。
哈佛结构是:数据和代码分开存在。
什么是代码:函数 什么是数据:全局变量、局部变量
DRAM是动态内存,SRAM是静态内存。
位(位永远都是1bit) 字节(字节永远都是8bit) 半字(一般是16bit) 字(一般是32bit)
硬件内存的实现本身是有宽度的(内存位宽在逻辑上是任意的),有些内存条就是8位的,而有些就是16位的。。。
内存编址是以字节为单位(8bit)
内存和数据类型的关系:
C语言中的基本数据类型有:char short int long float double
int 整形和CPU本身的数据位宽是一样的(效率高)
C语言中,函数就是一段代码的封装。函数名的实质就是这一段代码的首地址,本质也是一个内存地址。
指针来间接访问内存:类型只是对后面数字或者符号(即内存地址)所表征的内存的一种长度和解析方法
结构体内嵌指针实现面向对象:C语言是面向过程的,但是C语言写出的linux系统是面向对象的
struct s //使用这样的结构体就可以实现面向对象。
{int age; // 普通变量
void (*pFunc)(void);}; // 函数指针,指向 void func(void)这类的函数
栈 是一种数据结构(小内存、自动化),C语言中使用栈来保存局部变量。先进后出:栈;先进先出:队列
栈是有大小的,太小怕溢出,太大怕浪费内存。栈的溢出危害很大,局部变量不能定义太多或者太大,减少递归调用
堆 大块内存、手工分配。申请及释放都需要申请malloc及释放free。申请内存使用后未释放,这段内存就丢失了
数据结构:链表、哈希表、二叉树、图等都是数据结构。
位与时两个操作数是按照二进制位彼次对应位相与的,逻辑与是两个操作数作为整体来相与的
位或时两个操作数是按照二进制位彼次对应位相与的,逻辑或是两个操作数作为整体来相或的。
按位取反是将操作数的二进制位逐个按位取反;而逻辑取反是真(非0)变成假(0)
任何非0的数被按逻辑取反再取反就会得到1; 任何非0的数倍按位取反再取反就会得到他自己;
位异或^ 与1位异或会取反,与0位异或无变化(两个位相同为0,不同为1)
左移位<< 与右移位>>:
对于无符号数,左移时右侧补0;右移时左侧补0
对于有符号数,左移时右侧补0;右移时左侧补符号位(正数就补0,负数就补1
嵌入式中研究的移位,以及使用的移位都是无符号数
操作寄存器时读-改-写三部曲。
特定位清零用&:如 a &= ~(1<<2);//将a第二位清0
特定位置1用|:如 a |= (1<<2);//将a第二位置1
特定位取反用^:如a ^= (1<<2);//将a第二位取反
用宏来置位、复位(最右边为第1位)。
* #define SET_NTH_BI
T(x, n) (x | ((1U)<<(n-1)))
* #define CLEAR_NTH_BIT(x, n) (x & ~((1U)<<(n-1)))
截取变量的部分连续位#define GETBITS(x, n, m) ((x & ((0U)<<(m-n+1))<<(n-1)) >> (n-1))
复杂宏怎么分析:
((x & ((0U)<<(m-n+1))<<(n-1)) >> (n-1))
第一步,先分清楚这个复杂宏分为几部分:然后逐步拆解
(x & ((0U)<<(m-n+1))<<(n-1)) >> (n-1)
指针完整的名字应该叫指针变量,它跟普通变量没有任何本质区别
为什么需要:指针的出现是为了实现间接访问,而高级语言并不是没有指针,而是被封装了
定义指针变量、关联指针变量、解引用,如:int a;int * p; p=&a; *p = 32;
指针定义时,*结合前面的类型用于表明要定义的指针的类型;第二种功能是指针解引用,解引用时*p表示p指向的变量的内容
取地址符&使用时直接加在一个变量的前面,表示这个变量的地址。
野指针(危害极大):指针指向的位置是不可知的,可能触发运行时段错误(Sgmentation fault)
避免野指针:1.定义指针时,同时初始化为NULL
2.在指针解引用之前,先去判断这个指针是不是NULL
3.指针使用完之后,将其赋值为NULL
4.在指针使用之前,将其赋值绑定给一个可用地址空间
c语言的NULL是什么, #ifdef _cplusplus // 定义这个符号就表示当前是C++环境
#define NULL 0 // 在C++中NULL就是0
#else
#define NULL (void *)0 // 在C中NULL是强制类型转换为void *的0
#endif
判断指针是否野指针时,都写成if (NULL != p)
const关键字与指针: const int *p;//p是指针,它指向一个只读的int型变量
int * const p; //p是不可被改变的指针,指向可以改变内容的int型变量
(gcc环境下)const修饰的变量其实是可以改的,gcc把const类型的常量放在了data段,只读是通过编译器校验实现。
数组变量也是变量,和普通变量和指针变量并没有本质不同。
数组中如数组a[];a做右值表示数组首元素的首地址(a不能做右值)
&a不能做左值(数组的地址是常量),做右值时表示整个数组的地址。
&a[0]字面意思就是数组第0个元素的首地址
指针方式来访问数组元素:不能整体访问,只能单个访问。
数组方式:数组名[下标]; (注意下标从0开始)
指针格式:*(指针+偏移量);//如果指针是数组首元素地址(a或者&a[0]),那么偏移量就是下标
指针参与运算的特点是,指针变量+1,并不是真的加1,而是加1*sizeof(指针类型);
sizeof是C语言的一个运算符,sizeof的作用是用来返回()里面的变量或者数据类型占用的内存字节数。
strlen是一个C库函数,用来返回一个字符串的长度(注意不计算字符串末尾的'\0'的)。
注意strlen接收的参数必须是一个字符串(字符串的特征是以'\0'结尾)
sizeof(数组名)实际返回的是整个数组所占用内存空间(以字节为单位的)
函数传参,形参是可以用数组的,实际传递是不是整个数组,而是数组的首元素首地址。
实际相当于传递的是指针(没有元素个数的信息)。
宏和typedef #define dpChar char * //单纯的替换
typedef char *tpChar; //定义了tpChar类型
结构体变量作为函数形参:和普通变量传参时表现是一模一样的。
结构体因为自身太大,所以传参应该用指针来传。(传结构体变量过去C语言也是允许)
输入型参数与输出型参数:
输入型参数:传参中使用const指针(如const int *p;),声明在函数内部不会改变这个指针所指向的内容
函数传参如果传的是普通变量(不是指针)肯定是输入型参数;
输出型参数:函数可以向外部返回多个值(利用输出型参数)
linux风格函数中,返回值是不用来返回结果的,用来表示程序执行是成功还是失败
指针数组与数组指针:
指针数组的实质是一个数组,存储的内容是指针变量:int *p[5];
数组指针的实质是一个指针,指针指向的是一个数组:int (*p)[5];
[] . ->这几个优先级比较高
函数的实质是一段代码,函数名表示第一句代码的地址
假设有函数是:void func(void); 对应的函数指针:void (*p)(void); 类型是:void (*)(void);
char *strcpy(char *dest, const char *src);),对应的函数指针是:char *(*pFunc)(char *dest, const char *src);
linux中命令行默认是行缓冲的,只要没有遇到\n(或者程序终止,或者缓冲区满)都不会输出而会不断缓冲
结构体内嵌函数指针实现分层:
完成一个计算器,我们设计了2个层次:上层是framework.c,实现应用程序框架;下层是cal.c,实现计算器。
实际工作时cal.c是直接完成工作的,但是cal.c中的关键部分是调用的framework.c中的函数来完成的。
先写framework.c,由一个人来完成。这个人在framework.c中需要完成计算器的业务逻辑,并且把相应的接口写在对应的头文件中发出来,将来别的层次的人用这个头文件来协同工作。
另一个人来完成cal.c,实现具体的计算器;这个人需要framework层的工作人员提供头文件来工作(但是不需要framework.c)
上层注重业务逻辑,与我们最终的目标相直接关联,而没有具体干活的函数。
下层注重实际干活的函数,注重为上层填充变量,并且将变量传递给上层中的函数(其实就是调用上层提供的接口函数)来完成任务。
C语言的2种类型:内建类型ADT、自定义类型UDT。typedef定义(或者叫重命名)类型而不是变量
二重指针和一重指针的本质都是指针变量。二重指针就是:指针数组指针
二维数组的两种访问方式:以int a[2][5]为例,(合适类型的)p = a;
a[0][0]等同于*(*(p+0)+0); a[i][j]等同于 *(*(p+i)+j)
内存来源:栈(stack)、堆(heap)、数据区(.data)
栈:运行时自动分配&自动回收,反复使用,临时性的,有大小限制会溢出
堆:操作系统堆管理器管理,内存空间大,程序手动申请&释放,脏内存,临时性
malloc():malloc(0)C语言并没有明确规定malloc(0)时的表现
malloc(4)gcc中的malloc默认最小是以16B为分配单位的。
代码段:代码段就是程序中的可执行部分,直观理解代码段就是函数堆叠组成的。
数据段(也被称为数据区、静态数据区、静态区):数据段就是程序中的数据,直观理解就是C语言程序中的全局变量。(注意:全局变量才算是程序的数据,局部变量不算程序的数据,只能算是函数的数据)
bss段(又叫ZI(zero initial)段):bss段的特点就是被初始化为0,bss段本质上也是属于数据段,bss段就是被初始化为0的数据段。
都可以给程序提供可用内存,都可以用来定义变量给程序用。
栈内存对应C中的普通局部变量;堆内存完全是独立于我们的程序存在和管理的,程序需要内存时可以去手工申请malloc,使用完成后必须尽快free释放。;数据段对于程序来说对应C程序中的全局变量和静态局部变量。
C语言的字符串类型:没有原生字符串类型,没有String类型,通过字符指针来间接实现的。
C语言中字符串的本质:指针指向头、固定尾部的地址相连的一段内存
字符串和字符数组:字符数组和字符串有本质差别。字符数组本身是数组,自带内存空间,可用来存东西;字符串本身是指针只占4字节,只能把字符串地址存在p中。
结构体:结构体使用时先定义结构体类型再用类型定义变量。访问方式:表面上有2种方式(数组下标方式和指针方式);实质上都是指针方式访问。
结构体的对齐访问:32位编译器,一般编译器默认对齐方式是4字节对齐。
结构体整体本身必须安置在4字节对齐处,结构体对齐后的大小必须4的倍数
gcc支持但不推荐的对齐指令:#pragma pack()//设置编译器1字节对齐(不对齐)
#pragma pack(n) (n=1/2/4/8字节)
gcc推荐的对齐指令
__attribute__((packed))使用时放在要进行内存对齐的类型定义的后面,范围只有加了这个东西的这一个类型。packed的作用就是取消对齐访问。
__attribute__((aligned(n)))使用时放在要进行内存对齐的类型定义的后面,范围只有加了这个东西的这一个类型。它的作用是让整个结构体变量整体进行n字节对齐
(注意是结构体变量整体n字节对齐,而不是结构体内各元素也要n字节对齐)
offsetof宏与container_of宏***详细
offsetof宏的作用是:用宏来计算结构体中某个元素和结构体首地址的偏移量
container_of宏作用:知道一个结构体中某个元素的指针,反推这个结构体变量的指针。
共用体union(联合):不存在内存对齐的问题。sizeof测到的大小实际是union中各个元素里面占用内存最大的那个元素的大小。
大小端模式:大端模式,是指数据的高字节保存在内存的低地址中。
小端模式,是指数据的高字节保存在内存的高地址中。(可以想象高对高差异小,高对低差异大)
在通信协议中,大小端是非常重要的
枚举在C语言中其实是一些符号常量集。枚举是将多个有关联的符号封装在一个枚举中,而宏定义是完全散的。
当我们要定义的常量是一个有限集合时,最适合用枚举。(定义的常量符号之间无关联,或者无限的)用宏定义。
由源码到可执行程序的过程:
源码.c->(预处理)->.i源文件->(编译)->汇编文件.S->(汇编)->目标文件.o->(链接)->elf可执行程序
处理这些过程的工具集合叫编译工具链
常见的预处理:
#include(#include <>专门用来包含系统提供的头文件,只会到系统指定目录寻找,编译器还可用-I来附加指定其他的包含路径)去寻找这个头文件
#include ""首先在用户自定义目录下找,然后再去系统、标准库下找)
#if #elif #else #endif #ifdef #ifndef
gcc中只预处理不编译:-o xx可以指定可执行程序的名称.-E参数可以实现只预处理不编译。
typedef是由编译器来处理而不是预处理器处理的
定义带参宏时,每一个参数在宏体中引用时都必须加括号,最后整体再加括号,括号缺一不可。
如:#define MAX(a, b) (((a)>(b)) ? (a) : (b)) #define SEC_PER_YEAR (365*24*60*60UL)
宏定义不会检查参数的类型,返回值也不会附带类型;而函数有明确的参数类型和返回值类型。当我们调用函数时编译器会帮我们做参数的静态类型检查,如果编译器发现我们实际传参和参数声明不同时会报警告或错误。
内联函数和inline关键字,内联函数是编译器负责处理的,编译器可以帮我们做参数的静态类型检查,不用调用开销,而是原地展开
当我们的函数内函数体很短(譬如只有一两句话)的时候,我们又希望利用编译器的参数类型检查来排错,我还希望没有调用开销时,最适合使用内联函数。
宏定义来实现条件编译(#define #undef #ifdef)
函数的目的就是实现模块化编程:函数的返回类型、函数名、参数列表等
一个函数只做一件事情。传参不宜过多.
最好用传参、返回值来和外部交换数据,不要用全局变量。
函数三要素:定义、声明、调用。
函数原型的作用:让编译器静态类型检查。函数声明的主要作用是告诉编译器函数的原型
递归函数:调用了自己本身这个函数的函数。典型就是:求阶乘、求斐波那契数列
使用递归函数的原则:
递归函数必须有一个终止递归的条件。收敛性
递归是占用栈内存的,在栈内存耗尽之前递归收敛(终止),否则就会栈溢出。
函数库:写好的函数的集合。函数是模块化的,可以被复用。
现在的标准的函数库.譬如说glibc(开源方式、源码方式)
库(主要有2种:静态库和动态库)的形式来提供。
静态链接库:将自己函数库源码只编译不链接形成.o文件,用ar工具归档为.a归档文件(静态链接文件)
(商业公司发布.a和.h文件,使用时链接器链接.a文件,形成可执行程序)
动态链接库(效率更高):(.so文件)动态链接库本身不将库函数的代码段链接入可执行程序,只是做个标记。
然后当应用程序在内存中执行时,运行时环境发现它调用了一个动态库中的库函数时,会去加载这个动态库到内存中,
然后以后不管有多少个应用程序去调用这个库中的函数都会跳转到第一次加载的地方去执行(不会重复加载)。
gcc默认使用动态库,要用静态库需要用-static强制静态链接。
函数的使用需要注意:包含相应的头文件;调用库函数时注意函数原型
有些库函数链接时需要额外用-lxxx来指定链接;如果是动态库,要注意-L指定动态库的地址。
字符串函数:指定了开头(字符串的指针)和结尾(结尾固定为字符'\0'),而没有指定长度(长度由开头地址和结尾地址相减得到)
字符串处理的需求是客观,面试笔试时,常用字符串处理函数
//学习方法:man手册、头文件、百度、博客、总结
数学库函数定义在:/usr/include/i386-linux-gnu/bits/mathcalls.h
使用时只需要包含math.h即可。数学库链接时需加-lm。高版本的gcc中,可能没有加也可以链接成功
注意区分编译时警告/错误,和链接时的错误:
编译时警告/错误:
4.6.10.math.c:9:13: warning: incompatible implicit declaration of built-in function ‘sqrt’ [enabled by default]
double b = sqrt(a);
链接时错误:
4.6.10.math.c:(.text+0x1b): undefined reference to `sqrt'
collect2: error: ld returned 1 exit status
制作静态链接库并使用:
制作:使用【gcc 库源文件.c -o 目标文件名.o -c】参数只编译不连接,生成.o文件;然后使用ar工具进行打包成.a归档文件
库名不能随便乱起,一般是lib+库名称,后缀名是.a表示是一个归档文件。发布时需要发布.a文件和.h文件
链接:ar工具: ar -rc lib+库名.a 库所需的.o文件
nm工具:nm 库名.a 可查看.a文件中有哪些.o文件,有哪些函数
使用:把.a和.h都放在需引用的文件夹下,然后在.c文件中包含库的.h,然后直接使用库函数。
编译时:gcc 要使用库的文件.c -o 可执行程序名 -l库名 -L库路径(不加只在默认位置找)
制作动态链接库并使用:后缀.so(windows下的.dll)
制作:【gcc 库源文件.c -o 目标文件名.o -c -fPIC】只编译不连接,生成.o文件 -fPIC生成位置无关代码
链接:【gcc -o lib库名.so 库使用的目标文件.o -shared】-shared表示用共享库方式链接
使用:与静态链接相同:编译时:gcc 要使用库的文件.c -o 可执行程序名 -l库名 -L库路径(不加只在默认位置找)
这样还会报错,动态链接库运行时需要被加载,编译器会去固定目录尝试加载
(不推荐)可以将动态库放到固定目录下,一般是/usr/lib
使用环境变量LD_LIBRARY_PATH:【export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:绝对路径】
linux的ldd命令可以查看可执行程序用到的库位置,查看库能否找到并解析
存储类就是存储类型,也就是描述C语言变量在何种地方存储。描述这个变量存储在何种内存段中。
作用域:描述这个变量起作用的代码范围
生命周期:描述这个变量什么时候诞生及什么时候死亡
链接属性:C语言中的符号有三种链接属性:外连接属性、内链接属性、无连接属性。
符号就是编程中的变量名、函数名等。运行时变量名、函数名能够和相应的内存对应起来,靠符号来做链接的。
.o的目标文件链接生成最终可执行程序的时候,其实就是把符号和相对应的段给链接起来。
linux下C程序的内存映像:
代码段又叫文本段(.text):对应着程序中的代码(函数)
只读数据段:const修饰的常量有可能是存在只读数据段的(但是不一定,const常量的实现方法在不同平台是不一样的)
数据段:显式初始化为非0的全局变量;显式初始化为非0的static局部变量。在main函数运行之前就已经被初始化了,是重定位期间完成的初始化。
bss段(ZI段,零初始化段):显式初始化为0或者未显式初始化的全局变量;
显式初始化为0或未显式初始化的static局部变量。
堆:C语言不会自动向堆中存放东西。程序员使用堆内存时,自己申请、使用、释放
文件映射区:进程打开了文件后,将这个文件的内容从硬盘读到进程的文件映射区,
以后就直接在内存中操作这个文件,读写完了后在保存时再将内存中的文件写到硬盘中去。
栈:局部变量分配在栈上;函数调用传参过程也会用到栈
内核映射区:将操作系统内核程序映射到这个区域了。每一个进程都活在自己独立的进程空间中,0-3G的空间每一个进程是不同的(因为用了虚拟地址技术),但是内核是唯一的。
OS下和裸机下C程序加载执行的差异:操作系统中运行程序时程序员自己不用操心,会自动完成重定位和清bss。裸机中需要手动
存储类相关关键字:
auto:作用,修饰局部变量。表示这个局部变量是自动局部变量,自动局部变量分配在栈上。
static:作用1:用来修饰局部变量形成静态局部变量。非静态局部变量分配在栈上,而静态局部变量分配在数据段/bss段上。
作用2:用来修饰全局变量,形成静态全局变量。与非静态全局变量在链接属性上不同
静态局部变量和全局变量的区别是:作用域、连接属性。静态局部变量作用域是代码块作用域(和普通局部变量是一样的)、链接属性是无连接;全局变量作用域是文件作用域(和函数是一样的)、链接属性方面是外连接。
register:(慎用)作用:register修饰的变量。编译器会尽量将它分配在寄存器中。寄存器数量有限,不保证一定放在寄存器中
extern:主要用来声明全局变量,声明的目的主要是在本文件使用其他文件的全局变量
volatile:用来修饰一个变量,表示这个变量可以被编译器之外的东西改变。没有这个会被编译器优化。
(中断isr中引用的变量,多线程中共用的变量,硬件会更改的变量)正确区分,该加的时候加不该加的时候不加,如果不能确定该不该加为了保险起见就加上。
restrict:c99中才支持的。只用于限定指针;该关键字用于告知编译器,所有修改该指针所指向内容的操作全部都是基于(base on)该指针的,即不存在其它进行修改操作的途径;
这样的后果是帮助编译器进行更好的代码优化,生成更有效率的汇编代码。
typedef:C语言关键字归类上属于存储类关键字,但是实际上和存储类没关系。
作用域:局部变量的代码块作用域,可以被访问和使用的范围仅限于定义这个局部变量的代码块中定义式之后的部分。
函数名和全局变量的文件作用域,整个.c文件中都可以访问这些东西。
准确的说:全局变量/函数的作用域都是自己所在的文件,但是定义式之前的部分因为缺少声明所以没法用,解决方案是:1、把它定义到前面去;2、定义到后面但是在前面加声明;
局部变量因为没法声明,所以只能定义在前面去。
同名变量的掩蔽规则:如果两个同名变量作用域有交叠,C语言规定在作用域交叠范围内,作用域小的一个变量会掩蔽掉作用域大的那个
生命周期:
栈变量的生命周期:局部变量(栈变量)存储在栈上,生命周期是临时的。
堆变量的生命周期:从malloc申请时诞生,然后使用,直到free时消亡。
数据段、bss段变量的生命周期:全局变量的生命周期是永久的,在程序被执行时诞生,在程序终止时消亡。不能被程序自己释放
代码段、只读段的生命周期:程序执行的代码,其实就是函数,它的生命周期是永久的。(const类型的常量、字符串常量有时候放在rodata段,有时候放在代码段,取决于平台)
链接属性:C语言程序的组织架构:多个C文件+多个h文件
编译以文件为单位、链接以工程为单位
编译器工作时是将所有源文件依次读进来,单个为单位进行编译的
)链接的时候实际上是把第一步编译生成个单个的.o文件整体的输入,然后处理链接成一个可执行程序。
三种链接属性:外连接、内链接、无链接
外连接:外部链接属性,也就是说可以在整个程序范围内(可以跨文件)进行链接,譬如普通的函数和全局变量属于外连接。
内链接:(c文件内部)内部链接属性,不能在当前c文件外面的其他c文件中进行访问、链接。static修饰的函数/全局变量属于内链接。
无连接:这个符号本身不参与链接,它跟链接没关系。所有的局部变量(auto的、static的)都是无连接的
函数和全局变量的同名冲突:一个程序中的所有c文件中不能出现同名的函数/同名的全局变量。
static的第二种用法:修饰全局变量和函数
普通的(非静态)的函数/全局变量,默认的链接属性是外部的
static(静态)的函数/全局变量,链接属性是内部链接。
总结:
普通(auto)局部变量:分配在栈上;作用域为代码块;生命周期在代码块内;链接属性,无连接
静态局部变量:分配在数据段/BSS段;作用域为代码块;生命周期为程序运行的整个周期;链接属性为无连接
静态全局变量、静态函数和普通全局变量/普通函数:
区别在于static使全局变量/函数的链接属性由外部链接(整个程序所有文件范围)转为内部链接(当前c文件内)。
存储类决定生命周期,作用域决定链接属性
宏和inline函数的链接属性为无连接。
操作系统:
裸机程序:代码量小,功能简单、所有代码都和直接目的有关,没有服务性代码
操作系统:本身不产生价值,主要是管理所有资源,为应用程序提供服务。
操作系统调用通道:API函数
c库函数和API关系:单纯的API只是提供了极简单没有任何封装的服务函数,。应用程序为了好用,就对这个API进行了二次封装,于是就成了C库函数。
有时完成一个功能,有相应的库函数可以完成,也有API可以完成
不同平台(windows、linux、裸机)下库函数的差异
不同操作系统API是不同的,但是都能完成所有的任务,只是完成一个任务所调用的API不同。
库函数在不同操作系统下也不同,但是相似性要更高一些。封装API成库函数的时候,尽量使用了同一套接口,所以封装出来的库函数挺像的。但是还是有差异
跨操作系统可移植平台,譬如QT、譬如Java语言。
main函数
标准c语言 int main(void); int main(int argc,char **argv); int main(int argc,char*argv[])
main函数的返回值应当是int,void不正确只是一些单片机里这么写。
main函数是特殊的,c语言规定main函数是整个程序的入口。
linux一个程序的执行
可在linux命令行下被调用,可通过shell脚本调用,可在程序中调用另一个程序(fork exec)。
本质:一个进程的创建、加载、运行、消亡;执行程序就是创建新进程中执行这个程序直到结束;
一个进程被它的父进程开启调用。main函数的返回值返回给这个程序(进程)的父进程。
main函数返回值给父进程一个答复。一般0表示成功,负数表示失败。
linux shell中用$?这个符号来存储和表示上一个程序执行结果。
main函数的参数:
调用main函数所在的程序的它的父进程给main函数传参,并且接收main的返回值。
main传参通过argc和argv这两个C语言预订的参数来实现,argv是一个字符串数组,这个数组用来存储多个字符串,每个字符串就是我们给main函数传的一个参数。调用程序本身如./a.out 本身是第一个参数
程序调用本质上都是父进程fork一个子进程,然后字进程和一个程序绑定起来去执行(exec函数族),我们在exec的时候可以给他同时传参。
程序调用时可以被传参(也就是main的传参)是操作系统层面的支持完成的
各个参数之间是通过空格来间隔的,在程序内部如果要使用argv,那么一定要先检验argc。
void类型的本质
C语言属强类型语言,所有的变量都有明确的类型。C语言中的一个变量都要对应内存中的一段内存,编译器需要这个变量的类型来确定这个变量占用内存的字节数和这一段内存的解析方法
void类型的正确的含义是:不知道类型,不确定类型,还没确定类型。
void *类型的指针指向的内存是尚未确定类型的,因此我们后续可以使用强制类型转换强行将其转为各种类型。
NULL:不是关键字,是宏定义出来的
#ifdef _cplusplus // 条件编译
#define NULL 0
#else
#define NULL (void *)0 // 这里对应C语言的情况
#endif
C++的编译环境中,编译器预先定义了一个宏_cplusplus,程序中可以用条件编译来判断当前的编译环境是C++的还是C的。
NULL的本质解析:NULL的本质是0,但是这个0不是当一个数字解析,而是当一个内存地址来解析的
几乎所有CPU中,内存的0地址处是被操作系统严格管理的,应用程序不能随便访问。
定义一个标准的指针流程:
1.定义指针 赋值为 NULL
2.给指针赋值
3.检查是否为NULL
{4.使用指针}
5.使用后再赋值为NULL
注意不要混用'\0' 和 '0' 和 0 和 NULL
'\0'用法是C语言字符串的结尾标志,一般用来比较字符串中的字符以判断字符串有没有到头
'0'是字符0,对应0这个字符的ASCII编码,一般用来获取0的ASCII码值
0是数字,一般用来比较一个数字是否等于0;
NULL是一个表达式,一般用来比较指针是否是一个野指针。
运算中的临时匿名变量
C语言叫高级语言,汇编语言叫低级语言。高级语言(C语言)它对低级语言进行了封装(C语言的编译器来完成),给程序员提供了一个靠近人类思维的一些语法特征
更高级的语言如java、C#等只是进一步强化了C语言提供的人性化的操作界面语法,在易用性上、安全性上进行了提升。
高级语言中有一些元素是机器中没有的
高级语言在运算中允许我们大跨度的运算。低级语言中需要好几步才能完成的一个运算,在高级语言中只要一步即可完成。
顺序结构
顺序结构说明CPU的工作状态,就是以时间轴来顺序执行所有的代码语句直到停机。
选择和循环结构内部还是按照顺序结构来执行的。
每个c文件编译的时候,编译器是按照从前到后的顺序逐行进行编译的。
链接过程链接器实际上是在链接脚本指导下完成的。所以链接时的.o文件的顺序是由链接脚本指定的。如果链接脚本中没有指定具体的顺序则链接器会自动的排布。
程序调试的debug宏
程序调试的常见方案:单步调试、裸机LED调试、打印信息、log文件
单步调试:直观,缺点是限制性大、速度慢。用于代码量小,不能printf的时候
利用硬件调试:LED、蜂鸣器等,适合代码量小的裸机程序
printf函数打印调试,作为程序员必须学会使用打印信息调试,具有普遍性
log文件(日志文件):适合于系统级或者大型程序的调试。
打印信息不能太多也不能太少,太少会不够信息找到问题所在,太多会有大量的无用的信息淹没有用信息
调试(DEBUG)版本和发行(RELEASE)版本 #ifdef DEBUG
#define dbg() printf()
#else
#define dbg()
#endif
如果我们要输出DEBUG版本则在条件编译语句前加上#define DEBUG
调试语句dbg()就会被替换成printf从而输出
c语言预定义宏: #include
#include
double average(int num,...)
{
va_list valist;
double sum = 0.0;
int i;
/* 为 num 个参数初始化 valist */
va_start(valist, num);
/* 访问所有赋给 valist 的参数 */
for (i = 0; i < num; i++)
{
sum += va_arg(valist, int);
}/* 清理为 valist 保留的内存 */
va_end(valist);
return sum/num;
}
int main()
{
printf("Average of 2, 3, 4, 5 = %f\n", average(4, 2,3,4,5));
printf("Average of 5, 10, 15 = %f\n", average(3, 5,10,15));
}
链表:
数组缺陷:数组元素类型必须一致,数组元素个数需指定并且不能更改
数组第一个缺陷由结构体解决,链表是为了解决第二个问题
链表可以看作一个元素个数可变大变小的数组。
链表是由若干个节点组成的(链表的各个节点结构是完全类似的),节点是由有效数据和指针组成的。有效数据区域用来存储信息完成任务的,指针区域用于指向链表的下一个节点从而构成链表。
链表就是用来解决数组的大小不能动态扩展的问题,所以链表其实就是当数组用的。链表用来存数据相对于数组来说优点就是灵活性,需要多少个动态分配多少个,不占用额外的内存。
链表的操作:遍历、插入、删除、逆序
单链表:链表由节点构成,节点包括有效数据和指针
定义一个struct node ;和一个头指针,用结构体创建一个节点,用头指针指向这个节点的地址,以后依次类推。
1、申请堆内存,大小为一个节点的大小(检查申请结果是否正确);2、清理申请到的堆内存;3、把申请到的堆内存当作一个新节点;4、填充新节点的有效数据和指针区域。
头指针是一个普通指针,只占4字节。头指针的类型是struct node *类型的
单链表局限性:指针只能单向移动,算法有局限性
linux内核链表:
实际项目中的链表,节点中存储的数据其实是一个结构体,这个结构体中包含若干的成员,这些成员加起来构成了我们的节点数据区域。
内核链表中自己实现了一个纯链表(纯链表就是没有数据区域,只有前后向指针)的封装,以及纯链表的各种操作函数(节点创建、插入、删除、遍历······)。这个纯链表本身自己没有任何用处,它的用法是给我们具体链表作为核心来调用。
内核中核心纯链表的实现在include/linux/list.h文件中
list.h中就是一个纯链表的完整封装,包含节点定义和各种链表操作方法。
内核链表只有纯链表,设计的使用方法是将内核链表作为将来整个数据结构的结构体的一个成员内嵌进去。利用container_of宏
状态机:
常说的状态机是有限状态机FSM。FSM指的是有有限个状态
关键点:当前状态、外部输入、下一个状态
两种状态机:Moore型和Mealy型
Moore型状态机特点是:输出只与当前状态有关(与输入信号无关)。相对简单,考虑状态机的下一个状态时只需要考虑它的当前状态就行了。
Mealy型状态机的特点是:输出不只和当前状态有关,还与输入信号有关。状态机接收到一个输入信号需要跳转到下一个状态时,状态机综合考虑2个条件(当前状态、输入值)后才决定跳转到哪个状态。
状态机的主要用途:电路设计、FPGA程序设计、软件设计(框架类型的设计,譬如操作系统的GUI系统、消息机制)
多线程:
操作系统下的并行执行机制:并行分微观上的并行和宏观上的并行。
宏观上的并行就是从长时间段(相对于人来说)来看,多个任务是同时进行的;微观上的并行就是真的在并行执行。
操作系统要求实现宏观上的并行。宏观上的并行有2种情况:第一种是微观上的串行,第二种是微观上的并行。
单核CPU本身只有一个核心,同时只能执行一条指令,这种CPU只能实现宏观上的并行,微观上一定是串行的。微观上的并行要求多核心CPU。多核CPU中的多个核心可以同时微观上执行多个指令,因此可以达到微观上的并行,从而提升宏观上的并行度。
进程和线程是操作系统的两种不同软件技术,目的是实现宏观上的并行(通俗一点就是让多个程序同时在一个机器上运行,达到宏观上看起来并行执行的效果)。
进程和线程在实现并行效果的原理上不同。而且这个差异和操作系统有关。(linux中线程就是轻量级的进程)。
最终目标都是实现并行执行。
现代操作系统设计时考虑到了多核心CPU的优化问题,保证了:多线程程序在运行的时候,操作系统会优先将多个线程放在多个核心中分别单独运行。所以说多核心CPU给多线程程序提供了完美的运行环境。所以在多核心CPU上使用多线程程序有极大的好处。
线程同步和锁
多线程程序运行时要注意线程之间的同步。
PIC位置无关码:代码与内存地址无关
位置有关码:运行地址与编译链接的地址必须相同
运行地址:程序实际执行的地址,下载的地址
链接地址:程序应当运行的地址,编译链接时指定
代码段:.text 文本段,函数、可执行代码
数据的:.data 全局非0变量
ZI段: .bss 0初始化段,初始化为0的全局变量(显式、隐式)
SECTIONS
{
. = 0xNNN; //.表示当前位置 = 表示赋值
.text:{
start.o //start.o的顺序
*(.text)
}
.data :{
*(.data)
}
bss_start = .; //当前地址,此符号可被程序引用
.bss:{
*(.bss)
}
bss_end = .; //当前地址
}
C语言需要使用堆(内存)和栈(局部变量)
&& ||,如果前面的不成立/成立,后面的不会被执行
使用C语言可使用-E -o 仅预处理来查看预处理后的内容,方便学习
尽量使用可重入函数
常用【LinuxAPI】&【c标准库】::
文件相关:
open、close、write、read、lseek(移动文件描述符)
dup2和dup:复制文件描述符
fcntl:多功能文件管理
ctime :从time_t出发想得到字符串格式的时间
localtime :把time得到的秒数变成一个struct tm结构体表示的国际时间
gmtime :把time得到的秒数变成一个struct tm结构体表示的计算机设置的所在时区本地时间
mktime :用来完成相反方向的转换(struct tm到time_t)
asctime :从struct tm出发想得到字符串格式
strftime :从struct tm出发想得到字符串格式
rand :函数可以返回一个伪随机数序列(使用前需要设置种子,种子默认是1)
srand :函数用来设置rand获取的伪随机序列的种子
atexit:注册进程终止处理函数,注 册的多个进程终止处理函数,先注册的后执行
文件属性
stat、fstat、lstat(可查符号链接本身) :查看文件属性信息
access :测试得到当前执行程序的那个用户在当前那个环境下对目标文件是否具有某种操作权限。
chmod/fchmod :权限修改
chown/fchown/lchown :属主修改
umask :新创建的文件的默认权限
目录文件
opendir 打开一个目录后得到一个DIR类型的指针给readdir使用
readdir 函数调用一次就会返回一个struct (包含了目录信息)
时间相关:
time :从jjffies得到当前时间,返回从1970-01-01 00:00:00 +0000(UTC)过去的秒数
gettimeofday :返回的时间是由struct timeval和struct timezone这两个结构体来共同表示。timeval表示时间,而timezone表示时区
settimeofday :用来设置当前的时间和时区的
正常终止:
return、exit、_exit
环境变量:
getenv:获取指定环境变量
进程相关:
getpid :获取当前进程
getppid :获取父进程
getuid :当前用户ID
geteuid :当前组id
getgid :有效用户id
getegid :有效组id
setsid :将当前进程设置为一个新的会话期session
chdir() :设置当前进程工作目录
多进程相关:
fork :创建子进程,fork后就有了一个子进程了,返回值为0的那份就是子进程,父进程的fork会返回子进程的进程ID
wait :用来回收子进程(阻塞)
waitpid :可以回收指定PID的子进程,可以阻塞式或非阻塞式
exec族函数:用来运行一个可执行程序
execl和execv :最基本的exec,都可以用来执行一个程序,区别是传参的格式不同。必须指定可执行程序的全路径
execlp和execvp :会先去环境变量PATH所指定的目录下去找,然后去去路径下找,执行程序。
execle和execvpe :执行可执行程序时会多传一个环境变量的字符串数组给待执行的程序。
system :函数 = fork+exec。原子操作。整个操作一旦开始就会不被打断的执行完,不会引起竞争态。
main函数的原型也可以是int main(int argc, char **argv, char **env),第三个参数为当前环境变量的拷贝。
进程间通讯:
pipe、write、read、close:管道通信的函数;只能在父子进程间通信
mkfifo、open、write、read、close:有名管道,任意2个进程都可
信号相关:
signal:指定信号编号处理信号的函数
sigaction:更具有可移植性,参数不是函数指针而是sigaction结构体指针
alarm函数:闹钟API,指定时间到后会有SIGALARM信号
pause函数:让当前进程暂停运行,交出CPU给其他进程去执行。(阻塞需要被信号唤醒。)
并发式IO:
select 和poll函数实现:外部阻塞式,内部非阻塞式自动轮询多路阻塞式IO
线程:
pthread_create :主线程 创建线程
pthread_join :主线程 阻塞等待回收子线程
pthread_detach :主线程分离子线程,主线程不再回收,子线程自己回收资源
pthread_cancel 一般都是主线程调用该函数去取消子线程
pthread_setcancelstate 子线程设置自己是否允许被取消
pthread_setcanceltype 子线程设置自己是被取消的模式(立即或其他)
pthread_exit与return退出 正常线程退出一般调用pthread_exit
pthread_cleanup_push 在线程中保存锁、等资源时,需要压进相应的解锁或其他函数,在线程被取消或是终止时,压进的内容会被执行
pthread_cleanup_pop 参数可以让压进的资源弹出执行或是不执行
pthread_self 获取线程id
线程同步:
信号量
sem_init :初始化信号量
sem_destroy :销毁一个信号量
sem_post :激活信号量
sem_wait :子线程判断信号量是否被激活
互斥锁
pthread_mutex_init 初始化一个互斥锁
pthread_mutex_destroy 销毁一个互斥锁
pthread_mutex_lock 上锁
pthread_mutex_unlock 解锁
条件变量
pthread_cond_init 初始化一个条件变量
pthread_cond_destroy 销毁一个条件变量
pthread_cond_wait 等待一个条件变量成立,否则阻塞
pthread_cond_signal 发条件变量,可激活一个线程
pthread_cond_broadcast 发条件变量,可激活多个线程
网络编程
建立连接
socket :类似于open,用来打开一个网络连接
bind :服务器绑定一个socket到ip地址
listen :服务器进行监听设置
accept :阻塞等待客户端接入,通过这个和客户端进行读写操作
connect :客户端连接到服务器的ip
发送和接收
send和write :发送
recv和read :接收
守护进程(任何一个进程都可以将自己实现成守护进程)
create_daemon函数要素:子进程等待父进程退出;子进程使用setsid创建新的会话期,脱离控制台;调用chdir将当前工作目录设置为/;umask设置为0以取消任何文件权限屏蔽;关闭所有文件描述符;将0、1、2定位到/dev/null
文件类型:
linux文件io
1.1 基于linux去做应用编程
通过调用linux的系统API来实现应用需要完成的任务
1.2. 文件操作API
(1)API是一些函数,这些函数是由linux系统提供支持的,由应用层程序来使用。
(2)应用层程序通过调用API来调用操作系统中的各种功能。
(3)学习一个操作系统,就是学习使用这个操作系统的API。
linux常用文件IO接口:open、close、write、read、lseek
文件操作:
linux系统中要操作一个文件,先open打开一个文件(如果打开本身失败,后面就不用操作了),得到一个文件描述符,然后对文件进行读写操作(或其他操作),最后close关闭文件
文件描述符:
文件描述符其实实质是一个数字,在一个进程中表示一个特定的含义,当我们open打开一个文件时,操作系统在内存中构建了一些数据结构来表示这个动态文件,然后返回给应用程序一个数字作为文件描述符,就是用来区分一个程序打开的多个文件的。文件描述符的作用域就是当前进程
1.3. 简单文件读写
【open】文件:linux中的文件描述符fd的合法范围是0或者一个正正数,不可能是一个负数。open返回的fd程序必须记好,这个文件所有操作都要fd去对应这个文件,关闭文件时也需要fd去指定关闭这个文件。
【read】文件内容:ssize_t read(int fd, void buf, size_t count);
fd表示要读取哪个文件,一般由前面的open返回得到,buf是应用程序自己提供的一段内存缓冲区,用来存储读出的内容,count是我们要读取的字节数,返回值ssize_t类型是linux内核用typedef重定义的一个类型(int),返回值表示成功读取的字节数。
【write】文件内容:用write系统调用,write的原型和理解方法和read相似
【close】文件:参数为fd
----open函数的flag:
读写权限:【O_RDONLY】表示只读打开,【O_WRONLY】表示只写打开,【O_RDWR】表示可读可写打开
【O_APPEND】、【O_TRUNC】:O_TRUNC属性去打开文件时,如果这个文件中本来是有内容的,则原来的内容会被丢弃。O_APPEND属性去打开文件时,如果这个文件中本来是有内容的,则新写入的内容会接续到原来内容的后面。若同时出现O_TRUNC起作用。
exit、_exit、_Exit退出进程,正式终止进程(程序)应该使用exit或者_exit或者_Exit之一。
打开不存在的文件时:【O_CREAT、O_EXCL】
open中加入O_CREAT后,不管原来这个文件存在与否都能打开成功,如果原来这个文件不存在则创建一个空的新文件,如果原来这个文件存在则会重新创建这个文件,原来的内容会被消除掉
O_EXCL标志和O_CREAT标志来结合使用,没有文件时创建文件,有这个文件时会报错提醒我们。可用open第三个参数[mode]来指定要创建的文件的权限。mode使用4个数字来指定权限的如0777
1.4 阻塞与非阻塞(只用在设备文件,而不是普通文件)
如果一个函数是阻塞式的,则调用这个函数时当前进程有可能被卡住,函数被阻塞住了就不能立刻返回;如果一个函数是非阻塞式的那么我们调用这个函数后一定会立即返回,但是函数有没有完成任务不一定。
阻塞式的结果有保障但是时间没保障;非阻塞式的时间有保障但是结果没保障。
操作系统提供的API和由API封装而成的库函数,有很多本身就是被设计为阻塞式或者非阻塞式的
打开文件默认是阻塞式的,要以非阻塞的方式打开文件,则open中要加【O_NONBLOCK】标志
open的【O_SYNC】标志:无它时,write只是将内容写入底层缓冲区而不是硬件,有这个标志会阻塞等待底层写入硬件才返回
1.5 文件读写细节
【errno】和【perror】:
linux系统中对各种常见错误做了个编号,当函数执行错误时,函数会返回一个特定的errno编号来告诉我们这个函数到底哪里错了。
errno是由OS来维护的一个全局变量(编号),任何OS内部函数都可以通过设置errno
linux系统提供了一个函数perror,函数内部会读取errno并将这个编号对应的错误信息字符串输出打印
read和write的count
count参数表示我们想要写或者读的字节数,返回值表示实际完成的要写或者读的字节数。实现的有可能等于想要读写的,也有可能小于
要读取或者写入的是一个很庞大的文件,不可能把count设置为21024*1024,而应把count设置为一个合适的数字(譬如2048、4096),通过多次读取来实现全部读完
文件IO效率和标准IO
文件IO就指的是API函数构成的一套用来读写文件的体系
应用层C语言库函数提供了一些用来做文件读写的函数列表,叫标准IO,标准IO函数其实是由文件IO封装而来的。标准IO加了封装之后主要是为了在应用层添加一个缓冲机制,标准IO库自己根据操作系统单次write的最佳count来选择好的时机来完成write到内核中的buf
1.6 linux如何管理文件
硬盘中的静态文件和inode(i节点)
文件平时都在存放在硬盘中的,硬盘中存储的文件以一种固定的形式存放的,叫静态文件。
硬盘中可以分为两大区域:
硬盘内容管理表项:以文件为单位记录了各个文件的各种信息,每一个文件有一个信息列表(我们叫inode,i节点,其实质是一个结构体,这个结构体有很多元素,每个元素记录了这个文件的一些信息,其中就包括文件名、对应的扇区号、块号·····)
内容存储的区域
硬盘管理的时候是以文件为单位的,每个文件一个inode,每个inode有一个数字编号,对应一个结构体,结构体中记录了各种信息。操作系统最初拿到的信息是文件名,最终得到的是文件内容。
内存中被打开的文件和vnode(v节点)
一个程序的运行就是一个进程,每个进程都有一个数据结构用来记录这个进程的所有信息【进程信息表】,表中有一个指针会指向一个文件管理表,文件管理表中记录了当前进程打开的所有文件及其相关信息。
【文件管理表】中用来索引各个打开的文件的index就是文件描述符fd,我们最终找到的就是一个已经被打开的文件的管理结构体vnode
我们只要知道这个文件的fd,就可以很容易的找到这个文件的vnode进而对这个文件进行各种操作。
文件与流:
【(IO)流】(stream):文件被读出/写入时都只能一个字符一个字符的进行,一个文件中N多的个字符被挨个一次读出/写入时,这些字符就构成了一个字符流。
一般都是IO相关的,常叫IO流。文件操作时就构成了一个IO流。
1.7 lseek详解
打开读写一个文件时,操作的是动态文件,在内存中以文件流形式存在
动态文件中,我们会通过文件指针来表征这个正在操作的位置。
【文件指针】是我们文件管理表这个结构体里面的一个指针。是vnode中的一个元素。这个指针表示当前我们正在操作文件流的哪个位置。这个指针【不能被直接访问】
linux系统用【lseek】函数来访问这个文件指针。
write和read函数本身自带移动文件指针的功能。read和write函数都是从当前文件指针处开始操作的
linux中并没有一个函数可以直接返回一个文件的长度,利用lseek来写一个函数得到文件长度即可。
lseek构建空洞文件:
空洞文件就是这个文件中有一段是空的,普通文件中间是不能有空的,用lseek往后跳过一段,再write写入一段,就会构成一个空洞文件。
空洞文件方法对多线程共同操作文件是及其有用的。有时候我们创建一个很大的文件,如果从头开始依次构建时间很长。有一种思路就是将文件分为多段,然后多线程来操作每个线程负责其中一段的写入。(如迅雷下载)
1.8 多次打开同一文件,O_APPEND
重复打开同一文件读写
一个进程中两次打开同一个文件,然后分别读取。一种是fd1和fd2分别读写,第二种是接续读。实际是分别读取fd1和fd2所对应的文件指针是不同的2个独立的指针。
有时候我们希望接续写而不是分别写,在open时加O_APPEND标志即可
加了O_APPEND,fd1和fd2还是各自拥有一个独立的文件指针,但是这两个文件指针关联起来了
O_APPEND对文件指针的影响,对文件的读写是原子的。
原子操作的含义是:整个操作一旦开始是不会被打断的,必须直到操作结束其他代码才能得以调度运行。每种操作系统中都有一些机制来实现原子操作
1.9 文件共享实现方式
文件共享就是同一个文件(同一个文件指的是同一个inode,同一个pathname)被多个独立的读写体,同时(一个打开尚未关闭的同时另一个去操作)操作。
可以通过文件共享来实现多线程同时操作同一个大文件,以减少文件读写时间,提升效率。
文件共享的3种实现方式
第一种是同一个进程中多次使用open打开同一个文件
第二种是在不同进程中去分别使用open打开同一个文件(这时候因为两个fd在不同的进程中,所以两个fd的数字可以相同也可以不同)
第三种情况是linux系统提供了dup和dup2两个API来让进程复制文件描述符。
文件共享时的核心关注点在于:分别写/读还是接续写/读.根据需要选择
1.10. 文件描述符
文件描述符的本质是一个数字,这个数字本质上是进程表中文件描述符表的一个表项,进程通过文件描述符作为index去索引查表得到文件表指针,再间接访问得到这个文件对应的文件表。
文件描述符这个数字是open系统调用内部由操作系统自动分配的
操作系统规定,fd从0开始依次增加。fd也是有最大限制的,文件描述符表其实就是一个数组,fd是index,文件表指针是value
open时,内核会从文件描述符表中挑选一个最小的未被使用的数字给我们返回。
fd中0、1、2已经默认被系统占用了,这三个文件分别叫stdin、stdout、stderr。
printf函数其实就是默认输出到标准输出stdout上了。stdio中还有一个函数叫fpirntf,这个函数就可以指定输出到哪个文件描述符中。
dup和dup2函数介绍:
dup系统调用对fd进行复制,会返回一个新的文件描述符
dup系统调用不能指定复制后得到的fd的数字是多少,dup2系统调用修复了这个缺陷
dup2和dup的作用是一样的,都是复制一个新的文件描述符。但是dup2允许用户指定新的文件描述符的数字。
命令行中重定位命令 【>】
linux中的shell命令执行后,打印结果都是默认进入stdout的。定位的符号>把ls、pwd等命令的输出给重定位到一个文件中
实现原理,其实就是利用open+close+dup,open
1.11fcntl函数
fcntl函数是一个多功能文件管理的工具箱,接收2个参数+1个变参。对文件的一些操作
第一个参数是fd表示要操作哪个文件,第二个参数是cmd表示要进行哪个命令操作。变参是用来传递参数的,要配合cmd来使用。
1.12标准IO库
【标准IO】是【C库函数】,而【文件IO】是linux系统的【API】
C库函数具有可移植性而API不具有可移植性。C语言库函数是由API封装而来的。
性能上和易用性上看,C库函数一般要好一些。譬如IO,文件IO是不带缓存的,而标准IO是带缓存的,因此标准IO比文件IO性能要更高
文件属性
2.1 linux文件类型(一切皆文件)
普通文件【-】:文本文件&二进制文件
文本文件:字符集合的文件。本质内容是数字。如.h .c .txt等文件
二进制文件:存储的也是数字。常见的是可执行程序
区分:linux操作系统不区分,只能本身知道文件类型,使用该文件的用法去用。有时候用后缀名人为标记
目录文件【d】:linux下目录是一种特殊文件
内容包括 文件路径 文件列表等等
linux有API来操作目录文件
设备文件:字符设备文件【c】&块设备文件【b】
设备文件对应硬件设备,并不是真正在硬盘上的文件,文件系统虚拟制造。
设备文件需要使用API产生或使用。
管道文件【p】:通讯手段
套接字文件【s】:网络编程使用
符号链接文件【l】:软硬链接
2.2 文件属性
linux shell命令【stat】:可得到文件属性
API函数:stat、fstat、lstat 文件属性获取
stat这个API的作用就是让内核将我们要查找属性的文件的属性信息结构体的值放入我们传递给stat函数的buf中,当stat这个API调用从内核返回的时候buf中就被填充了文件的正确的属性信息
stat是从文件名出发得到文件属性信息结构体,而fstat是从一个已经打开的文件fd出发得到一个文件的属性信息。
对于符号链接文件,stat和fstat查阅的是符号链接文件指向的文件的属性,而lstat查阅的是符号链接文件本身的属性。
struct stat结构体:在
这个结构体中的所有元素加起来就是我们的文件属性信息。
文件权限:属主(owner、user)、组(group)、其他用户(others)
access API函数:测试得到当前执行程序的那个用户在当前那个环境下对目标文件是否具有某种操作权限。
chmod shell:用来修改文件的各种权限属性。只有root用户才有权利去执行修改。
chmod的API:修改文件的各种权限属性
chown shell: 修改文件属主
chown/fchown/lchown API:修改文件属主
umask与文件权限掩码:用来设定我们系统中新创建的文件的默认权限的。
umask命令就是用umask API实现的
opendir与readdir API函数:读取目录文件
readdir调用一次只能读出一个目录项,要想读出目录中所有的目录项必须多次调用readdir函数。readdir函数内部户记住哪个目录项已经被读过了哪个还没读,所以多次调用后不会重复返回已经返回过的目录项。当readdir函数返回NULL时就表示目录中所有的目录项已经读完了。
可重入函数:有些函数是可重入的有些是不可重入的
一个可重入的函数简单来说就是可以被中断的函数,也就是说,可以在这个函数执行的任何时刻中断它,转入OS调度下去执行另外一段代码,而返回控制时不会出现什么错误;
不可重入的函数由于使用了一些系统资源,比如全局变量区,中断向量表等,所以它如果被中断的话,可能会出现问题,这类函数是不能运行在多任务环境下的。
C库,提供了对应的可重复版本(一般是不可重入版本函数名_r)
获取系统信息
时间:
GMT时间:(以前使用)国际时间(格林尼治)。一般一个国家统一时间统一使用一个当地时间
UTC时间:UTC时间是0时区时间,Date: Fri, 08 Nov 2002 09:42:22 +0800 【表示一个北京时间9:42】
【UTC+时区差=本地时间】UTC +0800就表示背景时区时间(东八区)
点时间和段时间:时间点 和 时间段 段时间=时间点-时间点
定时器(timer)与实时时钟(RTC): 定时器定的是段时间,RTC是和点时间相关的
linux系统中的时间
jiffies:linux内核的全局变量,jiffies记录以(内核节拍)为单位长度的数值。
内核配置了一个节拍时间,linux内核调度系统工作时,以节拍时间为时间片的。
jiffies-jiffies开机的基准值=开机经过了多少节拍 *每个节拍的时间 =开机了多长时间
内核开机时会读取RTC硬件获取一个时间作为初始基准时间,对应一个jiffies值。(RTC时间-1970-01-01 00:00:00(UTC),再换算为jiffies值作为基准值)。系统运行一个节拍,就会将jiffies+1
当前时间=jiffies对应的时间点+1970-01-01 00:00:00(UTC)
一个时钟节拍取决于系统配置,一般10ms或1ms,就是调度时间。内核用Hz表示
time :从jjffies得到当前时间,返回从1970-01-01 00:00:00 +0000(UTC)过去的秒数
ctime :从time_t出发想得到字符串格式的时间
localtime :把time得到的秒数变成一个struct tm结构体表示的国际时间
gmtime :把time得到的秒数变成一个struct tm结构体表示的计算机设置的所在时区本地时间
mktime :用来完成相反方向的转换(struct tm到time_t)
asctime :从struct tm出发想得到字符串格式
strftime :从struct tm出发想得到字符串格式
gettimeofday :返回的时间是由struct timeval和struct timezone这两个结构体来共同表示。timeval表示时间,而timezone表示时区
settimeofday :用来设置当前的时间和时区的
随机数:
真正的完全随机的数列是不存在的,只是一种理想情况。我们平时要用到随机数时一般只能通过一些算法得到一个伪随机数序列。
rand函数可以返回一个伪随机数序列(使用前需要设置种子,种子默认是1)
srand函数用来设置rand获取的伪随机序列的种子
操作系统级别的调试
(1)简单程序单步调试
(2)复杂程序printf打印信息调试
(3)框架体系日志记录信息调试
(4)内核调试的
proc文件系统内核调试:
在内核中构建一个虚拟文件系统/proc,内核运行时将内核中一些关键的数据结构以文件的方式呈现在/proc目录中的一些特定文件中,这样相当于将不可见的内核中的数据结构以可视化的方式呈现给内核的开发者。
通过实时的观察/proc/xxx文件,来观看内核中特定数据结构的值。来调试内核操作系统
proc目录下的文件大小都是0,因为这些文件本身并不存在于硬盘中,不是一个真实文件,只是一个接口,去读取这个文件时,内核并不是去硬盘上找这个文件,而是映射为内核内部一个数据结构被读取并且格式化成字符串返回给我们。
常用proc中的文件介绍
(1)/proc/cmdline
(2)/proc/cpuinfo
(3)/proc/devices
(4)/proc/interrupts
使用:cat以手工查看
程序中可以文件IO访问
shell程序中用cat命令结合正则表达式来获取并处理内核信息
sys文件系统:
本质上和proc文件系统是一样的,都是虚拟文件系统(一个是/proc目录,另一个是/sys目录)
/proc中的文件只能读,但是/sys中的文件可以读写。
/sys是/proc的升级版,/sys目录使用有一定规则
进程
main函数
编译链接时的引导代码。操作系统下的应用程序其实在main执行前也需要先执行一段引导代码才能去执行main,我们写应用程序时不用考虑引导代码的问题,链接时由链接器将编译器中事先准备好的引导代码给连接进去和我们的应用程序一起构成最终的可执行程序。
加载器是操作系统中的程序,当我们去执行一个程序时加载器负责将这个程序加载到内存中去执行这个程序。
程序如何结束
正常终止:return、exit、_exit
非正常终止:自己或他人发信号终止进程
atexit注册进程终止处理函数
atexit注册多个进程终止处理函数,先注册的后执行
return和exit效果一样,都是会执行进程终止处理函数,但是用_exit终止进程时并不执行atexit注册的进程终止处理函数。
环境变量
export命令查看环境变量
每一个进程中都有一份所有环境变量构成的一个表格,也就是说我们当前进程中可以直接使用这些环境变量。进程环境表其实是一个字符串数组,用environ变量指向它。程序中通过environ全局变量使用环境变量
获取指定环境变量函数getenv
进程运行的虚拟地址空间
操作系统中每个进程在独立地址空间中运行,每个进程的逻辑地址空间均为4GB(32位系统),0-1G为OS,1-4G为应用,虚拟地址到物理地址空间的映射
意义:进程隔离,提供多进程同时运行
进程:(动态过程而不是静态实物)程序的一次运行过程
进程控制块(process control block),内核中专门用来管理一个进程的数据结构。
进程ID(PID):shell 【ps (-a/aux)】可查看
getpid :获取当前进程
getppid :获取父进程
getuid :当前用户ID
geteuid :当前组id
getgid :有效用户id
getegid :有效组id
多进程调度
操作系统同时运行多个进程,宏观上的并行和微观上的串行,现代操作系统最小的调度单元是线程而不是进程
fork创建子进程
如果操作系统需要一个新进程来运行一个程序,那么操作系统会用一个现有的进程来复制生成一个新进程。老进程叫父进程,复制生成的新进程叫子进程。
fork函数调用一次会在父进程返回一次,子进程返回一次,使用fork后然后用if判断返回值,并且返回值大于0时就是父进程,等于0时就是子进程。fork的返回值在子进程中等于0,在父进程中等于本次fork创建的子进程的进程ID。
子进程继承父进程中打开的文件。父子进程各自独立打开同一文件实现共享(O_APPEND)
父进程在没有fork之前自己做的事情对子进程有很大影响,但是父进程fork之后在自己的if里做的事情就对子进程没有影响了。fork内部实际上已经复制父进程的PCB生成了一个新的子进程,并且fork返回时子进程已经完全和父进程脱离并且独立被OS调度执行。子进程最终目的是要独立去运行另外的程序
进程的诞生和消亡
进程0和进程1,fork(复制生成一个子进程),vfork
正常终止和异常终止
进程在运行时需要消耗系统资源(内存、IO),进程终止时理应完全释放这些资源
linux系统设计时规定:每一个进程退出时,操作系统会自动回收这个进程涉及到的所有的资源。但是操作系统只是回收了这个进程工作时消耗的内存和IO,而并没有回收这个进程本身占用的内存(8KB,主要是task_struct和栈内存),每个进程都需要一个帮助它收尸的人,这个人就是这个进程的父进程。
僵尸进程:子进程先于父进程结束。子进程结束后父进程此时并不一定立即就能帮子进程“收尸”,在这一段子进程就被成为僵尸进程。(子进程除task_struct和栈外其余内存空间皆已清理)父进程可以使用wait或waitpid以显式回收子进程的剩余待回收内存资源并且获取子进程退出状态。父进程也可以不使用wait或者waitpid回收子进程,此时父进程结束时一样会回收子进程的剩余待回收内存资源。
孤儿进程:父进程先于子进程结束,子进程成为一个孤儿进程。
linux系统规定:所有的孤儿进程都自动成为一个特殊进程(进程1,也就是init进程)的子进程。
父进程wait回收子进程:
子进程结束时,系统向其父进程发送SIGCHILD信号,父进程调用wait函数后阻塞,父进程被SIGCHILD信号唤醒然后去回收僵尸子进程.父子进程之间是异步的,SIGCHILD信号机制就是为了解决父子进程之间的异步通信问题.若父进程没有任何子进程则wait返回错误
wait函数:用来回收子进程(阻塞)
waitpid函数:可以回收指定PID的子进程,可以阻塞式或非阻塞式
竟态:竞争状态,多进程环境下,多个进程同时抢占系统资源(内存、CPU、文件IO)
竞争状态对OS来说是很危险的,此时OS如果没处理好就会造成结果不确定。
写程序时要尽量消灭竞争状态。操作系统给我们提供了一系列的消灭竟态的机制,我们需要做的是在合适的地方使用合适的方法来消灭竟态。
【exec族函数】
fork子进程是为了执行新程序.可以直接在子进程的if中写入新程序的代码,也可以使用exec族运行新的可执行程序
典型的父子进程程序是这样的:子进程需要运行的程序被单独编写、单独编译连接成一个可执行程序,主程序为父进程,fork创建了子进程后在子进程中exec来执行子程序,达到父子进程分别做不同程序同时(宏观上)运行的效果。
execl和execv
最基本的exec,都可以用来执行一个程序,区别是传参的格式不同。execl是把参数列表(本质上是多个字符串,必须以NULL结尾)依次排列而成,execv是把参数列表事先放入一个字符串数组中,再把这个字符串数组传给execv函数。实际上应当至少传一个参数,参数0可以写为文件名或路径
execlp和execvp
上面2个执行程序时必须指定可执行程序的全路径(如果exec没有找到path这个文件则直接报错),而加了p的传递的可以是file(也可以是path,只不过兼容了file。加了p的这两个函数会先去环境变量PATH所指定的目录下去找,然后去去路径下找,如果找到则执行如果没找到则报错)
execle和execvpe
函数的参数列表较上面中多了一个字符串数组envp形参,执行可执行程序时会多传一个环境变量的字符串数组给待执行的程序。
main函数的原型也可以是int main(int argc, char **argv, char **env),第三个参数为当前环境变量的拷贝。
用户在执行这个程序时没有传递第三个参数,则程序会自动从父进程继承一份环境变量(默认的,最早来源于OS中的环境变量);如果我们exec的时候使用execlp或者execvpe去给传一个envp数组,则程序中的实际环境变量是我们传递的这一份(取代了默认的从父进程继承来的那一份)
进程状态
(1)就绪态。这个进程当前所有运行条件就绪,得到了CPU时间就能直接运行。
(2)运行态。就绪态时得到了CPU就进入运行态开始运行。
(3)僵尸态。进程已经结束但是父进程还没来得及回收
(4)等待态(浅度睡眠&深度睡眠),进程在等待某种条件,条件成熟后可进入就绪态。等待态下给他CPU调度进程也无法执行。浅度睡眠等待时进程可以被(信号)唤醒,而深度睡眠等待时不能被唤醒只能等待的条件到了才能结束睡眠状态。
(5)暂停态。暂停并不是进程的终止,只是被被人(信号)暂停了,还可以恢复的。
进程状态切换-》图
system函数 = fork+exec
原子操作。原子操作意思就是整个操作一旦开始就会不被打断的执行完。原子操作的好处就是不会被人打断(不会引来竞争状态),坏处是自己单独连续占用CPU时间太长影响系统整体实时性,因此应该尽量避免不必要的原子操作,尽量使原子操作的时间缩短。
进程关系
(1)无关系
(2)父子进程关系
(3)进程组(group)由若干进程构成一个进程组
(4)会话(session)会话就是进程组的组
守护进程
进程查看命令ps -xxx
ps -ajx 偏向显示各种有关的ID号
ps -aux 偏向显示进程各种占用资源
向进程发送信号指令kill
kill -信号编号 进程ID,向一个进程发送一个信号
如:kill -9 xxx,将向xxx这个进程发送9号信号,也就是要结束进程
常见守护进程
syslogd,系统日志守护进程,提供syslog功能。
cron,用来实现操作系统的时间管理,linux中实现定时执行程序的功能就要用到
简单守护进程(任何一个进程都可以将自己实现成守护进程)
create_daemon函数要素
(1)子进程等待父进程退出
(2)子进程使用setsid创建新的会话期,脱离控制台
(3)调用chdir将当前工作目录设置为/
(4)umask设置为0以取消任何文件权限屏蔽
(5)关闭所有文件描述符
(6)将0、1、2定位到/dev/null
void create_daemon(void)
{
pid_t pid = 0;
pid = fork();
if (pid < 0)
{
perror(“fork”);
exit(-1);
}
if (pid > 0)
{
exit(0); // 父进程直接退出
}
// 执行到这里就是子进程
// setsid将当前进程设置为一个新的会话期session,目的就是让当前进程 脱离控制台。
pid = setsid();
if (pid < 0)
{
perror(“setsid”);
exit(-1);
}
// 将当前进程工作目录设置为根目录
chdir("/");
// umask设置为0确保将来进程有最大的文件操作权限
umask(0);
// 关闭所有文件描述符
// 先要获取当前系统中所允许打开的最大文件描述符数目
int cnt = sysconf(_SC_OPEN_MAX);
int i = 0;
for (i=0; i
close(i);
}
open("/dev/null", O_RDWR);
open("/dev/null", O_RDWR);
open("/dev/null", O_RDWR);
}
使用syslog来记录调试信息(openlog、syslog、closelog)
一般log信息都在操作系统的/var/log/messages这个文件中存储着,但是ubuntu中是在/var/log/syslog文件中的。
操作系统中有一个守护进程syslogd(开机运行,关机时才结束),这个守护进程syslogd负责进行日志文件的写入和维护。任何需要写日志的进程都可以通过openlog/syslog/closelog这三个函数来利用syslogd提供的日志服务。这就是操作系统的服务式的设计。
单例运行程序
守护进程一般都是服务器,服务器程序只要运行一个就够了,多次同时运行并没有意义甚至会带来错误。
最常用的一种方法就是:用一个文件的存在与否来做标志。具体做法是程序在执行之初去判断一个特定的文件是否存在,若存在则标明进程已经在运行,若不存在则标明进程没有在运行。然后运行程序时去创建这个文件。当程序结束的时候去删除这个文件即可。这个特定文件要古怪一点,确保不会凑巧真的在电脑中存在的
IPC(进程间通讯):指的是2个任意进程之间的通信。
每个进程都在独立的虚拟地址中运行,故不同的进程间通讯困难
99%的程序是不需要考虑进程间通信的。因为大部分程序都是单进程的。常见的如GUI、服务器、大型程序才需要。
IPC机制
管道(无名管道):只能父子进程,半双工,类似读写文件
(1)管道通信的原理:内核维护的一块内存,有读端和写端(管道是单向通信的)
(2)管道通信的方法:父进程创建管理后fork子进程,子进程继承父进程的管道fd
(3)管道通信的限制:只能在父子进程间通信、半双工(实际使用单工)
(4)管道通信的函数:pipe、write、read、close
有名管道(fifo)只能本机内进程通讯,半双工,类似读写文件
(1)有名管道的原理:实质也是内核维护的一块内存,表现形式为一个有名字的文件
(2)有名管道的使用方法:固定一个文件名,2个进程分别使用mkfifo创建fifo文件,然后分别open打开获取到fd,然后一个读一个写
(3)管道通信限制:半双工(注意不限父子进程,任意2个进程都可)
(4)管道通信的函数:mkfifo、open、write、read、close
SystemV IPC:系统通过一些专用API来提供SystemV IPC功能,实质也是内核提供的公共内存
消息队列
(1)本质上是一个队列,队列可以理解为(内核维护的一个)FIFO
(2)工作时A和B2个进程进行通信,A向队列中放入消息,B从队列中读出消息。
信号量
(1)实质就是个计数器(相当一个可以共同用来计数的变量)
(2)通过计数值来提供互斥和同步
共享内存
(1)大片内存直接映射
(2)类似于LCD显示时的显存用法
信号:
域套接字socket:进程间通讯、网络通讯
信号:信号是内容受限的异步通讯机制
信号原理:
目的是用来进程间通讯的,是异步的,能传输内容较少,本质是int型编号(事先定义好的)
信号的发出:用户中断按键;硬件异常,内核发出信号;用户kill命令/程序kill函数发送;软件条件满足后发送信号
信号的接收:
忽略信号;捕获信号(信号绑定了一个函数);默认处理(忽略或终止程序);
常见信号:
SIGINT 2 Ctrl+C时OS送给前台进程组中每个进程
SIGABRT 6 调用abort函数,进程异常终止
SIGPOLL SIGIO 8 指示一个异步IO事件,在高级IO中提及
SIGKILL 9 杀死进程的终极办法
SIGSEGV 11 无效存储访问时OS发出该信号
SIGPIPE 13 涉及管道和socket
SIGALARM 14 涉及alarm函数的实现
SIGTERM 15 kill命令发送的OS默认终止信号
SIGCHLD 17 子进程终止或停止时OS向其父进程发此信号
SIGUSR1 10 用户自定义信号,作用和意义由应用自己定义
SIGUSR2 12
进程信号处理:
用signal函数处理SIGINT信号
signal返回值:如果出错;之前绑定的函数指针
参数:信号,要绑定处理的函数指针
简单好用,捕获信号常用,无法简单直接得知之前设置的对信号的处理方法
sigaction函数
sigaction比signal更具有可移植性,参数不是函数指针而是sigaction结构体指针
可以一次得到设置新捕获函数和获取旧的捕获函数(还可以单独设置新的捕获或者单独只获取旧的捕获函数),而signal函数不能单独获取旧的捕获函数而必须在设置新的捕获函数的同时才获取旧的捕获函数。
alarm函数:闹钟API,指定时间到后会有SIGALARM信号
pause函数:让当前进程暂停运行,交出CPU给其他进程去执行。当前进程会表现为“卡住、阻塞住”,要退出pause状态当前进程需要被信号唤醒。
可以使用alarm和pause来模拟sleep
高级IO
阻塞与非阻塞IO
常见的阻塞:wait、pause、sleep等函数;read或write某些文件时
实现非阻塞IO访问:O_NONBLOCK和fcntl函数操作
并发式IO的解决方案
非阻塞式IO
IO多路复用(IO multiplexing):
select和poll函数实现:外部阻塞式,内部非阻塞式自动轮询多路阻塞式IO
异步通知(异步IO):几乎可以认为:异步IO就是操作系统用软件实现的一套中断响应系统
工作方法是:当前进程注册一个异步IO事件(使用signal注册一个信号SIGIO的处理函数),然后当前进程可以正常处理自己的事情,当异步事件发生后当前进程会收到一个SIGIO信号从而执行绑定的处理函数去处理这个异步事件。
函数:fcntl(F_GETFL、F_SETFL、O_ASYNC、F_SETOWN)
signal或者sigaction(SIGIO)
存储映射IO
mmap函数
LCD显示和IPC之共享内存
存储映射IO的特点
(1)共享而不是复制,减少内存操作
(2)处理大文件时效率高,小文件不划算
6.线程
(pthread相关的man手册。安装方法:1、虚拟机上网;2、sudo apt-get install manpages-posix-dev)
多进程可以用来实现并发:CPU分时复用,单核CPU可实现宏观并行。可以用来实现多任务。
劣势:进程间切换开销大,进程间通讯麻烦且效率低
线程技术:
保留了进程所实现的多任务特性,像进程一样可被OS调度。
改进了切换和通讯的效率,同一进程的多个线程通讯效率高。
在多核CPU(对称多处理器架构SMP)效率最大化。
特点:线程相当轻量级进程,是参与内核调度的最小单元,一个进程可以有多个线程
线程常见函数:用到了pthread静态库,故gcc时 加-lpthread
线程创建与回收
pthread_create :主线程 创建线程
pthread_join :主线程 阻塞等待回收子线程
pthread_detach :主线程分离子线程,主线程不再回收,子线程自己回收资源
线程创建后应当在两个回收资源方式中选择一个
线程取消
pthread_cancel 一般都是主线程调用该函数去取消子线程
pthread_setcancelstate 子线程设置自己是否允许被取消
pthread_setcanceltype 子线程设置自己是被取消的模式(立即或其他)
线程函数退出相关
pthread_exit与return退出 正常线程退出一般调用pthread_exit
pthread_cleanup_push 在线程中保存锁、等资源时,需要压进相应的解锁或其他函数,在线程被取消或是终止时,压进的内容会被执行
pthread_cleanup_pop 参数可以让压进的资源弹出执行或是不执行
获取线程id
pthread_self
线程同步——
信号量
子线程被阻塞,主程序可以激活,这是线程同步问题。
信号量:应当是全局变量
sem_init :初始化信号量
sem_destroy :销毁一个信号量
sem_post :激活信号量
sem_wait :子线程判断信号量是否被激活
互斥锁又叫互斥量(mutex):可以认为互斥锁是一种特殊的信号量,主要用来实现关键段保护
相关函数:pthread_mutex_init 初始化一个互斥锁
pthread_mutex_destroy 销毁一个互斥锁
pthread_mutex_lock 上锁
pthread_mutex_unlock 解锁
条件变量
pthread_cond_init 初始化一个条件变量
pthread_cond_destroy 销毁一个条件变量
pthread_cond_wait 等待一个条件变量成立,否则阻塞
pthread_cond_signal 发条件变量,可激活一个线程
pthread_cond_broadcast 发条件变量,可激活多个线程
网络基础
网络域套接字socket,网络通信其实就是位于网络中不同主机上面的2个进程之间的通信。
网络通信的层次
(1)硬件部分:网卡
(2)操作系统底层:网卡驱动
(3)操作系统API:socket接口
(4)应用层:低级(直接基于socket接口编程)
(5)应用层:高级(基于网络通信应用框架库)
(6)应用层:更高级(http、网络控件等)
网络通信的发展历程
(1)单机阶段
(2)局域网阶段
(3)广域网internet阶段
(4)移动互联网阶段
(5)物联网阶段
三大网络:电信网、电视网络、互联网
网络通信的传输媒介
无线传输:WIFI、蓝牙、zigbee、4G/5G/GPRS等
有线通信:双绞线、同轴电缆、光纤等
**OSI 7层网络模型:物理层,数据链路层,网络层,传输层,会话层,表示层和应用层。
物理层(Physical Layer)
实际上就是布线、光纤、网卡和其它用来把两台网络通信设备连接在一起的东西。
数据链路层(Data Link Layer)
运行以太网等协议。交换机可以看成网桥,网桥都在这层工作,仅关注以太网上的MAC地址。数据链路层把数据帧转换成二进制位供物理层处理
网络层(Network Layer)
网络层的任务就是选择合适的网间路由和交换结点, 确保数据及时传送。网络层将数据链路层提供的帧组成数据包,包中封装有网络层包头,其中含有逻辑地址信息- -源站点和目的站点地址的网络地址。IP是网络层问题的一部分,此外还有一些路由协议和地址解析协议(ARP)。有关路由的一切事情都在第3层处理。地址解析和路由是网络层的重要目的。
处理信息的传输层(Transport Layer)
负责获取全部信息,提供端对端的通信管理。
数据单元也称作数据包(packets),TCP等具体的协议时又有特殊的叫法,TCP的数据单元称为段(segments)而UDP协议的数据单元称为“数据报(datagrams)
会话层( Session Layer)
会话层及以上的高层次中,数据传送的单位不再另外命名,统称为报文。会话层不参与具体的传输,它提供包括访问验证和会话管理在内的建立和维护应用之间通信的机制。如服务器验证用户登录便是由会话层完成的。
表示层(Presentation Layer)
解决用户信息的语法表示问题。将欲交换的数据从适合于某一用户的抽象语法,转换为适合于OSI系统内部使用的传送语法。即提供格式化的表示和转换数据服务。数据的压缩和解压缩, 加密和解密等工作都由表示层负责。
“应用层”(Application Layer)
确定进程之间通信的性质以满足用户需要以及提供网络与用户应用软件之间的接口服务。SMTP、DNS和FTP都是7层协议。
网卡
(1)计算机上网必备硬件设备,CPU靠网卡来连接外部网络
(2)串转并设备
(3)数据帧封包和拆包
(4)网络数据缓存和速率适配
集线器(HUB)
(1)信号中继放大,相当于中继器
(2)组成局域网络,用广播方式工作。
(3)注意集线器是不能用来连接外网的
交换机
(1)包含集线器功能,但更高级
(2)交换机中有地址表,数据包查表后直达目的通信口而不是广播
(3)找不到目的口时广播并学习
路由器
路由器是局域网和外部网络通信的出入口,将整个internet划分成一个个的局域网,却又互相联通。
路由器对内管理子网(局域网),可以在路由器中设置子网的网段,设置有线端口的IP地址,设置dhcp功能等,因此局域网的IP地址是路由器决定的。
路由器对外实现联网,联网方式取决于外部网络(如ADSL拨号上网、宽带帐号、局域网等)。这时候路由器又相当于是更高层级网络的其中一个节点而已。
路由器相当于有2个网卡,一个对内做网关、一个对外做节点。
路由器的主要功能是为经过路由器的每个数据包寻找一条最佳路径(路由)并转发出去。其实就是局域网内电脑要发到外网的数据包,和外网回复给局域网内电脑的数据包。
路由器技术是网络中最重要技术,决定了网络的稳定性和速度。
DNS(Domain Name Service 域名服务)
IP地址难记、不直观,域名用来代替IP地址
DNS服务器就是专门提供域名和IP地址之间的转换的服务的
访问一个网站的流程是:先使用IP地址访问DNS服务器IP地址,查询我们要访问的域名的IP地址,然后再使用该IP地址访问我们真正要访问的网站。这个过程被浏览器封装屏蔽,其中使用的就是DNS协议。
DHCP(dynamic host configuration protocl,动态主机配置协议)
动态分配是局域网内的DHCP服务器来协调的,很多设备都能提供DHCP功能,方便接入和断开、有限的IP地址得到充分利用
NAT(network address translation,网络地址转换协议)
IP地址分为公网IP(internet范围内唯一的IP地址)和私网IP(内网IP),局域网内使用的都是私网IP。
网络通信的数据包中包含有目的地址的IP地址当局域网中的主机要发送数据包给外网时,路由器要负责将数据包头中的局域网主机的内网IP替换为当前局域网的对外外网IP。这个过程就叫NAT。缓解IPv4的IP地址不够用问题,IPv6解决了这个问题
IPv4地址分类
IP地址实际是一个32位二进制构成,在网络通信数据包中就是32位二进制,而在人机交互中使用点分十进制方式显示。IP地址 = 网络地址 + 主机地址
IP地址中32位实际包含2部分,分别为:网络地址和主机地址。子网掩码,用来说明网络地址和主机地址各自占多少位。
由网络地址和主机地址分别占多少位的不同,将IP地址分为若干类,常用:
A类:由一字节的?网络地址和三字节的主机地址组成。网络地址的最高位?必须为0b,即0127。A类地址的第一个地址块(网络号为0)和最后一个地址块(网络号为127)保留使用。即全0表示本地网络,全1表示保留诊断作用,因此A类地址的有效网络范围为1126.
B类:由二字节的网络地址和二字节的主机地址组成,网络地址最高位必须为10b,即128 ~191
C类:由三字节的网络地址和一字节的主机地址组成,网络地址最高位必须为110b,即192~223
本地回环地址指的是以127开头的地址(127.0.0.1 - 127.255.255.254),通常用127.0.0.1来表示。
判断2个IP地址是否在同一子网内
网络标识 = IP地址 & 子网掩码;
2个IP地址的网络标识一样,那么就处于同一网络。
源IP地址:发出数据包的网络的IP地址
目标IP地址:要接收数据包的计算机的IP地址
网络编程
CS架构(client server,客户端服务器架构)
BS架构(broswer server,浏览器服务器架构)
TCP/IP协议:用的最多的网络协议实现。分为4层,对应OSI的7层
编程时最关注应用层,了解传输层,网际互联层和网络接入层不用管
特点:
TCP协议工作在传输层,对上服务socket接口(API),对下调用IP层(网络链路层)
TCP协议面向连接,通信前必须先3次握手建立连接关系后才能开始通信。
TCP协议提供可靠传输,不怕丢包、乱序等。
TCP如何保证可靠传输
(1)TCP在传输有效信息前要求通信双方必须先握手,建立连接才能通信
(2)TCP的接收方收到数据包后会ack给发送方,若发送方未收到ack会丢包重传
(3)TCP的有效数据内容会附带校验,以防止内容在传递过程中损坏
(4)TCP会根据网络带宽来自动调节适配速率(滑动窗口技术)
(5)发送方会给各分割报文编号,接收方会校验编号,一旦顺序错误即会重传。
TCP的建立连接需要三次握手,建立连接条件:服务器listen时客户端主动发起connect
TCP的关闭连接需要四次握手,服务器或者客户端都可以主动发起关闭
这些握手协议已经封装在TCP协议内部,socket编程接口平时不用管
基于TCP通信的服务模式
(1)具有公网IP地址的服务器(或者使用动态IP地址映射技术)
(2)服务器端socket、bind、listen后处于监听状态
(3)客户端socket后,直接connect去发起连接。
(4)服务器收到并同意客户端接入后会建立TCP连接,然后双方开始收发数据,收发时是双向的,而且双方均可发起
(5)双方均可发起关闭连接
常见的使用了TCP协议的网络应用:http、ftp、QQ服务器、mail服务器
端口号,实质就是一个数字编号,用来在我们一台主机中(主机的操作系统中)唯一的标识一个能上网的进程。端口号和IP地址一起会被打包到当前进程发出或者接收到的每一个数据包中。每一个数据包将来在网络上传递的时候,内部都包含了发送方和接收方的信息(就是IP地址和端口号),所以IP地址和端口号这两个往往是打包在一起不分家的。
socket编程接口:网络通讯统一使用网络字节序,即大端模式
建立连接
socket:类似于open,用来打开一个网络连接,如果成功则返回一个网络文件描述符(int类型),之后我们操作这个网络连接都通过这个网络文件描述符。客户端和服务器都会调用
bind:服务器绑定一个socket到ip地址
listen:服务器进行监听设置
accept:阻塞等待客户端接入,返回值是一个fd,通过这个连接来和客户端进行读写操作
connect:客户端连接到服务器的ip
服务器socket返回的fd叫做监听fd,是用来监听客户端的,不能用来和任何客户端进行读写;accept返回的fd叫做连接fd,用来和连接那端的客户端程序进行读写。
发送和接收
send和write:发送
recv和read:发送
辅助性函数:IP地址格式转换函数,会自己考虑大小端模式
(1)inet_aton、inet_addr、inet_ntoa
(2)(兼容IPv6)inet_ntop(二进制IP转点分十进制字符串ip)、inet_pton(反过来)
htonl ;htons; ntohl; ntohs;//h:host n:net l:long s:short
表示IP地址相关数据结构:都定义在 /usr/include/netinet/in.h
struct sockaddr,这个结构体是网络编程接口中用来表示一个IP地址的,兼容IPv4和IPv6的
typedef uint32_t in_addr_t; 网络内部用来表示IP地址的类型
struct in_addr //将上面这种类型封装成结构体
{
in_addr_t s_addr;
};
struct sockaddr 这个结构体是linux的网络编程接口中用来表示IP地址的标准结构体,bind、connect等函数中都需要这个结构体,这个结构体是兼容IPV4和IPV6的。在实际编程中这个结构体会被一个struct sockaddr_in或者一个struct sockaddr_in6所填充。
struct sockaddr_in //表示IPv4的标准结构体
{
_SOCKADDR_COMMON (sin);
in_port_t sin_port; /* Port number. /
struct in_addr sin_addr; / Internet address. */
/* Pad to size of `struct sockaddr'. */
unsigned char sin_zero[sizeof (struct sockaddr) -
__SOCKADDR_COMMON_SIZE -
sizeof (in_port_t) -
sizeof (struct in_addr)];
};
client和server之间的通信是异步的,依靠应用层协议来解决通讯问题
UDP:…