linux C的学习笔记

来源:互联网 发布:易知投资有限责任公司 编辑:程序博客网 时间:2024/05/22 06:43
linux 的文件系统通常由4部分组成
                                      引导块、超级块、索引节点表和数据块
 
文件类型:
        普通文件--文本文件、二进制文件
 
        目录文件--每个目录文件的前两项是两个特设的文件"."和"..",即目录文件本身索引节点和 父目录索引节点
        链接文件--硬链接文件、软链接文件
        设备文件--块设备文件、字符设备文件
        管道文件--有名管道、无名管道
 
文件描述符:
        文件描述符的前三项对于一般的进程是固定的且由系统自动打开。
        文件描述符0标准输入 文件、1标准输出文件、2标准错误输出文件
        (STDIN_FILENO  STDOUT_FILENO  STDERR_FILENO)<unistd.h>
 
平台这个词有很多种解释:
        可以指计算机体系结构(Architecture),也可以指操作系统(Operating System),也可以指开发
平台(编译器、链接器等)。
 
局部变量 在这里“局部”有两层含义:
                    1、一个函数中定义的变量不能被另一个函数使用
                    2、每次调用函数时局部变量都表示不同的存储空间
局部变量可以用类型相符的任意表达式来初始化,而全局变量只能用常量表达式(Constant Expression)初始化
如果全局变量在定义时不初始化则初始值是0,如果局部变量在定义时不初始化则初始值是不确定的
 
Integer Promotion
    在一个表达式中,凡是可以使用int或unsigned int类型做右值的地方也都可以使用有符号或无符号的char型、short型和Bit-field。如果原始类型的取值范围都能用int型表示,则其类型被提升为int,如果原始类型的取值范围用int型表示不了,则提升为unsigned int型
Usual Arithmetic Conversion
两个算术类型的操作数做算术运算比如a + b,如果两边操作数的类型不同,编译器会自动做类型转换,使两边类型相同之后才做运算转换规则如下:
        1. 如果有一边的类型是long double,则把另一边也转成long double。
        2. 否则,如果有一边的类型是double,则把另一边也转成double。
        3. 否则,如果有一边的类型是float,则把另一边也转成float。
        4. 否则,两边应该都是整型,首先按上一小节讲过的规则对a和b做Integer Promotion,然后如果类型仍不相同,则需要继续转换。首先我们规定char、short、int、long、long long的转换级别(Integer Conversion Rank)一个比一个高,同一类型的有符号和无符号数具有相同的Rank。转换规则如下:
a. 如果两边都是有符号数,或者都是无符号数,那么较低Rank的类型转换成较高Rank的类型。例如unsigned int和unsigned long做算术运算时都转成unsigned long。
                                
b. 否则,如果一边是无符号数另一边是有符号数,无符号数的Rank不低于有符号数的Rank,则把有符号数转成另一边的无符号类型。例如unsigned long和int做算术运算时都转成unsigned long,unsigned long和long做算术运算时也都转成unsigned long。
                                
c. 剩下的情况是:一边有符号另一边无符号,并且无符号数的Rank低于有符号数的Rank。这时又分为两种情况,如果这个有符号数类型能够覆盖这个无符号数类型的取值范围,则把无符号数转成另一边的有符号类型。例如遵循LP64的平台上unsignedint和long在做算术运算时都转成long。   
                                
d. 否则,也就是这个有符号数类型不足以覆盖这个无符号数类型的取值范围,则把两边都转成有符号数的Rank对应的无符号类型。例如在遵循ILP32的平台上unsignedint和long在做算术运算时都转成unsigned long
 
前为止我们学过的+ - * / % > < >= <= == !=运算符都需要做Usual Arithmetic Conversion,因为都要求两边操作数的类型一致,在下一章会介绍几种新的运算符也需要做Usual ArithmeticConversion。单目运算符+ - ~只有一个操作数,移位运算符<< >>两边的操作数类型不要求一致,这些运算不需要做Usual Arithmetic Conversion,但也需要做Integer Promotion(晋升).
                           
:把X加上或者减去2N的整数倍,使结果落入[0, 2N-1]的范围内
掩码
1、取出8~15位。
    unsigned int a, b, mask = 0x0000ff00;
    a = 0x12345678;b = (a & mask) >> 8; /* 0x00000056 */
2、将8~15位清0。
    unsigned int a, b, mask = 0x0000ff00;
    a = 0x12345678;b = a & ~mask; /* 0x12340078 */
