锐单电子商城 , 一站式电子元器件采购平台!
  • 电话:400-990-0325

unix文件内核结构、进程、信号集、管道、IPC、线程等概述(此外不定期更新关于c/c++内容)

时间:2023-10-27 18:37:01 3fd3y96变送器

Unix高级编程系统
业务逻辑:根据业务需要和设计的逻辑规则处理信息,与系统无关。
实现操作系统提供的各种功能辅助业务逻辑。
标准函数:scanf/printf - 源代码级兼容
系统函数:read/write - 接口级兼容
环境、性能、功能。
一、Unix系统简介
1.Unix系统的背景
1961-1969:史前时代
CTSS(Compatible Time-Sharing System,兼容分时系统),以MIT开发小组,小而简单的实验室原型。
Multics(Multiplexed Information and Computing System,多路信息与计算系统),庞大而负责,不堪重负。
Unics(Uniplexed information and Computing System,单路信息与计算系统),回归自然,走上正道。
1969-1971:创世纪
Ken Thompson,肯.汤普逊,Unix之父,B语言之父,内核用B语言 汇编语言开发,PDP-7,第一个Unix系统的核心和简单的应用来移植到PDP-11平台,功能更加完善。
1971-1979:出谷纪
Dennis Ritchie,丹尼斯.里奇,C语言之父用C语言重写Unix大大提高了系统内核Unix可读性、可维护性和可移植性——Unix V7.第一个真正意义上的Unix系统。
1980-1985:第一次Unix战争
AT&T贝尔实验室:SVR4
加州大学伯克利分校:BSD TCP/IP
DARPA,ARPANET(INTERNET)
IEEE,国际电气电子工程师协会,POSIX为Unix内核和外壳制定了一系列技术标准和规范,消除了系统版本之间的差异和统一操作系统。
1988-1990:第二次Unix战争
AT&T Sun
IBM DEC HP
比尔.盖茨->Windows
1990-现在
1991,Linus Torvalds创建了Linux系统的内核
1993,Linux已达到产品级操作系统水平
1993,AT&T将Unix系统卖给Novell
1994,Novell将Unix系统卖给X/Open组织
1995,X/Open将Unix系统捐给SCO
2000,SCO将Unix系统卖给Celdear——Linux发行商
Linux就是现代版本的Unix。
2.Linux系统的背景
类Unix免费开源操作系统。
不同发行版本使用相同的内核。
支持手机、路由器、视频游戏控制器、个人电脑、大型计算机等多种硬件平台。
隶属于GNU工程。GNU = GNU Not Unix。
受GPL许可证限制:如果发布了可执行的二进制代码,则必须同时发布可读源代码,并在发布任何基础上发布GPL任何限制性条款都不能添加到许可证软件中。
3.Linux系统的版本
早期版本:0.01,0.02..,1.00
旧计划:1.0.1..,2.6.0 (A.B.C)
A - 主版本号,内核大幅更新
B - 次版本号,重大内核修改,奇数测试版,偶数稳定版
C - 补丁序号,内核轻微修改
新计划:A.B.C-D.E
D - 施工次数反映了极小的更新
E - 描述信息
rc/r - 候选版本
smp - 支持对称多处理器
EL - Rad Hat的企业版本
mm - 试验新技术
...
cat /proc/version
4.Linux系统的特点
遵循GNU/GPL许可证
开放性
多用户
多任务
设备无关性
丰富网络功能
系统安全可靠
可移植性好
5.Linux的发行版本
Ubuntu - 流行,简单易用
Linux Mint - 新潮前位
Fedora - Red Hat的桌面版本
openSUSE - 华丽
Debian - 自由开放
Slackware - 简单,简单,简单
Red Hat - 经典,稳定,企业应用,支持全面
二、GNU编译器(gcc)
1.GCC的基本特点
1)支持各种硬件架构
x86-64
Alpha
ARM
PowerPC
SPARC
VAX
...
2)支持多种操作系统
Unix
Linux
BSD
Android
Mac OS X
iOS
Windows
3)支持多种编程语言
C
C
Objective-C
Java
Fortran
Pascal
Ada
4)GCC的版本
gcc -v
2.构建过程
源代码(.c)-预编译->头文件和宏扩展-编译->汇编码(.s)-汇编->目标码(.o)-链接->可执行代码(a.out)
代码:hello.c
vi hello.c - 编写源代码
gcc -E hello.c -o hello.i - 预编译(编译预处理)
gcc -S hello.i - 获得汇编代码(hello.s)
gcc -c hello.s - 获取目标代码(hello.o)
gcc hello.o -o hello - 获取可执行代码(hello)
./hello - 操作可执行代码
3.文件名后缀
.h - C语言源代码头文件 \
.c - C语言源代码文件预处理前 > 可读文本
.s - 汇编语言文件 /
.o - 目标文件 \
.a - 静态库文件 > 不可读的二进制
.so - 共享(动态)库文件 /
.out - 可执行文件 /
4.编译选项
gcc [选项] [参数] 文件1 文件2 ...
-o: 指定输出文件
如:gcc hello.c -o hello
-E: 预编译,缺省输出到屏幕,使用-o指定输出文件
如:gcc -E hello.c -o hello.i
-S: 将高级语言文件编译成汇编语言文件
如:gcc -S hello.c
-c: 汇编语言文件汇编成机器语言文件
如:gcc -c hello.s
-Wall:全部警告
如:gcc -Wall wall.c
代码:wall.c
-Werror:以警告为错误处理
如:gcc -Werror werror.c
代码:werror.c
-x: 指定源代码的语言
xxx.c - C语言
xxx.cpp - C 语言
xxx.for - Fortran语言
xxx.java - Java语言
...
gcc -x c cpp.c -lstdc -o cpp
代码:cpp.c
-O0/O1/O2/O3: 指定优化等级,O不优化,缺省O1优化
5.头文件
1)头文件写什么?
头文件卫士
#ifndef __XXX_
#define __XXX_
...
#endif
a.h
/ \
b.h c.h
\ /
d.c
包括其他头文件
宏定义
#define PI 3.14159
自定义类型
struct Circle {
double x, y, r;
};
类型别名
typedef struct Circle C;
外部变量声明
extern double e;
函数声明
double circleArea(C c);
a.h
/ \
b.c c.c
| |
b.o c.o
\ /
d - 重定义
一个头文件可能包含在多个源文件中,写在头文件中的函数定义将被预处理器扩展到多个包含头文件的源文件中,并在编译阶段编译到多个不同的目标文件中,这将导致链接错误:multiple definition,多重定义。
2)去哪里找文件?
gcc -I<附加搜索路径索路径>
#include
先找-I找系统目录指定目录。
#include "my.h"
先找-I指定目录,然后找到当前目录,最后找到系统目录。
头文件系统目录:
/usr/local/include - 第三方库
/usr/lib/gcc/i686-linux-gnu/5.4.0/include - 编译器库
代码:calc.h、calc.c、math.c
6.预处理指令
#include - 将指定的文件内容插至此指令处
#define - 定义宏
#undef - 删除宏
#if - 如果
#ifdef - 如果宏已定义
#ifndef - 如果宏未定义
#else - 否则,与#if/#ifdef/#ifndef配合使用
#elif - 否则如果,与#if/#ifdef/#ifndef配合使用
#endif - 结束判定,与#if/#ifdef/#ifndef配合使用
#error - 产生错误,结束预处理
#warning - 产生警告,继续预处理
代码:error.c
#line - 指定行号
代码:line.c
#pragma - 设定编译器的状态或者指示编译器的操作
#pragma GCC dependency 被依赖文件
a<-b
#pragma GCC poison 语法禁忌
#pragma pack(按几字节对齐:1/2/4/8)
#pragma pack() - 按缺省字节数对齐
代码:dep.c、pragma.c
7.预定义宏
无需自行定义,预处理器会根据事先设定好的规则将这些宏扩展成其对应的值。
__BASE_FILE__: 正在被处理的源文件名
__FILE__: 所在文件名
__LINE__: 所在行的行号
__FUNCTION__: 所在函数的函数名
__func__: 同__FUNCTION__
__DATE__: 处理日期
__TIME__: 处理时间
__INCLUDE_LEVEL__: 包含层数,从0开始
a - 0
#include "b" - 1
    #include "c" - 2
