C/C++ Learning

目录

1. C/C++中的关键字
2. C/C++中的标识符
3. 编译选项MD(d)、MT(d)编译选项的区别
4. C++类模板、函数模板
5. C++修饰符
6. 调用约定
7. 错误处理
8. 环境表

9. 内存管理与进程映射

10. 系统调用

11.  文件管理(Unix C)

12. 进程管理

 

1. C/C++中的关键字

0x1: extern关键字

在C++环境下使用C函数的时候,常常会出现编译器无法找到obj模块中的C函数定义,从而导致链接失败的情况,这是什么原理呢?

1. C++语言在编译的时候为了解决函数的多态问题,会将函数名和参数联合起来生成一个中间的函数名称
2. 而C语言则不会,C语言没有重载这个概念
3. 如果在C++程序中直接使用extern进行函数引入,则可能会因为重载的关系导致函数名被"重定义"
4. 因此会造成链接时找不到对应函数的情况,此时C函数就需要用extern "C"进行链接指定,这告诉编译器,请保持我的名称,不要给我生成用于链接的中间函数名

下面是一个标准的写法:

//在xx.h文件的头上
#ifdef __cplusplus
#if __cplusplus
extern "C"
{
 #endif
 #endif /* __cplusplus */ 
 …
 …
 //.h文件结束的地方
 #ifdef __cplusplus
 #if __cplusplus
}
#endif
#endif /* __cplusplus */ 

extern关键字有两个作用

1. 引入别的模块的"extern变量/函数"
置于变量或者函数前,以标示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义(逐个obj搜索)
例如: extern int iRI; 
//就是告诉编译器:你现在编译的文件中,有一个标识符虽然没有在本文件中定义,但是它是在别的文件中定义的全局变量
当它与"C"一起连用时,如: extern "C" void fun(int a, int b); 
则告诉编译器在编译fun这个函数名时按着C的规则去翻译相应的函数名而不是C++的,C++的规则在翻译这个函数名时,因为要支持函数重载的原因,会把fun这个名字变得面目全非,可能是fun@aBc_int_int#%$也可能是别的,这要看编译器的"脾气"了(不同的编译器采用的方法不一样)
/*
现代编译器一般采用按文件编译的方式,因此在编译时,各个文件中定义的全局变量是互相透明的,也就是说,在编译时,全局变量的可见域限制在文件内部 
例如在头文件中: extern int g_Int; 
要注意的是,它是一个声明不是定义,在其他文件的某个地方需要真正定义并声明这个变量
全局变量使得真正意义上的超全局变量得以实现,因为C/C++编译器的单文件编译的限制,传统的全局变量只能在本文件域内生效,而extern能够把作用域扩展到整个obj链接过程中
*/
 
2. 在C++文件中调用C方式编译的函数
C方式编译和C++方式编译
相对于C,C++中新增了诸如重载等新特性。所以全局变量和函数名编译后的命名方式有很大区别。
int a;
int functionA();
对于C方式编译:
int a;=> _a
int functionA(); => _functionA
对于C++方式编译:
int a; =>xx@xxx@a
int functionA(); => xx@xx@functionA
可以看出,因为要支持重载,所以C++方式编译下,生成的全局变量名和函数名复杂很多。与C方式编译的加一个下划线不同,如果C++文件中想要使用C编译的函数/变量,就要告诉编译器,待使用的函数/变量是使用C方式编译的,即使用extern "C" {..}进行声明(注意是声明,不是定义),这样,编译器就会按照C的方式进行链接

Relevant Link:

http://freetoskey.blog.51cto.com/1355382/931085
http://blog.csdn.net/zhangjg_blog/article/details/28264737

0x2: 使用extern可能存在的问题

从最本质上来说,extern的作用是将本模块的变量或者函数暴露给外部的模块使用,从而达到模块间协作的目的,但是,使用extern这种机制来进行跨模块的变量/函数引用可能会出现"不一致性"的问题

1. 在使用extern时候要严格对应声明时的格式
2. 当函数提供方单方面修改函数原型时,如果使用方不知情继续沿用原来的extern申明,这样编译时编译器不会报错。但是在运行过程中,因为少了或者多了输入参数,往往会照成系统错误

针对这个问题,通常的解决方案是: 只在头文件中做声明,真理总是这么简单

1. 提供方在自己的xxx_pub.h中提供对外部接口的声明
int add(int x,int y);

2. 然后调用方include该头文件
#include xxx_pub.h

3. 从而省去extern这一步,以避免这种错误 

0x3: dllexport、dllimport关键字

dllexport和dllimport存储类特性是C和C++语言的Microsoft专用扩展。可以使用它们从DLL中"导出"或向其中"导入"函数、数据和对象

__declspec( dllimport ) declarator
__declspec( dllexport ) declarator

特性

1. 显式定义 DLL 到其客户端的接口,可以是可执行文件或另一个 DLL。 将函数声明为 dllexport 可消除对模块定义 (.def) 文件的需要,至少在有关导出函数的规范方面。 dllexport 特性可替换 __export 关键字。
2. 如果将类标记为 declspec(dllexport),则类层次结构中类模板的任何专用化都将隐式标记为 declspec(dllexport)。 这意味着类模板将进行显式实例化,且必须定义类的成员。
3. 函数的 dllexport 使用其修饰名公开该函数。 对于 C++ 函数,这包括名称重整。 对于 C 函数或声明为 extern "C"的函数,这包括基于调用约定的平台特定的修饰。 如果您不需要名称修饰,请使用 .def 文件(EXPORTS 
关键字)
4. 在声明 dllexport 或 dllimport 时,您必须使用扩展特性语法和 __declspec 关键字

Relevant Link:

http://msdn.microsoft.com/zh-cn/library/3y1sfaz2.aspx

0x4: __declspec关键字

用于指定存储类信息的扩展特性语法使用 __declspec 关键字,该关键字指定给定类型的实例将与下面所列的 Microsoft 专用存储类特性一起存储。 其他存储类修饰符的示例包括 staticextern 关键字。 但是,这些关键字是
C 和 C++ 语言的 ANSI 规范的一部分,并且本身不包含在扩展特性语法中。 扩展特性语法简化并标准化了 Microsoft 专用的 C 和 C ++ 语言扩展。

简单来说,__declspec关键字是一个类型修饰符,语法

__declspec  ( extended-decl-modifier-seq )
extended-decl-modifier-seq:
1. align( # )
2. allocate(" segname ")
3. appdomain
4. code_seg(" segname ")
5. deprecated
6. dllimport: 声明导入DLL中的函数
7. dllexport: 声明将DLL中的函数导出
8. jitintrinsic
9. naked
10. noalias
11. noinline
12. noreturn
13. nothrow
14. novtable
15. process
16. property( {get=get_func_name | ,put= put_func_name})
17. restrict
18. safebuffers
19. selectany
20. thread
21. uuid(" ComObjectGUID ")

示例

#define DllImport   __declspec( dllimport )
#define DllExport   __declspec( dllexport )

DllExport void func();
DllExport int i = 10;
DllImport int j;
DllExport int n;

Relevant Link:

http://msdn.microsoft.com/zh-cn/library/dabb5z75.aspx
http://hi.baidu.com/baiyw920/item/35384a440b7b71ad61d7b9d6

0x5: __declspec(dllexport) & __declspec(dllimport)

1. __declspec(dllexport)
声明一个导出函数,是说这个函数要从本DLL导出。要给别人用。一般用于dll中 
省掉在DEF文件中手工定义导出哪些函数的一个方法。当然,如果你的DLL里全是C++的类的话,你无法在DEF里指定导出的函数,只能用__declspec(dllexport)导出类

2. __declspec(dllimport)
声明一个导入函数,是说这个函数是从别的DLL导入。我要用。一般用于使用某个dll的exe中 
不使用 __declspec(dllimport) 也能正确编译代码,但使用 __declspec(dllimport) 使编译器可以生成更好的代码。编译器之所以能够生成更好的代码,是因为它可以确定函数是否存在于 DLL 中,这使得编译器可以生成跳过间接寻址级别的代码,而这些代码通常会出现在跨 DLL 边界的函数调用中。但是,必须使用 __declspec(dllimport) 才能导入 DLL 中使用的变量 

Relevant Link:

http://www.cnblogs.com/xd502djj/archive/2010/09/21/1832493.html

0x6: inline关键字

1. inline定义的类的内联函数,函数的代码被放入符号表中,在使用时直接进行替换,(像宏一样展开),没有了调用的开销,效率也很高 
2. 很明显,类的内联函数也是一个真正的函数,编译器在调用一个内联函数时,会首先检查它的参数的类型,保证调用正确。然后进行一系列的相关检查,就像对待任何一个真正的函数一样。这样就消除了它的隐患和局限性。
3. inline可以作为某个类的成员函数,当然就可以在其中使用所在类的保护成员及私有成员 

需要注意的是,内联函数一般只会用在函数内容非常简单的时候,这是因为,内联函数的代码会在任何调用它的地方展开,如果函数太复杂,代码膨胀带来的恶果很可能会大于效率的提高带来的益处。内联函数最重要的使用地方是用于类的存取函数

Relevant Link:

http://baike.baidu.com/item/inline
http://xinklabi.iteye.com/blog/676313

 

2. C/C++中的标识符

0x1: pragma comment

#pragma comment( comment-type ,["commentstring"] )
1. comment-type
是一个预定义的标识符,指定注释的类型,应该是下一字符之一
    1) compiler:放置编译器的版本或者名字到一个对象文件,该选项是被linker忽略的 
    
    2) exestr:在以后的版本将被取消
    
    3) lib:
    放置一个库搜索记录到对象文件中,这个类型应该是和commentstring(指定你要Linker搜索的lib的名称和路径)这个库的名字放在Object文件的默认库搜索记录的后面,linker搜索这个库就像你在命令行输入这个命令一样。