3、将8~15位置1。
    unsigned int a, b, mask = 0x0000ff00;
    a = 0x12345678;b = a | mask; /* 0x1234ff78 */
 
异或运算的一些特性
1、一个数和自己做异或的结果是0
2、从异或的真值表可以看出,不管是0还是1,和0做异或保持原值不变,和1做异或得到原值的相反值
3、如果a1 ^ a2 ^ a3 ^ ... ^ an的结果是1,则表示a1、a2、a3...an之中1的个数为奇数个,否则为 偶数个
4、x ^ x ^ y == y,因为x ^ x == 0,0 ^ y == y
 
CPU最核心的功能单元包括:
    寄存器(Register)
    程序计数器(PC,Program Counter)
    指令译码器(Instruction Decoder)
    算术逻辑单元(ALU,Arithmetic and Logic Unit)
    地址和数据总线(Bus)
 
MMU
            虚拟地址和物理地址。
如果处理器没有MMU,或者有MMU但没有启用,CPU执行单元发出的内存地址将直接传到芯片引脚上,
被内存芯片(以下称为物理内存,以便与虚拟内存区分)接收,这称为物理地址(Physical Address,以下简称PA)
 
如果处理器启用了MMU,CPU执行单元发出的内存地址将被MMU截获,从CPU到MMU的地址称为虚拟地址(Virtual Address,
以下简称VA),而MMU将这个地址翻译成另一个地址发到CPU芯片的外部地址引脚上,也就是将VA映射成PA
       MMU将VA映射到PA是以页(Page)为单位的,32位处理器的页尺寸通常是4KB。例如,MMU可以通过一个映射项将VA的一页0xb7001000~0xb7001fff映射到PA的一页0x2000~0x2fff,如果CPU执行单元要访问虚拟地址0xb7001008,则实际访问到的物理地址是0x2008。物理内存中的页称为物理页面或者页帧(Page Frame)。虚拟内存的哪个页面映射到物理内存的哪个页帧是通过页表(Page Table)来描述的,页表保存在物理内存中,MMU会查找页表来确定一个VA应该映射到什么PA。
 
操作系统和MMU是这样配合的:
1. 操作系统在初始化或分配、释放内存时会执行一些指令在物理内存中填写页表,然后用指令设置
MMU,告诉MMU页表在物理内存中的什么位置。
 
2. 设置好之后,CPU每次执行访问内存的指令都会自动引发MMU做查表和地址转换操作,
地址转换操作由硬件自动完成,不需要用指令控制MMU去做。
 
段错误
    就是指访问的内存超出了系统所给这个程序的内存空间    它是这样产生的:
1. 用户程序要访问的一个VA,经MMU检查无权访问。
2. MMU产生一个异常,CPU从用户模式切换到特权模式,跳转到内核代码中执行异常服务程序。
3. 内核把这个异常解释为段错误,把引发异常的进程终止掉。
 
Memory Hierarchy(等级)
                      
 
 
 
链接主要作用:
    一是修改目标文件中的信息,对地址做重定位
    二是把多个目标文件合并成一个可执行文件
 
ELF文件格式
    是一个开放标准,各种UNIX系统的可执行文件都采用ELF格式,它有三种不同的类型:
    可重定位的目标文件(Relocatable,或者Object File)
    可执行文件(Executable)
    共享库(Shared Object,或者Shared Library) ----PDF(280)
 
gcc命令的选项
        
 
C语言的作用域分为以下几类:
            函数作用域(Function Scope)
            文件作用域(File Scope)
            块作用域(Block Scope)
            函数原型作用域(Function Prototype Scope)
 
命名空间可分为以下几类:
    语句标号单独属于一个命名空间。例如在函数中局部变量和语句标号可以重名,互不影响。由于使用标号
    的语法和使用其它标识符的语法都不一样,编译器不会把它和别的标识符弄混
 
    struct,enum和union(下一节介绍union)的类型Tag属于一个命名空间。由于Tag前面总是带struct,
    enum或union关键字,所以编译器不会把它和别的标识符弄混。
 
    struct和union的成员名属于一个命名空间。由于成员名总是通过.或->运算符来访问而不会单独使用,
   所以编译器不会把它和别的标识符弄混。
 
    所有其它标识符,例如变量名、函数名、宏定义、typedef的类型名、enum成员等等都属于同一个命名空间。
    如果有重名的话,宏定义覆盖所有其它标识符,因为它在预处理阶段而不是编译阶段处理,除了宏定义之外其它
   几类标识符按上面所说的规则处理,内层作用域覆盖外层作用域。
 