__cplusplus: C++有定义,C无定义
代码:print.h、predef.h、predef.c
8.环境变量
在进程向下文中保存的一些数据:键(功能,是什么)=值(内容)。
env
C_INCLUDE_PATH
C语言头文件的附加搜索路径,相当于-I选项。
CPATH
同C_INCLUDE_PATH
CPLUS_INCLUDE_PATH
C++语言头文件的附加搜索路径,相当于-I选项。
LIBRARY_PATH
链接库路径
LD_LIBRARY_PATH
加载库路径
代码:calc.h、calc.c、math.c
#include "/.../.../xxx.h" - 移植性差
#include "xxx.h"
gcc -I/.../... ... - 推荐
C_INCLUDE_PATH/CPATH=/.../...:/... - 易冲突
三、库
a.c -> a.out
foo()
bar()
hum()
main()
单一模型:将程序中所有功能全部实现于一个单一的源文件内部。编译时间长,不易于维护和升级,不易于协作开发。
分离模型:将程序中的不同功能模块划分到不同的源文件中。缩短编译时间,易于维护和升级,易于协作开发。
a.c   -> a.o \
foo()            | -> ...
bar()            |
b.c  -> b.o /
hum()
a.o \
b.o   | -> 库 + 其它模块 -> ...
c.o   | 
...    /
1.静态库
静态库的本质就是将多个目标文件打包成一个文件。
链接静态库就是将库中被调用的代码复制到调用模块中。
使用静态库的程序通常会占用较大的空间,库中代码一旦修改,所有使用该库的程序必须重新链接。
使用静态库的程序在运行无需依赖库,其执行效率高。
静态库的形式:libxxx.a
构建静态库:
.c -> .o -> .a
ar -r libxxx.a x.o y.o z.o
              ^      \_________/
               |_________|
使用静态库:
gcc ... -lxxx -L<库路径>
export LIBRARY_PATH=<库路径>
gcc ... -lxxx
--------------------------------------------------------------
代码:static/
2.动态(共享)库
动态库和静态库最大的不同就是,链接动态库并不需要将库中被调用的代码复制到调用模块中,相反被嵌入到调用模块中的仅仅是被调用代码在动态库中的相对地址。
如果动态库中的代码同时为多个进程所用,动态库的实例在整个内存空间中仅需一份,因此动态库也叫共享库或共享对象(Shared Object, so)。
使用动态库的模块所占空间较小,即使修改了库中的代码,只要接口保持不变,无需重新链接。
使用动态库的代码在运行时需要依赖库,执行效率略低。
动态库的形式:libxxx.so
构建动态库:
gcc -c -fpic xxx.c -> xxx.o
               |
   生成位置无关码
   库内部的函数调用也用相对地址表示
gcc -shared -o libxxx.so x.o y.o z.o
                                 ^      \_________/
                                  |_________|
