当前位置:   article > 正文

Linux系统编程及应用——ubuntu环境下_ubuntu系统宏定义

ubuntu系统宏定义

Linux系统编程及应用——ubuntu环境下

程序由业务逻辑系统访问两部分构成的。其中,业务逻辑是根据业务需求,按照设计好的逻辑规则,处理信息,与系统(平台)无关的;而系统访问则是利用操作系统所提供的各种功能,来辅助业务逻辑的实现,是跟系统相关的(平台相关性)。

使用标准库函数(如scanf/printf)实现的程序,可以做到源码级兼容,因为标准库函数的实现虽然基于系统函数,但是直接使用标准库函数时是不需要考虑系统函数的,也就是说标准库函数屏蔽了操作系统/平台之间的差异。与之相对,底层的系统调用函数只能做到接口级兼容。

既然标准库函数这么好,为什么还需要使用系统函数呢?——环境(裸板程序没有标准库)、性能、功能(某些功能标准库没有)

一、Unix系统简介

1、Unix系统的背景

PS:有些内容的描述不准确

Ø 1961-1969年:史前时代

CTSS(Compatible Time-Sharing System,兼容分时系统),以MIT(麻省理工)为首的开发小组(包括AT&T的贝尔实验室),小而简单的实验室原型

分时系统分时操作系统是指在一台主机上连接多个带有显示器和键盘的终端,同时允许多个用户通过主机的终端,以交互方式使用计算机,共享主机中的资源。分时操作系统是一个多用户交互式操作系统。分时操作系统,主要分为三类:单道分时操作系统,多道分时操作系统,具有前台和后台的分时操作系统。分时操作系统将CPU的时间划分成若干个片段,称为时间片。操作系统以时间片为单位,轮流为每个终端用户服务。

Multics(Multiplexd Information and Computing System,多路信息与计算系统),庞大而复杂,不堪重负,开发和维护变得非常困难,半成品

Unics(Uniplexed Information and Computing System,单路信息与计算系统),返璞归真,走上正道(剔除了Multics中不实用和太过复杂的功能)

Ø 1969-1971年:创世纪

Ken Thompson(肯.汤姆逊),Unix之父,B语言之父,内核用B语言和汇编语言进行开发,在PDP-7(被废弃的)机器上开发的,第一个Unix系统的核心和简单的应用程序。后来被移植到PDP-11平台上,功能更加完善。

目前仍然在世,在谷歌工作,开发了go语言

Ø 1971-1979年:出谷记

Dennis Ritchie(丹尼斯.里奇),C语言之父,用C语言重写了Unix系统内核,极大地提升了Unix系统的可读性、可维护性和可移植性——Unix V7,第一个真正意义上的Unix系统。

Ø 1980-1985年:第一次Unix战争

AT&T贝尔实验室:SVR4

和BSD(加州大学伯克利分校):BSD+TCP/IP

DQRPA,ARPANET(INTERNET),更加支持BSD,导致BSD开发出TCP/IP

IEEE,国际电气电子工程师协会,制定了POSIX标准,为Unix内核和外壳制定了一系列技术标准和规范,消除了系统版本之间的分歧,大一统的操作系统。—>战争结束

Ø 1988-1990年:第二次Unix战争

AT&T+Sun:

IBM+DEC+HP:

比尔.盖茨–>windows突然发布,战争结束

Ø 1992-至今:

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操作系统,开源免费。

虽然有不同的发行版本,但是它们都是用相同的内核

支持多种硬件平台:得益于它的免费和开源,手机、路由器、视频游戏控制器、个人PC、大型计算机等等

隶属于GNU功能,GNU = GNU Not Unix

受GPL许可证的限制:如果发布了一个可执行的二进制代码,就必须同时发布可读的源代码,并且在发布任何基于GPL许可证的软件时,不能添加限制性条款。—>后来又有了LGPL

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(试验新技术)、…

如何查看Linux系统版本号:cat /proc/version或者使用uname命令

4、Linux系统的特点

遵循GNU/GPL许可证

开放性

多用户

多任务

设备无关性

丰富的网络功能

可靠的系统安全

良好的可移植性