标识符的链接属性(Linkage)有三种:
外部链接(External Linkage),如果最终的可执行文件由多个程序文件链接而成,一个标识符在任意程序文件中即使声明多次也都代表同一个变量或函数,则这个标识符具有ExternalLinkage。具有External Linkage的标识符编译后在符号表中是GLOBAL的符号。例如上例中main函数外面的a和c,main和printf也算。
内部链接(Internal Linkage),如果一个标识符在某个程序文件中即使声明多次也都代表同一个变量或函数,则这个标识符具有Internal Linkage。例如上例中main函数外面的b。如果有另一个foo.c程序和main.c链接在一起,在foo.c中也声明一个static int b;,则那个b和这个b不代表同一个变量。具有Internal Linkage的标识符编译后在符号表中是LOCAL的符号,但main函数里面那个a不能算Internal Linkage的,因为即使在同一个程序文件中,在不同的函数中声明多次,也不代表同一个变量。
无链接(No Linkage)。除以上情况之外的标识符都属于No Linkage的,例如函数的局部变量,以及不表示变量和函数的其它标识符。
 
存储类修饰符(Storage Class Specifier)关键字:
static,用它修饰的变量的存储空间是静态分配的,用它修饰的文件作
用域的变量或函数具有 Internal Linkage。
auto,用它修饰的变量在函数调用时自动在栈上分配存储空间,函数返
回时自动释放,例如上例中 main函数里的b其实就是用auto修饰的,只
不过auto可以省略不写,auto不能修饰文件作用域的变量。
register,编译器对于用register修饰的变量会尽可能分配一个专门的寄存器来存储,但如果实在分
配不开寄存器,编译器就把它当auto变量处理了,register不能修饰文件作用域的变量。现在一般编
译器的优化都做得很好了,它自己会想办法有效地利用CPU的寄存器,所以现在register关键字也用得
比较少了。
extern,上面讲过,链接属性是根据一个标识符多次声明时是不是代表同一
个变量或函数来分类的, extern关键字就用于多次声明同一个标识符,下一
章再详细介绍它的用法。
typedef,它并不是用来修饰变量的,而是定义一个类型名
 
变量的生存期:
静态生存期(Static Storage Duration),具有外部或内部链接属性,或者被static修饰的变量,在程序开始执行时分配和初始化一次,此后便一直存在直到程序结束。这种变量通常位于.rodata,.data或.bss段,例如上例中main函数外的A,a,b,c,以及main函数里的a。
 
自动生存期(Automatic Storage Duration),链接属性为无链接并且没有被static修饰的变量,这种变量在进入块作用域时在栈上或寄存器中分配,在退出块作用域时释放。例如上例中main函数里的b和c。
 
动态分配生存期(Allocated Storage Duration),以后会讲到调用malloc函数在进程的堆空间中分配内存,调用free函数可以释放这种存储空间。
 
Bit-field
#include <stdio.h>
typedef struct {
unsigned int one:1;
unsigned int two:3;
unsigned int three:10;
unsigned int four:5;
unsigned int :2;
unsigned int five:8;
unsigned int six:8;
} demo_type;
int main(void){
demo_type s = { 1, 5, 513, 17, 129, 0x81 };
printf("sizeof demo_type = %u\n", sizeof(demo_type));
printf("values: s=%u,%u,%u,%u,%u,%u\n", s.one, s.two, s.three, s.four, s.five, s.six);
return 0;
}
   
 
一个联合体的各个成员占用相同的内存空间,联合体的长度等于其中最长成员的长度
 
重复包含头文件有以下问题:
1. 一是使预处理的速度变慢了,要处理很多本来不需要处理的头文件。
2. 二是如果有foo.h包含bar.h,bar.h又包含foo.h的情况,预处理器就陷入死循环了(其实编译器都会规定一个包含层数的上限)。
3. 三是头文件里有些代码不允许重复出现,虽然变量和函数允许多次声明(只要不是多次定义就行),但头文件里有些代码是不允许多次出现的,比如typedef类型定义和结构体Tag定义等,在一个程序文件中只允许出现一次。
 
静态库
$ tree
.
|-- main.c
`-- stack
    |-- is_empty.c
    |-- pop.c
    |-- push.c
    |-- stack.c
    `-- stack.h