使用动态库:
gcc ... -lxxx -L<库路径>
export LIBRARY_PATH=<库路径>
gcc ... -lxxx
运行时所调用的动态库必须位于LD_LIBRARY_PATH环境变量所表示的路径中。
代码:shared/
gcc缺省链接共享库,可通过-static选项强制链接静态库。
代码:hello.c
3.动态加载动态库
#include \ 系统提供的针对动态
-ldl                             / 库的动态加载函数集
void* dlopen(const char* filename, int flag);
成功返回动态库的句柄,失败返回NULL。
------------------------
FILE* fp = fopen(...);
fread(fp...);
fwrite(fp...);
------------------------
filename - 动态库路径,若只给文件名,则根据LD_LIBRARY_PATH环境变量搜索动态库
flag - 加载方式,可取以下值:
RTLD_LAZY - 延迟加载,使用动态中的符号时才加载
RTLD_NOW - 立即加载
该函数所返回的动态库句柄唯一地标识了系统内核所维护的动态库对象,将作为后续函数调用的参数。
void* dlsym(void* handle, const char* symbol);
成功返回函数地址,失败返回NULL。
handle - 动态库句柄
symbol - 符号(函数或全局变量)名
该函数所返回的函数指针是void*类型,需要强制类型转换为实际的函数指针类型才能调用。
int dlclose(void* handle);
成功返回0,失败返回非零。
handle - 动态库句柄
char* dlerror(void);
之前若有错误发生则返回错误信息字符串,否则返回NULL。
代码:load.c
四、辅助工具
1.查看符号表:nm
列出目标文件(.o)、可执行文件、静态库文件(.a)或动态库文件(.so)中的符号
代码:nm.c
2.显示二进制模块的反汇编信息:objdump -S
3.删除目标文件(.o)、可执行文件、静态库文件(.a)或动态库文件(.so)中的符号表和调试信息:strip
4.查看可执行程序文件或动态库文件所依赖的动态库文件:ldd
五、错误号和错误信息
1.通过函数的返回值表达错误
返回整数的函数:通过返回合法值域以外的值表示错误
int age(char const* name) {
    ...
    return 1000;
}
返回指针的函数:通过返回NULL指针表示错误
不需要通过返回值输出信息的函数:返回0表示成功,返回-1表示失败。
int delete(char const* filename) {
    ...
    return 0;
    ...
    return -1;
}
2.通过错误号和错误信息表示产生错误的具体原因
#include
全局变量:errno,整数,标识最近一次系统调用的错误
#include
char* strerror(int errnum); // 根据错误号返回错误信息
#include
void perror(const char* s); // 打印最近错误的错误信息
printf函数的%m标记被替换为最近错误的错误信息
代码:errno.c
虽然所有的错误号都不是0,但是因为在函数执行成功的情况下错误号全局变量errno不会被清0,因此不能用errno是否为0作为函数成功失败的判断条件,是否出错还是应该根据函数的返回值来决定。
返回值 = 函数调用(...);
if (返回值表示函数调用失败) {
    根据errno判断发生了什么错误
    针对不同的错误提供不同的处理
}
代码:iferr.c
六、环境变量
每个进程都有一张独立的环境变量表,其中的每个条目都是一个形如“键=值”形式的环境变量。
env
全局变量:environ,需要自己在代码做外部声明。
environ->|    *    |->AAA=aaa\0
                  |    *    |->BBB=bbb\0
                  |    *    |->CCC=ccc\0
                  |NULL|
所谓环境变量表就是一个以NULL指针结束的字符指针数组,其中的每个元素都是一个字符指针,指向一个以空字符结尾的字符串,该字符串就是形如”键=值”形式的环境变量。
argv-> *      *      *     NULL
             |       |      |
          a.out  -c    b.c
根据环境变量名获取其值
char* getenv(char const* name);
成功返回变量名匹配的变量值,失败返回NULL。
name - 环境变量名,即等号左边的部分
添加或修改环境变量
int putenv(char* string);
成功返回0,失败返回-1。
string - 形如“键=值”形式的环境变量字符串
若其键已存在,则修改其中,若其键不存在,则添加新变量
添加或修改环境变量
int setenv(const char* name, const char* value,
    int overwrite);
成功返回0,失败返回-1。
name - 环境变量名,即等号左边的部分
value - 环境变量值,即等号右边的部分
overwrite - 当name参数所表示的环境变量名已存在,此参数取0则保持该变量的原值不变,若此参数取非0,则将该变量的值修改为value。
删除环境变量
int unsetenv(const char* name);
成功返回0,失败返回-1。
name - 环境变量名,即等号左边的部分
清空环境变量
int clearenv(void);
成功返回0,失败返回-1。
代码:env.c
七、内存
1.虚拟内存、物理内存、半导体内存和换页文件
虚拟内存:地址空间,虚拟的存储区域,应用程序所访问的都是虚拟内存。
物理内存:存储空间,实际的存储区域,只有系统内核可以访问物理内存。
虚拟内存和物理内存之间存在对应关系,当应用程序访问虚拟内存时,系统内核会依据这种对应关系找到与之相应的物理内存。上述对应关系存储在内核中的内存映射表中。
物理内存包括半导体内存和换页文件两部分。
当半导体内存不够用时,可以把一些长期闲置的代码和数据从半导体内存中缓存到换页文件中,这叫页面换出,一旦需要使用被换出的代码和数据,再把它们从换页文件恢复到半导体内存中,这叫页面换入。因此,系统中的虚拟内存比半导体内存大得多。
2. 进程映射(Process Maps)
每个进程都拥有独立的4G字节的虚拟内存,分别被映射到不同的物理内存区域。
内存映射和换入换出都是以页为单位,1页=4096字节。
4G虚拟内存中高地址的1G被映射到内核的代码和数据区,这1个G在各个进程间共享。用户的应用程序只能直接访问低地址的3个G虚拟内存,因此该区域被称为用户空间,而高地址的1个G虚拟内存则被称为内核空间。用户空间中的代码只能直接访问用户空间的数据,如果要想访问内核空间中的代码和数据必须借助专门的系统调用完成。
用户空间的3G虚拟内存可以进一步被划分为如下区域:
          ------------------
             系统内核(1G)
高地址------------------
               命令行参数
               和环境变量
          ------------------
                     栈区:非静态局部变量
          -  -  -  -  -  -  -  -
                       v
  3G
                       ^
          -  -  -  -  -  -  -  -
                    堆区:动态内存分配(malloc函数族)
          -----------------
                  BSS区:无初值的全局和静态局部变量
          -----------------
                  数据区:非const型有初值的全局和静态局部变量
          -----------------
               只读常量:字面值常量,const型有初值的全局
                                和静态局部变量
           代码区(正文段):可执行指令
低地址-----------------
代码:maps.c
--------------------------------------------------------------
通过size命令查看一个可执行程序的代码区、数据区和BSS区的大小。
每个进程的用户空间都拥有独立的从虚拟内存到物理内存的映射,谓之进程间的内存壁垒。
代码:vm.c
3.内存的分配与释放
malloc/calloc/realloc/free
                   |
                  v
            brk/sbrk
                   |
                  v
     mmap/munmap
                   |
                  v
        kmalloc/kfree
以增加方式分配或释放虚拟内存
分配:映射+占有
             |         \____________
 在地址空间(虚拟内存)和     \
 存储空间(物理内存)之间      指定内存空
         建立映射关系               间的归属性
释放:放弃占有+解除映射
                |                |  
     解除对内存空 消除地址空间(虚拟内存)和存储
     间的归属约束 空间(物理内存)之间的映射关系
#include
void* sbrk(intptr_t increment);
堆顶->-  -  -  -  -  -  -  -
                    堆区
           -----------------