5、Linux发行版本

ubuntu:大众化,简单易用

Linux Mint:新潮前卫,喜欢用一些新技术手段,可能不太稳定

Fedora:red hat的一个桌面版本

OpenSUSE:美观漂亮

Debian:自由开放

Slackware:简杰朴素、简陋

Red Hat:经典、稳定,企业应用,支持全面

CentOS:

Arch:

二、GNU编译器(GCC)

1、GCC的基本特点

(1)支持多种硬件架构:x86_64、alpha、arm、mips、powerpc、SPARC、VAX……

(2)支持多种操作系统(可执行程序的组织形式):Linux、Unix、BSD、Android、MacOS、IOS、windows……

(3)支持多种编程语言:C、C++、Object-C、Java、Fortran、Pascal、Ada……

(4)查看GCC版本:gcc -v

2、程序的构建过程

源代码—>预编译(预处理)—>头文件和宏扩展—>编译—>汇编码—>汇编(.s)—>目标码(.o)—>链接—>可执行代码

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NM2G4Kq6-1578914622438)(./插图/day01/01_编译过程.png)]

nm命令:查看二进制文件符号表

vi hello.c:编辑源代码

gcc -E hello.c -o hello.i:预编译(编译预处理)

gcc -S hello.i -o hello.s:获得汇编代码

gcc -c hello.s -o hello.o:获得目标代码

gcc hello.o -o hello:获得可执行代码

./hello:运行可执行代码

参考代码:day01/code/01_编译过程

3、文件名后缀

​ .h——C语言源代码头文件

​ .c——预处理之前的C语言代码文件

​ .s——汇编语言文件

以上文件都是可读的文本文件,以下文件是不可读的二进制文件(可以使用xxd/hexdump命令查看内容)

​ .o——目标文件

​ .a——静态库文件

​ .so——共享库文件(动态库文件)

​ .out——可执行文件,缺省的

4、GCC编译选项

gcc [选项][参数] 文件1 文件2 …

​ -o:指定输出文件

​ -E:预编译,缺省输出到屏幕,可以用-o指定输出文件

​ -S:编译,将高级语言文件编译成汇编语言文件

​ -c:汇编,将汇编语言文件汇编成机器语言文件

​ -Wall:产生全部警告

​ -std:指定编译器的版本

​ -Werror:将警告当做错误进行处理

​ -x:指定源代码语言

​ -g:产生调试信息,用于gdb调试

​ -O1/-O1/O2/O3:指定优化等级,O0不优化,缺省O1优化

5、头文件

(1)头文件里面写什么?

​ 头文件卫士(#ifndef…#define…#endif):避免在编译阶段产生错误

​ 其他头文件:

​ 宏定义:

​ 自定义类型:

​ 类型别名:

​ 外部变量的声明:

​ 函数声明:

问:为什么不建议将函数定义写在头文件中?

答:一个头文件可能会被多个源文件包含,写在头文件里的函数定义也会因此被预处理器扩展到多个包含该头文件的源文件中,并在编译阶段被编译到多个不同的目标文件中,这将导致链接错误:multipe definition(多重定义)

(2)去哪里找头文件?

gcc -I <头文件附加搜索路径>

#include <my.h>(尖括号):

​ 先找-I指定的目录,如果没有找到,再找系统指定目录

#include “my.h”(双引号):

​ 先找-I指定的目录,如果没有找到,再找当前目录,如果还没有找到,再找系统指定目录

头文件的系统目录:可以使用gcc <源文件> -v查看

​ usr/include:标准C库

​ /usr/local/include:第三方库

​ /usr/lib/gcc

​ /usr/lib/gcc/x86_64-linux-gnu/7/include:编译器库

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-blmH7CHd-1578914622439)(/home/jiwei/01_ESD/02_unixc/03_ESD1911/插图/day01/02_系统指定路径.png)]

代码: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 整数n——表示从下一行开始行号变更为第n行

参考代码:line.c