你可以在一个源文件中设置多个库记录,它们在object文件中的顺序和在源文件中的顺序一样。如果默认库和附加库的次序是需要区别的,使用Z编译开关是防止默认库放到object模块 我们在使用一些特殊的API的时候,常常需要使用这条指令明确指出一些特定的库文件,否则会造成
"无法解析的外部符号"这种错误 4) linker: 指定一个连接选项,这样就不用在命令行输入或者在开发环境中设置了。只有下面的linker选项能被传给Linker. 4.1) /DEFAULTLIB:/DEFAULTLIB 选项将一个 library 添加到 LINK 在解析引用时搜索的库列表 4.2) /EXPORT:可以从程序导出函数,以便其他程序可以调用该函数。也可以导出数据。通常在 DLL 中定义导出 4.3) /INCLUDE:/INCLUDE 选项通知链接器将指定的符号添加到符号表。 4.4) /MANIFESTDEPENDENCY 4.5) /MERGE 4.6) /SECTION 2. commentstring 是一个提供为comment-type提供附加信息的字符串

我们在编程中经常用到的是
#pragma comment(lib,"*.lib")这类的指令
#pragma comment(lib,"Ws2_32.lib")表示链接Ws2_32.lib这个库。和在工程设置里写上链入Ws2_32.lib的效果一样,不过这种方法写的 程序别人在使用你的代码的时候就不用再设置工程settings了

Relevant Link:

http://baike.baidu.com/view/3487831.htm

 

3. 编译选项MD(d)、MT(d)编译选项的区别

属性 -> C/C++ -> 代码生成 -> 运行库

它们之间的区别是

1. /MD: "multithread - and DLL-specific version"
运行时库由操作系统提供一个DLL,程序里不集成。MD的意思是多线程DLL版本,定义了它后,编译器把MSVCRT.lib安置到OBJ文件中,它连接到DLL的方式是动态链接,实际上工作的库是MSVCR80.DLL 

2. /MT: "multithread - static version"
运行时库由程序集成,程序不再需要操作系统提供运行时库DLL。MT的意思是多线程静态的版本,定义了它后,编译器把LIBCMT.lib安置到OBJ文件中,让链接器使用LIBCMT.lib 处理外部符号

0x1: MD、MT的选择原则

1. 选择/MD、不选/MT 
    1) 程序不需要静态链接运行时库,可以减小软件的大小 
    2) 所有的模块都采用/MD,使用的是同一个堆,不存在A堆申请,B堆释放的问题 

2. 为什么选择/MT、不选择/MD
    1) 有些系统可能没有程序所需要版本的运行时库,程序必须把运行时库静态链接上 

3. 多个模块,必须选择相同类型的运行时库,不要混合使用 
 
4. 选择/MT需要解决的堆空间释放问题
不同的模块各自有一份C运行时库代码,各个C运行库会有各自的堆(C/C++的库向系统申请了一大块堆内存并进行管理,向程序员封装了一层堆的使用接口),导致了各个模块会有各自的堆。如果在A堆中申请空间,到B堆中释放就会有崩溃(bad address free error),在模块A申请的空间,必须在模块A中释放 

Relevant Link:

http://www.cnblogs.com/cswuyg/archive/2012/02/03/2336424.html
http://blog.sina.com.cn/s/blog_6f7265cf0101nhs0.html
http://blog.csdn.net/nodeathphoenix/article/details/7550546

 

4. C++类模板、函数模板

类模板与函数模板的定义和使用类似。
有时,有两个或多个类,其功能是相同的,仅仅是数据类型不同,如下面语句声明了一个类

class Compare_int
{
public :
   Compare(int a,int b)
   {
      x=a;
      y=b;
   }
   int max( )
   {
      return (x>y)?x:y;
   }
   int min( )
   {
      return (x<y)?x:y;
   }
private :
   int x,y;
};

其作用是对两个整数作比较,可以通过调用成员函数max和min得到两个整数中的大者和小者。
如果想对两个浮点数(float型)作比较,需要另外声明一个类

class Compare_float
{
public :
   Compare(float a,float b)
   {
      x=a;y=b;
   }
   float max( )
   {
      return (x>y)?x:y;
   }
   float min( )
   {
      return (x<y)?x:y;
   }
private :
   float x,y;
}

显然这基本上是重复性的工作,应该有办法减少重复的工作。
C++在发展的后期增加了模板(template)的功能,提供了解决这类问题的途径。可以声明一个通用的类模板,它可以有一个或多个虚拟的类型参数,如对以上两个类可以综合写出以下的类模板

template <class numtype> //声明一个模板,虚拟类型名为numtype
class Compare //类模板名为Compare
{
public :
   Compare(numtype a,numtype b)
   {
      x=a;y=b;
   }
   numtype max( )
   {
      return (x>y)?x:y;
   }
   numtype min( )
   {
      return (x<y)?x:y;
   }
private :
   numtype x,y;
};

将此类模板和前面第一个Compare_int类作一比较,可以看到有两处不同 

1. 声明类模板时要增加一行: template <class 类型参数名>
template意思是"模板",是声明类模板时必须写的关键字。在template后面的尖括号内的内容为模板的参数表列,关键字class表示其后面的是类型参数。在本例中numtype就是一个类型参数名。这个名宇是可以任意取的,只要是合法的标识符即可。这里取numtype只是表示"数据类型"的意思而已。此时,numtype并不是一个已存在的实际类型名,它只是一个虚拟类型参数名。在以后将被一个实际的类型名取代。

2. 原有的类型名int换成虚拟类型参数名numtype。
在建立类对象时,如果将实际类型指定为int型,编译系统就会用int取代所有的numtype,如果指定为float型,就用float取代所有的numtype。这样就能实现"一类多用"

由于类模板包含类型参数,因此又称为参数化的类。如果说类是对象的抽象,对象是类的实例,则类模板是类的抽象,类是类模板的实例。利用类模板可以建立含各种数据类型的类

Relevant Link:

http://blog.csdn.net/oqqquzi1234567/article/details/43489291

 

5. C++修饰符

volatile是一个类型修饰符(type specifier)。它是被设计用来修饰被不同线程访问和修改的变量。如果不加入volatile,基本上会导致这样的结果:要么无法编写多线程程序,要么编译器失去大量优化的机会,volatile的作用是

1. 作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值
2. 编译器常常为了加快程序执行速度,使用寄存器作为运算的中间值存储介质,但是在多线程场景下,这有可能导致脏读、幻读的产生
3. 强制编译器不进行任何优化,完全按照原始代码逻辑进行编译

Relevant Link:

http://baike.baidu.com/item/volatile

 

6. 调用约定

__cdecl __fastcall与 __stdcall,三者都是调用约定(Calling convention),它决定以下内容

1. 函数参数的压栈顺序
2. 由调用者还是被调用者把参数弹出栈
3. 产生函数修饰名的方法 

它们的定义如下

1. __stdcall调用约定: 函数的参数自右向左通过栈传递,被调用的函数在返回前清理传送参数的内存栈 
2. _cdecl是C和C++程序的缺省调用方式。每一个调用它的函数都包含清空堆栈的代码,所以产生的可执行文件大小会比调用_stdcall函数的大。函数采用从右到左的压栈方式
//注意: 对于可变参数的成员函数,始终使用__cdecl的转换方式。
3. __fastcall调用约定: 它是通过寄存器来传送参数的(实际上,它用ECX和EDX传送前两个双字(DWORD)或更小的参数,剩下的参数仍旧自右向左压栈传送,被调用的函数在返回前清理传送参数的内存栈)

Relevant Link:

http://www.cppblog.com/mzty/archive/2007/04/20/22349.html

 

7. 错误处理

0x1: 错误处理函数返回值原则

1. 如果函数的返回值类型是int类型,并且函数的返回值不可能是负数时,则返回0表示正常结束,返回-1表示出错
2. 如果一个函数的返回值存在确定的"合法值域",那么就可以通过返回合法值域之外的值表示函数执行失败
3. 如果函数的返回值类型是指针类型,则返回NULL表示失败,其他值表示正常结束
4. 如果不考虑函数是否出错的情况(即该函数不可能失败或完全由自身处理失败的函数),则返回类型使用void

//最佳实践: 数据和处理结果分离
5. 用指针作为函数的形参将函数的结果返回给调用方,并定义一系列枚举值表示该函数处理逻辑本身是否成功
    1) 返回0: 表示成功
    2) 返回-1: 表示失败

1. 返回非法只表示失败

long fsize(const char* path){
    FILE* fp = fopen(path, "r");
    if(!fp){
        return -13;
    }
    fseek(fp, 0, SEEK_END);
    long size = ftell(fp);
    fclose(fp);
    
    return size;
}

2. 返回空指针表示失败

const char* strmax(const char* a, const char* b){
    return a && b ? (strcmp(a, b) > 0 ? a : b) : NULL;
}

3. 返回-1表示失败

int mod(int a, int b, int* c){
    if(b == 0)
        return -1;
    *c = a % b;
    return 0;
}

0x2: 错误号

1. 系统预定义的整型全局变量errno中存储了最近一次系统调用的错误编号
2. 头文件errno.h中包含对errno全局变量的外部声明和各种错误号的宏定义

需要注意的是,不能根据错误号直接判断是否出错(而应该根据调用函数的返回值来判断)

1. 虽然所有的错误号都不是零,但是因为在函数执行成功的情况下错误号全局变量errno不会被修改,所以不能用该变量的值为零或非零作为出错或没出错的判断依据
2. 正确的做法
    1) 根据调用函数的返回值判断是否出错,在确定出错的前提下再根据errno的值判断具体出了什么错

同时,在多线程编程中,我们还需要注意错误号线程不安全(错误号errno是非线程安全的全局变量),errno是一个全局变量,其值随时可能发生变化,尤其在多线程应用中

