读《Linux C编程一站式学习》I

读《LC编程一站式学习 --宋劲杉 I,后期的《读Linux C编程一站式学习》II。此Note 不含其中“文件系统”、“信号”、“TCP/IP”及“Socket编程”章节。剪辑部分大都来自书本,作为摘录型笔记。练习部分是在Debian/GNU  Linux(在一个笔记本电脑中安装了Debian Linux系统)下的编程练习或验证,作为练习笔记。


2 练习 || 总结

Part I

(1) Linux下gdb的安装及使用入门

(2) 再集查找与排序

(3) 深度优先搜索(堆栈)解决迷宫问题


Part II

(4) C类型转换

(5) 一个C源文件到可执行文件 [反汇编-函数栈帧 编译 链接]

(6) C的联合体 位段

(7) C内联汇编

(8) Makefile基础

(9) C实现 每隔1s向time.txt文件输出系统时间(C I/O函数)


PartIII

(10) 汇编版helloworld (write()与_exit()系统调用)

(11) 进程控制[fork() exec() wait() waitpid()]

(12) 进程通信 [fork() 管道( pipe() )  FIFO ]

(13) 线程控制[pthread_create()pthread_join()]  线程同步[互斥锁 条件变量 信号量]

(14) Linux  Shell  bash 脚本语法

(15) Shell脚本调试方法Shell脚本执行的过程

(16) 译man regex练习

(17) grep正则表达式规范  在sed、awk及C语言中用正则表达式


PART 其它

(18) C语言中void和NULL

(19) C语言 认识转换符 fscanf()用法

(20) C自定义函数的可变参数列表实现  Windows APIS目录遍历程序 [李园7舍_404]


1 剪辑

(1) 书本目标

读此书后经过一段时间后具备非常Solid的C编程能力,能熟练的使用Linux系统,同时对计算机体系结构与指令集、操作系统原理和设备驱动程序都有较深入的了解。大多特定的规则适合于x86/GNU Linux/gcc平台。


(2) 学习C

C语言的发展大致分为3个阶段:Old  Style C、C89和C99,C标准主要由两部分组成,一部分描述C的语法,另一部分描述C标准库(Linux上使用的C标准库是glibc,适用于嵌入式开发的C标准库有如uClibc)。K&R不能反映C89之后C语言的发展以及最新的C99标准(网传说依旧很经典)。学习编程有两种Approach,一种是Bottom up,一种是Top Down,各有优缺点,需要结合起来。C语言是一种面向底层的编程语言,要写好C程序,必须对操作系统的工作原理非常清楚。要彻底搞清楚C语言的原理,就必须深入到指令一层去理解。


理解组合规则是理解语法规则的关键所在。


学习编程语言不应该死记各种语法规则,如果能够想清楚设计者这么规定的原因,不仅有助于记忆,而且会有更多的收获。


C语言与平台和编译器是密不可分的。


(3) 概念

[1] 声明和定义

C语言中的声明有变量声明、函数声明和类型声明。如果一个变量或函数的声明要求编译器为它分配存储空间,那么也可以称为定义,因此定义是声明的一种。


[2] 赋值和初始化

变量声明中的类型表明这个变量代表多大一块存储空间,这样编译器才知道如何读写这块存储空间。定义一个变量后,将一个值存到变量代表的存储空间叫赋值。变量的定义和赋值可以一步完成,这叫做初始化。


局部变量可以用类型相符合的任意表达式来初始化,而全局变量只能用常量表达式来初始化。程序开始运行时需要适当的值来初始化全局变量,所以初始值必须保存在编译生成的可执行文件中,因此初始值在编译时就要计算出来。非常量表达式需要程序运行时才能够得到结果。


[3] 左值和右值

表达式所表示的存储位置叫左值(运行放在等号左边),我们所说的表达式的值也称为右值(只能放在表达式右边)。数组和函数左右值机制:数组名作为左值时表示整个数组的存储空间,而作为右值时则转换成指向数组首元素的指针。函数类型对象作为右值时也会自动转换为函数指针类型对象。字符串字面值类似于数组名,作为右值使用时自动转换成指向首元素的指针,这种指针应该是const  char*类型。