1directory, 6 files
我们把stack.c、push.c、pop.c、is_empty.c编译成目标文件:
$ gcc -c stack/stack.c stack/push.c stack/pop.c stack/is_empty.c
然后打包成一个静态库libstack.a:
$ ar rs libstack.a stack.o push.o pop.o is_empty.o
ar: creating libstack.a
库文件名都是以lib开头的,静态库以.a作为后缀,表示Archive。ar命令类似于tar命令,起一个打包
的作用,但是把目标文件打包成静态库只能用ar命令而不能用tar命令。选项r表示将后面的文件列表
添加到文件包,如果文件包不存在就创建它,如果文件包中已有同名文件就替换成新的。s是专用于生
成静态库的,表示为静态库创建索引,这个索引被链接器使用。ranlib命令也可以为静态库创建索引
,以上命令等价于:
$ ar r libstack.a stack.o push.o pop.o is_empty.o
$ ranlib libstack.a
然后我们把libstack.a和main.c编译链接在一起:
$ gcc main.c -L. -lstack -Istack -o main
-L选项告诉编译器去哪里找需要的库文件,-L.表示在当前目录找。-lstack告诉编译器要链接
libstack库,-I选项告诉编译器去哪里找头文件。注意,即使库文件就在当前目录,编译器默认也不
会去找的,所以-L.选项不能少。编译器默认会找的目录可以用-print-search-dirs选项查看
 
共享库
组成共享库的目标文件和一般的目标文件有所不同,在编译时要加-fPIC选项,例如:
$ gcc -c -fPIC stack/stack.c stack/push.c stack/pop.c stack/is_empty.c
 
ldd模拟运行一遍main,在运行过程中做动态链接,从而得知这个可执行文件依赖于哪些共享库,每个共享库都在什么路径下,加载到进程地址空间的什么地址。
 
共享库的搜索路径由动态链接器决定,从ld.so(8)的Man Page可以查到共享库路径的搜索顺序:
1. 首先在环境变量LD_LIBRARY_PATH所记录的路径中查找。
2. 然后从缓存文件/etc/ld.so.cache中查找。这个缓存文件由ldconfig命令读取配置文件/etc/ld.so.conf之后生成,稍后详细解释。
3. 如果上述步骤都找不到,则到默认的系统路径中查找,先是/usr/lib然后是/lib。
 
进程地址空间
 
0x0804 8000-0x080f 4000是从/bin/bash加载到内存的,访问权限为r-x,
表示TextSegment,包 含.text段、.rodata段、.plt段等
 
0x080f 4000-0x080f 9000也是从/bin/bash加载到内存的,访问权限为rw-,
表示Data Segment,包含 .data段、.bss段等。
 
0x0928 3000-0x0949 7000不是从磁盘文件加载到内存的,这段空间称为堆(Heap),以后会讲到用
malloc函数动态分配内存是在这里分配的。从0xb7ca 8000开始是共享库的映射空间,每个共享库也分
为几个Segment,每个Segment有不同的访问权限。可以看到,从堆空间的结束地址(0x0949 7000)到
共享库映射空间的起始地址(0xb7ca 8000)之间有很大的地址空洞,在动态分配内存时堆空间是可以
向高地址增长的。堆空间的地址上限(0x09497000)称为Break,堆空间要向高地址增长就要抬高
Break,映射新的虚拟内存页面到物理内存,这是通过系统调用brk实现的,malloc函数也是调用brk向
内核请求分配内存的。
 
0xbfac 5000-0xbfad a000是栈空间,其中高地址的部分保存着进程的环境变量和命令行参数,低地址的
部分保存函数栈帧,栈空间是向低地址增长的,但显然没有堆空间那么大的可供增长的余地,因为实际的应
用程序动态分配大量内存的并不少见
 
虚拟内存管理作用:
第一,虚拟内存管理可以控制物理内存的访问权限。物理内存本身是不限制访问的,任何地址都可
以读写,而操作系统要求不同的页面具有不同的访问权限
 
第二,虚拟内存管理最主要的作用是让每个进程有独立的地址空间。所谓独立的地址空间是指,不同进程中的同
一个VA被MMU映射到不同的PA,并且在某一个进程中访问任何地址都不可能访问到另外一个进程的数据
 
第三,VA到PA的映射会给分配和释放内存带来方便,物理地址不连续的几块内存可以映射成虚拟地址连续的一块内存
 