0x3: 错误信息

将整数形式的错误号转换为有意义的字符串

#include <string.h>
char* strerror(int errnum);

实例代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

int main(){
    char* p = (char*)malloc(-1);
    if(!p){
        fprintf(stderr, "malloc: %s\n", strerror(errno));
        exit(EXIT_FAILURE);
    }
    scanf("%s", p);
    printf("%s", p);
}

或者用perror简化代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

int main(){
    char* p = (char*)malloc(-1);
    if(!p){
        //fprintf(stderr, "malloc: %s\n", strerror(errno));
        perror("malloc");
        exit(EXIT_FAILURE);
    }
    scanf("%s", p);
    printf("%s", p);
}

判断函数的调用是否成功,必须依据函数的返回值,而确定出错的情况下,可以依据errno获取错误的原因 

 

8. 环境表

0x1: 基本概念

1. 环境变量表是进程的属性之一
2. 环境表主要是指环境变量的集合,每个进程中都有一个环境表,用于记录与当前进程相关的环境变量信息
    1) PATH=/bin:/usr/bin:/usr/local/bin:.
    2) CPATH=.
    3) LIBRARY_PATH=.
    4) LD_LIBRARY_PATH=/usr/local/lib:.
    5) SHELL=/bin/bash
    6) LANGUAGE=zh_CN:en_US:en
3. 环境表采用字符指针数组来表示: char* arr[4]  

0x2: 环境变量表数据结构

环境变量表是一个以空指针结尾的的字符指针数组,其中每个指针指向一个格式为: "变量名=变量值"的字符串,该指针数组的起始地址保存在全局变量environ中

#include <iostream>  
#include <stdlib.h>  
#include <stdio.h>  
#include <stdlib.h>  
  
//引用外部的全部环境变量  
extern char** environ;  
  
int main(int argc, const char * argv[]) {  
    char** env = environ;  
      
    while (*env) {  
        printf("%s\n",*env);  
        env++;  
    }  
    exit(0);  
    return 0;  
}  

0x3: 环境变量函数

1. 环境变量的一般形式

1. 变量名=变量值
2. 进程对环境变量的操作包括
    1) 根据变量名获取变量值
    2) 根据变量名设置变量值
    3) 增加新的环境变量
    4) 删除已有的环境变量
    5) 清除所有的环境变量
3. 所有对环境变量的操作所影响的都仅仅是调用进程自己的环境变量(进程独立),对其他进程包括其父进程,没有任何影响

2. 获取环境变量

1. 调用任何操作环境变量的函数都需要包含标准库头文件: #include <stdlib.h>
2. 根据环境变量的名称获取该变量的值: char* getenv(const char* name);
printf("PATH=%s\n", getenv("PATH"));

3. 设置环境变量

1. 增加新的环境变量或修改已有环境变量的值: int putenv(char* string);
putenv("MYNAME=littlehann");
若变量名不存在就增加该环境变量,否则就修改该环境变量的值
putenv的指针型参数被直接放到环境变量表中(地址传参)

2. 增加新的环境变量或修改已有环境变量的值: int setenv(const char* name, const char* value, int overwrite);
setenv("MYNAME", "littlehann", 0);
setenv("MYNAME", "littlehann", 1);
setenv根据参数3决定是否进行覆盖(当指定名称环境变量已存在时)
setenv的指针型参数将目标字符串复制到环境变量表中(值传参)

4. 删除环境变量

1. 根据环境变量的名称删除环境变量: int unsetenv(const char* name);
unsetenv("MYNAME");

5. 清空环境变量表

1. 清除所有的环境变量: int clearenv(void);
clearenv();

2. 该函数在清除掉所有环境变量后,把用于表示环境变量表首地址的全局变量environ设置成空指针

6. 实例代码

#include <stdio.h>
#include <stdlib.h>

int main(){
    printf("PATH=%s\n", getenv("PATH"));
    putenv("MYNAME=littlehann");
    printf("MYNAME=%s\n", getenv("MYNAME"));
    putenv("MYNAME=alibaab");
    printf("MYNAME=%s\n",getenv("MYNAME"));
    setenv("MYNAME", "hello", 1);
    printf("MYNAME=%s\n",getenv("MYNAME"));
    unsetenv("MYNAME");
    printf("MYNAME=%s\n",getenv("MYNAME"));

    return 0;
}

 

9. 内存管理与进程映射

0x1: C/C++ 进程内存布局分布(高地址向低地址)

1. 栈区: 存放局部变量(包括函数的形参)、const修饰的局部变量、块变量({}内的变量),该区域的内存由操作系统负责分配和回收
2. 堆区: 使用malloc/calloc/realloc/free等函数处理的内存区域,该区域的内存需要程序员手动申请和手动释放

{
3. BSS段: 存放没有初始化的全局变量、静态局部变量,该区域会在main函数执行之前进行自动清零
4. 全局区/数据区: 存放已经初始化的全局变量、static修饰的局部变量
}
//因为间隔距离较近,常常将BSS段和数据段(Data)统称为静态去(全局区)

5. 代码区: 存储功能代码、函数名所在的区域
6. 只读常量区: 存放字符串常量、const修饰的全局变量

基于Intel架构的Linux系统中的进程内存布局

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int i1 = 10;    //全局区
int i2 = 20;    //全局区
int i3;            //BSS段
const int i4 = 40;    //只读常量区

void fn(int i5){    //栈区
    int i6 = 60;    //栈区
    static int i7 = 70;    //全局区
    const int i8 = 80;    //栈区
    int* p1 = (int*)malloc(sizeof(int));    //堆区
    int* p2 = (int*)malloc(sizeof(int));    //堆区
    char* str = "good";        //只读常量区
    char strs[] = "good";    //栈区

    printf("只读常量区: &i4 = %p\n", &i4);
    printf("只读常量区: str = %p\n", str);
    printf("------------\n");
    printf("全局区: &i1 = %p\n", &i1);
    printf("全局区: &i2 = %p\n", &i2);
    printf("全局区: &i7 = %p\n", &i7);
    printf("------------\n");
    printf("BSS段: &i3 = %p\n", &i3);
    printf("------------\n");
    printf("堆区: p1 = %p\n", p1);
    printf("堆区: p2 = %p\n", p2);
    printf("------------\n");
    printf("栈区: &i5 = %p\n", &i5);
    printf("栈区: &i6 = %p\n", &i6);
    printf("栈区: &i8 = %p\n", &i8);
    printf("栈区: strs = %p\n", strs);

    return;
}

int main(){
    printf("代码区; fn = %p\n", fn);
    printf("------------\n");
    fn(5);

    return 0;
}

由此可见,栈区从高地址向低地址生成,而堆区正好相反,栈区和堆区之间并没有严格的分解,可以进行动态微调

0x2: 内存管理接口

从底层硬件到上层应用,各层都提供了各自的内存管理接口

Relevant Link: 

http://www.cnblogs.com/dejavu/archive/2012/08/13/2627498.html

0x3: 虚拟内存管理技术

Unix/Linux系统中的内存都是采用虚拟内存管理技术进行管理的,即每个进程都有0~4G的内存地址(虚拟的),由操作系统负责把虚拟内存地址和真实的物理内存映射起来,因此,不同的进程中虚拟地址空间看起来是相同的,但是对应的实际物理内存是不同的

0x4: 用户空间与内核空间

4GB的虚拟进程地址空间分成两部分

1. 0 ~ 3G-1: 用户空间,存放用户程序的代码和数据
2. 3G ~ 4G-1: 内核空间,存放系统内核的代码和数据
3. 用户空间的代码不能直接访问内核空间的代码和数据,但可以通过系统调用进入内核态,间接地与系统内核交互

0x5: 内存壁垒与段错误

1. 每个进程的用户空间都是0 ~ 3G-1,但它们所对应的物理内存却是各自独立的,系统为每个进程的用户空间维护一张专属于该进程的内存映射表,记录虚拟内存到物理内存的对应关系,因此在不同进程之间交换虚拟内存地址无意义的(刻舟求剑)
2. 所有进程的内核空间都是3G ~ 4G-1,它们所对应的物理内存只有一份,系统为所有进程的内核空间维护一张内存映射表: init_mm.pgd,记录虚拟内存到物理内存的对应关系,因此不同进程通过系统调用所访问的内核代码和数据是同一份
3. 用户空间的内存映射表会随着进程的切换而切换,内核空间的内存映射表则无需随着进程的切换为切换
4. 一切对虚拟内存的越权访问,豆浆导致段错误
    1) 试图访问没有映射到物理内存的虚拟内存
    2) 试图以非法方式访问虚拟内存,如对只读内存读写操作等
5. 内存地址的基本单位是字节,但是内存映射的基本单位是内存页,目前主流的操作系统的内存页大小是4Kb(4096字节)

常见导致段错误原因

1. 野指针/空指针使用
int* pi; //野指针
int* pi = NULL; //空指针
scanf("%d", pi);

2. 使用未映射的虚拟地址
3. 对没有操作权限的内存进行操作
    1) 对只读常量区进行写操作,可能引发段错误

0x6: 标准C内存分配函数(Glibc封装的函数)

1. 标准库提供的内存分配函数(malloc/calloc/realloc)在标准库内部维护一个线性链表,管理堆中动态分配的内存
2. 标准内存分配函数在分配内存时会附加若干(通常是12字节),存放控制信息(MCB 内存控制块),该信息一旦被意外损坏,可能在后续操作中引发异常
3. 虚拟内存到物理内存的映射以页(4K=4096bytes)为单位
#include <unistd.h>
int getpagesize(void);

4. 通过malloc函数首次分配内存,至少映射33页,即使通过free函数释放掉全部内存,最初的33页仍然保留

看以下代码

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