#pragma:设定编译器的状态或者指示编译器的操作

  • #pragma GCC dependency 被依赖文件:表示当前文件依赖于指定的文件名,如果指定的文件最后一次修改时间晚于当前文件,则产生警告信息

  • #pragma GCC poison 语法禁忌:一旦使用该标识符,则产生错误信息

  • #pragma pack(按几字节对齐,1/2/4/8)

  • #pragma pack()——按缺省字节数对齐

参考代码:dep.c pragma.c

7、预定义宏

无需自行定义,预处理器会根据事先设定好的规则将这些宏扩展成其对应的值,这些预定义宏不能被取消定义(#undef)或由编程人员重新定义

__BASE_FILE__:获取正在编译的文件名 %s

_FILE_ :获取当前宏所在的文件名 %s

_LINE_ :获取当前宏所在的行号 %d

_FUNCTION_ :获取当前宏所在的函数名 %s

__func___:同_FUNCTION

_DATE_ :处理日期%s

_TIME_ :处理时间%s

_INCLUDE_LEVEL_:包含层数,从0开始

__cplusplus: C++有定义,C无定义,可以检测环境是C编译器还是C++编译器

代码:print.h predef.h、predef.c

8、GCC相关的环境变量

在进程上下文中保存的一些数据:键(功能,是什么)=值(具体内容)。

env(1):查看环境变量

grep(1):查找字符串

echo $环境变量名:打印该环境变量的值

​ 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

Tips:包含自己的头文件的几种方式

(1) #include “头文件路径”:移植性差

(2) gcc -I指定头文件路径:推荐

(3) 通过C_INCLUDE_PATH或者CPATH指定头文件路径:易冲突

三、库(静态库、共享库)

单一模型:将程序中的所有功能全部实现于一个单一的源文件内。缺点是编译时间长,不易于升级和维护,不易于协作开发

分离模型:将程序的不同功能模块划分到不同的源文件中。优点是缩短了编译时间,易于维护和升级,易于协作开发。不足之处在于,不同的源文件会生成很多的目标文件,使用是需要将每个目标文件依次链接,不方便,所以需要制作成库文件,方便使用和携带。

1、静态库

静态库的本质就是将多个目标文件打包成一个文件。

链接静态库就是将库中被调用的代码(函数**)复制到调用模块**中。

使用静态库的程序通常会占用较大的空间,库中代码一旦修改,所有使用该库的程序必须重新链接(缺点)。

使用静态库的程序运行时无需依赖静态库,执行效率高(优点)。

静态库的形式:lib库名.a

nm命令:查看二进制文件符号表

构建静态库

​ 构建静态库的过程就是将.c源文件编译生成.o目标文件,然后将.o目标文件打包生成.a库文件的过程,使用下面的命令制作静态库和使用静态库:

ar -r -o lib库名.a *.o

使用静态库

  1. gcc … -l库名 -L库的路径
  2. export LIBRARY_PATH=<库路径>:通过LIBRARY_PATH环境变量指定库的路径
  3. gcc … 库文件名:直接链接静态库

参考代码:day02/static_lib/

补充:符号表含义
符号表是一种用于语言翻译器(例如编译器解释器)中的数据结构。在符号表中,程序源代码中的每个标识符都和它的声明或使用信息绑定在一起,比如其数据类型、作用域以及内存地址。对于符号表组织、构造和管理方法的好坏会直接影响编译系统的运行效率。

2、动态库(共享库)

ldd命令:检查可执行文件依赖的动态库

动态库和静态库最大的不同就是,链接动态库并不需要将库中被调用的代码复制到调用模块中,被嵌入到调用模块中的代码仅仅是被调用代码在动态库中的相对地址。

如果动态库中的代码同时被多个进程所用,动态库的实例(在内存中)在整个内存中仅需一份,因此动态库也叫共享库/共享对象(shared object)。

使用动态库的模块是所占空间较小,即使修改了库中的代码,只要接口保持不变,无需重新链接。

使用动态库的代码,在运行时需要依赖库,因此执行的效率略低。

动态库的形式:lib库名.so

构建动态库:

gcc -c -fpic xxx.c -o xxx.o
#-fpic:生成与位置无关的代码,即在库内的函数调用也用相对地址表示
gcc -shared -o lib库名.so *.o	#将生成的.o打包成动态库
  • 1
  • 2
  • 3

使用动态库(和使用静态库一样)

  1. gcc … -l库名 -L库的路径
  2. export LIBRARY_PATH=<库路径>:通过LIBRARY_PATH环境变量指定库的路径
  3. gcc … 库文件名:直接链接动态库

运行时所调用的动态库必须位于LD_LIBRARY_PATH环境变量所表示的路径(加载器的搜索路径)。

参考代码:day02/dynamic_lib/

gcc缺省链接共享库,可通过-static选项强制链接静态库。

3、动态加载动态库

系统提供的针对动态库的动态加载函数

#include <dlfcn.h>	//头文件
Link with -ldl	//编译选项
  • 1
  • 2

void *dlopen(const char *filename, int flags);
成功返回动态库的句柄,失败返回NULL。

  • filename-动态库的路径,若只给文件名,则根据LD_LIBRARY_PATH环境变量搜索动态库

  • **flags-**加载方式,可取以下值:

    • RTLD_LAZY-延迟加载(懒加载),使用动态库中的符号时才加载

    • RTLD_NOW-立即加载

该函数所返回的动态库句柄唯一的标识了系统内核所维护的动态库对象,将作为后续函数调用的参数

void *dlsym(void *handle, const char *symbol);
成功返回函数地址,失败返回NULL。

  • handle-动态库句柄
  • symbol-符号(函数、全局变量等)名

该函数所返回的函数指针是void*类型,使用时需要做强制类型为实际的函数指针类型才能调用。

int dlclose(void *handle);
关闭共享库,共享库的引用计数减一。如果引用计数为0,系统卸载共享库。成功返回0,失败返回非0

  • handle-动态库句柄

char *dlerror(void);
之前若有错误发生,则返回错误信息字符串,否则返回NULL

参考代码:load.c

#include <stdio.h>
#include <dlfcn.h>
int main(void)
{
	//动态加载动态库libmath.so
	void *handle = 
        dlopen("../dynamic_lib/libmath.so",RTLD_NOW);
	if(!handle){
		fprintf(stderr,"dlopen:%s\n",dlerror());
		return -1;
	}
	//从动态库中获取add函数的入口地址
	int (*add)(int, int) = 
        (int(*)(int, int))dlsym(handle,"add");
	if(!add){
		fprintf(stderr,"dlsym:%s\n",dlerror());
		return -1;
	}
	//从动态库中获取sub函数的入口地址
	int (*sub)(int, int) = 
        (int(*)(int,int))dlsym(handle,"sub");
	if(!sub){
		fprintf(stderr,"dlsym:%s\n",dlerror());
		return -1;
	}
	//从动态库中获取show函数的入口地址
	void (*show)(int,char,int, int) = 
        (void(*)(int,char,int,int))dlsym(handle,"show");
	if(!show){
		fprintf(stderr,"dlsym:%s\n",dlerror());
		return -1;
	}
	int a=30, b=20;
	show(a,'+',b,add(a,b));
	show(a,'-',b,sub(a,b));
    //卸载/关闭动态库
	if(dlclose(handle)){
		fprintf(stderr,"dlclose:%s\n",dlerror());
		return -1;
	}
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

四、辅助工具

1、查看符号表:nm

列出目标文件(.o)、可执行文件、静态库文件(.a)或动态库文件(.so)中的符号

参考代码:nm.c

int a;//全局变量
static int b; //静态全局变量
void foo(void)
{
    int c; //局部变量
    static int d; //静态局部变量
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ifqH9rfZ-1578914622439)(/home/jiwei/01_ESD/02_unixc/03_ESD1911/插图/day02/nm.png)]

2、显示二进制模块反汇编信息:objdump -S

3、删除符号表和调试信息:strip

删除目标文件(.o)、可执行文件、静态库文件(.a)或动态库文件(.so)中的符号表和调试信息,即瘦身

4、查看所依赖的动态库文件:ldd

查看可执行程序文件或动态库文件所依赖的动态库文件

五、错误号和错误信息

1、通过函数的返回值表达错误

返回整数的函数:通过返回合法值域以外的值表示错误

返回指针的函数:通过返回NULL指针表示错误

不需要通过返回值输出信息的函数:返回0表示成功,返回-1表示失败

2、通过错误号和错误信息表示产生错误的具体原因

#include <errno.h>—>全局变量errno(整数),标识最近一次系统调用产生的错误

**char *strerror(int errnum);**根据错误号返回错误信息

**void perror(const char *s);**打印最近错误的错误信息

printf函数的**%m**标记会被替换为最近错误的错误信息

参考代码:./day02/error/errno.c

虽然所有的错误号都不是0,但是因为在函数执行成功的情况下,错误号全局变量errno不会被清0,因此不能用errno是否为0作为函数成功或失败的判断条件,是否出错还是应该根据函数的返回值来决定。

伪代码:

函数调用;
if(返回值表示函数调用失败){
    根据errno判断发生了什么错误;
    针对不同的错误提供不同的处理
};
  • 1
  • 2
  • 3
  • 4
  • 5

六、环境变量

每个进程都有一张独立的环境变量表,其中的每个条目都是一个形如“键=值”形式的环境变量。

环境变量是跟进程紧密相关的。

env命令用于显示bash的环境变量。

系统全局变量:erviron,指向了系统的环境向量表,使用时,需要自己在代码中做外部声明:*extern char *environ

所谓环境变量表就是一个以空指针NULL结束的字符指针数组,其中的每个元素都是一个字符指针,指向一个空字符结尾的字符串,该字符串就是形如“键=值”形式的环境变量。

#include <stdio.h>
#include <stdlib.h>
void penv(char **env)
{
	while(env && *env){
		printf("%s",*env++);
	}
}
int main(int argc, char *argv[], char *envp[])
{
	extern char **environ;
	penv(environ);
	while(envp && *envp){
		printf("%s",*envp++);
	}
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

(1)根据环境变量名获取该环境变量的值

char *getenv(const char *name)
成功返回变量名匹配的变量值,失败返回NULL。

  • name-环境变量名,即等号左边的部分。

(2)添加或修改环境变量

int putenv(char * string);
成功返回0,失败返回-1

  • string-形如“键=值”形式的环境变量字符串。若其键已存在,则修改其;若其键不存在,则添加该环境变量

(3)添加或修改环境变量

int setenv(const char *name, const char *value, int overwrite);
成功返回0,失败返回-1

  • name-环境变量名,即等号左边的部分
  • value-环境变量值,即等号右边的部分
  • overwrite-当name参数所表示的环境变量名已存在,此参数取0,则保持该变量的原值不变;若此参数取非0,则将该变量的值修改为value

(4)删除环境变量

int unsetenv(const char * name);
成功返回0,失败返回-1

  • name-要删除的环境变量名

(5)清空环境变量

int clearenv(void)
成功返回0,失败返回-1

参考代码:./day02/env/env.c

补充

Linux环境变量的分类:

1.系统级环境变量:每一个登录到系统的用户都能够读取到系统级的环境变量,分为以下几类:

/etc/profile:在系统启动后第一个用户登录时运行,并从/etc/profile.d目录的配置文件中搜集shell的设置,使用该文件配置的环境变量将应用于登录到系统的每一个用户

/etc/bashrc(Ubuntu和Debian中是/etc/bash.bashrc):在 bash shell 打开时运行,修改该文件配置的环境变量将会影响所有用户使用的bash shell

/etc/environment:在系统启动时运行,用于配置与系统运行相关但与用户无关的环境变量,修改该文件配置的环境变量将影响全局

2.用户级环境变量:每一个登录到系统的用户只能够读取属于自己的用户级的环境变量

~/.profile:当用户登录时执行,每个用户都可以使用该文件来配置专属于自己使用的shell信息;

**/.bashrc**:当用户登录时以及每次打开新的shell时该文件都将被读取;注意:通常我们修改bashrc,有些linux的发行版本不一定有profile这个文件etc/profile等中设定的变量(全局)的可以作用于任何用户,而/.bashrc等中设定的变量(局部)只能继承/etc/profile中的变量, 他们是"父子"关系。

七、内存管理

1、虚拟内存、物理内存、半导体内存、换页文件

虚拟内存:地址空间,虚拟的存储区域,应用程序所访问的都是虚拟内存。

物理内存:存储空间,实际的存储区域,只有系统内核可以访问物理内存。物理内存包括半导体内存和换页文件两部分。当半导体内存不够用时,可以把一些长期闲置的代码和数据从半导体内存中缓存到换页文件中,这叫页面换出。一旦需要使用被换出的代码和数据,再把它们从换页文件恢复到半导体内存,这叫做页面换入。因此,系统中的虚拟内存比半导体内存大得多。

虚拟内存和物理内存之间存在对应关系,当应用程序访问虚拟内存时,系统内核会依据这种对应关系找到与之相应的物理内存。上述对应关系存储在内核中的内存映射表中。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-chkS95mb-1578914622439)(./插图/day02/内存映射关系.png)]