第四,一个系统如果同时运行着很多进程,为各进程分配的内存之和可能会大于实际可用的物理内存,虚拟内存管理使
得这种情况下各进程仍然能够正常运行。因为各进程分配的只不过是虚拟内存的页面,这些页面的数据可以映射到物理
页面,也可以临时保存到磁盘上而不占用物理页面,在磁盘上临时保存虚拟内存页面的可能是一个磁盘分区,也可能是
一个磁盘文件,称为交换设备(Swap Device)
 
系统中可分配的内存总量 = 物理内存的大小 + 交换设备的大小
 
内联函数
从源代码层看,有函数的结构,而在编译后,却不具备函数的性质。编译时,类似宏替换,使用函数体
替换调用处的函数名。一般在代码中用inline修饰,但是能否形成内联函数,需要看编译器对该函数定义的具体处理
 
#、##运算符
在函数式宏定义中,#运算符用于创建字符串,#运算符后面应该跟一个形参(中间可以有空格或Tab)
 
在宏定义中可以用##运算符把前后两个预处理Token连接成一个预处理Token,和#运算符不
同,##运算符不仅限于函数式宏定义,变量式宏定义也可以用
 
make会自动选择那些受影响的源文件重新编译,不受影响的源文件则不重新编译
1. make仍然尝试更新缺省目标,首先检查目标main是否需要更新,这就要检查三个
条件main.o、stack.o和maze.o是否需要更新。
 
2. make会进一步查找以这三个条件为目标的规则,然后发现main.o和maze.o需要更新,
因为它们都有一个条件是maze.h,而这个文件的修改时间比main.o和maze.o晚,所以执
行相应的命令更新main.o和maze.o。
 
3. 既然main的三个条件中有两个被更新过了,那么main也需要更新,所以执行命令gcc
main.ostack.o maze.o -o main更新main。
 
main: main.o stack.o maze.o
    gcc main.o stack.o maze.o -o main
main.o: main.c main.h stack.h maze.h
    gcc -c main.c
stack.o: stack.c stack.h main.h
    gcc -c stack.c
maze.o: maze.c maze.h main.h
    gcc -c maze.c
 
clean目标是一个约定俗成的名字在所有软件项目的Makefile中都表示清除编译生成的文件,
类似这样的约定俗成的目标名字有:
all,执行主要的编译工作,通常用作缺省目标。
 
install,执行编译后的安装工作,把可执行文件、配置文件、文档等分别拷到不同的安
装目录。
 
clean,删除编译生成的二进制文件。
 
distclean,不仅删除编译生成的二进制文件,也删除其它生成的文件,例如配置文件和
格式转换后的文档,执行make distclean之后应该清除所有这些文件,只留下源文件。
 
Makefile常用的特殊变量有:
=延时展开
:=立即展看
$@,表示规则中的目标。
$<,表示规则中的第一个条件。
$?,表示规则中所有比目标新的条件,组成一个列表,以空格分隔。
$^,表示规则中的所有条件,组成一个列表,以空格分隔。
 
自动处理头文件的依赖关系
-M选项把stdio.h以及它所包含的系统头文件也找出来了,如果我们不需要输出系统头文件的依赖关系,可以用-MM选项:
$ gcc -MM *.c
main.o: main.c main.h stack.h maze.h
maze.o: maze.c maze.h main.h
stack.o: stack.c stack.h main.h
接下来的问题是怎么把这些规则包含到Makefile中,GNU make的官方手册建议这样写:
all: main
 
main: main.o stack.o maze.o
    gcc $^ -o $@
 
clean:
    -rm main *.o
 
.PHONY: clean
 
sources = main.c stack.c maze.c
 
include $(sources:.c=.d)
 
%.d: %.c
    set -e; rm -f $@; \
    $(CC) -MM $(CPPFLAGS) $< > $@.$$$$; \
    sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \
    rm -f $@.$$$$
 
C语言类型总结:
            
C语言的类型分为函数类型、对象类型和不完全类型三大类。对象类型又分为标量类型和非标量类型。指针类型属于标量类型,因此也可以做逻辑与、或、非运算的操作数和if、for、while的控制表达式,NULL指针表示假,非NULL指针表示真。不完全类型是暂时没有完全定义好的类型,编译器不知道这种类型该占几个字节的存储空间
 