[4] 函数声明、函数定义和函数原型

void threeline(void)声明了一个函数的名字、参数类型及个数、函数返回值类型,这称为函数原型。

void threeline(void);单独一行,只能叫函数声明。带函数体的声明才能叫函数定义。


[5] 栈帧

每个函数调用的参数和局部变量的存储空间称为一个栈帧。操作系统为程序的运行预留了一块栈空间,函数调用时就在这个栈空间分配栈帧。


[6] Implementation-defined、Unspecified及Undefined

在C标准中没有做明确规定的地方会用Implementation-Defined、Unspecified及Undefined来表述。

Implementation-Defined:C标准没有明确规定,但要求编译器做出明确规定。

Unspecified:往往有几种可选的处理方式,编译器可以自己做决定选哪一种处理方式。并不需要将其写在编译器的文档中。

Undefined:C标准没有规定怎么处理,编译器可能也没有规定。甚至也没做出出错处理。


[7] Integer Promotion

凡是可以使用int或unsigned int类型作右值的地方也都可以使用有符号或无符号的char型、short型及Bit-field。如果原始类型范围能用int型表示,则其类型被提升为int,如果int表示不了则提升为unsigned int,这就是Integer Promotion。[C类型转换]


[8] Side Effect与Sequence Point

调用一个函数可能会产生副作用(Side Effect),如修改了某个全局变量的值。C标准规定代码中的某些点是Sequence Point(如一个完整的表达式末尾),当执行到一个Sequence Point时,之前的所有的Side Effect必须全部执行完,在此之后的Side Effect必须一个都没有发生。至于两个Sequence Point间的多个Side Effect谁先谁后发生没有规定。


程序语言通常都规定了执行中变量修改的最晚实现时刻(Sequence Point,称为顺序点、序点或执行点)。程序执行中存在一系列顺序点(时刻),语言保证一旦执行到达一个顺序点,在此之前发生的所有修改(副作用)都必须实现(必须反应到随后对同一存储位置的访问中),在此之后的所有修改都还没有发生。在顺序点之间则没有任何保证。


[9] 内存对齐 填充

所访问指令的地址应该是指令大小(字节为单位)的整数倍,这样的地址编排称为对齐。在有些平台上,如果内存未对齐将不能访问内存。在能够访问内存不对齐的平台之上,效率也会比内存对齐时的访问要差。


由于内存对齐,结构体的各元素都会保存到自身大小整数倍的地址之上,以可能导致两结构体元素之间不是紧密存储的,中间存在间隙,称为填充。


[10] 缓冲区

个人理解缓冲区:用来暂时保存信息的内存空间都叫缓冲区。比如用户定义char  buf[100]用来作fgets()函数第一个参数时,buf就是一个用户缓冲区。打开一个文件时,自动为文件开辟的一段供I/O函数读写的内存空间被称为C标准I/O缓冲区。C标准I/O缓冲区有三种类型:

  • 全缓冲:如果缓冲区写满了就写回内核。常规文件都是全缓冲的。
  • 行缓冲:如果用户程序写的数据中有换行符就把这一行写回内核,或者如果缓冲区写满了就写回内核。标准输入、输出对应终端设备时通常是行缓冲的。
  • 无缓冲:用户程序每次调库函数做写操作都要通过系统调用写回内核。标准错误输出通常是无缓冲的,这样用户程序产生的错误信息可以尽快输出到设备。

一般来说,各种文件的缓冲类型都可以通过函数来设定和修改(如open,fcntl)。

[11] Flush操作

用户程序把I/O缓冲区中的数据立刻传给内核,让内核写回设备,这称为Flush操作。对应的库函数是fflush,fclose函数在关闭文件之前也会做Flush操作。


[12] 库函数和系统调用

库函数调用由函数库或用户自己提供,运行于用户态。


系统调用(英语:system call),又称为系统呼叫,指运行在使用者空间的程序向操作系统内核请求需要更高权限运行的服务。系统调用提供了用户程序与操作系统之间的接口。大多数系统交互式操作需求在内核态执行。如设备IO操作或者进程间通信。