Unix/Linux操作系统采用虚拟内存管理技术,即每个进程都有各自互不干涉的4G线性虚拟地址空间(32位系统),用户所看到和接触到的都是该虚拟地址,无法看到实际的物理内存地址。利用这种虚拟地址不但能起到保护操作系统的效果(用户不能直接访问物理内存),而且更重要的是,用户程序可使用比实际物理内存更大的地址空间。

地址分类:物理地址、逻辑地址、线性地址。物理地址是内存单元的实际地址,用于芯片级内存单元寻址;逻辑地址是程序代码经过编译后出现在 汇编程序中地址,每个逻辑地址都由一个段和偏移量组成;线性地址其实就是虚拟地址,在32位CPU架构下,可以表示4GB的地址空间。

逻辑地址经段机制转化成线性地址,线性地址又经过页机制转化为物理地址。物理地址 = 段基址<<4 + 段内偏移(线性地址),但是在Linux系统中,令段的基地址为0,所以段内偏移量=线性地址,也就是说虚拟地址直接映射到了线性地址,Linux把段机制给绕过去了。

2、进程映射(Process Maps)

每个进程都拥有独立的4GB(32位系统)的虚拟内存(虚拟地址空间),分别被映射到不同的物理内存区域。

内存映射和换入换出都是以页为单位,1页=4096B(4K)。