简单的malloc和free实现
   

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也不会还给内核。

 
C标准库的I/O缓冲区     
  
 
C标准库的I/O缓冲区有三种类型:
全缓冲
    如果缓冲区写满了就写回内核。常规文件通常是全缓冲的。
 
行缓冲
如果用户程序写的数据中有换行符就把这一行写回内核,或者如果缓冲区写满了就写回内核。标准输
入和标准输出对应终端设备时通常是行缓冲的。
(除了写满缓冲区、写入换行符之外,行缓冲还有两种情况会自动做Flush操作。如果:
用户程序调用库函数从无缓冲的文件中读取
或者从行缓冲的文件中读取,并且这次读操作会引发系统调用从内核读取数据)
 
无缓冲
用户程序每次调库函数做写操作都要通过系统调用写回内核。标准错误输出通常是无缓冲的,这样用
户程序产生的错误信息可以尽快输出到设备。
                                                                                         
各种数据结构的search、insert和delete操作在平均情况下的时间复杂度比较
 
其它开发调试工具:
1. as,汇编器
2. ld,链接器 用--verbose选项可以显示默认链接脚本
3. readelf,读ELF文件信息
4. objdump,显示目标文件中的信息,本书主要用它做反汇编
5. hexdump,以十六进制或ASCII码显示一个文件
6. ar,把目标文件打包成静态库
7. ranlib,给ar打包的静态库建索引
8. nm,查看符号表
注意open函数与C标准I/O库的fopen函数有些细微的区别:
1、以可写的方式fopen一个文件时,如果文件不存在会自动创建,而open一个文件
时必须明确指定O_CREAT才会创建文件,否则文件不存在就出错返回。
2、以w或w+方式fopen一个文件时,如果文件已存在就截断为0字节,而open一个
文件时必须明确指定O_TRUNC才会截断文件,否则直接在原来的数据上改写
 
int open(const char *pathname, int flags, mode_t mode);
 
flags参数有一系列常数值可供选择,可以同时选择多个常数用按位或运算符连接起来,所以这些常数
的宏定义都以O_开头,表示or。
必选项:以下三个常数中必须指定一个,且仅允许指定一个。
    O_RDONLY 只读打开
    O_WRONLY 只写打开
    O_RDWR 可读可写打开
可选项可以同时指定0个或多个:
O_APPEND 表示追加。如果文件已有内容,这次打开文件所写的数据附加到文件的末尾而不覆盖原来的
内容。
 
O_CREAT 若此文件不存在则创建它。使用此选项时需要提供第三个参数mode,表示该文件的访问权限
 
O_EXCL 如果同时指定了O_CREAT,并且文件已存在,则出错返回。O_TRUNC 如果文件已存在,并且以
只写或可读可写方式打开,则将其长度截断(Truncate)为0字节。
 
O_NONBLOCK 对于设备文件,以O_NONBLOCK方式打开可以做非阻塞I/O(Nonblock I/O)
 
第三个参数mode指定文件权限,可以用八进制数表示,比如0644表示-rw-r--r--,也可以用S_IRUSR、S_IWUSR
等宏定义按位或起来表示,文件权限由open的mode参数和当前进程的umask掩码共同决定。
 
ext2文件系统的总体存储布局
启动块之后才是ext2文件系统的开始,ext2文件系统将整个分区划成若干个同样大小的块组(Block Group)
 
超级块(Super Block)描述整个分区的文件系统信息,例如块大小、文件系统版本号、
上次mount的时间等等。超级块在每个块组的开头都有一份拷贝。
 
块组描述符表(GDT,Group Descriptor Table)
由很多块组描述符组成,整个分区分成多少个块组就对应有多少个块组描述符。每个块组
描述符(Group Descriptor)存储一个块组的描述信息,例如在这个块组中从哪里开始是
inode表,从哪里开始是数据块,空闲的inode和数据块还有多少个等等
 
块位图(Block Bitmap)
块位图就是用来描述整个块组中哪些块已用哪些块空闲的,它本身占一个块,其中的每个
bit代表本块组中的一个块,这个bit为1表示该块已用,这个bit为0表示该块空闲可用。
 
inode位图(inode Bitmap)
和块位图类似,本身占一个块,其中每个bit表示一个inode是否空闲可用。
 
inode表(inode Table)
每个文件都有一个inode,一个块组中的所有inode组成了inode表。
 
数据块(Data Block)
对于常规文件,文件的数据存储在数据块中。
 