[13] 阻塞I/O与非阻塞I/O

UNIX的传统是一切皆是文件。在用open打开文件时,可以设置设备文件做非阻塞I/O。read和write系统调用读写阻塞I/O时会阻塞直到读到写到相应内容才会返回;而读写非阻塞I/O时,一旦不满足读或者写条件就立即返回。


read常规文件不会阻塞,不管读到多少字节一定会在有限的时间内返回。从终端或者网络设备读则不一定,从终端(如标输入)设备读一定要读到换行符才会返回,如果网络设备上没有收到数据包,则read就会一直阻塞。


write常规文件时,返回值通常等于写的字节数,而向终端或网络设备写则不一定。


除了用open在打开文件时改变设备的阻塞性,还可以用fcntl来改变一个已经打开文件的属性(读,写,追加,非阻塞等)。


[14] 文件描述符

每个进程在Linux内核中都有一个task_struct结构体来维护进程相关的信息,称为进程描述符。task_struct中有一个指针指向files_struct结构体,称为文件描述符表。用户程序不能直接访问内核中的文件描述附表,而只能用文件描述符表的索引(跟数组的索引即下标一样)即文件描述符,用int型变量保存。当调用open打开一个文件或创建一个文件时,内核分配一个文件描述符并返回给用户程序,该文件描述符表项中的指针指向新打开的文件。当读写文件时,用户程序把文件描述符传给read或write,内核根据文件描述符找到相应的表项,再通过表项中的指针找到相应的文件


文件描述符由系统按次序分配,当close关闭描述符1(标输)时,再用open打开一个常规文件,则文件描述符一定是1,这是标准输出不再是终端而是一个常规的文件,再调用printf就不会打印到屏幕上而是写到这个文件中了。


[15] 僵尸进程与init进程

如果一个进程已经终止,但是它的父进程尚未调用wait或waitpid对它进行清理,这时的进程状态称为僵尸(Zombie)进程。


如果一个父进程终止,而它的子进程还存在(这些子进程或者仍在运行,或者已经是僵尸进程了),则这些子进程的父进程改为init进程。init是系统中的一个特殊进程,通常程序文件是/sbin/init,进程id是1,在系统启动时负责启动各种系统服务,之后就负责清理子进程,只要有子进程终止,init就会调用wait函数清理它。


(4) 语句和机制

[1] goto

goto只能跳到同一个函数中的某一个标号处,而不能跳到别的函数中。一个函数中任何地方出现了错误条件都可以理解跳到函数末尾做出错处理(如释放先前分配的资源,恢复先前改动的全局变量等)。


[2] sizeof

sizeof是一个很特殊的运算符,它有两种形式:“sizeof  表达式”和“sizeof (类型名)”。 “sizeof  表达式”中的“表达式”并不求值,而只是根据类型转换规则求得“表达式”的类型,然后把这种类型所占的字节数作为整个表达式的值。


因为sizeof内的表达式不需要求值,所以不需要到运行时才计算,事实上在编译时就知道“sizeof表达式”的值了。同时,不能将“sizeof表达式”写在预处理中。


[3] 浮点数模型

由IEEE标准,浮点数的模型由三部分组成:符号位、指数部分和尾数部分。


Figure1.浮点数模型

如(0.10001)2 x 25,符号位为1;5为指数部分;0.1001为尾数部分。为了解决浮点数模型中指数部分的负数及减少模型的复杂度,(0.10001)2 x 25在浮点数模型中被表示为:

1

11001

0001

指数部分指定一个偏移值(如上指定为16,具体查看IEEE标准),大于偏移值表示指数为正,等于偏移值指数为0,否则为负,11001的值为21,实际表示指数值为21- 16=5。尾数部分本来为10001,为了减少浮点数的复杂度,将规定尾数小数点后第一位必须为1,既然必须为1,在浮点模型中将其省略以多一位出来提到浮点数的精度。


浮点数时一个相当复杂的话题,不同平台的浮点数表示和浮点运算也有较大差异。浮点数的所有bit都为0时(全局变量用0初始化的意思是用0填充每个bit),严格来说,对应的浮点值不一定为0。


