从零开始的操作系统

操作系统

Unix/Linux环境C语言,以学习操作系统的接口的方法来学习,理解操作系统的运行机制以及一些网络协议。
C/C++ 语言和数据结构算法 与平台无关,重点是算法逻辑
Unix/linux/Android/ios平台相关,系统接口
嵌入式/驱动/移植 硬件相关,硬件接口

环境介绍
内存管理
文件操作
文件管理
信号处理
进程管理
进程通信
网络通信
线程同步
线程管理

2、 Unix操作系统
系统特点:多用户、多任务,支持多种处理器架构,高安全性,高可靠性,高稳定性。
既可以构建大型关键业务系统的商用服务器,也可以构建面向移动终端的,手持设备等相关的嵌入式应用
三大衍生版本:System V(银行,电信在使用的服务器系统) , Berkley(MacOS IOS带界面) ,Hybrid(Minix Linux)
3、Linux操作系统
类Unix系统,免费开源,它指的是系统的内核,凡是使用这种内核的操作系统都叫Linux系统(发行版),严格意义上将Lunix指的是内核,隶属于GUN工程。
手机,平板电脑,路由器,视频游戏控制平台,PC,大型计算机,超级计算机
标志是一个企鹅
Minix操作系统是一个微型的类Unix系统、免费开源,而Linux之父就是参照了这款操作系统,才写出了第一个版本的Linux内核代码
GUN工程是 自由软件基金会 所创立的一个开源组织,基本原则就是共享,主旨是发展出一个有别于商业Unix的免费且完整的类Unix系统–GUN Not Unix.目前Linux内核由它进行维护,所以Linux也叫GUN Linux
GPL通用公共许可证:
允许对某些成果及派生成果重用,修改,复制,对所有人都是自由的,但不申明做了原始工作,或申明由他人所作。
POSIX标准:统一的系统编程接口规范:接口形式 名字 参数 返回值,它保证了应用程序源码级的可移植性
特点:
多用户、多用户
遵循GUN/GPL 具有开放性
有丰富的网络功能
可靠的系统安全
良好的可移植性
发行版:
Debian
Ubuntu
Fedora
Radhat
CentOS
4、GUN编译器(编译框架)
1、支持众多编程语言,平台
2、构建过程(C代码是如何变成可执行文件的)
预处理:把程序员所编译的C代码翻译成标准的C代码
汇编:把预处理后的C代码翻译成汇编代码
编译:把汇编代码翻译成二进制指令
链接:把若干个目标文件合并成一个可执行文件
3、查看版本:gcc -v
4、文件后缀
.h 头文件
.gch 头文件的编译结果,一般删除不要保留
.c 源文件
.i 预处理文件
.s 汇编文件
.o 目标文件
.a 静态库文件
.so 共享库文件
5、参数
-E 预处理
-S 汇编
-c 编译(只生成目标文件)
-o 指定编译结果的名字
-Wall 产生尽可能多的警告
-Werror 把警告当错误处理
-x 指定编译的语言
-g 生成调试信息
-On 编译优化的级别
头文件的作用是什么?编译时头文件找不到怎么办
-D 编译时定义宏
-l 链接里添加库
-I 指定头文件的查找路径,配置环境变量
1、打开 vim ~/.bashrc
2、在文件末尾,添加一行export C_INCLUDE_PATH=$C_INCLUDE_PATH:NEW_PATH
3、重新加载配置文件 source ~/.bashrc
注意:如果要删除的环境变量需要在~/.bashrc文件中删除环境变量后,退出终端重新打开

			考题1:#include<> / #include""区别?

			考题2:头文件中可以编写哪些内容?
			
			考题3:头文件的作用?
					1、说明对应的.c文件的内容有哪些(声明函数,全局变量)。
					2、定义结构、联合、枚举、宏
					3、类型重定义。
					虽然函数可以隐式申明,但不准确,而且非常有可能造成严重错误
6、预处理指令
	#include 文件包含,区分""和 <>的区别
	#define 定义宏常量或函数
	#undef 删除宏
	#把标志符转换成字符串  hehe->"hehe"
	##合并标识符
	#error 在编译期间产生错误
	#warning 在编译期间产生警告
	#pragma GCC dependency 用于监控文件,防止所依赖的文件,修改后不知道
	#pragma GCC poison 禁用某些表示符
	#pragma pack(n)设置结构、联合的补齐和对齐字节数
		n的值必须比默认的要小
		对齐边界必须是2的较小次方
	#line 指定当前行的行号

5、库
库就是目标文件(代码)的集合,我们把不需要升级更新维护的代码打包合并在一起,方便使用,也可以对源代码进行保密。

	1、静态库
		静态库在使用时是把被调用的代码复制到调用模块中,然后在执行程序时,静态库就不需要了
		静态库的执行速度快,但占用空间大。当库中的内容发生变化时,需要重新编译出新的程序,因此不能轻易修改库中的内容

	2、共享库
		共享库只是在调用模块中嵌入调用代码的在库的相对位置的地址,当执行程序时,共享库的程序一起加载到内存中,当执行到调用共享库中代码的指令时条转到共享中执行,执行完毕后在跳转回来。
		占用空间小,方便更新(共享库发生变化后,程序不需要再次编译),相对于静态库执行效率略低。
		静态库的扩展名.a  共享库(动态库)的扩展名.so 