对于目录,该目录下的所有文件名和目录名存储在数据块中,注意文件名保存在它
所在目录的数据块中,除文件名之外,ls -l命令看到的其它信息都保存在该文件的
inode中。注意这个概念:目录也是一种文件,是一种特殊类型的文件。
 
对于符号链接,如果目标路径名较短则直接保存在inode中以便更快地查找,如果
目标路径名较长则分配一个数据块来保存。
 
设备文件、FIFO和socket等特殊文件没有数据块,设备文件的主设备号和次设备号
保存在inode中。
 
mke2fs 可建立Linux的ext2文件系统。
mke2fs [-cFMqrSvV][-b <区块大小>][-f <不连续区段大小>][-i <字节>][-N <inode数>]
[-l <文件 >][-L <标签>][-m <百分比值>][-R=<区块数>][ 设备名称][区块数]
-b<区块大小>   指定区块大小,单位为字节。
-c   检查是否有损坏的区块。
-f<不连续区段大小>   指定不连续区段的大小,单位为字节。
-F   不管指定的设备为何,强制执行mke2fs。
-i<字节>   指定"字节/inode"的比例。
-N<inode数>   指定要建立的inode数目。
-l<文件>   从指定的文件中,读取文件西中损坏区块的信息。
-L<标签>   设置文件系统的标签名称。
-m<百分比值>   指定给管理员保留区块的比例,预设为5%。
-M   记录最后一次挂入的目录。
-q   执行时不显示任何信息。
-r   指定要建立的ext2文件系统版本。
-R=<区块数>   设置磁盘阵列参数。
-S   仅写入superblock与group descriptors,而不更改inode able inode bitmap以及block bitmap
-v   执行时显示详细信息。
-V   显示版本信息。
 
                用dumpe2fs工具可以查看这个分区的超级块和块组描述符表中的信息
 
目录中的文件类型编码
    
 
Linux内核的VFS子系统可以图示如下:
            
         
 
dup和dup2函数:
 
dup和dup2都可用来复制一个现存的文件描述符,使两个文件描述符指向同一个file结构体。如果两个
文件描述符指向同一个file结构体,File Status Flag和读写位置只保存一份在file结构体中,并且
file结构体的引用计数是2。如果两次open同一文件得到两个文件描述符,则每个描述符对应一个不同
的file结构体,可以有不同的File Status Flag和读写位置
 
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
int main(void)
{
    int fd, save_fd;
    char msg[] = "This is a test\n";
 
    fd = open("somefile", O_RDWR|O_CREAT, S_IRUSR|S_IWUSR);
    if(fd<0) {
        perror("open");
        exit(1);
    }
    save_fd = dup(STDOUT_FILENO);
    dup2(fd, STDOUT_FILENO);
    close(fd);
    write(STDOUT_FILENO, msg, strlen(msg));
    dup2(save_fd, STDOUT_FILENO);
    write(STDOUT_FILENO, msg, strlen(msg));
    close(save_fd);
    return 0;
}
    
                    
 
Linux内核的进程控制块是task_struct结构体
进程id。系统中每个进程有唯一的id,在C语言中用pid_t类型表示,其实就是一个非负整数
进程的状态,有运行、挂起、停止、僵尸等状态
进程切换时需要保存和恢复的一些CPU寄存器
描述虚拟地址空间的信息
描述控制终端的信息
当前工作目录(Current Working Directory)
umask掩码
文件描述符表,包含很多指向file结构体的指针
和信号相关的信息
用户id和组id
控制终端、Session和进程组
进程可以使用的资源上限(Resource Limit)
 
进程地址空间
   
               
 
环境变量
    
 
libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用时要用extern声明
    #include <stdio.h>
int main(void)
{
    extern char **environ;
    int i;
    for(i=0; environ[i]!=NULL; i++)
        printf("%s\n", environ[i]);
    return 0;
}
 
exec函数族
 
