第一章 Linux常见指令汇总
1. man指令
Linux的指令有很多参数,我们不一定能全部记住,我们可以通过查看联机手册获取帮助。访问Linux手册页的指令便是man



- man手册一共8章,常用的就是-2(系统调用)或者-3(库函数)
2. more指令

说明一下:
- more指令只能通过按回车键进行下翻操作,无法进行上翻操作
- 并且more指令在查看之前会先加载整个文件
3. less指令

说明一下:
- less指令可以通过按上下键进行上翻和下翻操作,
- 并且less指令在查看之前不会加载整个文件
4. head/tail指令


说明一下:
- head 文件名,默认输出前10行
- head -n 文件名,指定行数输出
- tail和head指令同理,只不过tail是从后开始打印
如何读取文件中间某一段内容
首先需要认识linux中的管道,管道是用于传输某种资源的,而在计算机中就是传输数据的,
在指令中管道用“|”来表示,管道左边接收产生的数据,右边读取数据


- 因此我们可以将test.txt文件的前1010行从管道左侧输入到管道,并在管道右侧读取数据进行进一步的操作即可
5. find指令
命令:-name 按照文件名查找文件

命令:-size 按照文件大小查找文件

6. which指令
Linux当中的每个指令其实都是用计算机语言编写出的可执行程序,这些程序都存储在文件当中,我们可以通过which指令查看某一指令的程序文件所在路径

当我们执行某一指令时,实际是调用其可执行程序,因此我们还可以通过其程序文件的所在路径进行调用该指令

7. alias指令

说明一下:
- 当你觉得某一指令太长时,你可以使用alias指令给它起一个别名
- 但只会在当前终端有效,重启之后就会失效
8. grep指令
grep指令类似于关键词搜索(行过滤器)

常用选项:
- grep -i 忽略大小写进行查找
- grep -n 输出行号
- grep -v 反向选择
9. zip/unzip指令
zip指令用于打包压缩目录或文件,unzip对应的就是解压缩了

- 压缩命令:zip -r xx.zip 源文件

- 解压缩:unzip xxx.zip -d 保存路径
- 如果直接使用 unzip xxx.zip 指令,就会直接解压到当前目录
10. tar指令
打包示例-c:

解包示例-x:

不解开压缩包,直接查看里面内容-tf:

11. file指令
查看文件的更多属性信息
12. uname指令

说明一下:
- -r 输出电脑和操作系统相关信息
- -a 详细输出所有信息
13. addser/passwd指令
添加一个普通用户账号




14. userdel指令
删除账号时之间输入userdel -r yourid(yourid是要删除的用户的用户名)


15. locate指令
locate是一个强大且快速的文件查找工具,通过预先构建的数据库索引文件来实现快速搜索,记住定期更新数据库,以确保结果的准确性,从索引库中快速查找文件或目录,默认情况下updatedb是每天执行一次,也可以手工更新索引库,

说明一下:
- 安装 locate:sudo apt update,sudo apt install mlocate
- 更新 sudo updatedb
- 查找文件 locate filename
- 使用通配符 locate '*.c'
- 限制输出结果的数量:locate -n 10 '*.c'
- 显示文件所在目录 而不是文件本身:locate -b 'demo01.c'
- 更新数据库:sudo updatedb
16. ping指令
检查网络连通性

17. ifconfig指令
显示或配置网络接口

18. netstat指令
显示网络连接,路由表,接口状态等(可能需要net-tools包)

- netstat -nltp 或者netstat -nltu
19. ln指令
ln指令用于在linux中创建连接,可以是软连接,也可以是硬链接
硬链接:是指向相同文件数据的多个文件名,删除一个硬链接不会影响文件数据,文件数据只有在所有硬链接都被删除后才会被释放
软链接(符号链接):符号链接是指向另一个文件或目录的指针,符号链接可以跨文件系统,指向目录,且指向的目标文件或目录被删除时,符号链接会变为无效的“悬挂”链接

- ln -sf 目标文件 要生成的链接名
- 不添加-s则创建硬链接,只能链接存在的文件
第二章 Shell的运行原理
我们都知道Windows以图形化界面为交互方式,而Linux以命令行界面为交互方式。Windows和Linux的交互方式虽然不同,但本质上是一样的,图形化界面和命令行界面都是为了让用户进行相关操作,而图形化界面和命令行界面就是我们所说的“外壳程序

Linux严格意义上说是一个操作系统,我们称之为“核心(kernel)”,但我们一般用户不能直接使用kernel,而是通过kernel的“外壳程序”,也就是所谓的Shell,来与kernel沟通
1.Shell定义
最简单来说就是“命令行解释器”,将使用者的命令翻译并交给核心(kernel)处理,然后再将核心的处理结果翻译并交给使用者,
windows的话就是通过图形接口,linux的话就是通过命令
2.Shell原理
通过创建子进程,让子进程进行命令行解释,这样子进程出现任何问题,都不影响父进程Shell
例如,当你的QQ出现卡死情况(程序异常)或你的QQ被关掉(程序终止),但其他子程序仍然可以运行
补充一点:Shell只是所有外壳程序的统称,而在centos7当中外壳程序,被称之为bash
3. shell编程(TODO)
第三章 Linux权限
1. 将普通用户添加到信任列表
那么首先你得先切换到超级用户,只有超级用户才有权力将普通用户添加到信任列表

切换到超级用户后,我们使用vim /ets/sudoers进行添加


- 添加完毕后,该用户就可以使用sudo指令,也就是拥有提升当前指令权限的能力了
2. 改变文件的访问权限

- chmod 权限 文件
3. 改变文件的拥有者

- chown 用户 文件
- 同时改变拥有者和所有组:chown 用户:用户 文件
4. 改变文件的所有组

- chgrp 用户 文件
5. 改变文件掩码
实际上,新建文件的默认权限为0666,新建目录的默认权限为0777。其中第一位的0与特殊权限有关,我们这里不必深究,而后面三位就是权限的八进制数值表示方法,我们将其翻译为字符表示方法

但实际上你会发现,你所创建出来的文件和目录的权限值往往不是我们所翻译出来的值,原因就是创建文件和目录的时候还要受到umask的影响,假设默认权限是mask,则实际创建出来的文件权限是:mask&(~umask)
通过指令umask就可以查看当前文件的掩码了, 超级用户的默认掩码为0022,普通用户的默认掩码为0002


说明一下:
- 我们可以简单的理解一下,凡是在umask中出现的权限位,都不能在最终权限中出现。
因此我们也可以通过修改umask来设置文件的访问权限

6. 目录的权限
1)-r: 如果用户没有该目录的可读权限,则无法通过ls指令查看目录中的文件内容。
2)-w: 如果用户没有该目录的可写权限,则无法通过一系列指令在目录中创建文件或删除文件。
3)-x: 如果用户没有该目录的可执行权限,则无法通过cd指令进入到目录当中
那么这就会出现一个问题
只要用户拥有某目录的可写权限,就可以删除该目录当中的文件,而不论该用户是否拥有该文件的可写权限,这显然是不合理的

- 你会发现一个普通用户,居然能删除root的文件,这显示是不合理的
添加 粘滞位
为了解决上面那个不合理的问题,Linux引入了粘滞位的概念,指令:chmod +t 目录名
当一个用户将某一个目录加上粘滞位后,该目录的权限值的最后一位变为字符“t”。