4GB虚拟内存中高地址的1GB被映射到内核的代码和数据区,这1GB虚拟地址空间在各个进程间共享。用户的应用程序只能直接访问低地址的3GB虚拟内存,该区域称为用户空间,而高地址的1GB虚拟内存(虚拟地址空间)被称为内核空间。用户空间中的代码,只能直接访问用户空间的数据;如果要想访问内核中的代码和数据必须借助专门的系统调用完成。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aZt1FAlY-1578914622440)(./插图/day02/进程地址空间布局.png)]

用户空间的3GB虚拟内存可以进一步划分为如下区域:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-InmjUg9G-1578914622440)(./插图/day02/内存分布.png)]

参考代码:./day03/proc_maps/maps.c

size命令可以查看一个可执行程序的代码区、数据区和BSS区的大小。

cat /proc/<pid>/maps:查看某个进程占用的内存区域

每个进程的用户空间都拥有独立的从虚拟内存到物理内存的映射,称之为进程间的内存壁垒(只能访问自己的用户空间,不能访问其他进程的用户空间)。

参考代码:./day03/proc_maps/vm.c

3、内存的分配与释放

malloc、calloc、realloc、free都是调用的brk/sbrk两个函数,brk/sbrk两个函数调用mmap/munmap这两个函数,mmap/munmap这两个函数调用kmalloc/kree这两个函数