char *const ps_argv[] ={"ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL};
char *const ps_envp[] ={"PATH=/bin:/usr/bin", "TERM=console", NULL};
 
execl("/bin/ps", "ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL);
execv("/bin/ps", ps_argv);
 
execle("/bin/ps", "ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL, ps_envp);
execve("/bin/ps", ps_argv, ps_envp);
 
execlp("ps", "ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL);
execvp("ps", ps_argv);
 
     
 
管道需要注意以下4种特殊情况(假设都是阻塞I/O操作,没有设置O_NONBLOCK标志):
1. 如果所有指向管道写端的文件描述符都关闭了(管道写端的引用计数等于0),而仍然有进程
从管道的读端读数据,那么管道中剩余的数据都被读取后,再次read会返回0,就像读到文件
末尾一样。
2. 如果有指向管道写端的文件描述符没关闭(管道写端的引用计数大于0),而持有管道写端的
进程也没有向管道中写数据,这时有进程从管道读端读数据,那么管道中剩余的数据都被读
取后,再次read会阻塞,直到管道中有数据可读了才读取数据并返回。
3. 如果所有指向管道读端的文件描述符都关闭了(管道读端的引用计数等于0),这时有进程向
管道的写端write,那么该进程会收到信号SIGPIPE,通常会导致进程异常终止。在第 33 章信
号会讲到怎样使SIGPIPE信号不终止进程。
4. 如果有指向管道读端的文件描述符没关闭(管道读端的引用计数大于0),而持有管道读端的
进程也没有从管道中读数据,这时有进程向管道写端写数据,那么在管道被写满时再
次write会阻塞,直到管道中有空位置了才写入数据并返回
 
Shell脚本的执行过程:
         
1. 交互Shell(bash)fork/exec一个子Shell(sh)用于执行脚本,父进程bash等待子进程sh终止。
2. sh读取脚本中的cd ..命令,调用相应的函数执行内建命令,改变当前工作目录为上一级目录。
3. sh读取脚本中的ls命令,fork/exec这个程序,列出当前工作目录下的文件,sh等待ls终止。
4. ls终止后,sh继续执行,读到脚本文件末尾,sh终止。
5. sh终止后,bash继续执行,打印提示符等待用户输入。
 
环境变量
环境变量可以从父进程传给子进程,因此Shell进程的环境变量可以从当前Shell进程传给
fork出来的子进程。用printenv命令可以显示当前Shell进程的环境变量。
本地变量
只存在于当前Shell进程,用set命令可以显示当前Shell进程中定义的所有变量
(包括本地变量和环境变量)和函数。
 
常用的位置参数和特殊变量:
 
$0相当于C语言main函数的argv[0]$1$2...位置参数(Positional Parameter),相当于C语言main函数的argv[1]argv[2]...$#相当于C语言main函数的argc - 1,注意这里的#后面不表示注释$@表示参数列表"$1" "$2" ...,例如可以用在for循环中的in后面。$?上一条命令的Exit Status$$当前Shell的进程号
 
 
产生信号的条件主要有:
用户在终端按下某些键时,终端驱动程序会发送信号给前台进程,例如Ctrl-C产生SIGINT信号,
Ctrl-\产生SIGQUIT信号,Ctrl-Z产生SIGTSTP信号
 
硬件异常产生信号,这些条件由硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如
当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释为SIGFPE信号发送给
进程。再比如当前进程访问了非法内存地址,,MMU会产生异常,内核将这个异常解释为SIGSEGV信号
发送给进程。
 
一个进程调用kill(2)函数可以发送信号给另一个进程。
 
可以用kill(1)命令发送信号给某个进程,kill(1)命令也是调用kill(2)函数实现的,如果不明确指定
信号则发送SIGTERM信号,该信号的默认处理动作是终止进程。
 
当内核检测到某种软件条件发生时也可以通过信号通知进程,例如闹钟超时产生SIGALRM信号,向读端
已关闭的管道写数据时产生SIGPIPE信号。
 
 信号的捕捉: 
      
1. 用户程序注册了SIGQUIT信号的处理函数sighandler。
 
2. 当前正在执行main函数,这时发生中断或异常切换到内核态。
 
3. 在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。
 
4. 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函数,
sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。
 
5. sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。
 
6. 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。
 
如果一个函数符合以下条件之一则是不可重入的:
调用了malloc或free,因为malloc也是用全局链表来管理堆的。
调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。
 
终端设备模块:
        
 
终端缓冲:
   
 
线程创建:
int pthread_create(
        pthread_t *restrict thread,
        const pthread_attr_t *restrict attr,
        void *(*start_routine)(void*),
        void *restrict arg
    );
终止某个线程而不终止整个进程,可以有三种方法:
从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
一个线程可以调用pthread_cancel终止同一进程中的另一个线程。
线程可以调用pthread_exit终止自己。
 
跨路由器通讯过程:
  
 
 
 

原创粉丝点击