sbrk(10)
堆顶->-  -  -  -  -  -  -  -
                  10字节
           -  -  -  -  -  -  -  -<-返回值
                    堆区
           -----------------
sbrk(-10)
           -  -  -  -  -  -  -  -<-返回值
                  10字节
堆顶->-  -  -  -  -  -  -  -
                    堆区
           -----------------
成功返回调用该函数之前的堆顶指针,失败返回-1。
increment
>0 - 堆顶指针上移,增大堆空间,分配虚拟内存
<0 - 堆顶指针下移,缩小堆空间,释放虚拟内存
=0 - 不分配也不释放虚拟内存,仅仅返回当前堆顶指针
系统内核维护一个指针,指向堆内存的顶端,即有效堆内存中最后一个字节的下一个位置。sbrk函数根据增量参数increment调整该指针的位置,同时返回该指针原来的位置,期间若发生内存耗尽或空闲,则自动追加或取消相应内存页的映射。
123____~~~~~~~~........
      ^    ^                   ^      ^
    堆顶
代码:sbrk.c
以绝对地址的方式分配或释放虚拟内存
int brk(void* end_data_segment);
成功返回0,失败返回-1。
end_data_segment
>当前堆顶,分配虚拟内存
<当前堆顶,释放虚拟内存
=当前堆顶,空操作
堆顶->-  -  -  -  -  -  -  -<-void* p = sbrk(0);
                    堆区
           -----------------
brk(p+10)
           -  -  -  -  -  -  -  -<-p+10
                  10字节
堆顶->-  -  -  -  -  -  -  -
                    堆区
           -----------------
brk(p)
堆顶->-  -  -  -  -  -  -  -<-p
                    堆区
           -----------------
系统内核维护一个指针,指向当前堆顶,brk函数根据指针参数end_data_segment设置堆顶的新位置,期间若发生内存耗尽或空闲,则自动追加或取消相应内存页的映射。
代码:brk.c
建立虚拟内存到物理内存或文件的映射
#include
void* mmap(void* start, size_t length, int prot,
    int flags, int fd, off_t offset);
成功返回映射区虚拟内存的起始地址,失败返回MAP_FAILED(void*类型的-1)。
start - 映射区虚拟内存的起始地址,NULL表示自动选择
length - 映射区的字节数,自动按页取整
prot - 访问权限,可取以下值:
PROT_READ - 可读
PROT_WRITE - 可写
PROT_EXEC - 可执行
PROT_NONE - 不可访问
flags - 映射标志,可取以下值:
MAP_ANONYMOUS - 匿名映射,将虚拟内存映射到物理内存,函数的最后两个参数fd和offset被忽略
MAP_PRIVATE - 私有映射,将虚拟内存映射到文件的内存缓冲区中而非磁盘文件
MAP_SHARED - 共享映射,将虚拟内存映射到磁盘文件中
MAP_DENYWRITE - 拒写映射,文件中被映射区域不能存在其它写入操作
MAP_FIXED - 固定映射,若在start上无法创建映射,则失败(无此标志系统会自动调整)
MAP_LOCKED - 定映射,禁止被换出到换页文件
fd - 文件描述符
offset - 文件偏移量,自动按页对齐
解除虚拟内存到物理内存或文件的映射
int munmap(void* start, size_t length);
成功返回0,失败返回-1。
start - 映射区的起始地址
length - 映射区的字节数
代码:mmap.c
八、系统调用
              应用程序--------------+
  vi/emacs/gftp/firefox          |
                    |                             |
       标准库、第三方库               |
        C/C++/Qt/X11               |
                    |                            |
              系统调用<------------+
brk/sbrk/mmap/munmap
1.Linux系统内核提供了一套用于实现各种系统功能的子程序,谓之系统调用。程序编写者可以象调用普通C语言函数一样调用这些系统调用函数,以访问系统内核提供的各种服务。
2.系统调用函数在形式上与普通C语言函数并无差别。二者的不同之处在于,前者工作在内核态,而后者工作在用户态。
3.在Intel的CPU上运行代码分为四个安全级别:Ring0、Ring1、Ring2和Ring3。Linux系统只使用了Ring0和Ring3。用户代码工作在Ring3级,而内核代码工作在Ring0级。一般而言用户代码无法访问Ring0级的资源,除非借助系统调用,使用户代码得以进入Ring0级,使用系统内核提供的功能。
4.系统内核内部维护一张全局表sys_call_table,表中的每个条目记录着每个系统调用在内核代码中的实现入口地址。
5.当用户代码调用某个系统调用函数时,该函数会先将参数压入堆栈,将系统调用标识存入eax寄存器,然后通过int 80h指令触发80h中断。
6.这时程序便从用户态(Ring3)进入内核态(Ring0)。
7.工作系统内核中的中断处理函数被调用,80h中断的处理函数名为system_call,该函数先从堆栈中取出参数,再从eax寄存器中取出系统调用标识,然后再从sys_call_table表中找到与该系统调用标识相对应的实现代码入口地址,挈其参数调用该实现,并将处理结果逐层返回到用户代码中。
九、文件
1.文件系统的物理结构
1)硬盘的物理结构:驱动臂、盘片、主轴、磁头、控制器
2)磁表面存储器的读写原理
硬盘片的表面覆盖着薄薄的磁性涂层,涂层中含有无数微小的磁性颗粒,谓之磁畴。相邻的若干磁畴组成一个磁性存储元,以其剩磁的极性表示二进制数字0和1。为磁头的写线圈中施加脉冲电流,可把一位二进制数组转换为磁性存储元的剩磁极性。利用磁电变换,通过磁头的读线圈,可将磁性存储元的剩磁极性转换为相应的电信号,表示二进制数。
3)磁道和扇区
磁盘旋转,磁头固定,每个磁头都会在盘片表面画出一个圆形轨迹。改变磁头位置,可以形成若干大小不等的同心圆,这些同心圆就叫做磁道(Track)。每张盘片的每个表面上都有成千上万个磁道。一个磁道,以512字节为单位,分成若干个区域,其中的每个区域就叫做一个扇区(Sector)。扇区是文件存储的基本单位。
4)柱面、柱面组、分区和磁盘驱动器
硬盘中,不同盘片相同半径的磁道所组成的圆柱称为柱面(Cylinder)。整个硬盘的柱面数与每张盘片的磁道数相等。
硬盘上的每个字节需要通过以下参数定位:
磁头号:确定哪个盘面    \
柱面号:确定哪个磁道     > 磁盘I/O
扇区号:确定哪个区域     /
偏移量:确定扇区内的位置
若干个连续的柱面构成一个柱面组
若干个连续的柱面组构成一个分区
每个分区都建有独立的文件系统
若干分区构成一个磁盘驱动器
2.文件系统的逻辑结构
磁盘驱动器:| 分区 | 分区 | 分区 |
分区:| 引导块 | 超级块 | 柱面组 | 柱面组 | 柱面组 |
柱面组:
| 引导块 | 柱面组 | i节点映 | 块位图 | i节点表 | 数据块集 |
| 副    本 | 信   息 | 射    表  |            |              |                |
i节点号:431479
i节点
    文件元数据
    100 | 200 | 300