int main(void){
    uint64_t* p1 = (uint64_t*)malloc(sizeof(uint64_t));
    uint64_t* p2 = (uint64_t*)malloc(sizeof(uint64_t));
    uint64_t* p3 = (uint64_t*)malloc(sizeof(uint64_t));
    uint64_t* p4 = (uint64_t*)malloc(sizeof(uint64_t));
    uint64_t* p5 = (uint64_t*)malloc(sizeof(uint64_t));

    printf("p1 = %p\n", p1);
    printf("p2 = %p\n", p2);
    printf("p3 = %p\n", p3);
    printf("p4 = %p\n", p4);
    printf("p5 = %p\n", p5);
    printf("------------\n");

    *p1 = 1;
    *(p1+1) = 2;
    *(p1+2) = 3;
    *(p1+3) = 4;
    *(p1+4) = 5;

    printf("*p1 = %ld, *p2 = %ld\n", *p1, *p2);
    
    free(p1);
    free(p2);
    free(p3);
    free(p4);
    free(p5);
    return 0;
}

0x7: malloc/free的简化实现

1. 内存控制块

内存控制块用于管理每次分配的内存块,记录该内存块的字节大小(即传入malloc的size参数)、忙闲状态,以及相邻内存控制块的首地址(链式结构)

typedef struct mem_control_block{
    size_t size;    //块大小
    bool freel    //空闲标志
    struct mem_control_block* next;    //后块指针
} MCB;

2. 单向链表栈(以链表方式存储的栈结构)

多次堆内存分配会产生多个内存控制块(每次通过malloc申请的内存都会对应一个MCB),可将其组织成链表栈的形式以便于集中管理,可由栈顶指针遍历该链表

MCB* g_top = NULL;
MCB* mcb;
for(mcb = g_top; mcb; mcb = mcb->next)
    ..

3. 分配内存

遍历内存控制块链表,若有大小足够的空闲块,则重用该块,否则分配新的足量内存并将其控制块(每块内存的头部都有一个MCB)压入链表栈,通过MCB链表栈

4. 释放内存

先将被释放内存块标记为空闲,然后遍历内存控制块链表,将靠近栈顶的连续空闲块及其内存控制块一并释放

5. size命令 

1. 通过size命令可以观察特定可执行程序的代码区(text)、数据区(data)、BSS区(bss)的大小,以字节为单位
2. 符合ELF标准的可执行程序文件只包含代码区、数据区的内容,在进程加载阶段被exec函数读入进程的内存空间

6.  代码示例

#include <stdio.h>
#include <stdbool.h>
#include <unistd.h>

typedef struct mem_control_block{
    size_t size;
    bool free;
    struct mem_control_block* next;
} MCB;
MCB* g_top = NULL;

void* my_malloc(size_t size){
    MCB* mcb;
    for(mcb= g_top; mcb; mcb = mcb->next){
        if(mcb->free && mcb->size >= size){
            break;
        }
    }
    if(!mcb){
        mcb = sbrk(sizeof(MCB) + size);
        if((void*)-1 == mcb)
            return NULL;
        mcb->size = size;
        mcb->next = g_top;
        g_top = mcb;
    }
    mcb->free = false;
    return mcb + 1;
}

void my_free(void* ptr){
    if(ptr){
        MCB* mcb = (MCB*)ptr - 1;
        mcb->free = true;
        //because mcb single way link stack, we need loop from the start
        for(mcb = g_top; mcb->next; mcb = mcb->next){
            if(!mcb->free)
                break;
        }
        //当内存控制块链表中只有一个结点时,删除该节点
        if(mcb->free){
            g_top = mcb->next;
            brk(mcb);
        }
        //mcb是栈结构,头指针从栈顶一直遍历过一段连续的空闲块空间,然后一次性释放这段内存
        else if(mcb != g_top){
            g_top = mcb;
            brk((void*)mcb + sizeof(MCB) + mcb->size);
        }
    }
}

int main()
{
    printf("1\n");
    int *p = my_malloc(sizeof(int));
    printf("2\n");
    *p = 10;
    printf("3\n");
    printf("%d\n", *p);
    printf("4\n");
    my_free(p);
    return 0;
}

0x8: UnixC(系统调用)的内存分配函数

1. sbrk函数: 主要用于调整所申请堆内存的大小

#include <unistd.h>
void *sbrk(intptr_t increment);
    1) increment > 0: 申请内存空间
    2) increment < 0: 释放内存空间
    3) increment = 0: 获取内存空间当前位置
返回值
    1) 成功: 返回调整内存大小之前的位置(即上次调用sbrk/brk后的堆尾指针)
    2) 失败: (void*)-1

系统内ubweihu一个指针,指向当前堆尾,即堆区最后一个字节的下一个位置,sbrk函数根据增量参数调整该指针的位置,同时返回该指针调整前的位置,其间若发生内存页耗尽(OOM)或空闲,则自动追加或取消内存页的映射,使用sbrk申请内存时,不会申请额外的空间存储管理内存的相关元信息

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(void){
    void* p1 = sbrk(4);
    void* p2 = sbrk(4);
    void* p3 = sbrk(4);
    printf("p1 = %p, p2 = %p, p3 = %p\n", p1, p2, p3);
    
    void* cur = sbrk(0);
    printf("cur = %p\n", cur);
    
    void* p4 = sbrk(-4);
    printf("p4 = %p\n", p4);
    return 0;
}

2. brk函数: 主要用于根据参数指定的目标位置调整内存大小

int brk(void *addr);
1. 目标位置 > 之前的目标位置: 申请内存
2. 目标位置 < 之前的目标位置: 释放内存
3. 目标位置 = 之前的目标位置: 内存不变

系统内部维护一个指针,指向当前堆尾,即堆区最后一个字节的下一个位置,brk函数根据指针参数设置该指针的位置,其间若发现内存页耗尽或空闲,则自动追加或取消内存页的映射

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(void){
    void* p = sbrk(0);
    brk(p + 4);

    void* p2 = sbrk(0);
    printf("p = %p, p2 = %p\n", p, p2);

    brk(p+8);
    p2 = sbrk(0);
    printf("p = %p, p2 = %p\n", p, p2);
    
    brk(p);
    p2 = sbrk(0);
    printf("p = %p, p2 = %p\n", p, p2);
    return 0;
}

使用brk函数释放内存比较方便,因此我们使用brk和sbrk函数搭配使用

1. UnixC的brk和sbrk更加本质的揭示了堆内存的管理本质,即一段连续的内存空间,我们熟悉的glibc malloc/free是在brk和sbrk的基础上在内部维护了一套数据结构,使用MCB链表栈的形式进行堆内存管理,并向上层用户提供内存池

1. 使用sbrk函数负责申请内存、使用brk函数负责释放内存
2. sbrk和brk本质上是移动堆尾指针的两种不同方法,移动过程中还要兼顾虚拟内存和物理内存之间映射关系的建立和解除(以页为单位)
3. 用sbrk分配内存比较方便,用多少内存就传多少增量参数,同时返回指向新分配内存区域的指针,但是用sbrk做一次性内存释放比较麻烦,因为必须将所有的既往增量进行累加
4. 用brk释放内存比较方便,只需将堆尾指针设回到一开始的位置即可一次性释放掉之前分多次分配的内存,但用brk分配内存比较麻烦,因为必须根据所需要的内存大小计算出堆尾指针的绝对位置
5. 最佳实践: 用sbrk分多次分配适量内存,最后用brk一次性整体释放

0x9: 内存映射的建立与解除

1. 建立内存映射

在当前进程的虚拟地址空间中建立虚拟内存到物理内存或文件的映射

#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
1. addr: 映射区内存起始地址,传入NULL则系统自动选定后返回(推荐传入NULL)
2. length: 映射区字节长度,自动按页(4K)取整
3. prot: 映射区访问权限,可取以下值(可按位或)
    1) PROT_EXEC: Pages may be executed.
    2) PROT_READ: Pages may be read.
    3) PROT_WRITE: Pages may be written.
    4) PROT_NONE: Pages may not be accessed. 
4. flags: 映射标志,可取以下值
    1) MAP_SHARED: Share this mapping. 对映射区的写操作直接反映到文件中
    2) MAP_PRIVATE: Create  a private copy-on-write mapping. 对映射区的写操作只反应到内存缓冲区中,并不会真正写入文件
    3) MAP_ANONYMOUS: The  mapping is not backed by any file; its contents are initialized to zero. 匿名映射,将虚拟内存映射到物理内存而非文件,忽略fd和offset参数

    4) MAP_32BIT (since Linux 2.4.20, 2.6): Put the mapping into the first 2 Gigabytes of the process address space.    
    5) MAP_FIXED: Don't interpret addr as a hint: place the mapping at exactly that address. 若在start上无法创建映射,则失败(如果无此标志则系统会自动调整)
    6) MAP_GROWSDOWN: Used for stacks. Indicates to the kernel virtual memory system that the mapping should extend downward in memory.
    7) MAP_HUGETLB (since Linux 2.6.32): Allocate the mapping using "huge pages."   
    8) MAP_LOCKED (since Linux 2.5.37): Lock the pages of the mapped region into memory in the manner of mlock(2).锁定映射区,保证其不被swap换出  
    9) MAP_NONBLOCK (since Linux 2.5.46): Only meaningful in conjunction with MAP_POPULATE. Don't perform read-ahead: create page tables entries only for pages that are already present in RAM.  
    10) MAP_NORESERVE: Do not reserve swap space for this mapping. When swap space is reserved, one has the guarantee that it is possible to modify the mapping. When swap space is not  reserved one  might  get  SIGSEGV upon a write if no physical memory is available.  
    11) MAP_POPULATE (since Linux 2.5.46): Populate  (prefault) page tables for a mapping. For a file mapping, this causes read-ahead on the file.  Later accesses to the mapping will not be blocked by  page  faults. 
    12) MAP_STACK (since Linux 2.6.27): Allocate the mapping at an address suitable for a process or thread stack.  
    13) MAP_UNINITIALIZED (since Linux 2.6.33): Don't clear anonymous pages. This flag is intended to improve performance on embedded devices.    