分配:映射+占有

映射:在地址空间(虚拟内存)和存储空间(物理内存)建立映射关系

占有:指定内存空间的归属性

释放:解除映射+放弃占有

放弃占有:解除对内存空间的归属约束

解除映射:消除地址空间(虚拟内存)和存储空间(物理内存)之间的映射关系

(1) 以增量方式分配或者释放虚拟内存

*void sbrk(intptr_t increment);

成功返回调用该函数之前的堆顶指针,失败返回-1

  • increment:可取以下值

    >0—堆顶指针上移,增大堆空间,分配内存

    <0—堆顶指针下移,缩小堆空间,释放内存

    =0—不分配也不释放虚拟内存,仅仅返回当前堆顶指针

系统内核维护一个指针,指向堆内存的顶端,即有效堆内存中最后一个字节的下一个位置。sbrk函数根据增量参数increment调整该指针的位置,同时返回该指针原来的位置。期间若发生内存页耗尽或空闲,则自动追加或取消相应内存页的映射。

参考代码:./day03/sbrk/sbrk.c

(2) 以绝对地址的方式分配或者释放虚拟内存

*int brk(void end_data_segment);

成功返回0,失败返回-1

  • end_data_segment:可取以下值

    >当前堆顶—分配虚拟内存

    <当前堆顶—释放虚拟内存

    =当前堆顶—空操作