根据目录文件中记录的i节点编号检索i节点映射表,获得i节点下标,用该下标查i节点表,获得i节点,i节点中包含了数据块索引表,利用数据块索引从数据块集中读取数据块,即获得文件数据。
直接块:存储文件实际数据内容
间接块:存储下级文件数据块索引表
100
-----
xxx

200
-----
xxx

300
----
400 | 500 | 600
3.文件分类
普通文件(-):可执行程序、文本、图片、音频、视频、网页
目录文件(d):该目录中每个硬链接名和i节点号的对应表
符号链接文件(l):存放目标文件的路径
管道文件(p):有名管道,进程间通信
套接字文件(s):进程间通信
块设备文件(b):按块寻址,顺序或随机读写
字符设备文件(c):按字节寻址,只能以字节为单位顺序读写
4.文件的打开与关闭
打开:在系统内核中建立一套数据结构,用于访问文件
进程表项
    ...
    文件描述符表
        |文件描述符标志 | 文件表项指针 | 0
        |文件描述符标志 | 文件表项指针 | 1
        |文件描述符标志 | 文件表项指针 | 2
        ...                                    |             ^
+-----------------------------+             |
 |                                                   文件描述符
 v
文件表项
    文件状态标志
    文件读写位置
    v节点指针
    ...    |
+-----+
 |
v
v节点
    i节点内容
    ...
/home/tarena/unixc/day03/vm.c
关闭:释放打开文件过程中建立的数据结构
FILE* fp = fopen("reame.txt", "r");
fread(fp, ...);
#include
打开已有的文件或创建新文件
int open(const char* pathname, int flags,
    mode_t mode);
成功返回文件描述符,失败返回-1。
--------------------------------------------------------------
pathname - 文件路径
flags - 状态标志,可取以下值:
O_RDONLY - 只读  \
O_WRONLY - 只写  > 只选其一
O_RDWR - 读写      /
O_APPEND - 追加
O_CREAT - 创建,不存在即创建,已存在即打开,除非与以下两个标志之一合用,由此标志mode参数才有效。
O_EXCL - 排它,已存在即失败
O_TRUNC - 清空,已存在即清空,同时有O_WRONLY或O_RDWR
O_SYNC - 写同步,在数据被写到磁盘之前写操作不会完成,读操作本来就是同步的,此标志对读操作没有意义
O_ASYNC - 异步,在文件可读写时产生一个SIGIO信号,在对信号的处理过程中读写I/O就绪的文件,只能用于终端设备或网络套接字,而不能用于磁盘文件
O_NONBLOCK - 非阻塞,读操作不会因为无数据可读而阻塞,写操作也不会因为缓冲区满而阻塞,相反会返回失败,并设置特定的errno
mode - 权限模式,三位八进制:0XXX
                                            ______/  |  \_____                                                                    /             |           \
                                 拥有者用户 同组用户 其它用户
4: 可读
2: 可写
1: 可执行
所创建文件的实际权限除了跟mode参数有关,还受权限掩码的影响。
mode=0666
umask=0002
权限=mode&~umask=0664
创建新文件
int creat(const char* pathname, mode_t mode);
flags: O_WRONLY | O_CREAT | O_TRUNC
打开已有文件
int open(const char* pathname, int flags);
关闭文件
int close(int fd);
成功返回0,失败返回-1。
fd - 文件描述符
代码:open.c
作为文件描述符表项在文件描述符表中的下标,合法的文件描述符一定是大于或等于0的整数。每次产生新的文件描述符表项,系统总是从下标0开始在文件描述符表中寻找最小的未使用项。每关闭一个文件描述符,无论被其索引的文件表项和v节点是否被删除,与之对应的文件描述符表项一定会被标记为未使用,并在后续操作中为新的文件描述符所占用。系统内核缺省为每个进程打开三个文件描述符:
#include
#define STDIN_FILENO    0 // 标准输入,即键盘
#define STDOUT_FILENO 1 // 标准输出,终端屏幕,有缓冲
#define STDERR_FILENO  2 // 标准错误,终端屏幕,无缓冲
               UC C          C++