5. fd: 文件描述符,单纯把mmap用作内存映射(申请一段可用的虚拟内存)时,传入0即可
6. offset: 文件偏移量,自动按页(4K)对齐,单纯把mmap用作内存映射(申请一段可用的虚拟内存)时,传入0即可

2. 解除内存映射

解除虚拟内存到物理内存或文件的映射

#include <sys/mman.h>
int munmap(void *addr, size_t length);
1. addr: 映射区内存起始地址,必须是页的首地址
2. length: 映射区字节长度,自动按页(4K)取整

需要注意的是

1. mmap/munmap底层不维护任何数据结构,只是返回一个首地址,所分配的虚拟内存位于堆中
2. brk/sbrk底层维护一个指针,记录所分配的内存结尾,所分配的虚拟内存位于堆中,底层调用的就是mmap/munmap
3. malloc底层维护一个线性链表和MCB控制信息,不可越界访问,所分配的虚拟内存位于堆中,底层调用的就是brk/sbrk
4. 每个进程都有4G的虚拟内存空间,虚拟内存地址只是一个数字,在于实际物理内存建立映射之前是不能访问的
5. 所谓内存分配与释放,其本质就是建立或解除从虚拟内存到物理内存的映射,并在底层维护不同形式的数据结构,以把虚拟内存的占用与空闲情况记录下来

3. 示例代码

#include <stdio.h>
#include <sys/mman.h>
#include <stdlib.h>
#include <string.h>

int main(){
    char* p = (char*)mmap(
            NULL,
            8192,
            PROT_READ | PROT_WRITE,
            MAP_ANONYMOUS | MAP_PRIVATE,
            0,
            0
        );
    if(p == MAP_FAILED){
        perror("mmap");
        exit(EXIT_FAILURE);
    }
    strcpy(p, "hello, memory!");
    printf("%s\n", p);

    if(-1 == munmap(p, 4096)){
        perror("munmap");
        exit(EXIT_FAILURE);
    }
    strcpy(p += 4096, "hello memory twice");
    printf("%s\n", p);
    if(-1 == munmap(p, 4096)){
        perror("munmap");
        exit(EXIT_FAILURE);
    }

    return 0;
}

 

10. 系统调用

0x1: Unix应用的层次结构

1. Unix/Linux系统的大部分功能都是通过系统调用实现的,如open、close等
2. Unix/Linux的系统调用已经被封装成C函数的形式,但它们并不是C语言标准库的一部分
3. 标准库函数大部分时间运行在用户态,但部分函数偶尔也会调用系统调用,进入内核态,如malloc、free等
4. 我们自己编写的代码也可以跳过标准库,直接使用系统调用,如brk、sbrk、mmap、munmap等,与操作系统内核交互,进入内核态
5. 系统调用在内核中实现,其外部接口定义在C库中,该接口的实现借助软中断进入内核

 

11.  文件管理(Unix C)

0x1: 文件的打开与关闭

文件在使用之前需要将其打开才能使用,打开文件时,系统会为其在内核中维护一套专门的数据结构,保存该文件的信息。而当文件不再使用之后,需要将其关闭,并删除在内核中专门的数据结构

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

int main(){
    long openmax = sysconf(_SC_OPEN_MAX);
    printf("一个进程最多能持有%ld个处于打开状态的文件描述符\n", openmax);

    int fd = open("open.txt", O_RDWR | O_CREAT | O_TRUNC, 0666);
    if(-1 == fd){
        perror("open");
        exit(EXIT_FAILURE);
    }

    if(-1 == close(fd)){
        perror("close");
        exit(EXIT_FAILURE);
    }

    return 0;
}

0x2: I/O重定向

为了解决内核对象在可访问性与安全性之间的矛盾,Unix系统通过所谓的文件描述符,将位于内核空间中的文件表项间接地提供给运行于用户空间中的程序代码。
为了便于管理在系统中运行的各个进程,内核会维护一张存有各进程信息的列表,谓之进程表。系统中的每个进程在进程表中都占有一个表项。每个进程表项都包含了针对特定进程的描述信息,如进程ID、用户ID、组ID等,其中也包含了一个被称为文件描述符表的数据结构。
文件描述符表的每个表项都至少包含两个数据项——文件描述符标志和文件表项指针,而所谓文件描述符,其实就是文件描述符表项在文件描述符表中从0开始的下标。
系统内核缺省为每个进程打开三个文件描述符,它们在unistd.h头文件中被定义为三个宏

#define STDIN_FILENO 0 // 标准输入,一般从键盘输入
#define STDOUT_FILENO 1 // 标准输出,一般输出到显示器
#define STDERR_FILENO 2 // 标准错误,一般输出到显示器

//可以通过控制台命令改变上面三个文件描述符的I/O定向

#include <stdio.h>
#include <string.h>

int main(){
    char buf[1024];
    scanf("%s", buf);
    printf("%s\n", buf);

    strcpy(buf, "标准输出已被I/O重定向为输出到文件o.txt");
    printf("%s\n", buf);

    return 0;
}

./ioredirect 0<i.txt 1>o.txt

1. ./ioredirect: 可执行文件名
2. 0<i.txt: 关闭文件描述符0,打开i.txt,该文件获得文件描述符0,即代码中的scanf函数从i.txt文件中输入内容
3. 1>o.txt: 关闭文件描述符1,打开o.txt,该文件获得文件描述符1,即代码中的printf函数将内容输出到o.txt

0x3: 写入文件

文件被打开后,就可以向其中写入字节流,并在不使用文件时需要将文件关闭

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>

int main(){
    int fd = open("data.txt", O_RDWR | O_CREAT | O_TRUNC, 0666);
    if(-1 == fd){
        perror("open");
        exit(EXIT_FAILURE);
    }

    char text[] = "将这些内容写入到文件中";
    size_t towrite = strlen(text);
    ssize_t written = write(fd, text, towrite);
    if(-1 == written){
        perror("write");
        exit(EXIT_FAILURE);
    }

    if(-1 == close(fd)){
        perror("close");
        exit(EXIT_FAILURE);
    }

    return 0;
}

0x4: 读取文件

文件被打开后,就可以从其中读出字节流,注意在不使用文件时需要将文件关闭

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>

int main(){
    int fd = open("data.txt", O_RDONLY, 0444);
    if(-1 == fd){
        perror("open");
        exit(EXIT_FAILURE);
    }

    char text[1024] = {};
    size_t toread = 1024;
    ssize_t readed = read(fd, text, toread);
    if(-1 == readed){
        perror("read");
        exit(EXIT_FAILURE);
    }
    printf("%s\n", text);

    if(-1 == close(fd)){
        perror("close");
        exit(EXIT_FAILURE);
    }

    return 0;
}

0x5: 二进制读写

基于系统调用的文件读写本来就是面向二进制字节流的,因此对二进制读写而言,无需做任何额外的工作

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>

typedef struct Employee{
    char name[256];
    int age;
    float salary;
} EMP;

int main(){
    //write binary data
    int fd = open("data.txt", O_RDWR | O_CREAT | O_TRUNC, 0666);
    if(-1 == fd){
        perror("open");
        exit(EXIT_FAILURE);
    }

    EMP emp = {"赵云", 25, 8000};
    if(-1 == write(fd, &emp, sizeof(emp))){
        perror("write");
        exit(EXIT_FAILURE);
    }

    if(-1 == close(fd)){
        perror("close");
        exit(EXIT_FAILURE);
    }

    //read binary data
    fd = open("data.txt", O_RDONLY, 0444);
    if(-1 == fd){
        perror("open");
        exit(EXIT_FAILURE);
    }

    EMP emp_read = {};
    if(-1 == read(fd, &emp_read, sizeof(emp))){
        perror("read");
        exit(EXIT_FAILURE);
    }
    printf("%s,%d,%f", emp_read.name, emp_read.age, emp_read.salary);

    if(-1 == close(fd)){
        perror("close");
        exit(EXIT_FAILURE);
    }

    return 0;
}

0x6: 文本读写

因为基于系统调用(Unix C)的文件读写是面向二进制字节流的,因此对写入内容的格式化、以及对读取内容的解格式化,都必须通过自己编写的代码进行处理(自己构造出一个字符串)

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>

typedef struct Employee{
    char name[256];
    int age;
    float salary;
} EMP;

int main(){
    //write ascii data
    int fd = open("data.txt", O_RDWR | O_CREAT | O_TRUNC, 0666);
    if(-1 == fd){
        perror("open");
        exit(EXIT_FAILURE);
    }
    
    EMP emp_write = {"赵云", 25, 8000};
    char buf[2014] = {};
    sprintf(buf, "%s %d %0.2f", emp_write.name, emp_write.age, emp_write.salary);
    if(-1 == write(fd, buf, strlen(buf))){
        perror("write");
        exit(EXIT_FAILURE);
    }
    
    if(-1 == close(fd)){
        perror("close");
        exit(EXIT_FAILURE);
    }

    //read ascii data
    fd = open("data.txt", O_RDONLY, 0444);
    if(-1 == fd){
        perror("open");
        exit(EXIT_FAILURE);
    }

    EMP emp_read = {};
    memset(buf, 0x0, sizeof(buf) / sizeof(buf[0]));
    if(-1 == read(fd, buf, 1024)){
        perror("read");
        exit(EXIT_FAILURE);
    }
    sscanf(buf, "%s%d%f", emp_read.name, &emp_read.age, &emp_read.salary);
    printf("%s,%d,%f\n", emp_read.name, emp_read.age, emp_read.salary);

    if(-1 == close(fd)){
        perror("close");
        exit(EXIT_FAILURE);
    }

    return 0;
}