此时另一个用户就算有该目录的可写权限,也无法删除该目录下的文件,虽然目录被加上了粘滞位,但如果用户有该目录的可写权限,则不影响其在该目录下创建文件(未演示)
当一个目录被设置为粘滞位,则该目录下的文件只能由:
- 超级用户删除
- 该目录的拥有者删除
- 该文件的拥有者删除
第四章 Linux环境基础开发工具
1. 包管理器 - yum
1.1 Linux下安装软件的方式
方式一:下载到程序的源代码,自行进行编译,得到可执行程序
方式二:获取rpm安装包,通过rpm命令进行安装(未解决软件的依赖关系)
方式三:通过yum进行安装软件(常用)
1.2 认识yum
yum是一个在Fedora、RedHat以及CentOS中的前端软件包管理器,能够从指定的服务器自动下载RPM包并且安装,可以自动处理依赖性关系,并且一次安装所有依赖的软件包,无须繁琐地一次次下载、安装
且一个服务器同一时刻只允许一个yum进行安装,不能在同一时刻同时安装多个软件
常见命令:
- yum list,列出可供下载的全部软件
- yum list | grep 软件名,查找指定软件
- sudo yum install 软件名,安装指定软件
- sudo yum remove 软件名,删除指定软件
2. 编辑器 - vim
2.1 vim推荐配置
curl -sLf https://gitee.com/HGtz2222/VimForCpp/raw/master/install.sh -o ./install.sh && bash ./install.sh
- 如果你的linux是centos 7或8的发行版本,就可以一件配置vim环境
2.2 基本规定
- Ctrl-λ 等价于 <C-λ>
- :
command等价于:command <回车> - n 等价于 数字
- blank字符 等价于 空格,tab,换行,回车等
2.3 复制
- yy 等价于 shift + y → 复制当前行
- nyy → 从本行向下,复制n行
- yw → 从光标开始复制到这个单词尾
- ynw → 从光标开始复制到n个单词尾
2.4 删除字符
-
x→ 删除光标所在的一个字符 s → 删除光标所在的一个字符 并 进入插入模式 - X → 删除光标前一个字符
nx→ 删除光标所在的n字符- nX → 删除光标前n一个字符
dt字符→ 删除所有的内容,直到遇到字符
比如说:dt"表示从当前光标开始 一直删除 直到''
2.5 查找并删除
- f + 字符 → 在当前行查找字符
-
df + 字符 → 从这个光标开始 一直 删除到字符结束
-
cf + 字符 → 从这个光标开始 一直 删除到字符结束 并进入插入模式
2.6 进入插入模式
-
i → 光标不动 并 进入插入模式
-
a → 光标向后 并 进入插入模式
-
o → 向下新增一行 并 进入插入模式
-
O → 向上新增一行 并 进入插入模式
-
cw → 从光标位置 删除 连续字符 并 进入 插入模式(可以有多种组合)
-
cnw → 从光标位置 删除 连续n字符 并 进入 插入模式(可以有多种组合)
-
shfit + i → 光标移动到行首 并 进入插入模式
-
shift + a → 光标移动到行尾 并 进入插入模式
2.7 简单的光标移动
0→ 数字零,到行头- shift+4→ 到本行行尾
- shift+6→ 到本行第一个不是blank字符的位置
g_→ 到本行最后一个不是blank字符的位置(vscode支持不强)
2.8 重复命令
.→ (小数点) 可以重复上一次的命令
2.9 高效的光标移动
按行移动
gg→ 到第一行- G → 到最后一行
- 数字+
G→ 到第 几行,或者: 数字→到第几行
按单词移动
以默认方式(字母,数字和下划线) 为分割符
w→ 移动到下一个单词的开头。b 则表示 上一个单词的开头e→ 移动到本单词 或 下一个单词的结尾。
以blank字符为分隔符
- shift + w → 到下一个单词的开头,shift + b 则表示 上一个单词的开头
- shift + e → 移动到本单词 或 下一个单词的结尾
(最强)在同一行的光标移动
从左向右 (正方向)
- f+字符→光标移动到这个字符处
- t+字符→光标移动到这个字符的前一个位置
从右向左 (反方向)
- shift + f+字符→光标移动到这个字符处
- shift + t+字符→光标移动到这个字符的后一个位置
注: 数字 + f + 字符→移动到当前的第几个字符上
最强的光标移动
%: 匹配括号移动,包括(,{,[.(需要把光标先移到括号上)
2.10 替换字符
- r → 替换光标所在位置的字符
2.11 快速注释

- ctrl + v : 进入可视化模式
- j 或 k 移动,选中行
- shift + i:进入插入模式
- 输入//
- 再按esc退出键
2.12 缩进
<或>→ 左右缩进=→ 自动给缩进(非常好用) vscode好像也不支持·
2.13 vim改键位的操作(了解)


- vim的配置文件都在vimrc里面
- nnoremap 新按键 老按键
3. 编译器 - gcc/g++
3.1 预处理
gcc -E test.c -o test.i

3.2 编译
gcc -S test.i -o test.s

3.3 汇编
gcc -c test.s -o test.o

3.4 链接
gcc test.o -o test

3.5 动静态库
静态库是指编译链接时,把库文件的代码全部加入到可执行文件当中,因此生成的文件比较大,但在运行时也就不再需要库文件了,静态库一般以.a为后缀
动态库与之相反,在编译链接时并没有把库文件的代码加入到可执行文件当中,而是在程序运行时由链接文件加载库,这样可以节省系统的开销,动态库一般以.so为后缀
动态链接:
- 优点:省空间(磁盘的空间,内存的空间),bin体积小,加载速度快
- 缺点:依赖动态库,程序可移植性较差
静态链接:
- 优点:不依赖第三方库,程序的可移植性较高
- 缺点:浪费空间
gcc和g++默认生成的二进制程序是动态链接的,我们可以使用file指令进行查看

其次,我们还可以使用ldd指令查看动态链接的可执行文件所依赖的库

(图中的/lib64/libc.so.6就是当前云服务器当中的C标准库 )
虽然gcc和g++默认采用的是动态链接,但如果我们需要使用静态链接,带上-static选项即可
gcc test.c -o test_s -static
此时生成的可执行文件就是静态链接的了

我们可以查看源代码相同,但链接方式不同而生成的两个可执行程序test和test_s的大小

这也证明了动态链接比较节省空间,而静态链接比较浪费空间
4. 调试器 - gdb
在Linux当中gcc/g++默认生成的可执行程序是release版本的,是不可被调试的。如果想生成debug版本,就需要在使用gcc/g++生成可执行程序时加上-g选项


4.1 调试
- r:运行调试(启动调试)
- n:逐过程
- s:逐语句
- 行号:跳转指定行
- finishi:结束函数调用
- c:跳转到下个断点
4.2 显示
- display 变量:将变量加入常显示
- undisplay 变量:取消变量常显示
4.3 断点
- b 行号:在指定行中打断点
- info b:查看已打断点的信息
- d 编号:删除指定编号的断点
- disable 编号:禁用指定编号的断点
- enable 编号:启动指定编号的断点
4.4 退出
- q:退出gdb
5. 自动化构建工具 - make/Makefile
5.1 方式一

5.2 方式二

- $@:表示为表示依赖关系中的目标文件 (冒号左侧)
- $^:表示依赖关系中的依赖文件列表(冒号右侧全部)
- $<:表示依赖关系中的第一个依赖文件(冒号右侧第一个)
5.3 项目清理


说明一下:
- 一般将这种clean的目标文件设置为伪目标,用.PHONY修饰,伪目标的特性是:总是被执行
6. 跨平台构建系统-cmake
6.1 配置环境
在vscode中安装插件

在linux中安装cmake

6.2 创建cmake项目






6.3 配置launch.json




- 这里是"${workspaceFolder}/build/main" 或者bin/main
6.4 配置task.json

注意:这个要在cmakelist.txt中按ctrl+shift+b,

// tasks.json文件配置
{"version": "2.0.0","options": {"cwd": "${workspaceFolder}/build"},"tasks": [{"type": "shell","label": "cmake","command": "cmake","args": [".."]},{"label": "make","group": {"kind": "build","isDefault": true},"command": "make","args": []},{"label": "Build", //### launch.json中选择了这个"dependsOrder": "sequence","dependsOn": ["cmake","make"]}]
}
- 然后无脑直接ctrl+c ,ctrl+v
6.5 配置CMakeList.txt
参考文档:
# 1.前提
cmake_minimum_required(VERSION 3.2) #最低camke版本需求
PROJECT(test) #创建工程,example为工程名SET(CMAKE_BUILD_TYPE Debug) #debug模式
#SET(CMAKE_BUILD_TYPE Release) #Release模式# 2.需要的依赖库
find_package(OpenCV 4.2)
find_package(Eigen3 3.1.0 REQUIRED)
find_package(Pangolin REQUIRED)
find_package(realsense2)# 3.设置可执行文件与链接库保存的路径
set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin)
set(LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/lib)# 4.设置头文件目录使得系统可以找到对应的头文件
include_directories(
${PROJECT_SOURCE_DIR}/include #自己目录的头文件
${EIGEN3_INCLUDE_DIR} #其他系统目录的头文件 也可以直接写路径/usr/include/eigen3
${Pangolin_INCLUDE_DIRS}
)# 5.编译动态库并链接库文件
add_library(${PROJECT_NAME} SHARED
src/IMUPreIntergral.cpp
include/IMUPreIntergral.h
)
target_link_libraries(${PROJECT_NAME}
${OpenCV_LIBS}
${EIGEN3_LIBS}
${Pangolin_LIBRARIES}
-lboost_serialization
-lcrypto
)# 6.生成可执行文件
#选择需要编译的源文件,有几个文件写几个
add_executable(IMUPreIntergral src/IMUPreIntergral.cpp)
target_link_libraries(IMUPreIntergral ${PROJECT_NAME})#add_executable(IMUPreIntergral src/IMUPreIntergral.cpp)
#target_link_libraries(IMUPreIntergral ${PROJECT_NAME})
当时配置

cmake_minimum_required(VERSION 3.10.0)
project(code VERSION 0.1.0 LANGUAGES C)#debug模式
SET(CMAKE_BUILD_TYPE Debug) #debug模式
#SET(CMAKE_BUILD_TYPE Release) #Release模式#暂时不管,测试
include(CTest)
enable_testing()# 2.需要的依赖库 现在没有# 3.设置可执行文件与链接库保存的路径
set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin)
set(LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/lib)# 4.设置头文件目录使得系统可以找到对应的头文件
include_directories(
${PROJECT_SOURCE_DIR}/inc #自己目录的头文件
${EIGEN3_INCLUDE_DIR} #其他系统目录的头文件 也可以直接写路径/usr/include/eigen3
${Pangolin_INCLUDE_DIRS}
)# 5.编译动态库并链接库文件# 6.生成可执行文件
#选择需要编译的源文件,有几个文件写几个
#add_executable(my src/main.c src/test.c)
# 扫描src目录下的所有C文件
aux_source_directory(src SRC_FILES)
add_executable(main ${SRC_FILES})set(CPACK_PROJECT_NAME ${PROJECT_NAME})
set(CPACK_PROJECT_VERSION ${PROJECT_VERSION})#配置cmake项目c语言的版本 99
set(CMAKE_C_STANDARD 99)
# 设置交叉编译器的位置
set(CMAKE_C_COMPILER gcc)
include(CPack)
7. 打包动静态库
静态库:在编译程序的时候静态库的内容会被完整的复制到程序的内部,而它的优点是运行的程序不会出现缺失的问题,但它的缺点是不利于功能更新,且需要占用更多的内存
动态库:在编译的时候动态库并没被复制到程序中,而是程序运行时由链接文件加载库,而它的优点相对于静态库来说占用更少的内存,对程序执行效率也有一定的提升,缺点是程序执行的时候需要有动态库的支持,

7.1 打包静态库
创建静态库

说明一下:
- gcc foor.c -c foor.o
- gcc bar.c -c bar.o
- ar rcs libmylib.a foor.o bar.o
- #r:插入文件(如果静态库已存在,则替换静态库中的对象文件)
- #c:创建静态库
- #s:索引静态库,使其更快被查找
使用静态库

- gcc main.c -o mybin -L . -l mylib
- #-L . :指定静态库所在的目录(我这里是当前目录)
- #-l mylib : 链接名为libmylib.a的静态库
7.2 打包动态库
创建动态库

- 编译源代码文件:使用gcc编译这两个源代码文件为对象文件(.o文件),并加上生成位置无关代码(Position Independent Code,PIC)的选项-fPIC
- gcc -fPIC -c foor.c -o foor.o
- gcc -fPIC -c bar.c -o bar.o
- gcc foor.o bar.o -o libmylib.so -shared
使用动态库
编译并链接动态库:使用gcc编译main.c并链接动态库libmylib.so,且我们在运行之前,需要保证动态库路径可被系统识别,我们可以通过rpath或runpath选项将路径嵌入到可执行文件中:

- gcc main.c -o mybin -L . -l mylib -Wl,-rpath=.
- #-L . :指定动态库所在的目录(我这里是当前目录)
- #- mybin:链接名为libmylib.so的动态跨
7.3 cmake打包动静态库
cmake_minimum_required(VERSION 3.10.0)
project(code VERSION 0.1.0 LANGUAGES C)#debug模式
SET(CMAKE_BUILD_TYPE Debug) #debug模式
#SET(CMAKE_BUILD_TYPE Release) #Release模式#暂时不管,测试
include(CTest)
enable_testing()# 2.需要的依赖库 现在没有
#设置源文件 《《新增的》》
set(SOURCESsrc/foor.csrc/bar.c
)
# 3.设置可执行文件与链接库保存的路径
set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin)
set(LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/lib)# 4.设置头文件目录使得系统可以找到对应的头文件
include_directories(
${PROJECT_SOURCE_DIR}/inc #自己目录的头文件
${EIGEN3_INCLUDE_DIR} #其他系统目录的头文件 也可以直接写路径/usr/include/eigen3
${Pangolin_INCLUDE_DIRS}
)#5.动静库 《《新增的》》
# 创建静态库
add_library(mylib_static STATIC ${SOURCES})
# #共享库(动态库)
# add_library(mylib_shared SHARED ${SOURCES})# # 设置 rpath 仅对共享库生效
# set_target_properties(mylib_shared PROPERTIES
# INSTALL_RPATH "\$ORIGIN"
# BUILD_WITH_INSTALL_RPATH TRUE)# 6.生成可执行文件
#选择需要编译的源文件,有几个文件写几个
#add_executable(my src/main.c src/test.c)
# 扫描src目录下的所有C文件
aux_source_directory(src SRC_FILES)
add_executable(main ${SRC_FILES})# 链接共享库
target_link_libraries(main mylib_static) #《《新增的》》
# target_link_libraries(main mylib_shared) #《《新增的》》set(CPACK_PROJECT_NAME ${PROJECT_NAME})
set(CPACK_PROJECT_VERSION ${PROJECT_VERSION})#配置cmake项目c语言的版本 99
set(CMAKE_C_STANDARD 99)
# 设置交叉编译器的位置
set(CMAKE_C_COMPILER gcc)
include(CPack)

第五章 初步认识操作系统
简单来说,操作系统就是一款进行软硬件资源管理的软件
1. 设计操作系统的目的
- 与硬件进行交互,管理所有的软硬件资源
- 为用户程序(应用程序)提供一个良好的执行环境
2. 操作系统设计流程
2.1 硬件
首先,我们肉眼可见的就是计算机实物,也就是计算机底层的硬件,这些硬件看似是一个个罗列出来的,但实际在底层都遵守冯诺依曼的组织形式

2.2 操作系统
而单单只有这些硬件是不够的,还需要有一个软件来对这些硬件进行管理。例如,内存何时从输入设备读取数据?读取多少数据?内存何时刷新缓冲区到输出设备?是按行刷新还是全刷新?这些都是由软件进行管理的,而这个软件就是操作系统(Operator System)

此时这里有一个问题:难道操作系统直接和底层硬件打交道吗?
举个例子,如果操作系统自己来完成键盘的读取操作,那么只要你的键盘读取方式进行了改变,那么操作系统的内核源代码就需要进行重新编译,这对操作系统来说维护成本太高了。
于是我们又在操作系统与底层硬件之间增加了一层驱动层,驱动层的主要工作就是单独去控制底层硬件的。例如,键盘有键盘驱动,网卡有网卡驱动,硬盘有硬盘驱动,磁盘有磁盘驱动。驱动简单来说就是去访问某个硬件,访问这个硬件的读、写以及硬件当前的状态等等,驱动层就是直接和硬件打交道的。而驱动一般是由硬件制造厂商提供的,或是由操作系统相关的模块进行开发的(例如网卡)
此时操作系统就只需关心何时读取数据,而不用关心数据是如何读取的了,也就是完成了操作系统与硬件之间的解耦

那操作系统究竟管理些什么呢?操作系统主要进行以下四项管理:
- 内存管理:内存分配、内存共享、内存保护以及内存扩张等等。
- 驱动管理:对计算机设备驱动驱动程序的分类、更新、删除等操作。
- 文件管理:文件存储空间的管理、目录管理、文件操作管理以及文件保护等等。
- 进程管理:其工作主要是进程的调度

2.3 用户层
而操作系统再往上就是我们所处的位置,在这里我们就可以用命令行或是图形化界面进行各种操作,这一层被称为用户层

2.4 系统调用接口
但操作系统为了保护自己,对上只暴露了一些接口,而不会让用户直接访问操作系统,这一系列接口被称为系统调用接口

2.5 用户调用接口
但这些系统调用接口对我们普通用户来说使用成本又太高了,因为要使用系统调用前提条件是你得对系统有一定了解。所以在系统调用接口之上又构建出了一批库,例如libc和libc++。实际上在语言级别上使用的各种库,就是封装了系统调用接口的,我们就是通过调用这些库当中的各种函数(例如printf和scanf)进行各种程序的编写

3. 理解(先描述,再组织)
要想学好操作系统,那么就必须正确理解到底什么是管理
这里我们举一个实际的例子来谈谈管理,现在给出三个角色:学生、辅导员和校长。很明显,校长在这三个人当中是管理者,学生是被管理者,那么辅导员充当什么角色呢

仔细想想,实际上完成任何一件事都要经过两个过程,首先是决定要不要做这件事或是如何做这件事(决策),然后就是去做这件事情(执行)。校长作为管理者来管理学生,校长实际上就是那个做决策的人,但是校长作出决策后并不需要自己来执行,而是让辅导员去执行,所以辅导员的主要任务就是执行管理者的决策,我们通常将其称为执行者

虽然说校长是管理学生的,但是我们在学校一般情况下是看不到校长本人的,那么校长是如何做到在不看到我们的情况下对我们进行管理的呢?
举个例子,现在校长要求辅导员将计算机成绩排名前十的学生的各科资料以及平时表现记录拿过来,他将从这十名同学之中选出三名学生参加本次的编程大赛,当辅导员将资料拿来后校长选出三名学生说:“就这三个了,你找个老师对这三名学生进行一下强化训练,然后参加本次的编程大赛。”然后校长就什么也不管了。
在这个过程中,校长根本没有见过这三名同学,就对其进行了管理,他根据的是什么?没错,他根据的就是数据
实际上,学校将我们每个学生的各种信息都进行了管理,基本信息、成绩信息以及健康信息等等

每这么一套信息就描述了一名学生,校长通过对这些信息的管理就能做到对学生的管理,这么一套信息在C语言当中我们称之为抽象结构体,而在C++当中又叫做面向对象
当学生的数量多起来了,校长就可以将全部学生的信息组织起来,当然组织的方式有很多种(链表、顺序表、树),而每种组织方式都有其自己的优势,于是就有了一门课程专门教我们管理数据的方式,那就是数据结构。这里我们假设校长以双链表的形式将学生的信息组织起来

此时校长对各个学生的管理,实际上就变成了对这个双链表的增删查改。当有新生时直接向该双链表加入一个结点,当学生毕业后直接将学生信息从该双链表当中移除即可
总结
- 管理者管理被管理者,实际上是先将被管理者的各种信息进行描述
- 然后再将多个被管理者的描述信息根据某种数据结构组织起来
- 最后管理者管理被管理者实际上就是对数据结构的管理
第六章 进程概念
进程就是一个正在执行的程序,也是担当分配系统资源(CPU时间,内存)的实体
当你的代码进行编译链接后便会生成一个可执行程序,这个可执行程序本质上是一个文件,是放在磁盘上的。当我们双击这个可执行程序将其运行起来时,本质上是将这个程序加载到内存当中了,因为只有加载到内存后,CPU才能对其进行逐行的语句执行,而一旦将这个程序加载到内存后,我们就不应该将这个程序再叫做程序了,严格意义上将应该将其称之为进程

1. PCB进程描述
系统当中可以同时存在大量进程,使用命令ps aux便可以显示系统当中存在的进程

而当你开机的时候启动的第一个程序就是我们的操作系统(即操作系统是第一个加载到内存的),我们都知道操作系统是做管理工作的,而其中就包括了进程管理。而系统内是存在大量进程的,那么操作系统是如何对进程进行管理的呢
这时我们就应该想到管理的六字真言:先描述,再组织。操作系统管理进程也是一样的,操作系统作为管理者是不需要直接和被管理者(进程)直接进行沟通的,当一个进程出现时,操作系统就立马对其进行描述,之后对该进程的管理实际上就是对其描述信息的管理。
进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合,课本上称之为PCB(process control block)
操作系统将每一个进程都进行描述,形成了一个个的进程控制块(PCB),并将这些PCB以双链表的形式组织起来

这样一来,操作系统只要拿到这个双链表的头指针,便可以访问到所有的PCB。此后,操作系统对各个进程的管理就变成了对这条双链表的一系列操作
例如创建一个进程实际上就是先将该进程的代码和数据加载到内存,紧接着操作系统对该进程进行描述形成对应的PCB,并将这个PCB插入到该双链表当中。而退出一个进程实际上就是先将该进程的PCB从该双链表当中删除,然后操作系统再将内存当中属于该进程的代码和数据进行释放或是置为无效
总的来说,操作系统对进程的管理实际上就变成了对该双链表的增、删、查、改等操
2. 进程控制块-task_struct
PCB实际上是对进程控制块的统称,在Linux中描述进程的结构体叫做task_struct,task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含进程的信息
内容分类
- 标示符: 描述本进程的唯一标示符,用来区别其他进程
- 状态: 任务状态,退出代码,退出信号等
- 优先级: 相对于其他进程的优先级
- 程序计数器(pc): 程序中即将被执行的下一条指令的地址
- 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- 上下文数据: 进程执行时处理器的寄存器中的数据
- I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表
- 记账信息: 可能包括处理器时间总和,使用的时钟总和,时间限制,记账号等
- 其他信息
3. 查看进程
3.1 通过系统目录查看
在根目录下有一个名为proc的系统文件夹

文件夹当中包含大量进程信息,其中有些子目录的目录名为数字

这些数字其实是某一进程的PID,对应文件夹当中记录着对应进程的各种信息。我们若想查看PID为1的进程的进程信息,则查看名字为1的文件夹即可

3.2 通过ps命令查看
单独使用ps命令,会显示所有进程信息 ps aux

ps命令与grep命令搭配使用,即可只显示某一进程的信息 ps aux | head -1 && ps 进程名 | grep proc | grep -v grep

3.3 通过系统调用获取进程的PID和PPID
通过使用系统调用函数,getpid和getppid即可分别获取进程的PID和PPID,我们可以通过一段代码来进行测试

我们可以通过ps命令查看该进程的信息,即可发现通过ps命令得到的进程的PID和PPID与使用系统调用函数getpid和getppid所获取的值相同

3.4 fork函数创建子进程获取进程PID和PPID
fork是一个系统调用级别的函数,其功能就是创建一个子进程
例如,运行以下代码:

若是代码当中没有fork函数,我们都知道代码的运行结果就是循环打印该进程的PID和PPID。而加入了fork函数后,代码运行结果如下:

说明一下:
- 运行结果是循环打印两行数据,第一行数据是该进程的PID和PPID,第二行数据是代码中fork函数创建的子进程的PID和PPID
- 我们可以发现fork函数创建的进程的PPID就是proc进程的PID,也就是说proc进程与fork函数创建的进程之间是父子关系
每出现一个进程,操作系统就会为其创建PCB,fork函数创建的进程也不例外

我们知道加载到内存当中的代码和数据是属于父进程的,那么fork函数创建的子进程的代码和数据又从何而来呢?
我们看看以下代码的运行结果:

运行结果:

说明一下:
- 实际上,使用fork函数创建子进程,在fork函数被调用之前的代码被父进程执行
- 而fork函数之后的代码,则默认情况下父子进程都可以执行
- 需要注意的是,父子进程虽然代码共享,但是父子进程的数据各自开辟空间(采用写时拷贝)
- 使用fork函数创建子进程后就有了两个进程,这两个进程被操作系统调度的顺序是不确定的,这取决于操作系统调度算法的具体实现
上面说到,fork函数创建出来的子进程与其父进程共同使用一份代码,但我们如果真的让父子进程做相同的事情,那么创建子进程就没有什么意义了
实际上,在fork之后我们通常使用if语句进行分流,即让父进程和子进程做不同的事
fork函数的返回值
- 如果子进程创建成功,在父进程中返回子进程的PID,而在子进程中返回0
- 如果子进程创建失败,则在父进程中返回 -1
既然父进程和子进程获取到fork函数的返回值不同,那么我们就可以据此来让父子进程执行不同的代码,从而做不同的事


4. Linux进程状态
4.1 运行状态-R
一个进程处于运行状态(running),并不意味着进程一定处于运行当中,运行状态表明一个进程要么在运行中,要么在运行队列里。也就是说,可以同时存在多个R状态的进程
所有处于运行状态,即可被调度的进程,都被放到运行队列当中,当操作系统需要切换进程运行时,就直接在运行队列中选取进程运行
4.2 浅度睡眠状态-S
一个进程处于浅度睡眠状态(sleeping),意味着该进程正在等待某件事情的完成,处于浅度睡眠状态的进程随时可以被唤醒,也可以被杀掉(这里的睡眠有时候也可叫做可中断睡眠(interruptible sleep))
4.3 深度睡眠状态-D
一个进程处于深度睡眠状态(disk sleep),表示该进程不会被杀掉,即便是操作系统也不行,只有该进程自动唤醒才可以恢复。该状态有时候也叫不可中断睡眠状态(uninterruptible sleep),处于这个状态的进程通常会等待IO的结束
例如,某一进程要求对磁盘进行写入操作,那么在磁盘进行写入期间,该进程就处于深度睡眠状态,是不会被杀掉的,因为该进程需要等待磁盘的回复(是否写入成功)以做出相应的应答。(磁盘休眠状态)
4.4 暂停状态-T
在Linux当中,我们可以通过发送SIGSTOP信号使进程进入暂停状态(stopped),发送SIGCONT信号可以让处于暂停状态的进程继续运行
当然调试,跳转到断点的时候也是暂停状态
4.5 僵尸状态-Z
一个进程若是正在等待其退出信息被读取,那么我们称该进程处于僵尸状态
首先,僵尸状态的存在是必要的,因为进程被创建的目的就是完成某项任务,那么当任务完成的时候,调用方是应该知道任务的完成情况的,所以必须存在僵尸状态,使得调用方得知任务的完成情况,以便进行相应的后续操作

- 实际上这个0就是返回给操作系统的,告诉操作系统代码顺利执行结束
可以通过使用echo $?命令获取最近一次进程退出时的退出码

4.6 死亡状态-X
死亡状态只是一个返回状态,当一个进程的退出信息被读取后,该进程所申请的资源就会立即被释放,该进程也就不存在了,所以你不会在任务列表当中看到死亡状态(dead)
4.7 僵尸进程
前面说到,一个进程若是正在等待其退出信息被读取,那么我们称该进程处于僵尸状态。而处于僵尸状态的进程,我们就称之为僵尸进程
例如,对于以下代码,fork函数创建的子进程在打印5次信息后会退出,而父进程会一直打印信息。也就是说,子进程退出了,父进程还在运行,但父进程没有读取子进程的退出信息,那么此时子进程就进入了僵尸状态
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{printf("I am running...\n");pid_t id = fork();if(id == 0){ //childint count = 5;while(count){printf("I am child...PID:%d, PPID:%d, count:%d\n", getpid(), getppid(), count);sleep(1);count--;}printf("child quit...\n");exit(1);}else if(id > 0){ //fatherwhile(1){printf("I am father...PID:%d, PPID:%d\n", getpid(), getppid());sleep(1);}}else{ //fork error}return 0;
}

说明一下:
- 监控脚本:while :; do ps axj | head -1 && ps axj | grep proc | grep -v grep;echo "######################";sleep 1;done
僵尸进程的危害
- 若是一个父进程创建了很多子进程,但都不进行回收,那么就会造成资源浪费,因为数据结构对象本身就要占用内存
- 僵尸进程申请的资源无法进行回收,那么僵尸进程越多,实际可用的资源就越少,也就是说,僵尸进程会导致内存泄漏
4.8 孤儿进程
在Linux当中的进程关系大多数是父子关系,若子进程先退出而父进程没有对子进程的退出信息进行读取,那么我们称该进程为僵尸进程。但若是父进程先退出,那么将来子进程进入僵尸状态时就没有父进程对其进行处理,此时该子进程就称之为孤儿进程
若是一直不处理孤儿进程的退出信息,那么孤儿进程就会一直占用资源,此时就会造成内存泄漏。因此,当出现孤儿进程的时候,孤儿进程会被1号init进程领养,此后当孤儿进程进入僵尸状态时就由int进程进行处理回收
例如,对于以下代码,fork函数创建的子进程会一直打印信息,而父进程在打印5次信息后会退出,此时该子进程就变成了孤儿进程
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{printf("I am running...\n");pid_t id = fork();if(id == 0){ //childint count = 5;while(1){printf("I am child...PID:%d, PPID:%d\n", getpid(), getppid(), count);sleep(1);}}else if(id > 0){ //fatherint count = 5;while(count){printf("I am father...PID:%d, PPID:%d, count:%d\n", getpid(), getppid(), count);sleep(1);count--;}printf("father quit...\n");exit(0);}else{ //fork error}return 0;
}

说明一下:
- 观察代码运行结果,在父进程未退出时,子进程的PPID就是父进程的PID
- 而当父进程退出后,子进程的PPID就变成了1,即子进程被1号进程领养了
5. 进程优先级
获取CPU资源分配的先后顺序,就是指进程的优先权(priority),而之所以有优先级,是因为CPU资源是有限的,一个CPU一次只能跑一个进程,而进程是可以有多个的,所以需要存在进程优先级,来确定进程获取CPU资源的先后顺序
在Linux或者Unix操作系统中,用ps -l命令会类似输出以下几个内容

列出的信息当中有几个重要的信息,如下:
- UID:代表执行者的身份。
- PID:代表这个进程的代号。
- PPID:代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号。
- PRI:代表这个进程可被执行的优先级,其值越小越早被执行。
- NI:代表这个进程的nice值
5.1 PRI与NI的关系
PRI代表进程的优先级(priority),通俗点说就是进程被CPU执行的先后顺序,该值越小进程的优先级别越高
NI代表的是nice值,其表示进程可被执行的优先级的修正数值
他们之间的关系是:PRI(new) = PRI(old) + NI ,NI的取值范围是-20至19,一共40个级别,在Linux操作系统当中,PRI(old)默认为80,即PRI = 80 + NI
当我们创建一个进程后,我们可以使用ps -al命令查看该进程优先级的信息

5.2 更改nice值
使用renice命令,后面跟上更改后的nice值和进程的PID即可


5.3 竞争性 && 独立性 && 并发 && 并行
竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便有了优先级
独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰
并行: 多个进程在多个CPU下分别同时进行运行,这称之为并行
并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发
6. 环境变量
环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数
6.1 常见环境变量
- PATH: 指定命令的搜索路径。
- HOME: 指定用户的主工作目录(即用户登录到Linux系统中的默认所处目录)。
- SHELL: 当前Shell,它的值通常是/bin/bash
6.2 查看环境变量的方法
echo $PATH

6.3 设置程序到PATH中
方式一:将当前程序拷贝到环境变量中:cp XXX /usr/bin

- 缺点:会污染别人写好的指令集
方式二:将当前程序目录设置成环境变量中:export PATH=$PATH:路径


6.4 环境变量相关的命令
echo:显示某个环境变量的值

export:设置一个新的环境变量

env:显示所有的环境变量

6.5 环境变量的组织方式
每个程序都会收到一张环境变量表,环境表是一个字符指针数组,每个指针指向一个以’\0’结尾的环境字符串,最后一个字符指针为空

6.6 获取环境变量
方式一:通过main函数的第三个参数
main函数的第三个参数接收的实际上就是环境变量表,我们可以通过main函数的第三个参数来获取系统的环境变量


通过第三方变量environ来获取


通过系统调用getenv获取环境变量
getenv函数可以根据所给环境变量名,在环境变量表当中进行搜索,并返回一个指向相应值的字符串指针,
例如:使用getenv函数获取环境变量PATH的值


7. 虚拟地址
代码当中用fork函数创建了一个子进程,其中让子进程相将全局变量g_val该从100改为200后打印,而父进程先休眠3秒钟,然后再打印全局变量的值
按道理来说子进程打印的全局变量的值为200,而父进程是在子进程将全局变量改后再打印的全局变量,那么也应该是200,
而上面代码的意思是同一个变量,能存储2个不同的值?这显然是不合理的,父子进程的g_val是两个不同的变量,开辟了2个不同的地址空间,但是打印的地址又是一样的?这显然不合理
所以只有一种可能了,打印的地址不是真的物理空间的地址,如下图

说明一下:
- 虚拟地址和物理地址之间的转化由操作系统完成
8. 进程地址空间-mm_struct
进程地址空间也就是上面那个虚拟地址空间,不是真实的物理内存
每个进程被创建时,其对应的进程控制块(task_struct)和进程地址空间(mm_struct)也会随之被创建。而操作系统可以通过进程的task_struct找到其mm_struct,因为task_struct当中有一个结构体指针存储的是mm_struct的地址
例如,父进程有自己的task_struct和mm_struct,该父进程创建的子进程也有属于其自己的task_struct和mm_struct,父子进程的进程地址空间当中的各个虚拟地址分别通过页表映射到物理内存的某个位置,如下图:

8.1 写时拷贝技术
而当子进程刚刚被创建时,子进程和父进程的数据和代码是共享的,即父子进程的代码和数据通过页表映射到物理内存的同一块空间。只有当父进程或子进程需要修改数据时,才将父进程的数据在内存当中拷贝一份,然后再进行修改
例如,子进程需要将全局变量g_val改为200,那么此时就在内存的某处存储g_val的新值,并且改变子进程当中g_val的虚拟地址通过页表映射后得到的物理地址即可


这种在需要进行数据修改时再进行拷贝的技术,称为写时拷贝技术
为什么数据要进行写时拷贝?
进程具有独立性。多进程运行,需要独享各种资源,多进程运行期间互不干扰,不能让子进程的修改影响到父进程
为什么不在创建子进程的时候就进行数据的拷贝?
子进程不一定会使用父进程的所有数据,并且在子进程不对数据进行写入的情况下,没有必要对数据进行拷贝,我们应该按需分配,在需要修改数据的时候再分配(延时分配),这样可以高效的使用内存空间
代码会不会进行写时拷贝
90%的情况下是不会的,但这并不代表代码不能进行写时拷贝,例如在进行进程替换的时候,则需要进行代码的写时拷贝
8.2 为什么存在进程地址空间
理由一:有了进程地址空间后,就不会有任何系统级别的越界问题存在了,即进程1不会错误的访问到进程2的物理地址空间
理由二:有了进程地址空间后,我们在编写程序的时候就只需关注虚拟地址,而无需关注数据在物理内存当中实际的存储位置
理由三:有了进程地址空间后,每个进程都认为自己在独占内存,这样能更好的完成进程的独立性以及合理使用内存空间(当实际需要使用内存空间的时候再在内存进行开辟),而且还能将进程调度与内存管理进行解耦或分离
8.3 创建进程的简单理解
一个进程的创建实际上伴随着其进程控制块(task_struct)、进程地址空间(mm_struct)以及页表的创建
9. 认识内核进程调度队列
一个CPU拥有一个运行队列,但如果有多个CPU,就要考虑进程个数的父子均衡问题

9.1 队列下标
queue下标说明:普通优先级:100~139,实时优先级:0~99
我们进程的都是普通的优先级,前面说到nice值的取值范围是-20~19,共40个级别,依次对应queue当中普通优先级的下标100~139,而实时优先级对应实时进程,实时进程是指先将一个进程执行完毕再执行下一个进程,现在基本不存在这种机器了,所以对于queue当中下标为0~99的元素我们不关心
9.2 活动队列
时间片还没有结束的所有进程都按照优先级放在活动队列当中

- nr_active代表总共有多少个运行状态的进程
-
bitmap[5]:queue数组当中一共有140个元素,即140个优先级,一共140个进程队列,为了提高查找非空队列的效率,就可以用5× 32个比特位表示队列是否为空,这样一来便可以大大提高查找效率
- queue[140]数组当中的一个元素就是一个进程队列,相同优先级的进程按照FIFO规则(先进先出)进程排队调度
调度过程
- 从0下标开始遍历queue[140]。
- 找到第一个非空队列,该队列必定为优先级最高的队列。
- 拿到选中队列的第一个进程,开始运行,调度完成。
- 接着拿到选中队列的第二个进程进行调度,直到选中进程队列当中的所有进程都被调度。
- 继续向后遍历queue[140],寻找下一个非空队列
9.3 过期队列
过期队列和活动队列的结构相同,但过期队列上放置的进程都是时间片耗尽的进程,当活动队列上的进程被处理完毕之后,需要对过期队列的进程进行时间片重新计算

9.4 active指针&&expired指针
active指针永远指向活动队列,expired指针永远指向过期队列

说明一下:
- 由于活动队列上时间片未到期的进程会越来越少,而过期队列上的进程数量会越来越多
- 那么总会出现活动队列上的全部进程的时间片都到期的情况,这时将active指针和expired指针的内容交换
- 就相当于让过期队列变成活动队列,活动队列变成过期队列,就相当于又具有了一批新的活动进程(一直如此循环)
第七章 进程控制
1. 进程创建
1.1 fork函数
在Linux中,fork函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程

返回值:在子进程中返回0,父进程中返回子进程的PID,子进程创建失败返回-1
但是,fork之后,父进程和子进程谁先执行完全由调度器决定
如果创建子进程失败,原因如下
- 要么是进程太多内存空间不足,
- 要么就是实际用户的进程数超过了限制
1.1 fork函数为什么要给子进程返回0,给父进程返回子进程的PID?
- 一个父进程可以创建多个子进程,而一个子进程只能有一个父进程
- 对于子进程来说,父进程是不需要被标识的;而对于父进程来说,子进程是需要被标识的
- 因为父进程创建子进程的目的是让其执行任务的,父进程只有知道了子进程的PID才能很好的对该子进程指派任务
1.2 为什么fork函数有两个返回值?
父进程调用fork函数后,为了创建子进程,fork函数内部将会进行一系列操作,包括
- 创建子进程的进程控制块(task_struct)
- 创建子进程的进程地址空间(mm_struct)
- 创建子进程对应的页表等等

子进程创建完毕后,操作系统还需要将子进程的进程控制块添加到系统进程列表当中,此时子进程便创建完毕了
简单来说,fork函数内部执行return语句之前,子进程就已经创建完毕,则return后的语句父子进程都需要运行
2. 进程退出
进程退出只有三种情况:a. 代码运行完毕,结果正确 b. 代码运行完毕,结果不正确 c. 代码异常终止(进程崩溃)
2.1 退出码
main函数只是用户级别代码的入口,main函数也是被__tmainCRTStartup函数调用的(在VS2013),而__tmainCRTStartup函数又是通过加载器被操作系统所调用的,也就是说main函数是间接性被操作系统所调用的
当我们的代码运行起来就变成了进程,当进程结束后main函数的返回值实际上就是该进程的进程退出码
查看最近一次进程退出的退出码信息的指令:echo $?


2.2 strerror函数

说明一下:
- 0表示代码执行成功,以非0表示代码执行错误,而使用strerror函数就可以查看对应的错误信息
- 退出码都有对应的字符串含义,帮助用户确认执行失败的原因,
- 而这些退出码具体代表什么含义是人为规定的,不同环境下相同的退出码的字符串含义可能不同
2.3 return退出
main函数中使用return退出进程是我们常用的方法


2.4 exit退出
exit函数可以在代码中的任何地方退出进程
并且exit函数在退出进程前会做一系列工作:清理函数、冲刷缓冲,关闭流等操作,调用_exit函数终止进程


说明一下:
- exit终止进程前会将缓冲区当中的数据输出
2.5 _exit函数
_exit函数会直接终止进程,并不会在退出进程前会做任何收尾工作,实际中使用较少


说明一下:
- _exit终止进程前不会将缓冲区当中的数据输出
2.6 三者之间的区别
- 在main函数当中的return才能起到退出进程的作用,子函数当中return不能退出进程
- exit函数和_exit函数在代码中的任何地方使用都可以起到退出进程的作用

使用exit函数退出进程前,exit函数会执行用户定义的清理函数、冲刷缓冲,关闭流等操作,然后再调用_exit函数终止进程,
而_exit函数会直接终止进程,不会做任何收尾工作
2.7 三者之间的联系
执行return num等同于执行exit(num),因为调用main函数运行结束后,会将main函数的返回值当做exit的参数来调用exit函数

2.8 进程异常退出
情况一:向进程发生信号导致进程异常退出,比如发生kill -9信号
情况二:代码错误导致进程运行时异常退出
3. 进程等待
3.1 为什么需要进程等待
对于一个进程来说,最关心自己的就是其父进程,因为父进程需要知道自己派给子进程的任务完成的如何,就需要通过进程等待的方式,回收子进程资源,获取子进程的退出信息
子进程退出,父进程如果不读取子进程的退出信息,子进程就会变成僵尸进程,进而造成内存泄漏,且进程一旦变成僵尸进程,那么就算是kill -9命令也无法将其杀死,因为谁也无法杀死一个已经死去的进程
3.2 wait函数

说明一下:
- 函数原型:
pid_t wait(int* status); - 作用:等待任意子进程
- 返回值:等待成功返回被等待进程的pid,等待失败返回-1。
- status参数:输出型参数,获取子进程的退出状态,不关心可设置为NULL
例如,创建子进程后,父进程可使用wait函数一直等待子进程,直到子进程退出后读取子进程的退出信息
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{pid_t id = fork();if(id == 0){//childint count = 10;while(count--){printf("I am child...PID:%d, PPID:%d\n", getpid(), getppid());sleep(1);}exit(0);}//fatherint status = 0;pid_t ret = wait(&status);if(ret > 0){//wait successprintf("wait child success...\n");if(WIFEXITED(status)){//exit normalprintf("exit code:%d\n", WEXITSTATUS(status));}}sleep(3);return 0;
}

说明一下:
- 监控脚本:
while :; do ps axj | head -1 && ps axj | grep 进程名 | grep -v grep;echo "######################";sleep 1;done - 子进程退出后,父进程读取了子进程的退出信息,子进程也就不会变成僵尸进程了
3.3 waitpid函数

说明一下:
- 函数原型:
pid_t waitpid(pid_t pid, int* status, int options); - 作用:等待指定子进程或任意子进程
- 返回值:
- 等待成功返回被等待进程的pid
- 如果第三个参数传的是WNOHANG宏,而调用中waitpid发现没有已退出的子进程可收集(即子进程没有退出),则返回0
- 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在
- 参数:
- pid:待等待子进程的pid,若设置为-1,则等待任意子进程
- status:输出型参数,获取子进程的退出状态,不关心可设置为NULL
- options:这个参数默认为0,当设置为
WNOHANG时,若等待的子进程没有结束,则waitpid函数直接返回0,不予以等待。若正常结束,则返回该子进程的pid
例如,创建子进程后,父进程可使用waitpid函数一直等待子进程(此时将waitpid的第三个参数设置为0),直到子进程退出后读取子进程的退出信息
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{pid_t id = fork();if (id == 0){//child int count = 10;while (count--){printf("I am child...PID:%d, PPID:%d\n", getpid(), getppid());sleep(1);}exit(0);}//father int status = 0;pid_t ret = waitpid(id, &status, 0);if (ret >= 0){//wait success printf("wait child success...\n");if (WIFEXITED(status)){//exit normal printf("exit code:%d\n", WEXITSTATUS(status));}else{//signal killed printf("killed by siganl %d\n", status & 0x7F);}}sleep(3);return 0;
}
在父进程运行过程中,我们可以尝试使用kill -9命令将子进程杀死,这时父进程也能等待子进程成功

注意: 被信号杀死而退出的进程,其退出码将没有意义
3.4 status输出型参数
如果对status参数传入NULL,表示不关心子进程的退出状态信息。否则,操作系统会通过该参数,将子进程的退出信息反馈给父进程
status是一个整型变量,但status不能简单的当作整型来看待,status的不同比特位所代表的信息不同,具体细节如下(只研究status低16比特位)

在status的低16比特位当中,高8位表示进程的退出状态,即退出码。进程若是被信号所杀,则低7位表示终止信号,而第8位比特位是core dump标志
exitCode = (status >> 8) & 0xFF; //退出码
exitSignal = status & 0x7F; //退出信号
对于此,系统当中提供了两个宏来获取退出码和退出信号:
- WIFEXITED(status):用于查看进程是否是正常退出,本质是检查是否收到信号
- WEXITSTATUS(status):用于获取进程的退出码,wifexited,wexitstatus
需要注意的是,当一个进程非正常退出时,说明该进程是被信号所杀,那么该进程的退出码也就没有意义了
3.5 基于非阻塞接口的轮询检测方案
上述所给例子中,当子进程未退出时,父进程都在一直等待子进程退出,在等待期间,父进程不能做任何事情,这种等待叫做阻塞等待
实际上我们可以让父进程不要一直等待子进程退出,而是当子进程未退出时父进程可以做一些自己的事情,当子进程退出时再读取子进程的退出信息,即非阻塞等待
做法很简单,向waitpid函数的第三个参数potions传入WNOHANG,这样一来,等待的子进程若是没有结束,那么waitpid函数将直接返回0,不予以等待。而等待的子进程若是正常结束,则返回该子进程的pid
例如,父进程可以隔一段时间调用一次waitpid函数,若是等待的子进程尚未退出,则父进程可以先去做一些其他事,过一段时间再调用waitpid函数读取子进程的退出信息
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{pid_t id = fork();if (id == 0){//childint count = 3;while (count--){printf("child do something...PID:%d, PPID:%d\n", getpid(), getppid());sleep(3);}exit(0);}//fatherwhile (1){int status = 0;pid_t ret = waitpid(id, &status, WNOHANG);if (ret > 0){printf("wait child success...\n");printf("exit code:%d\n", WEXITSTATUS(status));break;}else if (ret == 0){printf("father do other things...\n");sleep(1);}else{printf("waitpid error...\n");break;}}return 0;
}

说明一下:
- 运行结果就是,父进程每隔一段时间就去查看子进程是否退出,若未退出,则父进程先去忙自己的事情,过一段时间再来查看,直到子进程退出后读取子进程的退出信息
4. 进程替换
用fork创建子进程后,子进程执行的是和父进程相同的程序(但有可能执行不同的代码分支),若想让子进程执行另一个程序,往往需要调用一种exec函数
当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,并从新程序的启动例程开始执行

当进行进程程序替换时,有没有创建新的进程?
进程替换并没有创建一个新进程,它的PCB、进程地址空间以及页表等数据结构都没有发生改变,只是进程在物理内存当中的代码和数据发生了改变,且进程程序替换前后该进程的pid并没有改变
子进程进行进程程序替换后,会影响父进程的代码和数据吗?
- 子进程刚被创建时,与父进程共享代码和数据,
- 但当子进程需要进行进程程序替换时,也就意味着子进程需要对其数据和代码进行写入操作,
- 这时便需要将父子进程共享的代码和数据进行写时拷贝,
- 此后父子进程的代码和数据也就分离了,因此子进程进行程序替换后不会影响父进程的代码和数据
4.1 execl函数
int execl(const char *path, const char *arg, ...);
- 第一个参数是要执行程序的路径,第二个参数是可变参数列表,表示你要如何执行这个程序,并以NULL结尾
- execl("/usr/bin/ls", "ls", "-a", "-i", "-l", NULL);
4.2 execlp函数
int execlp(const char *file, const char *arg, ...);
-
第一个参数是要执行程序的名字,第二个参数是可变参数列表,表示你要如何执行这个程序,并以NULL结尾
-
execlp("ls", "ls", "-a", "-i", "-l", NULL);
4.3 execle函数
int execle(const char *path, const char *arg, ..., char *const envp[]);
- 第一个参数是要执行程序的路径,第二个参数是可变参数列表,表示你要如何执行这个程序,并以NULL结尾,第三个参数是你自己设置的环境变量
- char* myenvp[] = { "MYVAL=2021", NULL };
execle("./mycmd", "mycmd", NULL, myenvp);
4.4 execv函数
int execv(const char *path, char *const argv[]);
- 第一个参数是要执行程序的路径,第二个参数是一个指针数组,数组当中的内容表示你要如何执行这个程序,数组以NULL结尾
- char* myargv[] = { "ls", "-a", "-i", "-l", NULL };
execv("/usr/bin/ls", myargv);
4.5 execvp函数
int execvp(const char *file, char *const argv[]);
- 第一个参数是要执行程序的名字,第二个参数是一个指针数组,数组当中的内容表示你要如何执行这个程序,数组以NULL结尾
- char* myargv[] = { "ls", "-a", "-i", "-l", NULL };
execvp("ls", myargv);
4.6 execve函数
int execve(const char *path, char *const argv[], char *const envp[]);
- 第一个参数是要执行程序的路径,第二个参数是一个指针数组,数组当中的内容表示你要如何执行这个程序,数组以NULL结尾,第三个参数是你自己设置的环境变量
- char* myargv[] = { "mycmd", NULL };
char* myenvp[] = { "MYVAL=2021", NULL };
execve("./mycmd", myargv, myenvp);