标准输入 0    stdin    cin
标准输出 1    stdout cout
标准错误 2    stderr  cerr
数据类型 int FILE*    iostream
文件描述符是用户程序和系统内核关于文件的唯一联系方式。
5.文件的读取和写入
向指定文件写入字节流
ssize_t write(int fd, const void* buf, size_t count);
成功返回实际写入的字节数(0表示未写入),失败返回-1。
fd - 文件描述符
buf - 内存缓冲区
count - 期望写入的字节数
ssize_t read(int fd, void* buf, size_t count);
成功返回实际读取的字节数(0表示读到文件尾),失败返回-1。
ABCDEFGHIJLMNOPQ
4
1)ABCD
2)EFGH
3)IJLM
4)NOPQ
5)->0
fd - 文件描述符
buf - 内存缓冲区
count - 期望读取的字节数
代码:write.c、read.c
基于系统调用的文件读写本来就是面向二进制字节流的,因此对二进制读写而言,无需做任何额外的工作。如果要求文件中内容必须是可阅读的,那么就必须通过格式化和文本解析处理二进制形式的数据和文本字符串之间的转换。
代码:binary.c、text.c
6.顺序与随机读写
ABCdef
^     ^   ^
0      3    6
每个打开的文件都有一个与其相关的文件读写位置保存在文件表项中,用以记录从文件头开始计算的字节偏移。文件读写位置通常是一个非负的整数,用off_t类型表示,在32位系统上被定义为long int,而在64位系统上则被定义为long long int。打开一个文件时,除非指定了O_APPEND标志,否则文件读写位置一律被设为0,即文件首字节的位置。每一次读写操作都从当前的文件读写位置开始,并根据所读写的字节数,同步增加文件读写位置,为下一次读写做好准备。因为文件读写位置是保存在文件表项而不是v节点中的,因此通过多次打开同一个文件得到多个文件描述符,各自拥有各自的文件读写位置。
人为调整文件读写位置
off_t lseek(int fd, off_t offset, int whence);
成功返回调整后的文件读写位置,失败返回-1。
fd - 文件描述符
offset - 文件读写位置相对于whence参数的偏移量
whence
SEEK_SET - 从文件开始
SEEK_CUR - 从当前位置开始
SEEK_END - 从文件尾开始
lseek函数仅仅是修改文件表项中的文件读写位置,并不引发实际的I/O操作,速度很快。
lseek(fd, 10, SEEK_SET);
lseek(fd, -10, SEEK_END);
lseek(fd, 0, SEEK_CUR); // 返回当前读写位置
lseek(fd, 0, SEEK_END); // 返回文件总字节数
lseek(fd, -10, SEEK_SET); // 错误
lseek(fd, 10, SEEK_END); // 允许,空洞部分补0
代码:seek.c
7.标准I/O和系统I/O
             应用程序----------+
                    |                     |
                    v                    |
           标准(库)I/O            |
   fopen/fwrite/fclose    |
                    |                    |
                   v                    |
         系统(库)I/O             |
   open/write/close <--+
代码:stdio.c、sysio.c
标准库通过缓冲区优化,减少系统调用的次数,降低在用户态和内核态之间来回切换的频率,提高运行速度,缩短运行时间。
8.复制文件描述符(表项)
进程表项
    ...
    文件描述符表
        |文件描述符标志 | 文件表项指针 | 0
        |文件描述符标志 | 文件表项指针 | 1
        |文件描述符标志 | 文件表项指针 | 2
int fd  = open(...); // fd: 3
进程表项
    ...
    文件描述符表
        |文件描述符标志 | 文件表项指针 | 0
        |文件描述符标志 | 文件表项指针 | 1
        |文件描述符标志 | 文件表项指针 | 2
        |文件描述符标志 | 文件表项指针 | 3 -> 文件表项
int dup(int oldfd);
成功返回目标文件描述符,失败返回-1。
oldfd - 源文件描述符
int fd2 = dup(fd); // fd2: 7
进程表项
    ...
    文件描述符表
        |文件描述符标志 | 文件表项指针 | 0
        |文件描述符标志 | 文件表项指针 | 1
        |文件描述符标志 | 文件表项指针 | 2
        |文件描述符标志 | 文件表项指针 | 3->文件表项->v节点
        ...                                                                 ^
        |文件描述符标志 | 文件表项指针 | 7 --------+
fd2(7)和fd(3)对应同一个文件表项,访问同一个文件。
dup函数将oldfd参数所对应的文件描述符表项复制到文件描述符表第一个空闲项中,同时返回该表项所对应的文件描述符。
close(fd);
close(fd2);
int dup2(int oldfd, int newfd);
成功返回目标文件描述符,失败返回-1。
oldfd - 源文件描述符
newfd - 目标文件描述符
dup2函数在复制oldfd参数所标识的源文件描述符表项时,会首先检查由newfd参数所标识的目标文件描述符表项是否空闲,若空闲则直接将前者复制给后者,否则会先将目标文件描述符newfd关闭,再行复制。
fd1 = open("1.txt", ...); --> 文件表项 \
                                                                > v节点
fd2 = open("1.txt", ...); --> 文件表项 /

fd1 = open("2.txt", ...); \
                                           > 文件表项 -> v节点
fd2 = dup(fd1);              /
代码:dup.c
9.文件控制
int fcntl(int fd, int cmd, ...);
复制文件描述符(表项)
int fcntl(int oldfd, F_DUPFD, int newfd);
成功返回目标文件描述符,失败返回-1。
oldfd - 源文件描述符
newfd - 目标文件描述符
该函数类似dup2函数,但略有不同。如果newfd处于打开状态,该函数并不会象dup2函数那样关闭它,而是另外寻找一个比它大的最小的空闲文件描述符作为复制目标。
代码:fcntl.c
--------------------------------------------------------------
获取/设置文件描述符标志
截止目前只有一个文件描述符标志位:FD_CLOEXEC
一个进程可以通过exec函数族启动另一个进程取代其自身。
原进程中无FD_CLOEXEC标志位的文件描述符在新进程中会依然保持打开状态,这也是文件描述符的默认状态。如果原进程中的某个文件描述符带有此标志位,那么在新进程中该文件描述符会被关闭。
// 获取文件描述符标志
int fcntl(int fd, F_GETFD);
成功返回文件描述符标志,失败返回-1。
// 设置文件描述符标志
int fcntl(int fd, F_SETFD, int flags);
成功返回0,失败返回-1。
代码:fd.c
获取/追加文件状态标志
// 获取文件状态标志
int fcntl(int fd, F_GETFL);
成功返回文件状态标志,失败返回-1。
与文件创建有关的三个状态标志:
O_CREAT/O_EXCL/O_TRUC,无法被获取。
只读标志O_RDONLY的值为0,不能与位与检测。
int flags = fcntl(fd, F_GETFL); 
if ((flags & O_ACCMODE) == O_RDONLY)
    // 只读文件
if (flags & O_WRONLY)
    // 只写文件
if (flags & O_RDWR)
    // 读写文件
...
// 追加文件状态标志
int fcntl(int fd, F_SETFL, flags);
成功返回0,失败返回-1。
只有O_APPEND和O_NONBLOCK两个状态标志可被追加。
代码:fl.c
10.文件锁
读写冲突
hello
world
hewollrlod
                              期望的访问
                              读取    写入