0x7: 顺序与随机读写

1. 文件读写位置

1. 每个打开的文件都有一个与其相关的文件读写位置保存在文件表项中,用以记录从文件头开始计算的字节偏移
2. 文件读写位置通常是一个非负的整数,用off_t类型表示,在32位系统上被定义为long int,而在64位系统上则被定义为long long int
3. 打开一个文件时,除非指定了O_APPEND标志,否则文件读写位置一律被设置为0,即文件首字节的位置

2. 顺序读写

1. 每一次读写操作都从当前的文件读写位置开始,并根据所读写的字节数,同步增加文件读写位置,为下一次读写做好准备
2. 因为文件读写位置是保存在文件表项而不是v节点中的,因此通过多次打开同一个文件得到多个文件描述符,各自拥有文件读写位置

3. 随机读写

1. 人为调整文件读写位置

4. 文件空洞

1. 可以通过lseek函数将文件读写位置设置到文件尾之后
2. 在超过文件尾的位置上写入数据,将在文件中形成空洞,位于文件中但没有被写过的字节都被设置为0
3. 文件空洞不占用磁盘空间,但被计算在文件大小之内

5. 示例代码

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>

typedef struct Employee{
    char name[256];
    int age;
    float salary;
} EMP;

int main(){
    //write ascii data
    int fd = open("data.txt", O_RDWR | O_CREAT | O_TRUNC, 0666);
    if(-1 == fd){
        perror("open");
        exit(EXIT_FAILURE);
    }
    
    EMP emp_write = {"赵云", 25, 8000};
    char buf[2014] = {};
    sprintf(buf, "%s %d %0.2f", emp_write.name, emp_write.age, emp_write.salary);
    if(-1 == write(fd, buf, strlen(buf))){
        perror("write");
        exit(EXIT_FAILURE);
    }

    //set the point to the start of this file
    lseek(fd, 0, SEEK_SET);

    EMP emp_read = {};
    memset(buf, 0x0, sizeof(buf) / sizeof(buf[0]));
    if(-1 == read(fd, buf, 1024)){
        perror("read");
        exit(EXIT_FAILURE);
    }
    sscanf(buf, "%s%d%f", emp_read.name, &emp_read.age, &emp_read.salary);
    printf("%s,%d,%f\n", emp_read.name, emp_read.age, emp_read.salary);

    if(-1 == close(fd)){
        perror("close");
        exit(EXIT_FAILURE);
    }

    return 0;
}

0x8: 复制文件描述符

1. dup

复制文件描述符表项

#include <unistd.h>
int dup(oldfd);
1. dup函数将oldfd参数所对应的文件描述符表项复制到文件描述符表的第一个空闲项中,同时返回该表项所对应的文件描述符
2. dup函数返回的文件描述符一定是调用进程当前未使用的最小文件描述符
3. dup函数只复制文件描述符表项,不复制文件表项和v节点,因此该函数所返回的文件描述符可以看作是参数oldfd的副本,它们标识同一个文件表项

需要注意的是,当关闭文件时,即使是由dup函数产生的文件描述符副本,也应该通过close函数关闭,因为只有当关联于一个文件表项的所有文件描述符(指向文件表项的指针)都被关闭了,该文件表项才会被销毁
由dup函数返回的文件描述符与作为参数传递给该函数的文件描述符标识的是同一个文件表项,而文件读写位置是保存在文件表项而非文件描述符表项中,因此通过这些复制出来的/或者原始的文件描述符中的任何一个,对文件进行读写或随机访问,都会影响通过其他文件描述符操作的文件读写位置,这与多次通过open函数打开同一个文件不同

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

int main(){
    int fd = open("dup.txt", O_RDWR | O_CREAT | O_TRUNC, 0666);
    if(-1 == fd){
        perror("open");
        exit(EXIT_FAILURE);
    }

    char buf[1024] = "old fd's content";
    if(-1 == write(fd, buf, strlen(buf) * sizeof(buf[0]))){
        perror("write");
        exit(EXIT_FAILURE);
    }

    int newfd = dup(fd);
    strcpy(buf, "dup's fd's content");
    if(-1 == write(newfd, buf, strlen(buf) * sizeof(buf[0]))){
        perror("write");
        exit(EXIT_FAILURE);
    }

    if(-1 == close(fd)){
        perror("close");
        exit(EXIT_FAILURE);
    }
    if(-1 == close(newfd)){
        perror("close");
        exit(EXIT_FAILURE);
    }

    return 0;
}

2. dup2

复制文件描述符表项到指定位置

#include <unistd.h>
int dup2(int oldfd, int newfd);
1. dup2函数的功能与dup函数几乎完全一样,唯一不同就是允许调用者通过newfd参数指定目标文件描述符(dup的newfd是系统自动指定的),正常情况下该函数的返回值应该与newfd参数的值相等
2. dup2函数在复制由oldfd参数所标识的源文件描述符表项时,会首先检查由newfd参数所表示的目标文件描述符表项是否空闲,若空闲则直接将oldfd复制给newfd,否则会先将指定的目标文件描述符newfd关闭,再进行复制

示例代码

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

int main(){
    int fd = open("dup2.txt", O_RDWR | O_CREAT | O_TRUNC, 0666);
    if(-1 == fd){
        perror("open");
        exit(EXIT_FAILURE);
    }

    char buf[1024] = "this is origin file's content";
    if(-1 == write(fd, buf, strlen(buf) * sizeof(buf[0]))){
        perror("write");
        exit(EXIT_FAILURE);
    }

    int newfd = dup2(STDOUT_FILENO, fd);
    strcpy(buf, "after dup2, this content will be displayed into console");
    if(-1 == write(newfd, buf, strlen(buf) * sizeof(buf[0]))){
        perror("write");
        exit(EXIT_FAILURE);
    }
    
    //can't do it, fd has already be closed
    /*
    if(-1 == close(fd)){
        perror("close");
        exit(EXIT_FAILURE);
    }
    */
    if(-1 == close(newfd)){
        perror("close");
        exit(EXIT_FAILURE);
    }

    return 0;
}

dup/dup2函数用于复制文件描述符时,实质上是复制文件描述符所对应的文件表地址指针,也就是让多个文件描述符对应了同一个文件表,从而对应同一个文件

0x9: 文件控制

1. 复制文件描述符

#include <fcntl.h>
int fcntl(int oldfd, int cmd, int newfd);
1. oldfd: 源文件描述符
2. cmd: 控制命令: 取F_DUPFD
3. newfd: 目标文件描述符

fcntl(F_DUPFD)的功能与dup2函数几乎完全一样,唯一不同就是当目标文件描述符newfd处于打开状态时,并不关闭它,而是继续往下找一个大于等于指定值的fd作为目标

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

int main(){
    int fd = open("fcntl.txt", O_RDWR | O_CREAT | O_TRUNC, 0666);
    if(-1 == fd){
        perror("open");
        exit(EXIT_FAILURE);
    }

    char buf[104] = "this is old fd's content";
    if(-1 == write(fd, buf, strlen(buf) * sizeof(buf[0]))){
        perror("write");
        exit(EXIT_FAILURE);
    }

    int newfd = fcntl(STDOUT_FILENO, F_DUPFD, fd);
    strcpy(buf, "this is fcntl' fd's content, but i will display in console");
    if(-1 == write(newfd, buf, strlen(buf) * sizeof(buf[0]))){
        perror("write");
        exit(EXIT_FAILURE);
    }

    if(-1 == write(newfd, buf, strlen(buf) * sizeof(buf[0]))){
        perror("write");
        exit(EXIT_FAILURE);
    }

    if(-1 == close(fd)){
        perror("close");
        exit(EXIT_FAILURE);
    }
    if(-1 == close(newfd)){
        perror("close");
        exit(EXIT_FAILURE);
    }

    return 0;
}

2. 获取/设置文件描述符标志

1. 文件描述符表的每个表项都至少包含来年哥哥数据项
    1) 文件描述符标志: 用于表示特定文件描述符的属性
    2) 文件表项指针
2. 文件描述符标志由多个二进制位组合而成,每个二进制位表示某一方面的属性

获取文件描述符标志

#include <fcntl.h>
int fcntl(int fd, int cmd);
1. fd: 文件描述符
2. cmd: 控制指令,去F_GETFD

设置文件描述符标志

#include <fcntl.h>
int fcntl(int fd, int cmd, int flags);
1. fd: 文件描述符
2. cmd: 控制命令,取F_SETFD
3. flags: 文件描述符标志

3. 获取/设置文件状态标志

由文件状态标志是由open函数的flags参数设定的,用于表示特定文件描述符的状态保存在文件表项中
文件状态标志由多个二进制位组合而成,每个二进制位表示某一方面的状态,包括 

1. O_RDONLY - 只读 O_WRONLY - 只写 O_RDWR - 读写 O_APPEND - 追加
2. O_CREAT - 创建 O_EXCL - 排斥 O_TRUNC - 清空 O_SYNC - 同步
3. O_ASYNC - 异步 O_NONBLOCK - 非阻塞

获取文件状态标志

#include <fcntl.h>
int fcntl(int fd, int cmd);
1. fd: 文件描述符
2. cmd: 控制指令,取F_GETFL

示例代码

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

int main(){
    int fd = open("data.txt", O_RDWR | O_CREAT | O_TRUNC, 0666);
    if(-1 == fd){
        perror("open");
        exit(EXIT_FAILURE);
    }

    int flags = fcntl(fd, F_GETFL);
    if((flags & O_ACCMODE) == O_RDONLY)
        printf("read only\n");
    if(flags & O_WRONLY)
        printf("write only\n");
    if(flags & O_RDWR)
        printf("read write\n");
    if(flags & O_APPEND)
        printf("append\n");
    if(flags & O_SYNC)
        printf("sync\n");
    if(flags & O_ASYNC)
        printf("async\n");
    if(flags & O_NONBLOCK)
        printf("no block");

    if(-1 == close(fd)){
        perror("close");
        exit(EXIT_FAILURE);
    }

    return 0;
}