[4] Implementation DefinedUnspecifiedUndefined各种情况

C标准的Rationale之一:优先考虑效率,而可移植性尚在其次。

Implementation Defined:

  • char是有符号还是无符号是Implementation Defined的
  • 有符号数在计算机中的表示是Sign and Magnitude、1s' complement还是2’s complement是Implementation Defined的
  • char型在C标准中明确的规定占1个字节,其它整型占几个字节是Implementation Defined的

Unspecified:函数调用各个实参表达式按什么顺序求值;

Undefined:

  •  数组访问越界
  • 两个Sequence Point间的多个Side Effect谁先谁后发生

[6] Sequence Point处

  • 如&&、||、逗号运算符、?:的一个操作数求值后是一个Sequence Point
  • 函数调用前是一个Sequence Point
  • 在一个完整的声明(这个声明不是另一个声明的一部分)末尾是一个Sequence Point。
  • 完整表达式(这个表达式不是另一个表达式的一部分)末尾是一个Sequence Point。完整表达式包括变量初始化表达式,表达式语句,return语句的表达式,以及条件、循环和switch语句的控制表达式(for头部有三个控制表达式)。
  • 库函数即将返回时是一个Sequence Point。函数调用中对所有实际参数和函数名表达式(需要调用的函数也可能通过表达式描述)的求值完成之后(进入函数体之前)。

[7] AT&T汇编寻址方式

内存寻址在指令中可以表示成如下通用格式:

ADDRESS_OR_OFFSET(%BASE_OR_OFFSET, %INDEX, MULTIPLIER),它所表示的地址可以这样计算出来:FINAL_ADDRESS         = ADDRESS_OR_OFFSET + BASE_OR_OFFSET +MULTIPLIER * INDEX


[8] 汇编的用武之地

  • 虽然C编译器的优化已经做得很好了,但C代码还是没有汇编程序的效率高。
  • 有些平台相关的指令必须手写,在C语言中没有等价的语法。

而C程序比直接用会编写简洁易懂,可读性移植性都好。汇编的用武之地用C汇编内联来解决。


[9] volatilerestrict

程序员应该明确的告诉编译器哪些内存单元的访问是不能优化的(多线程访问的全局变量内存,设备寄存器),在C语言中可以用volatile限定符修饰变量,告诉编译器即使编译器启动了优化选项,每次读写这个变量都要老老实实去内存中进行读写。


但针对拥有cache平台,在cache已经缓存了这个地址的情况(就从cache中读写数据),cache中没有数据地址就从内存中读,这些步骤由硬件完成。volatile没法让cache每次都重新在内存中进行读写数据。通常,有cache的平台都有办法对某一段地址范围禁用cache,一般是在页表中设置的,可以设置哪些页面运行cache缓存,哪些页面不允许,MMU不仅要做地址转换盒访问权限检查,也要和cache协同工作。


restrict是C99才引入的关键字。restrict限制函数指针参数时就是告诉调用者,这个函数的实现可能会做些优化,编译器也可能会做些优化,传进来的指针不允许指向重叠的内存区间,否则结果可能是错的。


[10] const

指向非const变量的指针(const  *int i)或者非const变量的地址(int  *const i)可以传递给指向const变量的指针。但,指向const变量的指针或者const变量的地址不可以传给指向非const变量的指针,以免通过后者意外的修改了前者所指的内存单元。

即使不用const限定符也能写出正确的程序,但良好的编程习惯应该尽可能多地使用const,因为:

  • const给读代码的人传达非常有用的信息。比如一个函数的形参是const  char*,你在调用这个函数时就可以放心的传给它char *或者const char *指针,而不必担心指针所指内存单元会被改写。
  • 尽可能的使用const限定符,把不该变的都声明为只读,这样可以依靠编译器检查程序中的Bug,防止意外修改数据。
  • const对编译器优化是一个有用的提示,编译器也许会把const变量优化成常量


[11] NULL和void*