文件的某 无人访问  OK     OK
个区域正 多人在读  OK     NO
在被访问 一人在写  NO     NO
为了避免在读写同一个文件的同一个区域时发生冲突,进程之间应该遵循以下规则:
如果一个进程正在写,那么其它进程既不能写也不能读。
如果一个进程正在读,那么其它进程不能写但是可以读。
读共享,写独占。
代码:write.c、read.c
为了避免多个进程在读写同一个文件的同一个区域时发生冲突,操作系统引入了文件锁机制,并把文件锁分读锁和写锁两种,它们区别在于:
读锁:共享锁,对一个文件的特定区域可以同时加多个读锁
写锁:排它锁,对一个文件的特定区域只能加一把写锁
锁模式:加锁->读写->解锁
                              期望的加锁
                              读锁    写锁
文件的某 无任何锁                  OK     OK
个区域正 多把读锁                  OK     NO
在被访问 一把写锁                  NO     NO
int fcntl(int fd, F_SETLKW/F_SETLK, struct flock* lock);
                                   ^                ^
                                阻塞          非阻塞
struct flock {
    short int l_type;       // 锁类型
                                       // F_RDLCK/F_WRLCK/F_UNLCK
    short int l_whence;  // 锁区偏移起点
                                       // SEEK_SET/SEEK_CUR/
                                       // SEEK_END
    off_t        l_start;       // 锁区偏移
    off_t        l_len;          // 锁区长度(字节数),0表示锁到尾
    pid_t        l_pid;         // 加锁进程PID,-1表示自动设置
};
对相对于文件头10字节开始的20字节以阻塞模式加读锁。
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;
fcntl(fd, F_SETLKW, &lock);
对相对于当前位置10字节开始到文件尾以非阻塞方式加写锁。
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;
fcntl(fd, F_SETLK, &lock);
对整个文件解锁。
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;
fcntl(fd, F_SETLK, &lock);
代码:wlock.c、rlock.c
测试对文件的某个区域是否可以加某种锁,如果不能加锁,是什么原因导致加锁冲突?
int fcntl(int fd, F_GETLK, struct flock* lock);
成功返回0,失败返回-1。
调用该函数时,lock参数表示欲加之锁的细节。函数成功返回时,通过lock参数输出欲加之锁是否可加,以及存在冲突的锁信息。
代码:lock1.c、lock2.c
v节点
    i节点内容
    锁表指针->锁节点->锁节点->...
    ...                   锁的类型
                          锁区偏移
                          锁区大小
                          加锁进程的PID
每次对给定文件的特定区域加锁,都会通过fcntl函数向系统内核传递flock结构,该结构中包含了有关锁的一些细节,诸如锁的类型、锁区的起始位置和大小,甚至加锁进程的PID(填-1由系统自动设置)。系统内核会收集所有进程对该文件所加的各种锁,并把这些flock结构中的信息,以链表的形式组织成一张锁表,其起始地址就保存在该文件的v节点中。任何一个进程通过fcntl函数对该文件加锁,系统内核都要遍历这张锁表,一旦发现有与欲加之锁构成冲突的锁即阻塞或报错,否则即将欲加之锁插入锁表,而解锁的过程实际上就是调整或删除锁表中的相应节点。
文件锁属于劝谏锁,亦称协议锁。
11.文件元数据
i节点
    文件元数据
    数据块索引表
struct stat {
    dev_t st_dev; // 设备ID
    ino_t st_ino; // i节点号
    mode_t st_mode; // 文件类型和权限
    nlink_t st_nlink; // 硬链接数
    uid_t st_uid; // 用户ID
    gid_t st_gid; // 组ID
    dev_t st_rdev; // 特殊设备ID
    off_t st_size; // 总字节数
    blksize_t st_blksize; // I/O块字节数
    blkcnt_t st_blocks; // 块数
    time_t st_atime; // 最后访问时间
    time_t st_mtime; // 最后修改时间
    time_t st_ctime; // 最后状态改变时间
};
文件类型和权限的数据类型mode_t其实就是一个整数,其中只有低16位有效。
B15~B12 - 文件类型,掩码:S_IFMT
1000,S_IFREG,普通文件,-
0100,S_IFDIR,目录文件,d
1100,S_IFSOCK,本地套接字文件,s
0010,S_IFCHR,字符设备文件,c
0110,S_IFBLK,块设备文件,b
1010,S_IFLNK,符号链接文件,l
0001,S_IFFIFO,有名管道文件,p
B11~B9 - 设置用户ID位、设置组ID位和粘滞位
带有设置用户ID位(即B11位为1)的可执行文件(如:/usr/bin/passwd):
系统中的每个进程其实都有两个用户ID,一个叫实际用户ID,取决于运行该进程的登录用户,另一个叫有效用户ID。一般情况下,进程的有效用户ID就取自其实际用户ID。但是如果产生该进程的可执行文件带有设置用户ID位,那么该进程的有效用户ID就不再取自实际用户ID,而是取自该可执行文件的拥有者用户ID。进程对系统资源的权限判定是根据其有效用户ID做出的,因此通过这种方法,就可以提升普通用户执行进程的权限,完成本来只有高权限用户才能完成的任务,即有限提权。
带有设置组ID位(即B10位为1)的可执行文件:
设置组ID位(B10)的情况与上述类似,只是针对进程的有效组ID而已。
带有设置用户ID位的不可执行文件:毫无意义。
带有设置组ID位的不可执行文件:某些系统用这种无意义的状态作为强制锁标志。
带有粘滞位(B9位为1)的目录:除root以外的任何用户在该目录下,都只能删除或更名那些属于自己的文件或子目录,而对于其它用户的文件或子目录,既不能删除也不能改名。如:/tmp
B8B7B6 - 拥有者用户
  |    |    |
读  写  执行
B5B4B3 - 拥有者组
  |    |    |
读  写  执行
B2B1B0 - 其它用户
  |    |    |
读  写  执行
拥有者用户   拥有者组     其它用户
C1  C2  C3 C4  C5  C6  C7  C8  C9
-/r -/w -/x -/r -/w -/x -/r -/w -/x
设置用户ID位
             S/s
                    设置组ID位
                                S/s
                                              粘滞位
                                                   T/t
#include
int stat(char const* path, struct stat* buf);
int fstat(int fd, struct stat* buf);
int lstat(char const* path, struct stat* buf); // 不跟踪符
                                                                            // 号链接