6、静态库
1、创建静态库
编译源代码:vi .c/.h
编译源代码:gcc -c XXX.c->xxx.o
打包生成静态库:ar -r libxxx.a x1.o x2.o
ar命令的一些参数:
-r 把目标文件添加到静态库中,已经存在的更新
如果库文件不存在就创建
-q 将目标文件追加到静态库的末尾
-d 从静态库中删除目标文件
-t 显示静态库中有哪些目标文件
-x 把静态库展开成目标文件
2、调用静态库
直接调用:调用者和库在同一路径下
gcc main.c libxxx.a(一起gcc)
设置环境变量:设置方法与C_INCLUDE_PATH类似
1、打开vim ~/.bashrc 文件
2、在文件末尾添加一行
export LIBRARY_PATH=$LIBRARY_PATH:库文件的路径

				3、重新加载配置文件 source ~/.bashrc
				4、编译时要指定库名
				gcc main.c -lmath
			设置参数: -L路径
				gcc main.c -L路径 -lmath
3、运行
	在编译时已经把函数的二进制复制到可执行文件中了,在执行文件时不在需要静态库文件

7、共享库
1、创建共享库
编写代码: vi .c/.h
编译出位置无关的目标文件:
gcc -c -fpic xxx.c ->xxx.o
链接生成共享库:
gcc -shared xxx.o xxx2.o… -o libxxx.so
2、调用共享库
直接调用:调用者和库在同一路径下
gcc main.c libxxx.so(一起gcc)
设置环境变量:设置方法与C_INCLUDE_PATH类似
1、打开vim ~/.bashrc 文件
2、在文件末尾添加一行
export LIBRARY_PATH=$LIBRARY_PATH:库文件的路径

				3、重新加载配置文件 source ~/.bashrc
				4、编译时要指定库名
				gcc main.c -lmath
			设置参数: -L路径
				gcc main.c -L路径 -lmath
3、运行
	在使用共享库时,调用者只是记录了被调用代码在库的位置,因此在执行时需要共享库同时被加载
	操作系统会根据LD_LIBRARY_PATH中的环境变量的设置来共享库。

8、动态加载共享库
使用前加头文件
#include<dlfcn.h>

1、加载共享库:
	
2、获取标志符地址,使用handle:共享库的句柄
	symbol:标识符的名字
	返回值:标识符在共享库中的位置(地址可以解引用,或跳转过去)
3、卸载共享库
	handle:共享库的句柄
	返回值:成功返回0,错误返回-1
4、获取错误信息
	在共享库的过程中出现错误,以字符串的形式返回

9、辅助工具
nm:察看目标文件、可执行文件、静态库、共享库中的符号列表
ldd:查看可执行程序所依赖的共享库有哪些
strip:减肥,去掉目标文件,可执行文件,静态库和共享库中的符号列表,调试信息
objdump:显示二进制模版的反汇编信息