(x86/GNU Linux/gcc)NULL在C标准的头文件stddef.h中定义:#define  NULL  ((void *))。就是把地址0转换成指针类型,称为空指针,它的特殊之处在于,操作系统不会把任何数据保存在地址0及其附近。也不会把地址0 ~ 0xffff的页面映射到物理内存,所以任何对地址0 的访问都会导致段错误。


在编程时经常需要一种通用指针可以转换为任意其它类型的指针,任意其它类型的指针也可以转换为通用指针。最初C没有void *通用指针类型,是把char*当通用指针,需要转换时就用转换运算符()。void *指针与其它类型的指针之间可以隐式转换,而不必用类型转换符。void*指针不能直接Dereference(访问),而必须转换成别的类型的指针才能做Dereference。而不能用void类型来定义变量(类型暂时不确定的变量),因为编译器不知道该分配几个字节给变量。


[12] 命令行参数结束标志

NULL。


[13] 函数调用函数传参和返回

函数调用运算符()要求操作数是函数指针。函数类型对象作为右值时也会自动转换为函数指针类型对象。[ 函数类型与函数类型指针--- 跟整数类型和整数类型指针一个道理]。


看函数传参和返回机制,最好的方式还是看可执行文件的反汇编:一个C源文件到可执行文件 [反汇编-函数栈帧编译链接]。从这个C程序的反汇编可以看到:子函数形参的栈地址在调用子函数的父函数栈帧内,子函数内局部变量的栈地址在子函数栈帧内。函数的返回值会给寄存器(如整型返回值会给%eax)暂存,若父函数引用了子函数返回值,则在父函数中从寄存器(%eax)取函数返回值,如此,只要返回值在父函数中生存期还在,那么返回值就有意义


[14] 指针

指针用来保存虚拟地址,所以在32位平台上占4个字节;在64位平台上占8个字节。

[指向数组的指针]

读《Linux C编程一站式学习》I_第1张图片

Figure2.指向数组的指针


[15] malloc和free实现的简单描述

malloc(0)这种调用也是合法的,也会返回一个非NULL的指针,这个指针也可以传给free释放。但是不能通过这个指针访问内存。free(NULL)也是合法的,不做任何事情,但是,free野指针是不符法的(编译器很可能检查不出来)。

读《Linux C编程一站式学习》I_第2张图片
Figure3. malloc和free实现的简单描述

图中白色背景的框表示malloc管理的空闲内存块,深色背景的框不归malloc管,可能是已经分配给用户的内存块,也可能不属于当前进程,Break之上的地址不属于当前进程,需要通过brk系统调用向内核申请。每个内存块开头都有一个头节点,里面有一个指针字段和一个长度字段,指针字段把所有空闲块的头节点串在一起,组成一个环形链表,长度字段记录着头节点和后面的内存块加起来一共有多长,以8字节为单位(也就是以头节点的长度为单位)。

1)        一开始堆空间由一个空闲块组成,长度为7×8=56字节,除头节点之外的长度为48字节。


2)        调用malloc分配8个字节,要在这个空闲块的末尾截出16个字节,其中新的头节点占了8个字节,另外8个字节返回给用户使用,注意返回的指针p1指向头节点后面的内存块。


3)        又调用malloc分配16个字节,又在空闲块的末尾截出24个字节,步骤和上一步类似。


4)        调用free释放p1所指向的内存块,内存块(包括头节点在内)归还给了malloc,现在malloc管理着两块不连续的内存,用环形链表串起来。注意这时p1成了野指针,指向不属于用户的内存,p1所指向的内存地址在Break之下,是属于当前进程的,所以访问p1时不会出现段错误,但在访问p1时这段内存可能已经被malloc再次分配出去了,可能会读到意外改写数据。另外注意,此时如果通过p2向右写越界,有可能覆盖右边的头节点,从而破坏malloc管理的环形链表,malloc就无法从一个空闲块的指针字段找到下一个空闲块了,找到哪去都不一定,全乱套了。


5)        调用malloc分配16个字节,现在虽然有两个空闲块,各有8个字节可分配,但是这两块不连续,malloc只好通过brk系统调用抬高Break,获得新的内存空间。在[K&R]的实现中,每次调用sbrk函数时申请1024×8=8192个字节,在Linux系统上sbrk函数也是通过brk实现的,这里为了画图方便,我们假设每次调用sbrk申请32个字节,建立一个新的空闲块。