追加文件状态标志

#include <fcntl.h>
int fcntl(int fd, int cmd);
1. fd: 文件描述符
2. cmd: 控制指令,取F_SETFL

示例代码

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

int main(){
    int fd = open ("data.txt", O_RDWR | O_CREAT | O_TRUNC, 0666);
    if (fd == -1){
        perror ("open");
        exit (EXIT_FAILURE);
    }
    int flags = fcntl(fd, F_GETFL);
    if (flags & O_APPEND)
        printf ("1.追加\n");
    if (flags & O_NONBLOCK)
        printf ("1.非阻塞\n");
    
    if (fcntl (fd, F_SETFL, O_APPEND | O_NONBLOCK) == -1){
        perror ("fcntl");
        exit (EXIT_FAILURE);
    }
    flags = fcntl(fd, F_GETFL);
    if (flags & O_APPEND)
        printf ("2.追加\n");
    if (flags & O_NONBLOCK)
        printf ("2.非阻塞\n");
    if (close (fd) == -1){
        perror ("close");
        exit (EXIT_FAILURE);
    }
    
    return 0;
}

0x10: 文件锁

如果两个或两个以上的进程同时向一个文件的某个特定区域写入数据,那么最后写入文件的数据极有可能因为写操作的交错而产生混乱

1. 读锁和写锁

1. 为了避免多个进程在读写同一个文件的同一个区域时发生冲突,Unix/Linux系统引入了文件锁机制,并把文件锁分为读锁和写锁两种
    1) 读锁: 共享锁,对一个文件的特定区域可以加多把读锁
    2) 写锁: 排它锁,对一个文件的特定区域只能加一把写锁
2. 基于锁的操作模型是: 读/写文件中的特定区域之前,先加入读/写锁,锁成功了再读/写,读/写完成以后再解锁

2. 加锁和解锁

对给定文件的特定区域加锁或解锁

#include <fcntl.h>
int fcntl(int fd, int cmd, struct flock* lock);
1. fd: 文件描述符
2. cmd: 控制指令,可取以下值
    1) F_SETLKW: 阻塞模式
    2) F_SETLK: 非阻塞模式
3. lock: 决定是加锁还是解锁,若因其他进程持有锁而导致枷锁失败
    1) 阻塞模式下: 返回-1
    2) 非阻塞模式下: 返回-1,并设置错误码为EAGANIN

需要注意的是

1. 当通过close函数关闭文件描述符时,调用进程在该文件描述符上所加的一切锁将被自动解除
2. 当进程终止时,该进程在所有文件描述符上所加的一切锁将被自动解除
3. 文件锁仅在不同进程之间起作用,同一个进程的不同线程不同通过文件锁解决读写冲突问题
4. 通过fork、vfork函数创建的子进程,不继承父进程所加的任何文件锁
5. 通过exec函数族创建的新进程,会继承调用进程所加的全部文件锁,除非某文件描述符带有FD_CLOEXEC标志

示例代码: 从文件头10字节开始的20字节以阻塞模式加读锁

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

int main(){
    int fd = open("data.txt", O_RDWR | O_CREAT | O_TRUNC, 0666);
    if(-1 == fd){
        perror("open");
        exit(EXIT_FAILURE);
    }

    struct flock lock;
    lock.l_type = F_RDLCK;
    lock.l_whence = SEEK_SET;
    lock.l_start = 10;
    lock.l_len = 20;
    lock.l_pid = -1;
    if(-1 == fcntl(fd, F_SETLKW, &lock)){
        perror("fcntl");
        exit(EXIT_FAILURE);
    }

    if(-1 == close(fd)){
        perror("close");
        exit(EXIT_FAILURE);
    }

    return 0;
}

示例代码: 从当前位置10字节开始到文件尾以非阻塞模式加写锁

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>

int main(){
    int fd = open("data.txt", O_RDWR | O_CREAT | O_TRUNC, 0666);
    if(-1 == fd){
        perror("open");
        exit(EXIT_FAILURE);
    }

    struct flock lock;
    lock.l_type = F_WRLCK;
    lock.l_whence = SEEK_CUR;
    lock.l_start = 10;
    lock.l_len = 0;
    lock.l_pid = -1;
    if(-1 == fcntl(fd, F_SETLK, &lock)){
        if(EAGAIN != errno){
            perror("fcntl");
            exit(EXIT_FAILURE);
        }
        printf("waiting\n");
    }

    if(-1 == close(fd)){
        perror("close");
        exit(EXIT_FAILURE);
    }

    return 0;
}

示例代码: 对整个文件解锁

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>

int main(){
    int fd = open("data.txt", O_RDWR | O_CREAT | O_TRUNC, 0666);
    if(-1 == fd){
        perror("open");
        exit(EXIT_FAILURE);
    }

    struct flock lock;
    lock.l_type = F_UNLCK;
    lock.l_whence = SEEK_SET;
    lock.l_start = 0;
    lock.l_len = 0;
    lock.l_pid = -1;
    if(-1 == fcntl(fd, F_SETLKW, &lock)){
        perror("fcntl");
        exit(EXIT_FAILURE);
    }

    if(-1 == close(fd)){
        perror("close");
        exit(EXIT_FAILURE);
    }

    return 0;
}

文件锁是一种建议锁(君子协定)
对文件进行加写锁之后,还是可以向文件中写入数据内容的,结果说明文件锁是独立于文件的,并没有真正锁定对文件的读写操作,也就是说文件锁只能用于锁定同样遵循"操作前先尝试获取文件锁"的这种操作,如果对方不遵守该协定,则文件锁无法保证线性操作安全

0x11: 文件元数据

1. 获取文件元数据

从i节点中提取文件的元数据,即文件的属性信息

2. 辅助分析文件类型的实用宏(在内部实现也是调用了stat系列函数)

1. S_ISREG(): 是否普通文件
2. S_ISDIR(): 是否目录
3. S_ISSOCK(): 是否本地套接字
4. S_ISCHR(): 是否字符设备
5. S_ISBLK(): 是否块设备
6. S_ISLNK(): 是否符号链接
7. S_ISFIFO(): 是否有名管道

代码示例

#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

const char* mtos(mode_t m){
    static char s[11];
    if(S_ISDIR(m)){
        strcpy(s, "d");
    }
    else if(S_ISSOCK(m)){
        strcpy(s, "s");
    }
    else if(S_ISCHR(m)){
        strcpy(s, "c");
    }
    else if(S_ISBLK(m)){
        strcpy(s, "b");
    }
    else if(S_ISLNK(m)){
        strcpy(s, "l");
    }
    else if(S_ISFIFO(m)){
        strcpy(s, "p");
    }
    else
        strcpy(s, "-");

    strcat(s, m & S_IRUSR ? "r" : "-");
    strcat (s, m & S_IWUSR ? "w" : "-");
    strcat (s, m & S_IXUSR ? "x" : "-");
    strcat (s, m & S_IRGRP ? "r" : "-");
    strcat (s, m & S_IWGRP ? "w" : "-");
    strcat (s, m & S_IXGRP ? "x" : "-");
    strcat (s, m & S_IROTH ? "r" : "-");
    strcat (s, m & S_IWOTH ? "w" : "-");
    strcat (s, m & S_IXOTH ? "x" : "-");

    if(m & S_ISUID)
        s[3] = (s[3] == 'x' ? 's' : 'S');
    if(m & S_ISGID)
        s[6] = (s[6] == 'x' ? 's' : 'S');
    if(m & S_ISVTX)
        s[9] = (s[9] == 'x' ? 't' : 'T');
    
    return s;
}

int main(){
    int fd = open("data.txt", O_RDWR | O_CREAT | O_TRUNC, 0666);
    if(-1 == fd){
        perror("open");
        exit(EXIT_FAILURE);
    }
    struct stat statdata;
    fstat(fd, &statdata);

    printf("%s\n", mtos(statdata.st_mode));

    if(-1 == close(fd)){
        perror("close");
        exit(EXIT_FAILURE);
    }

    return 0;
}

0x12: 访问测试

测试调用进程对指定文件是否拥有足够的访问权限

#include <unistd.h>
int access(const char* pathname, int mode);
1 pathname: 文件路径
2. mode: 访问权限,可取以下值
    1) R_OK: 可读否
    2) W_OK: 可写否
    3) X_OK: 可执行否
    4) F_OK: 存在否

示例代码

#include <stdio.h>
#include <unistd.h>

int main(){
    char pathname[] = "a.out";
    printf("file%s\n", pathname);
    if(-1 == access(pathname, F_OK)){
        printf("not exist\n");
    }
    else{
        if(-1 == access(pathname, R_OK)){
            printf("can't read\n");
        }
        else{
            printf("can read\n");
        }

        if(-1 == access(pathname, W_OK)){
            printf("can't write\n");
        }
        else{
            printf("can write\n");
        }

        if(-1 == access(pathname, X_OK)){
            printf("can't execute\n");
        }
        else{
            printf("can execute\n");
        }
    }

    return 0;
}

0x13: 权限掩码

设置调用进程的权限掩码,进程的权限掩码会屏蔽掉该进程创建文件时指定的权限,调用unask函数可以认为改变调用进程的权限掩码,如果改为0000,则不屏蔽任何权限

#include <sys/stat.h>
mode_t umask(mode_t cmask);
1. cmask: 新权限掩码

注意,权限掩码是进程的属性之一,umask函数所影响的仅仅是调用该函数的进程创建新文件,对其父进程,比如shell进程,没有任何影响