系统内核维护一个指针,指向当前堆顶。brk函数会根据指针参数end_data_segment来设置堆顶的新位置。期间若发生内存页耗尽或空闲,则自动追加或取消相应内存页的映射。

参考代码:./day03/sbrk/brk.c

(3) 建立物理内存或文件到虚拟内存的映射

**void mmap(void start, size_t length, int prot, int flags, int fd, off_t offet);

成功返回映射区虚拟内存的起始地址,失败返回MAP_FAILED(void *类型的-1)

  • start—映射区虚拟内存的起始地址,NULL表示由内核自动选择

  • length—映射区的字节数,自动按页(4096)取整

  • port—访问权限,可取以下值

    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—文件偏移量,自动按页对齐

(4) 解除物理内存或文件到的虚拟内存映射,可以一次性解除映射,也可以分次解除映射

int munmaps(void *start,size_t length );
成功返回0,失败返回-1

  • start—映射区起始地址
  • length—映射区字节数

参考代码:mmap.c

PS:段错误的本质——访问了没有物理内存对应的虚拟地址或者该地址有物理内存对应但是没有访问权限

通常brk函数和sbrk函数配合使用:使用sbrk分配内存,使用brk释放内存

4、补充:内存分配原理brk和mmap

——摘自百度文库,仅作了解

从操作系统角度来看,进程分配内存有两种方式,分别由两个系统调用完成:brkmmap(不考虑共享内存):brk是数据段(.data)的最高地址指针_edata往高地址推,mmap是在进程的虚拟地址空间中(堆和栈中间,称为文件映射区域的地方)找一块空闲的虚拟内存。这两种方式分配的都是虚拟内存,没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系。

内存分配原理

八、系统调用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bAGovH4m-1578914622440)(./插图/day03/系统调用.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SQRYEvRK-1578914622440)(./插图/day03/操作系统层次.png)]

(1) Linux系统内核提供了一套用于实现各种系统功能的子程序,谓之系统调用。程序的编写者可以像调用普通C语言函数一样调用这些系统调用函数,以访问系统内核提供的各种服务。

(2) 系统调用函数在形式上与普通C语言函数并无差别,二者的不同之处在于,前者工作在内核态,而后者工作在用户态。

(3) 在intel的CPU上,运行代码分为4个安全级别: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)。每张盘片的每个表面都有成千上万个磁道。一个磁道按照512Bytes为单位分成若干个区域,其中的每个区域就叫做一个扇区(Sector)。扇区是文件存储的基本单位

(4) 柱面、柱面组、分区和磁盘驱动器

硬盘中不同盘片相同半径的磁道所组成的圆柱称为柱面(Cylinder)。整个硬盘的柱面数与每张盘片的磁道数是相等的。硬盘上的每个字节需要通过以下参数定位:磁头号用来确定哪个盘面,柱面号确定哪个磁道,扇区号确定哪个区域,偏移量确定扇区内的位置。磁头号、柱面号、扇区号和偏移量统称柱面I/O。若干个连续的柱面构成了一个柱面组,若干连续的柱面组构成了一个分区。每个分区都建有独立的文件系统,若干个分区组成磁盘驱动器。

2、文件系统的逻辑结构

磁盘驱动器:| 分区 | 分区 | 分区 |

分区

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/AllinToyou/article/detail/490905
推荐阅读
相关标签