作业
把链表、栈、队列、有序二叉树、查找、排序封装成算法共享库
把之前做项目常用的函数,封装成工具共享库(libtools.so

10、错误处理
1、通过函数返回值表示错误
返回值合法表示成功,非法表示失败
返回有效指针表示成功,空指针(NULL/0xffffffff)表示失败
返回0表示成功,-1表示失败
永远成功,printf
练习1:求字符串的长度 str_len,若指针为空则返回错误
练习2:字符串拷贝函数 str_cpy(char* dest,size_t dlen,char* src),考虑目标的溢出问题,如果目标位置无效或超出则报错
练习3:intmin 求两个整数的最小值,二者相等则报错
练习4:intagv 求两个整数的平均值,该函数永远成功

2、通过errno表示错误
errno是一个全局变量,它的声明在errno.h文件中,随时发生变化
可以将他转化成有意义的字符串strerror(errno)<=>perror("msg")
注意:在函数执行成功的情况下不会修改errno的值
因此不能以errno的值不等于0就判断函数执行出错了(还要看函数返回值,一般于之配合使用)通过返回值判断是否出错,而通过perror查询出了什么类型的错误。

11、环境变量
以字符串形式存在的,绝大多数记录的是路径信息,他表示了当前操作系统的资源配置,环境设置等相关消息。
1、环境变量表
每个程序运行时,操作系统都会把所有的环境变量记录到一张表中,传给程序。
(1)通过main函数参数获取 int main(int argc,char* argv,char* environ)
(2)通过申明为全局变量获取 extern char** environ
2、环境变量函数
char *getenv(const char *name);
功能:根据环境变量名,获取环境变量的值

		int putenv(char *string);
		功能:以name=value形式设置环境变量,如果环境变量存在则更新,不存在则添加
			返回值:成功返回0,失败返回-1
		
		int setenv(const char *name, const char *value, int overwrite)
		功能:设置name环境变量的值为value,如果name存在且overwrite不为0则更新,否则不变		
		
		int unsetenv(const char *name)
		功能:从环境变量表中删除name

		int clearenv(void);
		功能:清空环境变量标

	注意:操作系统记录的环境变量的数据记录是一块特殊的存储空间,而在程序自己添加的环境变量需要自己准备存储空间
	注意:对于环境变量的修改,只能影响自己,不能影响别人
	
	练习5:从文件中读取一个程序的配置信息
		 ServerIP = 192.168.0.1
		 Port = 8899
		 Maxsize = 100
		 ContinueSec = 3
		 LogPath = /zhizhen/temp/
		 DataDath = /zhizhen/data/
	练习6:给LIBRARY_PATH添加一个路径(/home/zhizhen/lib)

12、内存管理
自动分配/释放内存(智能指针auto_ptr) STL 调用标准C++中的new/delete
new/delete 构造/析构 C++ 标准C中的malloc/free
malloc/free 标准C 调用Linux系统接口
Linux的brk/sbrk POSIX(操作系统的标准接口)调用内核接口
mmap/munmap Linux 调用内核接口
kmalloc/vmalloc 内核层 调用驱动
get_free_page 驱动 。。。。
13、进程映像
程序是保存在磁盘上的可执行文件,加载到内存中被操作系统调用执行的程序叫进程。(正在运行的程序),同一个程序,可以被操作系统调用多次,形成多个不同的进程(身份不同,功能相同)
进程在内存空间的分布的情况叫进程映象,从低地址到高地址排列的是:
代码段/只读段:
放的是可执行指令(二进制指令、字符串字面值、具有const属性且被初始化过的全局变量静态变量)

	数据段:被初始化过的全局变量静态变量

	BSS段:没有被初始化过的全局变量静态变量,进程一旦加载成功就会把这段内存清理为零

	堆:动态的分配,动态的管理,需要程序员手动操作

	栈:非静态的局部变量,包括函数的参数、返回值
		从高地址向低地址使用,和对内存之间存在一段空隙
	命令行参数及环境变量表:能改。存放命令行参数、环境变量表(存储在栈里面)
	
	练习7:在一个程序中打印各段内存的地址,然后与操作系统中的内村分配情况表比较,然后一一对应内存的分配情况
		getpid() 可以获取进程的编号
		cat /proc/xxxx/maps
		size 程序名 查看text data bss个段的大小

14、虚拟内存(32位)
每一个进程都有各自独立的4G字节的虚拟地址空间,我们在编程时使用的永远都是这4G的虚拟地址空间中的地址,永远无法直接访问物理地址
操作系统不让程序直接访问物理内存而只能使用虚拟地址空间,一方面为了操作系统自身的安全,另一方面可以让程序使用到比物理内存更大的空间(把硬盘上的特殊文件与虚拟地址空间进行映射)。
这4G的虚拟地址空间被分为两个部分:
0-3G 为用户空间
3~4G 为内核空见(操作系统使用)
注意:用户空间的代码不能直接访问内核空间的代码和数据,但可以通过系统调用(不是函数,但以函数形式调用)进入到内核态,间接与内核交换数据 内核占据主动
如果使用了没有映射过或者访问了没有权限的的虚拟内存地址,就会产生段错误(非法内存访问)
一个进程对应一个用户空间,进程一旦切换,用户空间也会发生变化,内核空间由操作系统管理,不会随着进程的切换而变换。
内核空间由内核所管理的独立且唯一的init_mm表进行内存映射,而用户空间的表是每一个进程一张

注意:每个进程的内存空间完全独立,不同的进程间交换虚拟内存地址没有任何意义,所以进程之间不能直接进行通行。但可以由内核中转协调

虚拟内存到物理内存映射是以页为单位,一页= 4K = 4096 字节

15、内存管理API
sbrk,brk都可以进行映射内存的取消映射(系统级的内存管理)
void *sbrk(intptr_t increment);
increment:
0 获取未分配前的内存首地址(也就是已经分配的尾地址)
>0 增加内存空间
<0 释放内存空间
返回值:未分配前的内存首地址,以字节为单位
int brk(void *addr);
功能:设置未分配内存的首地址
返回值:成功返回0,失败返回-1

	它们背后维护着一个指针,该指针记录的是未分配内存的首地址,也是当前堆内存的最后一个字节的下一个的位置,每次至少一个页。
	但为了方便,一般用sbrk分配内存,用brk释放内存
	注意:sbrk和brk分配和释放的都是使用权,真正的映射工作由其他的系统调用完成(mmap/munmap)
	
	练习8:计算1000以内的素数,存储到堆内存中,不要浪费内存(sbrk,brk)

猜你喜欢

转载自blog.csdn.net/weixin_45050225/article/details/97122166
今日推荐