6)        新申请的空闲块和前一个空闲块连续,因此可以合并成一个。在能合并时要尽量合并,以免空闲块越割越小,无法满足大的分配请求。


7)        在合并后的这个空闲块末尾截出24个字节,新的头节点占8个字节,另外16个字节返回给用户。


8)        调用free(p3)释放这个内存块,由于它和前一个空闲块连续,又重新合并成一个空闲块。注意,Break只能抬高而不能降低,从内核申请到的内存以后都归malloc管了,即使调用free也不会还给内核。


[16] C标准库的I/O缓冲区

用户程序调用C标准I/O库函数读写文件或设备,而这些库函数要通过系统调用把读写请求传给内核,最终由内核驱动磁盘或设备完成I/O操作。C标准库为每个打开的文件分配一个I/O缓冲区以加速读写操作,通过文件的FILE结构体可以找到这个缓冲区,用户调用读写函数大多数时候都在I/O缓冲区中读写,只有少数时候需要把读写请求传给内核。


C标准库内核之间的关系就像CPU、Cache和内存之间的关系一样,C标准库之所以会从内核预读一些数据放在I/O缓冲区中,是希望用户程序随后要用到这些数据,C标准库的I/O缓冲区也在用户空间,直接从用户空间读取数据比进内核读数据要快得多。另一方面,用户程序调用像fputc这样的函数通常只是写到I/O缓冲区中,这样fputc函数可以很快地返回,如果I/O缓冲区写满了,fputc就通过系统调用把I/O缓冲区中的数据传给内核,内核最终把数据写回磁盘。

读《Linux C编程一站式学习》I_第3张图片

[17] main函数的调用

main被启动代码这样调用:exit(main(argc,argv));。main函数return时启动代码会调用exit,exit函数首先关闭所有尚未关闭的FILE  *指针(关闭前要做Flush操作),然后通过_exit系统调用进入内核退出当前进程


[18] C语言类型

读《Linux C编程一站式学习》I_第4张图片
Figure4.C语言类型总结

C语言的类型分为函数类型、对象类型和不完全类型三大类。对象类型又分为标量和非标量类型。不完全类型是暂时没有完全定义好的类型,编译器不知道这种类型该占几个字节的存储空间具有不完全类型的变量可以通过多次声明组合成一个完全类型。不完全类型的结构体有重要作用:

struct  s{

        char  data[6];

       struct  s  *next;

};