#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

void printm(mode_t m){
    static char s[11];
    memset(s, 0, 11);
    strcat(s, m & S_IRUSR ? "r" : "-");
    strcat(s, m & S_IWUSR ? "w" : "-");
    strcat(s, m & S_IXUSR ? "x" : "-");
    strcat(s, m & S_IRGRP ? "r" : "-");
    strcat(s, m & S_IWGRP ? "w" : "-");
    strcat(s, m & S_IXGRP ? "x" : "-");
    strcat(s, m & S_IROTH ? "r" : "-");
    strcat(s, m & S_IWOTH ? "w" : "-");
    strcat(s, m & S_IXOTH ? "x" : "-");
    
    printf("%s\n", s);
}

int main(){
    int fd = open("data.txt", O_RDWR | O_CREAT | O_TRUNC, 0666);
    if(-1 == fd){
        perror("open");
        exit(EXIT_FAILURE);
    }

    struct stat statdata;
    fstat(fd, &statdata);
    printf("before chmod: \n");
    printm(statdata.st_mode);

    if(-1 == close(fd)){
        perror("close");
        exit(EXIT_FAILURE);
    }

    umask(0000);

    fd = open("data1.txt", O_RDWR | O_CREAT | O_TRUNC, 0666);
    if(-1 == fd){
        perror("open");
        exit(EXIT_FAILURE);
    }
    printf("after chmod\n");
    fstat(fd, &statdata);
    printm(statdata.st_mode);
    
    if(-1 == close(fd)){
        perror("close");
        exit(EXIT_FAILURE);
    }

    return 0;
}

0x14: 内存映射文件

内存映射文件不仅提供了一种以访问内存的方式读写文件的方法,而且还在多个进程之间打通了一条基于文件共享的数据通道

#include <stdio.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

int main(){
    int fd = open("data.txt", O_RDWR | O_CREAT | O_TRUNC, 0666);
    if(-1 == fd){
        perror("open");
        exit(EXIT_FAILURE);
    }

    ftruncate(fd, 8192);
    char* p = (char*)mmap(NULL, 8192, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if(p == MAP_FAILED){
        perror("mmap");
        exit(EXIT_FAILURE);
    }

    strcpy(p, "Hello, File!");
    printf("%s\n", p);

    if(munmap(p, 4096) == -1){
        perror("munmap");
        exit(EXIT_FAILURE);
    }

    strcpy(p+=4096, "Hello, File Again!");
    printf("%s\n", p);  
    
    if(munmap(p, 4096) == -1){
        perror("munmap");
        exit(EXIT_FAILURE);
    }

    if(-1 == close(fd)){
        perror("close");
        exit(EXIT_FAILURE);
    }

    return 0;
}

0x15: 硬链接

硬链接的本质就是目录文件里一个文件名和i节点号的对应条目。通过该条目,就可以根据一个文件的文件名迅速地找到与之相对应的i节点号,进而访问该文件的数据

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

int main(){
    int fd = open("foo,txt", O_RDWR | O_CREAT, 0666);
    if(-1 == fd){
        perror("open");
        exit(EXIT_FAILURE);
    }

    int linkresult = link("foo.txt", "bar.txt");
    if(-1 == linkresult){
        perror("link");
        exit(EXIT_FAILURE);
    }

    unlink("foo.txt");
    rename("bar.txt", "newbar.txt");

    char text[] = "the content that be writen into foo.txt";
    size_t towrite = strlen(text);
    ssize_t written = write(fd, text, towrite);
    if(-1 == written){
        perror("write");
        exit(EXIT_FAILURE);
    }

    if(-1 == close(fd)){
        perror("close");
        exit(EXIT_FAILURE);
    }

    fd = open("newbar.txt", O_RDONLY, 0444);
    if(-1 == fd){
        perror("newbar.txt open");
        exit(EXIT_FAILURE);
    }

    char text1[1024] = {};
    size_t toread = 1024;
    ssize_t readed = read(fd, text1, toread);
    if(-1 == readed){
        perror("read");
        exit(EXIT_FAILURE);
    }
    printf("%s\n", text1);

    if(-1 == close(fd)){
        perror("close");
        exit(EXIT_FAILURE);
    }

    return 0;
}

0x16: 目录的创建与删除

在进程中使用mkdir函数创建一个空目录,使用函数rmdir删除一个空目录

#include <stdio.h>
#include <sys/stat.h>
#include <unistd.h>

int main(){
    mkdir("newpath", 0666);
    getchar();
    rmdir("newpath");

    return 0;
}

0x17: 当前工作目录

当前工作目录是每个进程的属性之一,保存在进程表中,它是系统内核为进程解析相对路径的起点

#include <stdio.h>
#include <unistd.h>
int main()
{
        char buf[1024] = {};
        getcwd(buf, 1024);
        printf("%s\n", buf);
        chdir("/home/tarena/unixc/day01");
        getcwd(buf, 1024);
        printf("%s\n", buf);
        return 0;
}

0x18: 获取目录内容

目录由若干条目组成,每个条目均包含文件名、文件类型、i节点号等信息。readdir函数可以连续调用,调用一次返回一个条目,再调用一次返回下一个条目,直到返回NULL表示已经读完整个目录或者发生错误,为了区分这两种情况,可以通过检查errno是否被重置来进行判断

#include <stdio.h>
#include <dirent.h>
#include <stdlib.h>
#include <stdbool.h>
#include <errno.h>


bool printpathname(DIR* dirp){
    errno = 0;
    struct dirent* direntp;
    direntp = readdir(dirp);
    if(!direntp){
        if(!errno){
            return true;
        }
        perror("readdir");
        exit(EXIT_FAILURE);
    }
    printf("%s\n", direntp->d_name);

    return false;
}

int main(){
    int i;
    DIR* dirp = opendir("/root/tarena/");
    if(!dirp){
        perror("opendir");
        exit(EXIT_FAILURE);
    }

    int offset;
    for(i = 0; ;i++){
        if(printpathname(dirp))
            break;
        if(i == 5)
            offset = telldir(dirp);
    }

    seekdir(dirp, offset);
    printpathname(dirp);

    rewinddir(dirp);
    for(i = 0; ;i++){
        if(printpathname(dirp))
            break;
    }
    closedir(dirp);

    return 0;
}

出于线程安全的原因,新的Linux版本将全局变量改成了局部变量。最直观的解决办法就是在程序开头加上#include <errno.h>,将extern int errno这一句省略

 

12. 进程管理

0x1: 父子进程共享文件表

fork函数成功返回以后,系统内核为父进程维护的文件描述符表也被复制到子进程的进程表项中,文件表项并不复制

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <stdbool.h>
#define BUFSIZE 1024 * 1024
bool lock(int fd)
{
struct flock lock;
    lock.l_type = F_WRLCK;
    lock.l_whence = SEEK_SET;
    lock.l_start = 0;
    lock.l_len = 0;
    lock.l_pid = -1;
if (fcntl (fd, F_SETLK, &lock) == -1)
    {
if (errno != EAGAIN)
        {
            perror ("fcntl");
            exit (EXIT_FAILURE);
        }
returnfalse;
    }
returntrue;
}
void unlock(int fd)
{
struct flock lock;
    lock.l_type = F_UNLCK;
    lock.l_whence = SEEK_SET;
    lock.l_start = 0;
    lock.l_len = 0;
    lock.l_pid = -1;
    if (fcntl (fd, F_SETLKW, &lock) == -1)
    {
        perror ("fcntl");
        exit (EXIT_FAILURE);
    }
}
void writedata(int fd, char *buf, char c)
{
while(!lock(fd));
for (int i = 0; i < BUFSIZE - 1; i++)
        buf[i] = c;
for (int i = 0; i <1024; i++)
    {
int writed;
if ((writed = write (fd, buf + i, 1024)) == -1)
        {
            perror ("write");
            exit (EXIT_FAILURE);
        }
        printf("111111111->%c,buf[0]=%c, writed = %d\n", c, *(buf + i), writed);
    }
    unlock(fd);
}
int main()
{
    int fd = open ("data.txt", O_RDWR | O_CREAT | O_TRUNC, 0666);
    if (fd == -1)
    {
        perror ("open");
        exit (EXIT_FAILURE);
    }
    pid_t pid;
    if ((pid = fork()) <0)
    {
        perror("fork");
        return1;
    }
    if (pid == 0)
    {
char buf[BUFSIZE] = {};
        writedata(fd, buf, '0');
    }
else
    {
char buf[BUFSIZE] = {};
        writedata(fd, buf, '1');
    }
if (close (fd) == -1)
    {
        perror ("close");
        exit (EXIT_FAILURE);
    }
    return0;
}

0x2: 孤儿与僵尸

父进程创建子进程以后,子进程在操作系统的调度下与其父进程同时运行。如果父进程先于子进程终止,子进程即成为孤儿进程,同时被init进程收养,即成为init进程的子进程,因此init进程又被称为孤儿院进程。一个进程成为孤儿进程是正常的,系统中的很多守护进程都是孤儿进程。

如果子进程先于父进程终止,但父进程由于某种原因,没有回收子进程的退出状态,子进程即成为僵尸进程。僵尸进程虽然已经不再活动,但其终止状态仍然保留,也会占用系统资源,直到被其父进程回收才得以释放。如果父进程直到终止都未回收它的已成僵尸的子进程,init进程会立即收养并回收这些处于僵尸状态的子进程,因此一个进程不可能既是孤儿进程同时又是僵尸进程。一个进程成为僵尸进程需要引起注意,如果它的父进程长期运行而不终止,僵尸进程所占用的资源将长期得不到释放

 

 

 

Copyright (c) 2016 LittleHann All rights reserved

 

posted @ 2015-04-08 14:50  郑瀚Andrew  阅读(661)  评论(0编辑  收藏  举报