成功返回0,失败返回-1。
path - 文件路径
buf - 文件元数据结构
fd - 文件描述符
代码:stat.c
---------------------------------------------------------------
12.访问测试
int access(const char* pathname, int mode);
成功返回0,失败返回-1。
pathname - 文件路径
mode - 访问权限,可取以下值:
R_OK: 可读否
W_OK: 可写否
X_OK: 可执行否
F_OK: 存在否
根据调用该函数的进程的实际用户ID和实际组ID,检测其是否可读、可写或可执行给定的文件,也可检测该文件是否存在。
代码:access.c
13.权限掩码
#include
mode_t umask(mode_t cmask);
永远成功,返回原来的权限掩码。
cmask - 新权限掩码
权限掩码是进程的属性之一,存储在系统内核中的进程表项里。umask函数所影响的仅仅是调用进程自己,对于其它进程,包括其父进程,如Shell,都没有任何影响。
代码:umask.c
14.修改权限
#include
int chmod(const char* pathname, mode_t mode);
int fchmod(int fd, mode_t mode);
成功返回0,失败返回-1。
pathname - 文件路径
mode - 文件权限
fd - 文件描述符
调用进程的有效用户ID必须与文件的拥有者用户ID匹配,或者是root用户,才能修改该文件的权限,且受权限掩码的影响。
代码:chmod.c
15.修改文件的拥有者和(或)拥有者组
#include
int chown(const char* path, uid_t owner, gid_t group);
int fchown(int fd, uid_t owner, gid_t group);
int lchown(const char* path, uid_t owner,
    gid_t group); // 不跟踪符号链接
成功返回0,失败返回-1。
path - 文件路径
owner - 拥有者用户ID,-1表示不修改
group - 拥有者组ID,-1表示不修改
fd - 文件描述符
如果调用进程的有效用户ID为root用户,则它可以任意修改任何文件的拥有者用户和组。如果调用进程的有效用户ID为普通用户,则它只能把自己名下文件的拥有者组改成自己隶属的其它组。
代码:chown.c
16.修改文件大小
#include
int truncate(const char* path, off_t length);
int ftruncate(int fd, off_t length);
成功返回0,失败返回-1。
path - 文件路径
length - 文件大小
fd - 文件描述符
大->小:截掉靠文件尾的部分。
小->大:在文件尾之后增加0。
代码:trunc.c
struct Student {
    char name[128];
    int age;
    float score;
    ...
};
| 学生1 | 学生2 | 学生4 | 学生5 |
                                     ^
mmap/munmap
虚拟内存<->磁盘文件
#include
void* mmap(void* start, size_t length, int prot,
    int flags, int fd, off_t offset);
             ^
              |
   MAP_SHARED
fd - 文件描述符
offset - 文件内偏移量,自动按页对齐
代码:mmap1.c、mmap2.c
17.硬链接
硬链接就是文件路径,即由各级目录、分隔符(/)和文件名共同组成的字符串,与一个特定的i节点号所形成的对应关系。
ln <目标路径(已经存在的路径)> <源路径(新建立的路径)>
                         \____________________________/
                                               |
                                       同一个文件
根据一个已有的硬链接创建一个新的硬链接
int link(const char* oldpath, const char* newpath);
成功返回0,失败返回-1。
oldpath - 已有的硬链接路径
newpath - 新的硬链接路径
oldpath->i节点编号<->newpath
                        \_____________/
                                   |
                            目录文件
oldpath必须存在,newpath中不能包含不存在目录。
删除硬链接
int unlink(const char* pathname);
成功返回0,失败返回-1。
pathname - 文件路径(不能是目录)
从pathname所对应的目录文件中删除包含该文件的条目,同时将其对应的i节点中的硬链接数减一,若该硬链接数被减至0,则将该文件所占用的磁盘空间释放出来。
修改硬链接
int rename(const char* oldpath,
    const char* newpath);
成功返回0,失败返回-1。
oldpath - 原路径
newpath - 新路径
原路径->i节点编号<->新路径
    \_________/\___________/
            |                  |
         删除        目录文件
rename("./a.txt", "./b.txt"); // 改名
rename("a/1.txt", "b/1.txt"); // 移动
rename("a/1.txt", "b/2.txt"); // 移动且改名
另一个版本的unlink,还可以删除空目录的硬链接:
int remove(const char* pathname);
代码:link.c、unlink.c、remove.c、rename.c
18.软链接
软链接文件的本质就是保存着另一个文件或目录的路径的文件。
根据一个已有的硬链接创建一个符号链接
int symlink(const char* oldpath,
    const char* newpath);
成功返回0,失败返回-1。
oldpath - 原有路径,可以是文件,也可以是目录,甚至可以不存在
newpath - 新建路径,不能包含不存在目录
读取软链接文件本身的内容
ssize_t readlink(const char* path, char* buf,
    size_t size);
成功返回拷入buf的符号链接文件内容的字节数,失败返回-1。
path - 软链接文件路径
buf - 缓冲区
size - 缓冲区大小
代码:slink.c
19.目录
创建一个空目录
#include
int mkdir(const char* pathname, mode_t model);
成功返回0,失败返回-1。
pathname - 目录路径
mode - 访问权限,目录的执行权限(x)表示可以进入
删除一个空目录
int rmdir(const char* pathname);
成功返回0,失败返回-1。
pathname - 目录路径
remove = unlink + rmdir
代码:dir.c
获取当前工作目录
char* getcwd(char* buf, size_t size);
成功返回工作目录字符串指针,即buf,失败返回NULL。
buf - 缓冲区
size - 缓冲区大小
自动追加结尾空字符。
当前工作目录作为进程的属性之一,也是系统内核进程表项的一部分。
改变当前工作目录
int chdir(const char* path);
成功返回0,失败返回-1。
代码:dir.c
打开目录
DIR* opendir(const char*name);
成功返回目录流指针,失败返回NULL。
读取目录
struct dirent* readdir(DIR* dirp);
成功返回目录条目指针,读完(不设置errno)或失败(设置errno)返回NULL。
struct dirent {
    ino_t                  d_ino;            // 节点号
    off_t                   d_off;           // 下一条位置(索引)
    unsigned short d_reclen;     // 记录长度
    unsigned char   d_type;  

锐单商城拥有海量元器件数据手册IC替代型号,打造电子元器件IC百科大全!

相关文章