当编译器处理到第一行struct  s{ 时,认为struct  s{ 是一个不完全类型,当处理到第三行struct  s *next;时,认为next是一个指向不完全类型的指针,当处理到第四行};时struct  s成了一个完全类型,next也成了一个指向完全类型的指针。但不可再结构体内定义struct  s next,因为它不知道next将占用几个字节,所以结构体可以递归的定义指针成员但不可以递归的定义变量成员


[19] 字符编码(ASCII  Unicode UTF-8)

扩展ASCII码

在图形界面中最广泛使用的扩展ASCII码是ISO-8859-1,也称为Latin-1。


ASCII码并没有规定编号为128~255的字符,为了能表示更多字符,各厂商制定了很多种ASCII码的扩展规范(制定128 ~ 255对应的字符),通常把这些规范称为扩展ASCII码(Extended ASCII),但其实它们并不属于ASCII码标准。


Unicode

为了统一全世界各国语言文字和专业领域符号(例如数学符号、乐谱符号)的编码,ISO制定了ISO 10646标准,也称为UCS(Universal Character Set)。UCS编码的长度是31位,可以表示231个字符。在ISO制定UCS的同时,另一个由厂商联合组织也在着手制定这样的编码,称为Unicode,后来两家联手制定统一的编码,但各自发布各自的标准文档,所以UCS编码和Unicode码是相同的。


BMP

如果两个字符编码的高位相同,只有低16位不同,则它们属于一个平面(Plane),所以一个平面由216个字符组成。目前常用的大部分字符都位于第一个平面(编码范围是U-00000000~U-0000FFFD),称为BMP(Basic Multilingual Plane)或Plane 0,为了向后兼容,其中编号为0~256的字符和Latin-1相同。


UTF-8

UTF(Unicode Transformation Format)。UTF-8具有以下性质:

  • 编码为U+0000~U+007F的字符只占一个字节,就是0x00~0x7F,和ASCII码兼容。
  • 编码大于U+007F的字符用2~6个字节表示,每个字节的最高位都是1,而ASCII码的最高位都是0,因此非ASCII码字符的表示中不会出现ASCII码字节(也就不会出现0字节)。
  • 用于表示非ASCII码字符的多字节序列中,第一个字节的取值范围是0xC0~0xFD,根据它可以判断后面有多少个字节也属于当前字符的编码。后面每个字节的取值范围都是0x80~0xBF,见下面的详细说明。
  • UCS定义的所有231个字符都可以用UTF-8编码表示出来。
  • UTF-8编码最长6个字节,BMP字符的UTF-8编码最长三个字节。
  • 0xFE和0xFF这两个字节在UTF-8编码中不会出现。

(5) 风格[Coding Style]

[1] 命名

短的单词可以省掉元音形成缩写。长的单词可以取单词的头几个字母形成缩写,注意平时读代码时的总结和积累。内核编码风格规定变量、函数和类型采用全小写加下划线的方式命名。


全局变量和函数的命名一定要写详细,不惜多用几个下划线和单词。


[2] 函数

实现一个函数只是为了做好一件事。函数内的缩进层次少于4层为宜。函数不宜写得过长,一般在24行的终端上不超过两屏,如果一个函数在概念上简单,只是长度很长也可以。如由switch中的众多case延长的代码。执行函数就是执行一个动作,函数名应包含动词。度量函数的复杂度还有就是看函数的局部变量,5到10个已经偏多。


(6) 计算机体系结构基础

现代的计算机都是基于Von Neumann体系结构,不管是嵌入式、PC还是服务器。这种体系结构的主要特点:CPU和内存是计算机的两个主要组成部分。内存中保存着数据和指令,CPU从内存中取指令,其中有些指令让CPU做运算,有的指令让CPU读内存中的数据。

读《Linux C编程一站式学习》I_第5张图片

Figure5.CPU 内存 设备 MMU

[1] 物理地址和虚拟地址

如果没有MMU(Memory Management Unit,内存管理单元),CPU执行单元发出的内存地址将直接传到芯片引脚上,被内存芯片接收,这称为物理地址。


如果处理器内添加了MMU,CPU执行单元发出的内存地址将被MMU截获,从CPU到MMU的地址成为虚拟地址,MMU将这个地址翻译成另外一个地址发到CPU芯片的外部地址引脚上。


如果是32位处理器,则地址总线是32位的(与MMU相连),而经过MMU转换之后的外地址总线则不一定是32位的。MMU将虚拟地址映射到物理地址以页为单位,32位处理器的页尺寸通常是4KB。如,MMU通过一个映射项将虚拟地址的一页0xb7001000 – 0xb70001fff映射到物理地址地址的一页0x2000 –0x2fff。如果CPU执行单元要访问虚拟地址0xb70001008,则实际访问的物理地址为0x2008。物理地址中的页称为物理页面或者页帧。虚拟内存的哪个页面映射到物理内存的哪个页帧是通过页表来描述的,页表保存在物理内存中,MMU会查找页表来确定一个虚拟地址该映射到什么物理地址。


操作系统会把虚拟地址划分为用户空间(内核态)内核空间(内核态)。内核、核心扩充(kernel extensions)、以及驱动程序,运行在核心空间上。而其他的应用程序,则运行在用户空间上。


[2] 内总线和外总线

和CPU直接相连的地址线和数据线称为内总线。内总线经过MMU和总线接口转换之后引出到芯片引脚才是外总线。地址线和数据线通常和CPU寄存器的位数是一样的。


[3] 内存映射I/O和端口I/O

内存映射I/O:无论是在CPU外部接总线的设备还是在CPU内部接总线的设备(从CPU核引出的地址线和数据线不经总线接口直接接到芯片内部集成的设备)都有各自的范围,都可以像访问内存一样访问。


端口I/O:CPU需要引出额外的地址线来接片内设备(和访问内存所用的地址线不同),访问设备寄存器时需要特殊的in/out指令,而不是和访问内存用同样的指令。


[4] 内存和设备访问的不同

内存只是保存数据而不会产生新的数据,如果CPU不去读它,它也不需要主动提供数据给CPU,所以内存总是被动地等待被读或被写。


设备往往会自己产生数据,并且需要主动通知CPU来读这些数据。每个设备都有一条中断线,通过中断控制器连接到CPU,当设备需要主动通知CPU时就引发一个中断信号,CPU正在执行的指令将被打断,程序计数器会指向某个固定的地址(这个地址由体系结构定义),CPU跳到这个地址去执行中断服务程序。完成中断处理后再回到先前被打断的地方继续执行。由于各种设备的操作方法各不相同,每种设备都需要专门的设备驱动程序。


[5] 操作系统的启动 内核 加载 进程

操作系统本身也是一段保存在磁盘上的程序,计算机在启动时执行一段固定的启动代码(Bootloader)首先把操作系统从磁盘加载到内存,然后执行操作系统中的代码把用户需要的其它程序加载到内存。


操作系统最核心的功能是管理进程调度、管理内存的分配使用和管理各种设备,做这些工作的程序称为内核。


保存在硬盘上的程序是不能被CPU直接取指令执行的,操作系统在执行程序时会把它从硬盘拷贝到内存,这样CPU才能取指令执行,这个过程称为加载。


程序加载到内存之后,称为操作系统调度执行的一个任务,这个任务就是进程。


(7) Linux系统编程基础

讲的是Linux平台特性,Linux内核的工作原理,涉及到体系结构时讲的是x86。Linux系统函数需要结合Linux内核工作原理来理解,因为系统函数是内核提供给应用程序的接口,同时要理解内核的工作就需要熟练的掌握C,因为内核是用C写的。


[1] C标准I/O库函数与Unbuffered I/O函数

open、read、write、close等系统函数称为无缓冲(Unbuffered)I/O函数(这些系统函数可能有内核I /O缓冲区)。Unbuffered I/O与C标准I/O库函数比较:

  • Unbuffered I/O库函数每次都要进入内核,调一个系统调用要比调一个用户空间函数慢的多,所以在用户空间开辟I/O缓冲区还是必要的,用C标准库函数就比较方便,省去了管理I/O缓冲区的麻烦。
  • 用C标准库函数要时刻注意I/O缓冲区和实际文件有可能不一致,必要时用fflush(3)。
  • I/O函数不仅可以用于读写常规文件,也可以读写设备,如终端盒网络设备。在读写设备时通常是不需要缓冲的,所以网络编程通常直接用Unbuffered I/O库函数。

[2] 进程线程更全面的学习参考书

  • 《AdvancedProgramming in the UNIX Environmen t,UNIX高级环境编程》Richard Stevens和StephenA.Rago
  • 《Understandingthe Linux Kernel,理解Linux内核》 DanielP. Bovet和Marco Cesati

3 工具

(1) gdb

gdb是GNU Debugger的简称,是GNU操作系统下标准的调试器。可用来调试Ada,C,C++,Objective-C,Free Pascal,Fortran,Java等编写的程序。


(2) as

汇编器。


(3) ld

链接器。


(4) readelf

读ELF文件信息。


(5) objdump

显示目标文件中的信息,可以做反汇编。


(6) hexdump

以十六进制或ASCII显示一个文件。


(7) ar

把目标文件打包成静态库。


(8) ranlib

给ar打包的静态库建索引。


(9) nm

查看符号表。


(10) man bash-builtins

查看内建命令。


(11) od

用户通常使用od命令查看特殊格式的文件内容。通过指定该命令的不同选项可以以十进制、八进制、十六进制和ASCII码来显示文件。


[2014.7.26 - 2014.8.16 - 14.20]
R《LC》Note Over.

你可能感兴趣的:(读《Linux C编程一站式学习》I)