完整内容可以下载上面的文件,无任何广告
目录
- ls:
- 对于目录,该命令会列出该目录下的子目录与文件,对于文件,将列出文件名及其他信息
- 常用选项:
- -a:列出目录下的所有文件,包括以.开头的隐藏文件
- -l:列出文件的详细信息,可省略成ll
- -R:列出所有子目录下的文件(递归),类似tree
- pwd:显示用户当前所在的目录
- cd:改变工作目录。将当前工作目录改变到指定的目录下
- 可使用绝对路径/相对路径/..(上级目录)/~(家目录)/-(最近访问的目录)
- touch:更改文档或目录的存取时间和更改时间,或创建一个不存在的文件
- mkdir:在当前目录下创建一个目录
- -p:mkdir -p test/test1/test 递归建立目录
- rmdir&&rm
- rmdir:删除空目录
- rm:删除文件或目录
- -f:强制删除
- -r:删除目录及其下面的所有文件
- man:访问Linux手册,其中
- 1是普通命令,
- 2是系统调用(如open,write)
- 3是库函数(如printf,fread)
- 4是特殊文件,也就是/dev下的各种设备文件
- 5是指文件的格式(如passwd,就会说明这个文件中各个字段的含义)
- 6是给游戏用的,由各个游戏自己定义
- 7是附件还有一些变量(如environ这种全局变量)
- 8是系统管理用的命令,这些命令只能由root使用,如ifconfig
- cp:复制文件或目录 cp [选项] 源文件或目录 目标文件或目录
- 将前者复制到后者,若存在两个以上,则将前面所有的目标复制到最后一个
- -f:强制复制文件或目录,不管目标文件或目录是否已经存在
- -r/-R:递归处理
- mv:移动文件或将文件改名 mv [选项] 源文件或目录 目标文件或目录
- 当第二个参数是文件时,第一个参数只能有一个,此时功能是改名
- 当第二个参数是已存在的目录名时,第一个参数可以有多个,此时为移动
- cat:查看文件的内容 cat [选项] [文件名]
- -n:对输出的所有行编号
- -s:不能输出多行空行
- -b:对非空输出行编号
- more:查看文件内容 more [选项] [文件名] (不如用less)
- less:less与more类似,但是less可以随意浏览文件,而more仅能向前移动,不能向后移动,而且less在查看之前不会加载整个文件 less 文件
- less是一个工具,自带搜索功能,下面是他的选项
- -i:忽略搜索时的大小写
- -N:显示行号
- /字符串:向下搜索“字符串”的功能
- ?字符串:向上搜索“字符串”的功能
- n:查看前一个搜索
- N:查看下一个搜索
- q:quit
- head:显示档案的开头至标准输出中,默认前10行
- tail:显示档案的结尾至标准输出中,默认后10行
- -f:会将其最尾部的内容显示在屏幕上,并且不断刷新
- date:指定格式显示时间 date +%Y:%m:%d:%H:%M:%S
- %Y:年
- %m:月
- %d:日
- %F:相当于%Y-%m-%d
- %H:时
- %M:分
- %S:秒
- %X:相当于%H:%M:%S
- %s:时间戳,1970年一月一日午夜0:00开始所经过的秒数
- cal:日历
- find:用于在文件树中查找文件,并作出相应的处理(可能访问磁盘)
- -name:find 目录 -name 文件 在指定目录下搜索指定文件
- -iname:用法同上,不区分大小写
- -type:find 目录 -type 文件类型(f,d) 按文件类型搜索
- -size:find 目录 -size 文件大小(+1M表示大于1M,-1M表示小于1M) 按文件大小搜索
- -user:按文件所有者搜索
- -group:按文件所属组搜索
- -mtime/-atime/ctime:按文件内容修改时间/文件访问时间/文件状态改变时间搜索(以天为单位)
- -exce:对搜索结果执行指定的命令
- 举例find /home -name ‘*.txt’ -exce cat {} \;
- //对每个txt文件执行cat命令
- {}是一个占位符,代表当前找到的文件名,\;表示-exce参数的结束
- -ok:与exce类似,但在执行命令之前会提示用户确认
- -print:打印文件名,默认选项,可省略
- -maxdepth:限制搜索的最大目录深度
- find /home -maxdepth 1 -name *.txt
- 最大目录深度设置成1,即只在/home目录下搜索,不包括子目录
- -mindepth:限制搜索的最小目录深度
- grep:在文件中搜索字符串,将找到的行打印出来
- grep [选项] 搜寻字符串 文件
- -i:忽略大小写的不同
- -n:输出行号
- -v:反向选择,即显示出没有“搜寻字符串”的那一行
- zip/unzip:将文件或目录压缩/解压
- zip 压缩后的文件名(一般后缀为.zip) 要压缩的文件或目录
- unzip 压缩文件 -d 指定目录
- -r:递归处理,将指定目录下的所有文件和目录一并处理
- tar:打包/解包,不打开文件,直接看内容
- tar [-cxtzjvf] 文件与目录 (-C 指定目录(可解压到指定目录))
- tar -czf 压缩后的名称.tgz 源文件名
- tar -xzf 目标文件 (-C 指定目录)
- -c:建立一个压缩文件的参数指令
- -x:解开一个压缩文件的参数指令
- -t:查看tarfile里的文件
- -z:表示使用gzip压缩
- -g:表示使用bzip2压缩
- -v:在压缩的过程中显示文件
- -f:使用档名,在f后要立即跟要压缩的文件名
- -C:解压到指定文件
- bc:计算器,可以很方便的进行浮点数运算
- uname:获取电脑和OS的相关信息 uname -r 获取更详细的信息
- 其他命令
- wc:计算字数
- which:which 命令 查看命令文件所处的位置
- whereis:whereis 名字 查看命令,文件,或归档文件所处的位置
- chmod:设置文件的访问权限。详细见Linux权限中的文件访问权限的设置
- chown:修改文件的拥有者
- chgrp:改变文件或目录所处的用户组
- umask:查看或修改文件掩码
- file:显示文件的类型
- su/sudo:su切换成root用户,su 用户名 切换到指定用户,sudo 命令提权
- stat:显示文件的状态信息,比ls详细
- useradd: 添加用户
- userdel:删除用户账号
- passwd:更新用户密码
- whoami:查看当前用户名
- clear:清屏
- tree:树状显示目录
- echo:在终端输出一行文本或者变量的值
- nano:简单的文本编辑器
- sort:sort [选项] 文件名 默认升序排序,不会改变文件原有的内容,-r降序
- uniq:去重,只能去除相邻的重复,且必须完全一样
- gcc/g++:编译工具
- top:Linux下的任务管理器
- alias:alias 名称=’指令名’ 起别名
- history:显示历史命令
- mkfifo:创建一个命名管道
- netstat:一个用于查看网络状态的重要工具
- n:拒绝显示别名,即显示端口号
- l:仅列出有在listen(监听)的服务状态
- p:显示建立相关链接的程序名(显示PID)
- t:只显示tcp相关
- u:只显示udp相关
- a:显示所有选项,一般默认不显示listen相关
- pidof:后面加进程名显示进程id
- 命令行的重定向:
- 标准输出重定向: > 例如:ls > file.txt
- 追加重定向: >> 例如:ls >> file.txt
- 标准输入重定向: < 例如:sort < file.txt
- 标准错误重定向: 2> 例如:command 2> file.txt
root用户和普通用户,使用su命令和sudo命令进行切换文件
- 访问者的分类
- 文件和目录的拥有者 user——u
- 所属组 group——g
- 其他 other——o
- 文件类型和访问权限
-
- 文件类型
- d:目录文件
- -:普通文件
- l:软连接(类似于Windows的快捷方式)
- b:块设备(例如硬盘,光驱)
- p:管道文件
- c:字符设备文件(丽日屏幕等串口设备)
- s:套接口文件
- 基本权限
- r:对文件来说,r是读权限,对目录来说,r是查看该目录的信息的权限
- w:对文件来说,是写权限,对目录来说,是删除或移动目录内文件的权限
- x:对文件来说,是执行权限,对目录来说,是进入目录的权限
- 注意,这些权限都是对普通用户而言的,root用户不受权限的约束
- 文件类型
- 文件访问权限的设置
- chmod设置文件的访问权限
- chmod 用户类型(u/g/o/a)+权限(r/w/x) 文件名
- chmod 三位8进制数字 文件名
- chown设置文件的拥有者
- chown [参数](-R递归) 用户名 文件名
- chgrp设置文件的所属组
- chgrp [参数](-R递归) 所属组名 文件名
- umask查看或修改文件掩码
- linux下新建文件的默认权限为0666,新建目录的默认权限为0777
- linux下创建文件的实际权限=默认权限&(~umask)即从默认权限中删去umask对应的权限
- file辨识文件类型
- file [选项] 文件或目录
- -c:详细显示指令执行过程,便于排错或分析程序执行的过程
- -z:尝试解读压缩文件的内容
- su/sudo:
- su:su切换成root用户,su 用户名 切换到指定用户
- sudo:sudo 命令 给后面的命令提权
- 注意,想要使用sudo命令,必须满足两个条件:
- 当前用户必须在/etc/sudoers文件中被授权使用sudo命令,这是通过将该用户添加到sudoers文件中完成的
- 当前用户必须知道自己的密码(如果sudo配置要求输入密码的话)
- chmod设置文件的访问权限
- 粘滞位
- 根据目录中rwx的意义,可以知道,用户只要拥有该目录的写权限,那么便可以删除该目录下不是自己的文件,为解决这个问题,linux添加了粘滞位
- chmod +t 文件名
- 当一个文件或目录被添加粘滞位时,只有三种用户可删除该文件
- root用户
- 该目录的拥有者
- 该文件的拥有者
- 总结
- 目录的可执行权限表示你能否进入该目录,进入目录后便可以在该目录下执行命令,
- 如果目录没有-x权限,则无法对该目录执行任何命令,甚至无法通过cd进入该目录,即使你有-r权限
- 如果有-x权限,没有-r权限,那么用户可以通过cd进入该目录,但由于没有-r权限,所以无法通过ls命令查看目录下的文件
- 权限都是对普通用户而言的,root用户不受权限的约束
- yum
- yum的所有操作必须保证主机网络通畅,可以通过ping指令验证,如ping www.baidu.com
- 通过yum list命令可以罗列出当前一共有哪些软件包,再通过管道过滤出自己想要的即可
- 通过yum install 命令可以安装软件包,一般需要sudo提权
- 通过yum remove命令删除软件,一般需要sudo提权
- vim
- 命令模式:无论当前vim在哪个模式下,都可以通过esc进入命令模式
- 插入模式:在命令模式下,通过i键可进入插入模式进行文本编辑(a和o也可以)
- 末行模式:在命令模式下,输入shift+;(即:)可进入末行模式
- 命令模式下:
- 移动光标
- 上下左右:h左,j下,k上,l右
- G(shift+g):跳转至文章最后
- gg:跳转至文章开头
- n G(n shift+g):跳转至指定行(第n行)
- $(shift+4):跳转至光标所在行的结尾
- ^(shift+6):跳转至光标所在行的开头
- n b:向前移动n个单词
- n w:向后移动n个单词
- 删除文字
- n x:行内删除,从左至右
- n X:行内删除,从右至左
- n dd:删除n行(本质是剪切,如果不复制那么就是删除)
- 复制
- n yy:复制n行
- n p:粘贴n次
- 替换
- n r:替换n个光标之后的字符
- 撤销
- u:撤销上一步的操作
- ctrl+r:对撤销进行撤销,即撤销的恢复
- 更改
- shift+ ` (~):快速进行大小写切换
- 移动光标
- 末行模式下:
- set nu/nonu:添加行号,删除行号
- %s/string1/string2/g:string1是要替换的内容,string2是替换后的内容
- /key:key是要查找的内容
- !command:在不离开vim的情况下执行命令,command指的是要执行的命令
- vs file:分屏打开多个文件,file指的是要打开的文件的名字
- 在vim模式下,光标在那个文件,我们就在编写那个文件,
- ctrl+ww:分屏打开多个文件时,该命令可切换光标所在的文件
- w保存,q退出,!表示强制,!要跟在wq的后面
- vim配置
- 背景知识:
- 预处理:头文件展开,条件编译,去注释,宏替换等 -E,后缀为.i
- 编译:将源代码转换成汇编代码 -S,后缀为.s
- 汇编:将汇编代码转换成二进制代码 -c,后缀为.o
- 链接:将多个目标文件(以及可能需要的库文件)组合成一个可执行文件或共享库文件 使用gcc/g++默认生成该文件,不需要添加参数,一般无后缀
- 动态库与静态库
- 静态库:在编译链接(静态链接)时,将库文件的代码全部加入到可执行文件中,因此生成的文件较大,但是运行时不需要库文件,后缀为.a或.lib
- 动态库:在编译链接(动态链接)时,没有将库文件的代码全部加入到可执行文件中,而是在程序运行时加载动态库这样可以节省OS的开销,后缀为.so
- gcc [选项] 要编译的文件 [选项] [目标文件]
- 选项:
- -E/-S/-c/无:预处理(.i)/编译(.s)/汇编(.o)/链接
- -g:在生成的可执行文件中包含调试信息,即debug版本,不添加-g则默认为release版本,无法使用gdb进行调试
- -o 文件名:指定生成的文件名
- 优化选项:
- -O0:默认选项,不进行优化
- -O1:进行基本优化
- -O2:进行更多的优化,但不增加编译时间
- -O3:进行所有可用的优化,可能增加编译时间
- -Os:优化生成代码的大小
- 背景
想要使用gdb进行调试,必须在使用gcc/g++是携带-g选项
- 命令
- 退出:ctrl+d或者quit命令
- 设置断点
- break(b) 函数名:在该函数开头设置断点
- break(b) 行号:在该行设置断点
- disable breakpoints: 禁用断点
- enable breakpoints:开启断点
- 查看断点信息
- info break(i b):查看断点信息
- 删除断点
- delete breakpoints:删除所有断点
- delete breakpoints n:删除序号为n的断点
- 运行和调试
- list(l) 行号:显示10行代码
- list(l) 函数:显示该函数
- run(r):运行程序
- next(n):执行下一行代码,不会进入函数内部(F10,逐过程)
- step(s):执行下一行代码,可以进入函数内部(F11,逐语句)
- continue(c):继续执行程序,直到下一个断点或函数结束(F5)
- finish:执行完当前函数并返回到调用它的函数
- 查看和修改变量
- print n:查看变量n的值
- set n=value:设置变量n的值为value
- display n:跟踪查看变量n的值,每次停下来都会显示他的值
- undisplay:取消对前面设置的变量的跟踪
- info locals:查看当前栈帧局部变量的值
- 其他
- breaktrace(bt):显示当前执行的调用栈信息,即查看各级函数调用及参数
- linux项目自动化构建工具
- 具体实现
testvim:testvim.o
gcc -o testvim testvim.o
testvim.o:testvim.s
gcc -c -o testvim.o testvim.s
testvim.s:testvim.i
gcc -S -o testvim.s testvim.i
testvim.i:testvim.c
gcc -E -o testvim.i testvim.c
.PHONY:clean
clean:
rm -f testvim testvim.o testvim.s testvim.i
- 安装git:yum install git
- 下载项目到本地:git clone [项目链接]
- git add 文件名:将需要用git管理的文件告知git
- git commit -m “string”:string中写好日志,描述好修改的地方
- git push:同步到远端服务器上
-
- 输入设备:键盘,网卡,摄像头,磁盘,话筒等
- 输出设备:显示器,网卡,磁盘等
- 因为有了内存(存储器)的存在,我们可以对数据进行预加载,cpu在进行数据计算的时候,就不需要问外设要数据了,而是通过内存获取数据,
- 操作系统
- 操作系统是管理计算机硬件与软件资源的计算机程序(软件)
- 管理的本质:先描述再组织
- 进程概念
- 计算机中一个程序关于某数据集合上的一次运动活动(是分配系统资源的实体),是系统进行资源分配和调度的基本单位,是操作系统结构的基础。
- 进程信息被放在一个名为进程控制块的数据结构中,这个数据结构统称PCB,Linux下的PCB是task_struct,他会被装载到RAM(内存)里并且包含着进程的信息。
- task_struct的内容分类
- 标识符:描述本进程的唯一标识符,用于区分不同的进程
- 状态:任务状态,退出代码,退出信号等
- 优先级:相对于其他进程的优先级
- 程序计数器:程序中即将被执行的下一条指令的地址
- 内存指针:包括程序代码和进程相关数据的指针,和其他进程共享的内存块的地址
- 上下文数据:进程执行时处理器的寄存器中的数据
- IO状态信息:包括显示的IO请求,分配给进程的IO设备,被进程使用的文件列表
- 记账信息:可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号
- 其他
- 查看进程:
- 进程的信息可以通过/proc系统目录查看
- top命令
- ps命令 ps -axj
- 进程状态
- R运行状态:并不意味着进程一定在运行中,他表明进程要么在运行中,要么在运行队列中
- S睡眠状态:意味着进程在等待事件的完成(可中断睡眠状态)
- D磁盘休眠状态:在这个状态的进程通常会等待IO的结束(不可中断睡眠状态)
- T停止状态:可通过发送SIGSTOP信号来停止进程,通过SIGCONT继续运行
- X死亡状态:这个状态只是一个返回状态,无法再任务列表中看到这个状态
- 僵尸进程与孤儿进程
- 僵尸进程(Z)
- 当进程退出且父进程没有读取到子进程退出的返回代码时就会产生僵尸状态,僵尸进程会以终止状态保持在进程表中,并且会在一直等待父进程读取退出状态代码,所以只要子进程退出,父进程还在,父进程没有读取子进程,哪么子进程便会一直处于僵尸状态
- 危害:
- 父进程一直不读取,哪么子进程便会一直处于僵尸状态
- 僵尸状态不退出,哪么PCB就要一直维护
- 会造成内存泄漏
- 孤儿进程
- 父进程先退出,子进程后退出,哪么子进程便会被1号init进程领养并由其回收,此时子进程称为孤儿进程
- 僵尸进程(Z)
- 进程优先级
- 进程信息
- UID:代表执行者的身份
- PID:代表这个进程的代号
- PPID:代表这个进程的父进程的代号
- PRI:代表这个进程可被执行的优先级
- NI:代表这个进程的nice值
- PRI与NI
- 进程的实际优先级=进程的PRI+nice
- nice的取值为-20到19
- 修改进程优先级
- 进入top后按“r”--->输入进程PID--->输入nice值
- 其他概念
- 竞争性:系统进程数目多,cpu少,所以进程之间有竞争性,产生了优先级
- 独立性:多进程运行,需要独享各种资源,互不干扰
- 并行:多进程在多个cpu下同时运行
- 并发:多个进程在一个cpu下通过进程切换,让多个进程都得以推进
- 进程信息
- 环境变量
- 概念
- 一般是指在操作系统中用来指定操作系统运行环境的一些参数
- 环境变量在操作系统中通常具有全局性
- 常见环境变量
- PATH:指定命令的搜索路径
- HOME:指定用户的主工作目录
- SHELL:指当前shell,他的值通常是/bin/bash
- 和环境变量相关的命令
- echo $name:显示某个环境变量值
- export A=B:设置一个新的环境变量,export可以将本地变两天添加到环境变量
- export A=A:B:在原有A的基础上追加B
- env:显示所有的环境变量
- unset:清除环境变量
- set:显示本地定义的shell变量和环境变量
- 环境变量的组织方式
- 概念
每个程序都会收到一张环境变量表,环境变量表是一个字符指针数组
-
- 获取环境变量
- 通过命令行第三个参数envp
- 命令行的三个参数:int main(int argc, char*argv[], char* envp[])
- 通过第三方变量environ获取
- environ是一个全局的char**类型的变量,存储着系统的环境变量
- 获取方式:extern char** environ;
- 通过系统调用获取或设置环境变量getenv
- getenv(“PATH”);
- 通过命令行第三个参数envp
- 云服务器/linux/test12
- gitee:
- 获取环境变量
HTTP://gitee.com/qq--1444354226/linux/blob/master/test12/EnvironmentVariable.c
- 命令行参数
- 命令行的三个参数:int main(int argc, char*argv[], char* envp[])
- 第一个参数表示第二个参数数组的长度,第二个参数是一个指针数组,里面存储着你输入的命令及其参数,(举例:如果你在运行该程序是使用了./mytest -a -b -c,哪么argc=4,argv[0]=”./mytest”,argv[1]=”-1”,argv[2]=”-b”,argv[3]=”-c”)
- 命令行参数的作用:执行命令的选项就以字符串的形式通过命令行参数传递给程序,对应程序内部对选项做判断,让相同的指令携带不同的选项从而执行不同的结果
- 程序地址空间与进程地址空间
- 程序地址空间
-
- 进程地址空间
- 程序地址空间
-
- 为什么要有进程地址空间(进程地址空间的作用是什么)
- 防止地址随意访问,保护物理内存与其他进程
- 将进程管理和内存管理进行解耦合(将页表一分为二,左边是进程管理,右边是内存管理)
- 可以让进程以统一的视角,看待自己的代码和数据
- malloc的本质
- 为什么要有进程地址空间(进程地址空间的作用是什么)
malloc的本质是一个动态内存分配函数,在Linux系统中,malloc的底层实现依赖于虚拟内存和物理内存之间的映射关系,当程序第一次访问虚拟空间时,如果页表查找失败,哪么就会产生缺页中断,然后系统会进行物理内存的分配,并建立虚拟内存地址和物理内存地址的映射关系(页表项)
- 进程创建
- fork的作用:从已存在的进程(父进程)中创建一个新进程(子进程)
- fork的用法:
- 一个父进程希望复制自己,使父子进程同时执行不同的代码段,如并行
- 头文件:#include<unistd.h>
- 函数声明:pid_t fork(void)
- 返回值:子进程返回0,父进程返回子进程的pid,出错返回-1
- fork之前父进程独立进行,fork之后,代码共享,通常通过if分流,父子两个执行流分别进行,谁先执行由调度器决定,
- 命令行启动的所有程序,都会变成进程,这些进程的父进程是bash
- 虚拟地址与写时拷贝
- 现象:fork之后在子进程修改fork之前创建的变量,分别在父进程和子进程中取该变量的地址,发现地址相同,内容不同,说明&并不是在取其在物理内存中的实际地址,而是一个虚拟地址
- 原理:fork被调用之后,内核并不会复制父进程的整个地址空间,而是让子进程和父进程共享相同的物理页面,这些页面是只读的。当父进程或子进程试图写入一个共享的页面时,操作系统会为该页面分配一个新的物理页面(谁先修改,谁就会发生写时拷贝),并将原来的内容拷贝上去,然后修改操作会在新页面上进行,原页面保持不变。
-
- fork失败的原因:
- 系统中进程过多
- 实际用户的进程超过限制
- fork失败的原因:
- 进程终止
- 三种退出场景:
- 运行完毕,结果正确
- 运行完毕,结果不正确
- 运行异常,程序终止
- 查看进程退出码:echo $?
- 进程常见退出方法
- _exit正常退出(系统调用)
- 头文件:#include<unistd.h>
- 函数声明:void _exit(int status)
- status定义了进程的终止状态,0或EXIT_SUCCESS表示成功,EXIT _FAILURT表示失败,status仅有低8位可以被父进程使用,调用_exit(-1)时,父进程捕获的值为255。父进程可以通过wait获取该值。
- exit正常退出(C语言库函数)
- 头文件:#include<unistd.h>
- 函数声明:void exit(int status)
- exit最后也会调用_exit,但是在调用_exit之前,首先会执行用户通过atexit和on_exit定义的清理函数,其次会关闭所有的流,冲刷缓冲区
- main函数返回,正常退出
- _exit正常退出(系统调用)
- 三种退出场景:
执行return n相当于执行exit(n)
-
-
- ctrl+c,信号终止
-
给进程发送SIGINT(2号信号),使其终止
- 进程等待
- 用于回收僵尸状态,处于僵尸状态的进程已经死亡,即使使用kill -9命令也无法控制,如果父进程想要知道子进程的执行结果,则必须将其回收
- 目的
- 避免内存泄漏
- 获取子进程执行的结果
- wait(系统调用)
- 头文件:#include<sys/types.h>
#include<sys/wait.h>
-
-
- 函数声明:pid_t wait(int* status)
- 成功返回被等待进程的pid,失败返回-1,status是一个输出型参数,能够获取子进程的退出状态,不关心可设置成NULL
- waitpid(既是C语言库函数,又是系统调用)
- 头文件:#Include<sys/types.h>
-
#include<sys/wait.h>
-
-
- 函数声明:pid_t waitpid(pid_t pid,int* status,int options)
- 参数:
- pid:pid=-1时,等待任意一个子进程,相当于wait,pid>0等待pid为pid的子进程
- status:输出型参数,是一个位图,我们只关心status的低16位,
-
-
-
-
-
- 进程退出码:获取方式(status>>8)&0xFF
- 进程退出信号:获取方式status&0x7F
- cure dump(核心转储):该信息与调试有关
- WIFEXITED(status):若为正常终止子进程返回的状态,则为真,用于查看进程是否正常退出
- WEXITSTATUS(status):若进程正常退出(WIFEXITED!=0),提取子进程退出码
- options:这个参数用于控制waitpid的行为,可以为0,或由以下标志按位(|)或组成。
- WNOHANG:如果指定的子进程没有结束,哪么waitpid会立即返回0,而不是阻塞等待
- WUNTRACED:如果子进程被暂停,哪么waitpid会返回,而不是继续等待子进程结束
-
- 返回值:
- 正常返回的时候waitpid返回收集到的子进程的PID
- 如果设置了WNOHANG选项,而调用中的waitpid发现没有已退出的子进程可收集,则返回0
- 如果出错,返回-1并设置error
-
-
- 进程替换
- 创建子进程的目的是什么?
- 让子进程执行父进程的一部分代码
- 让子进程指向一个全新的程序代码------->进程替换(进程替换没有创建新进程)
- 6种exce函数
- int execl(const char *path, const char *arg, ...);
- int execlp(const char *file, const char *arg, ...);
- int execle(const char *path, const char *arg, ...,char *const envp[]);
- int execv(const char *path, char *const argv[]);
- int execvp(const char *file, char *const argv[]);
- int exceve(const char*path,char* const argv[],char* const envp[]);//系统调用,上面的函数都是该函数的封装
- 返回值:
- 这些函数如果调用成功,则会加载新的程序,从启动代码开始执行,不再返回
- 这些函数如果调用失败,则会返回-1
- 命名理解:
- l:表示参数采用列表
- v:表示参数采用数组
- p:有p自动搜索环境变量PATH
- e:表示自己维护环境变量
- 参数:
- file:文件名及其路径
- arg:这是可变参数列表,以NULL结尾
- path:命令名,会自动在环境变量PATH中查找
- argv:指针数组,该数组需要以NULL结尾
- envp:自己组装的环境变量
- 若传一个自己定义的变量,则会覆盖全局的环境变量
- 若传environ,则无法传自己组装的环境变量
- 通过调用putenv()函数,传参时传入environ,则可以同时存在
- 实例:#include <unistd.h>
- 创建子进程的目的是什么?
int main()
{
char *const argv[] = {"ps", "-ef", NULL};
char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};
execl("/bin/ps", "ps", "-ef", NULL);
// 带p的,可以使用环境变量PATH,无需写全路径
execlp("ps", "ps", "-ef", NULL);
// 带e的,需要自己组装环境变量
execle("ps", "ps", "-ef", NULL, envp);
execv("/bin/ps", argv);
// 带p的,可以使用环境变量PATH,无需写全路径
execvp("ps", argv);
// 带e的,需要自己组装环境变量
execve("/bin/ps", argv, envp);
exit(0);
}
https://gitee.com/qq--1444354226/linux/blob/master/test13-mybash/mybash.c
- 文件理解
- 文件=内容+属性 => 对文件的修改=对文件内容的修改+对文件属性的修改
- 当文件没有被操作的时候,文件一般在什么位置?磁盘
- 当我们对文件进行操作的时候,文件在哪里?内存,为什么?冯诺依曼体系
- 当我们对文件进行操作的时候,至少要将文件的属性加载到内存中
- 内存中一定存在大量的不同的文件的属性
- 打开文件的本质是将需要的文件的属性加载到内存中
- 描述文件:struct file{{文件属性},struct file* next};
- 文件被打开,操作系统要为被打开的文件创建相应的内核数据结构
- 使用C语言进行文件操作
- 打开文件:fopen:FILE * fopen ( const char * filename, const char * mode );
- mode:r(只读)w(只写(会清空))a(追加)b(二进制)
- 关闭文件:fclose:int fclose ( FILE * stream );
- 写入:
- printf:int printf ( const char * format, ... )默认向显示器打印消息
- fprintf:int fprintf(FILE* stream,const char* format,...)向指定的文件打印消息
- sprintf:int sprintf(char* str,const char* format,...)向str格式化输出消息,不需要指定大小
- snprintf:int sprintf(char*str,size_t size,char* format,...)向str格式化输出消息,需要指定大小
- 读取:
- fscanf:int fscanf ( FILE * stream, const char * format, ... );
- 通常用于格式化读取
- 如果成功,该函数将返回成功填充的参数列表的项数。
- 由于匹配失败、读取错误或到达文件结尾,此计数可能与预期的项数匹配,也可能小于预期的项数(甚至为零)。
- 并且,如果在成功读取任何数据之前发生任何一种情况,将返回EOF。
- fgets:char * fgets ( char * str, int num, FILE * stream );
- 通常用于从文件中读取一行
- 从stream流中读取num-1个字符到str中,遇到空格或换行停止,所以str的长度不一定为num,
- 同时换行符是有效字符,也会被读取到str中,成功返回str。
- num可设为sizeof(str)
- fread:size_t fread ( void * ptr, size_t size, size_t count, FILE * stream )
- 通常用于从文件中读取指定数量的数据块,一般二进制,文本文件也可
- size是要读取的每个元素的大小,count是要读取的元素的数量
- fgetc/getc:int fgetc ( FILE * stream );
- 从stream流中读取单个字符
- getchar:int getchar(void)
- 虽然不是直接从文件读取,但可以用于读取从stdin重定向的文件
- 当使用命令行将文件重定向到程序的标准输入时(例如,./program < file.txt),可以使用getchar函数来逐个字符地读取内容。
- 失败返回EOF
- fscanf:int fscanf ( FILE * stream, const char * format, ... );
- 云服务器:/home/fjq/linux/test14-c_and_system_file_opearte
- gitee:
- 打开文件:fopen:FILE * fopen ( const char * filename, const char * mode );
https://gitee.com/qq--1444354226/linux/blob/master/test14-c_and_system_file_opearte/c_file.c
#include<sys/stat.h>
#include<fcntl.h>
-
-
- 声明:int open(const char *pathname, int flags);
-
int open(const char *pathname, int flags, mode_t mode);
-
-
- 参数:
- pathname:要打开的文件名及其地址
- flags:可以传入多个参数选项,用按位或组合|
- 以下三个选项必须用且只能用一个
- O_RDONLY:只读
- O_WRONLY:只写
- O_RDWD:读和写
- O_CREAT:若文件不存在,则创建,该选项需配合mode权限使用
- O_ADDEND:追加写
- O_PRUNC:若文件可以写入,则覆盖式写入
- 以下三个选项必须用且只能用一个
- mode:创建的文件的权限,受umask的影响
- 返回值:
- open()和creat()会创建新的文件描述符,失败返回-1,同时错误码errno被设置
- 参数:
- close关闭文件:
- 头文件:#include<unistd.h>
- 声明:int close(int fd)
- 参数:fd:文件描述符
- 返回值:成功返回0,错误返回-1,并设置错误码errno
- write向文件中写入:
- 头文件:#include<unistd.h>
- 声明:ssize_t write(int fd,const void* buf,size_t count);
- 参数:从缓冲区buf向指定的文件描述符为fd的文件写入最多count字节
- 返回值:成功返回写入的字节数,错误返回-1,并设置错误码errno
- read读取文件:
- 头文件:#include<unistd.h>
- 声明:ssize_t read(int fd,void* buf,size_t count)
- 参数:从文件描述符为fd的文件中读取count字节的数据到缓冲区buf中
- 返回值:成功后返回读取的字节数(0表示文件结束),失败返回-1,设置errno
- 云服务器:/home/fjq/linux/test14-c_and_system_file_opearte
- gitee:
-
test14-c_and_system_file_opearte/system_file.c · 樊继强/Linux - Gitee.com
- 文件描述符fd
- 什么是文件描述符:是一个用于表示文件,套接字或其他IO资源的非负整数
- stdin,stdout,stderr:
- stdin:标准输入,文件描述符为0,通常是键盘
- stdout:标准输出,文件描述符为1,通常是终端或控制台,举例:显示器
- stderr:标准错误输出,文件描述符为2,通常是中断或控制台,举例:显示器
- FILE:FILE是一个结构体,有C语言库提供,里面必定封装了文件描述符fd
- 文件描述符的本质:数组下标
- 文件描述符的分配规则:当前没有被使用的,最小的非负整数
- 重定向
- 什么是重定向:重定向指改变数据输入输出的方向,在上层无法感知的情况下,在操作系统内部,更改文件描述符表中的内容
- 程序的3种重定向:
- 输出重定向:关闭1,打开新的文件(O_WRONLY|O_CREAT)
- 输入重定向:关闭0,打开新的文件(O_RDONLY)
- 追加重定向:关闭1,打开新的文件(O_WRONLY|O_CREAT|O_ADDEND)
- dup2系统调用重定向:
- 头文件:#include<unistd.h>
- 函数声明:int dup2(int oldfd,int newfd);
- 作用:使用oldfd替换newfd并将newfd删除
- 返回值:成功返回新的文件描述符oldfd,失败返回-1,并设置errno
- 举例:输出重定向:dup2(my_fd,1);
- 命令行的重定向:
- 标准输出重定向: > 例如:ls > file.txt
- 追加重定向: >> 例如:ls >> file.txt
- 标准输入重定向: < 例如:sort < file.txt
- 标准错误重定向: 2> 例如:command 2> file.txt
- Linux下一切皆文件:“一切皆文件”是Linux系统的一个重要设计理念。它通过将不同类型的资源(目录,设备文件,管道,套接字)抽象为统一的文件类型,为系统提供了高度的灵活性和可扩展性。同时,这种设计也使得Linux系统具有更好的可移植性和可维护性。
- 文件系统
- 缓冲区:
- 引入缓冲区:
- 缓冲区:
经测试,正常运行输出结果只会打印两行,但如果重定向到新文件中会输出三行,其中有两行的stdout,并且wirte在第一行,stdout在后两行
-
-
- 为什么要有缓冲区:节省调用者的时间
- 缓冲区的刷新策略:
- 全缓冲:系统填满IO缓冲区后才进行实际的IO操作
- 行缓冲:遇到换行符时进程IO操作
- 无缓冲:数据会立即写入磁盘或进程IO操作,不会在缓冲区等待
- C库中显示器采用行缓冲刷新策略,普通文件采用全缓冲刷新策略,
- 缓冲区在哪:
-
在进行fopen打开文件的时候, 会得到FILE结构体, 缓冲区就在这个FILE结构体中
-
-
- 强制刷新缓冲区:int fsync(int fd)
- 云服务器:/home/fjq/linux/test15-file_operate_and_buffer
- gitee:
-
https://gitee.com/qq--1444354226/linux/blob/master/test15-file_operate_and_buffer/file_system.c
-
-
- 解释上述原因:
- C语言库的缓冲区和OS的缓冲区不是同一个缓冲区,缓冲区的刷新策略有三种:无缓冲,行缓冲,全缓冲
- C库中显示器采用行缓冲刷新策略,普通文件采用全缓冲刷新策略,即重定向到普通文件时C库采用全缓冲,显然stdout内容不满足全缓冲,而write直接写给OS,故重定向到普通文件中时会先出现wirte
- 全缓冲会在return时刷新,到fork创建子进程时,缓冲区未刷新,fork之后创建了子进程,继续向下执行代码,
- 子进程缓冲区刷新导致发生写时拷贝,不影响父进程,再之后,父进程return ,父进程中的缓冲区刷新,导致输出两行stdnut
- 解释上述原因:
- 尝试自己封装一个FILE
- 云服务器:/home/fjq/linux/test16-my_c_file_operate
- gitee:
-
https://gitee.com/qq--1444354226/linux/tree/master/test16-my_c_file_operate
-
- 磁盘的物理结构
- 盘片:磁盘是按摞的,也就是说一个磁盘有很多个盘片
- 盘面:一个盘片有两个盘面
- 磁头:每一个盘面都有一个磁头,磁头和盘面是没有接触的,是悬浮在磁盘上的
- 马达:盘片加电之后,马达会使盘片逆时针旋转,此时磁头会左右摆动,
- 磁盘的存储结构
- 盘片:盘片表面涂有磁性物质,这些磁性物质用来记录二进制数据,
- 磁盘的物理结构
-
-
- 磁道,扇区:每个盘片被划分为一个个磁道,每个磁道又被划分为一个个扇区
-
-
-
- 柱面:所有盘面中相同位置的磁道组成柱面
- 由此,可用(柱面号,盘面号,扇区号)来定位任何一次磁盘块,即CHS定位法
- 磁盘的逻辑结构
- 操作系统内部是不是直接使用CHS地址呢?不是
- 为什么?
- 操作系统是软件,磁盘是硬件,硬件通过CHS定位一个地址,如果操作系统直接使用该地址,硬件变化之后,操作系统也要随之变化,这样不行,操作系统要和硬件做好解耦工作。
- 对于操作系统而言,每次只读取一个扇区(512字节)的数据是低效的,操作系统实际进行IO的基本单位为4KB(可以主动修改,称为块设备),所以,操作系统要有一套新的地址,来进行块级别的访问
- 将磁盘的盘面按照线性方式展开
-
-
-
- 可以把一个磁道看成一个数组,此时定位一个扇区时只需要通过数组下标就可以定位
- 操作系统是以4KB为单位进行IO的,故一个OS级别的文件块要包括8个扇区,
- 计算机常规的访问方式:起始地址+偏移量,我们也可以把数据块看作一种类型,同理只需要知道数据块的起始地址(第一个扇区的下标地址)+4KB(块的大小)就可以访问
- 块的地址的本质就是数组的一个下标,表示一个块,可以采用线性下标的方式,定位一个块,所以,如果我们想要找到一个指定的扇区,只需要知道这个扇区的下标就可以定位该扇区在磁盘中的位置,在操作系统内部,我们称这种方式为LBA地址,即逻辑块地址
- CHS地址和LBA地址的相互转化
-
LBA(逻辑扇区号)=磁头数(255) × 每磁道扇区数(63) × 当前所在柱面号(C) + 每磁道扇区数(63) × 当前所在磁头号(H) + 当前所在扇区号(S) – 1
-
- 深入理解文件系统
-
-
- 分区:对于一个500GB的磁盘,文件系统IO单位为4KB时显然不够大,所以我们将其进行分区,分成100GB
- 分组:每100GB为一个分区时显然也不方便管理,所以我吗将其进行分组,每5GB一个组
- 采用分治的思想,我们只要管理好每一个分组,哪么其他组只需要复制这组的管理方法采用同样的方式进行管理,我们就可以管理好每一个分区,再对每一个分区进行同样的方式管理即可
-
-
-
-
- Block Group:ext2 文件系统会根据分区的大小划分为数个Block Group。而每个Block Group都有着相同的结构组成。
- 每个分区的第一部分数据是Boot Block启动块,后面才是各个分组,他与计算机开机相关,我们不用关心
- 分组的管理方法:
- Super Block(超级块):存放文件系统本身的结构信息。记录的信息主要有block和inode的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息,Super Block的信息被破坏,可以说整个文件系统就被破坏了。Super Block没有像Boot Block一样放在分区的位置而是在每个组中都有一个就是为了备份,一旦其他Super Block被破坏,就会把其他分组的正常的Super Block拷贝回来。
- GDT:块组描述符,描述块组属性信息。
- inode:文件=内容+属性,Linux操作系统中文件的内容和属性是分离的,inode用来存储文件属性,inode是固定大小的,一个文件一个inode,在分组的内部,可能会存在多个inode, 需要将inode区分开来,每一个inode都有自己的inode编号,inode编号也属于对应文件的属性id。
- inode Table:在一个分区中,内部会存在大量的文件,即存在大量的inode,一个组中需要有一个区域来专门存放该组内所有文件的inode节点,这个区域就是inode Table。
- inode Bitmap:inode对应的位图结构,每个bit表示一个inode是否空闲
- Data blocks:数据块,一个文件的内容是变化的,我们使用数据块来进行文件内容保存的,所以一个有效文件,要保存内容,就要用1到n个数据块,哪么有更多的文件,就需要更多的Data block,这就成了Data blocks
- Block Bitmap:每一个bit表示一个Data blok是否可用
- 深入理解inode:
- inode和文件名:Linux系统只认inode编号,文件的inode属性中,并不存在文件名,文件名是给用户看的
- 重新认识目录:
- 目录是文件吗?是的。
- 目录有inode编号吗?有,可通过ls -il查看
- 目录有内容吗?有
- 内容是什么?任何一个文件一定在目录内部,所以目录要有内容就要有数据块,目录的数据块中保存的内容是该目录下的文件名和文件inode编号对应的映射关系。在目录中,文件名和inode互为key值
- 访问文件的基本流程:
- 在当前目录下,根据文件名找到文件的inode编号
- 一个目录也是文件,也一定隶属于一个分区,结合inode,在该分区中找到该分组,在该分组的inode Table中找到inode
- 通过inode和对应的data block的映射关系,找到该文件的数据块,加载到操作系统中
- 深入理解文件的创建和删除:
- 创建:
- 在inode bitmap中找到为0的位置,设置成1
- 将文件的所有属性填写到inode Table对应的下标的空间中
- 在block bitmap中查找一个或多个为0的比特位编号,将其置为1
- 将文件的所有内容填写到data block对应编号下标空间中
- 修改super block,并将inode编号与文件名的映射关系写到目录中(该目录的data block)
- 删除:
- 根据文件名找到文件的inode编号
- 根据inode编号和inode属性中的映射关系,找到其在Block Bitmap对应的位置并将该比特位设置为0
- 根据inode编号将inode bitmap对应的比特位设置为0
- 创建:
- 细节补充:
- 如果文件被误删了,我们应该怎么做?
- 知道被删除文件的inode编号,先拿着inode编号在特定被删除文件的分组中将inode bitmap对应的比特位由0置1,此时文件就不会被覆盖了
- 再根据inode读取inode表从,inode表中提取文件所占用的数据块,再将数据块对应的block bitmap置1,此时文件的内容和属性便恢复了
- 如果文件被误删了,我们应该怎么做?
-
-
不过这一切的前提是原文件的inode没有被新文件使用,如果新文件使用了原文件的inode,哪么对应的inode table和data block中的数据便会被覆盖,所以文件被误删之后的最好做法便是什么都不做,避免新建文件将原文件的inode覆盖
-
-
-
- inode编号是在一个分区内唯一有效,不可以跨分区,可以跨分组,所以可以用inode编号确定分组
- 我们学到的分区和分组,填写系统属性是谁做的?什么时候做的?
-
-
操作系统做的,在分区完成之后,我们需要对分区做格式化,格式化的过程,就是操作系统向分区写入文件系统的管理属性信息
-
-
-
- inode如果只是单单的用数组建立和data block的关系,假设一个data block存4KB数据,有15个数组,是不是意味着一个文件内容最多放入15*4=60KB的数据呢?不是的
-
-
-
-
-
- 有没有可能,一个分组,数据块没用完,inode用完了或者inode没用完,数据块用完了?有可能,但这种情况基本不会出现
-
-
- 软硬链接:
- 软链接:
- 建立软链接:ln -s 原文件名 要建立的软链接名
- 用途:软链接相当于Windows下的快捷方式,当一个文件放的过于深时,可使用软链接
- 硬链接:
- 建立硬链接ln 原文件名 要建立的硬链接名
- 用途:硬链接和目标文件共用一个inode,意外着硬链接一定和目标文件使用一个inode,硬链接究竟干了什么?建立了新的文件名和老的inode的映射关系,硬链接数的本质是一种引用计数,
- 如果将原文件删除,通过硬链接仍可访问该文件的内容,因为只是去掉了一个映射关系,计数器-1,还存在一个映射关系可以访问。因此可以看出,只有当硬链接数为0时文件才会被删除,
- 删除:rm方式删除或者通过unlink 硬链接名 删除
- 理解.和..
- . :
- 在当前路径下,我们创建一个普通文件和一个目录文件,通过ls -al命令查看,我们发现普通文件的硬链接数为1,目录文件的硬链接数为2,进入目录文件,通过ls -al命令,发现文件名为.的文件的inode和刚才创建的目录文件的inode相同,由此得知,.是当前目录的硬链接
- .. :通过类似的方法,我们可以知道..是上级目录的硬链接,由此更加能证明,linux下的目录是树状结构
- 在linux下,不允许为目录文件添加硬链接,为什么?因为会造成环路路径问题
- . :
- 文件的acm时间
- a:最后访问时间
- c:文件内容最后修改时间
- m:文件属性最后修改时间
- 软链接:
- 动态库和静态库
- 见一见库:
-
-
- 系统已经预装了C/C++的头文件和库文件,头文件提供方法说明,库文件提供方法的实现,头和库是有对应关系的,要组合在一起使用
- 头文件是在预处理阶段就引入的,链接的本质是链接库
- 总结:
- 其实,我们在VS2022下安装开发环境,安装编译器,就是安装要开发的语言配套的库和文件
- 我们在使用编译器的时候,都会有语法的自动提醒功能,这需要先包含头文件。自动提醒功能是依赖头文件的
- 我们在编写代码的时候,我们的环境怎么知道我们的代码中有那些地方有语法报错,哪些地方定义变量有问题?不要小看编译器,有命令行的模式,还有其他自动化的模式帮助我们不断进行语法检查
- 为什么要有库:
-
提高开发效率
-
- 设计一个库:
- 预备知识
- 静态库(.a):程序在编译链接的时候把库的代码连接到可执行文件中,程序运行的时候将不再需要静态库
- 动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。
- 库文件的命名:库的真实名称为去掉前缀lib,去掉首次遇到的.之后(或版本号)。例如:libstdc++.so.6(stdc++)libc-2.14.so(c)
- 一般的云服务器,默认只存在动态库,不存在静态库,静态库需要单独安装。
- 动态库和静态库同时存在时,默认使用动态库
- 编译器在链接的时候如果既提供了动态库,又提供了静态库,则优先使用动态库,如果只提供了静态库,则使用静态库
- 设计一个静态库
- 方法一:将所有需要的文件提供给gcc
- 预备知识
- 设计一个库:
例如:gcc -o mytest main.c xxx.o xxx.o
-
-
-
- 方法二:将所有的.o文件打包形成静态库,并使用
- 命令:ar -rc libXXX.a *.o //将当前目录下所有的.o文件打包成静态库
- 通过gcc -o mytest main.c -I(头文件的目录) -L(库的目录) -l(库名) 可生成可执行程序
- 云服务器:/home/fjq/linux/test17-library/other2-static
- gitee
- 方法二:将所有的.o文件打包形成静态库,并使用
-
-
https://gitee.com/qq--1444354226/linux/tree/master/test17-library/other2-static
-
-
- 第三方库的使用
- 需要指定头文件和库文件
- 如果没有默认安装到系统gcc/g++默认的搜索路径下用户必须指明对应的选项,告知编译器
- -I(大写l):头文件在哪里
- -L:库文件在哪里
- -l(小写l):指定库名(库的真实名称)
- 将我们下载下来的库文件和头文件,拷贝到系统默认路径下——其实就是在linux下安装库
- 哪么卸载呢?对任何软件而言,安装和卸载的本质就是拷贝到系统默认的路径下
- 如果我们安装的库是第三方的(语言,操作系统接口是第一,二方)库,我们要正常使用,即便是已经全部安装到了系统中,gcc, g++必须用 -l(小写l)指定具体库的名称
- 动态库的配置
- 在建立动态库之前,我们需要使用gcc -fPIC -c XXX.c和gcc -fPIC -c XXX.c形成.o文件(fPIC:产生位置无关码)
- 然后通过gcc -shared -o libXXX.so *.o打成动态库
- 然后像静态库一样链接生成可执行程序,发现可执行程序并不能运行,我们不是已经告诉系统,库在哪里,叫什么了吗?为什么还是不能用?
- 原因:其实我们并不是告诉系统,而是告诉了编译器,运行的时候,我们的.so并不在系统默认的路径下,所以OS依旧找不到
- 为什么静态库就能找到呢?
- 原因:静态库的链接原则:将用户使用的二进制代码直接拷贝到目标可执行程序中,而动态库不会
- 方法一:环境变量LD_LIBRARY_PATH
- ldd命令能查看一个可执行程序依赖的第三方库
- 通过export LD_LIBRARY_PATH=LD_LIBRARY_PATH:动态库的路径 添加到环境变量中,但是因为环境变量具有临时性,下次登录时,我们自定义的环境变量就没有了
- 方法二:软链接
- 第三方库的使用
-
将库文件的软链接建立在系统的默认搜索路径下
例如:ln -s 动态库的路径/动态库全名 /lib64/动态库全名
-
-
-
- 方法三:配置文件
-
-
/etc/ld.so.conf(普通文件)和/etc/ld.so.conf.d/(目录)这两个文件或目录用于指定系统级别的动态库搜索路径,你可以将你的动态库的目录添加到这些文件中,然后使用ldconfig命令来更新动态链接器的缓存(sudo提权)
-
-
-
- 云服务器:/home/fjq/linux/test17-library/other3-dynamic
- gitee
-
-
https://gitee.com/qq--1444354226/linux/tree/master/test17-library/other3-dynamic
-
-
- 动静态库的加载理解
- 加载静态库
- 动静态库的加载理解
-
对于静态库而言,静态库不需要加载,而程序需要加载。当静态库链接的时候,实际上是将代码拷贝到程序中,所以后面程序运行的时候就不再依赖静态库。
而一旦有很多程序,静态库就会大量拷贝重复的代码给不同的程序。通过进程地址空间的知识,我们知道当静态库拷贝代码给程序时,实际上是把代码拷贝进了代码区。因此在程序运行形成进程地址空间时,静态库中的代码只能被映射到进程地址空间相应的代码区中,未来的这段代码,必须通过相对确定的地址位置进行访问。
-
-
-
- 加载动态库的
- 对于动态链接来说,可执行程序中存放的是动态库中某具体.o文件的地址,同时,由于组成动态库的可重定向文件是通过位置无关码fPIC产生的,所以这个地址并不是.o文件的真正地址,而是.o文件在动态库中的偏移量(与C++中的虚函数表类似)
- 然后就是程序运行的过程:OS会将磁盘中的可执行程序加载到物理内存中,然后创建mm_struct,建立页表映射,然后开始执行代码,当执行到库函数时,OS发现该库函数链接的是一个动态库的地址,且该地址是一个外部地址,OS就会暂停程序的运行,开始加载动态库
- 加载动态库:操作系统会将磁盘中动态库加载到物理内存中,然后通过页表将其映射到该进程的地址空间的共享区中,然后立即确定该动态库在地址空间中的地址,即动态库的起始地址,然后继续执行代码
- 此时OS就可以根据库函数中存放的地址(即偏移量),在加上动态库的起始地址得到.o文件的地址,然后跳转到共享区中执行该函数,执行完毕后跳转回来继续执行之后的代码。这就是动态库的加载过程
- 加载动态库的
-
-
-
-
-
- 库中地址的理解
-
-
- 理解进程间通信
- 进程间通信的前提
- 进程具有独立性,两个进程想要通信无疑成本较高
- 让两个进程进行通信的前提:两个进程看到同一份资源
- 前提:
- 想办法让不同的进程看到同一份资源
- 让一方写入,另一方读取,完成通信过程
- 进程间通信的目的
- 数据传输:一个进程需要将他的数据发送给另一个进程
- 资源共享:多个进程之间共享相同的资源
- 通知事件:一个进程需要向另一个(组)进程发送消息,通知他们发生了某种事件,(如进程终止时要通知父进程)
- 进程控制:有些进程希望完全控制另一进程的执行(如debug进程),此时控制进程希望能够拦截另一进程陷入的所有的异常状态,并能够及时知道他的状态改变
- 进程间通信的分类
- 管道
- 匿名管道
- 命名管道
- System V
- System V 消息队列
- System V 共享内存
- System V 信号量
- POSIX IPC
- 管道
- 进程间通信的前提
消息队列,共享内存,信号量,互斥量,条件变量,读写锁,比
-
- 匿名管道
- 管道的原理:
- 进程控制块PCB(task_struct)中包含一种结构体 struct files_strcut,而这个结构体里面包含了一个指针数组struct file* fd_array[],可以通过特定的文件描述符找到磁盘加载到内存中对应的文件。
- fork()创建子进程后,不会拷贝磁盘中的文件,而是拷贝一份struct files_struct同样指向父进程对应的struct file
- struct file是从磁盘加载到内存中的,而父子进程的每一次写入,struct file都不会从内存中刷新到磁盘(虽然通过一定听操作是可行的,但是进程与进程之间的通信是从内存到内存中的,没必要牵扯到磁盘,而且一旦刷新到磁盘,通信的效率便会大大降低),所以管道文件是一个内存级别的文件,不会刷新到磁盘。
- 管道的原理:
- 匿名管道
-
-
-
- 只有父进程在fork时打开管道的读和写,子进程才能继承管道的读和写的文件描述符,子进程才有读和写的功能
- 管道的实例:
- 头文件:#include<unistd.h>
- 函数声明:int pipe(int pipefd[2]);
- 功能:创建一个匿名管道
- 参数:pipefd:文件描述符数组,其中pipefd[0]表示读端,pipefd[1]表示写端
- 注意:父子进程要关闭无用的端口,子进程退出时要关闭所有端口
- 在父进程读取,子进程写入的情况下的四种情形:
- 父进程读取完毕,子进程不发送消息,我们就只能等待
- 子进程将write端写满了,写操作便会被阻塞,直到有空间可用
- 关闭写段,读取完毕,则再读的话就会返回0,表面读取到文件结尾
- 读端关闭,写端一直写,这种情况没有意义,OS追求效率,不会维护低效率,无意义或者浪费资源的事情,OS会通过信号(13,SIGPIPE)来杀死一直在写的进程
- 云服务器:/home/fjq/linux/test19-pipe/one
- gitee:
-
-
https://gitee.com/qq--1444354226/linux/tree/master/test19-pipe/one
-
-
- 管道的特点:
- 单向通信:管道是半双工的一种特殊情况
- 血缘关系:管道通常用于具有血缘关系的进程之间,兄弟进程也可
- 内存中的文件:管道是内存中的文件,不需要写入磁盘,因此比文件IO快
- 管道的本质是文件,因为fd的声明周期随进程,所以管道的生命周期随进程
- 在管道通信中,写入和读取的次数并不是严格匹配的,没有强相关,表现为字节流
- 具有一定的协同能力,让read和write能够按照一定的步骤进行通信,管道自带同步机制
- 匿名管道实现进程池:
- 管道的特点:
-
用管道实现父进程控制多个子进程,在这种情况下,父进程进行写入,子进程进行读取
云服务器:/home/fjq/linux/test19-pipe/many
gitee
https://gitee.com/qq--1444354226/linux/tree/master/test19-pipe/many
-
- 命名管道
- 什么是命名管道
- 匿名管道只能在具有血缘关系的进程之间进行通信
- 如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来进行这项工作,他经常被称为命名管道
- 命名管道是一种特殊类型的文件
- 创建一个命名管道
- 命令行创建命名管道
- 什么是命名管道
- 命名管道
mkfifo 文件名 管道是内存级别的文件,不会写入磁盘,所以就算向管道写入数据,使用ls -l命令查看时管道的大小依旧为0
-
-
-
- 程序内部创建删除命名管道
- 创建
- 头文件:#include<sys/types.h>
- 创建
- 程序内部创建删除命名管道
-
-
#include<sys/stat.h>
-
-
-
-
-
- 函数声明:int mkfifo(const char* pathname,mode_t mode)
- pathname:命名管道路径和名称,mode:权限,受umask影响
- 成功返回0,失败返回-1,错误码errno被设置
- 删除
- 头文件:#include<unistd.h>
- 函数声明:int unlink(const char* path)
- 成功返回0,失败返回-1,错误码errno被设置
-
-
- 深入理解命名管道
-
-
命名管道是如何让不同的进程看到同一份资源呢?
让不同的进程通过文件路径+文件名看到同一个文件(就是看到了同一份资源),并打开,即具备了进程间通信的前提
-
-
- 命名管道与匿名管道的区别
- 创建和打开方式:匿名管道由pipe函数创造并打开,命名管道由mkfifo函数创建,open函数打开
- 使用范围:匿名管道仅限于具有血缘关系的进程使用,命名管道可由任何两个有相应权限的进程使用
- 通信方向:匿名管道只能单向,命名管道可创建两个(一个进程读,一个进程写)从而实现双向通信
- 存在位置:匿名管道不存在于文件系统中,命名管道存在于文件系统中
- 关闭与删除:匿名管道当所有的文件描述符都关闭时,自动消失,无需显示删除。命名管道像文件一样,当所有打开了他的进程都关闭了对他的引用时,他并不会被自动删除,有需要的话可以向删除文件一样删除他
- 用命名管道实现客服端(client)和服务器(server)的通讯
- 云服务器:/home/fjq/linux/test19-pipe/namepipe
- gitee https://gitee.com/qq--1444354226/linux/tree/master/test19-pipe/namepipe
- 命名管道与匿名管道的区别
-
- 共享内存
- 共享内存的原理
- 在物理内存上申请一段空间
- 通过返回对应空间的起始地址将申请的空间映射到进程地址空间
- 当不想通信时:
- 取消掉进程和内存之间的映射关系
- 释放申请的内存
- 共享内存的原理
我们将申请的这块空间称之为共享内存,将映射关系称之为进程和共享内存进行挂接,将取消进程和内存的映射关系称之为去关联,释放内存释放的就是共享内存。
共享内存和malloc的区别
- malloc是现在堆上申请虚拟空间,当真正需要使用时建立映射关系,物理内存分配空间
- 共享内存是开辟一块物理空间,分别映射至通信进程的虚拟地址空间中
共享内存是一种通信方式,所有想通信的进程都可以使用,所以操作系统中会同时存在很多个共享内存
-
- 共享内存的相关命令
- 查看共享内存 ipcs -m
- 共享内存的相关命令
查看后prems代表共享内存的权限,nattch代表共享内存关联的进程数
-
-
- 删除共享内存 ipcrm -m 共享内存的shmid
- 共享内存函数
- 形成key ftok
- 头文件: #include<sys/types.h>
- 形成key ftok
-
#include<sys/ipc.h>
-
-
-
- 函数声明:key_t ftok(const char* pathname,int proj_id)
- 参数:pathname->路径字符串 proj_id->项目id,一般随便给
- 返回值:成功时返回key,失败是返回-1,错误码errno被设置
-
-
-
-
- 深入理解key
- OS中可以用shm来进行通信,并不是只能有一对进程使用共享内存--->所以在任意一个时刻,可能会有多个共享内存被用来进行通信--->所以系统中会同时存在多个shm,OS需要整体管理共享内存--->OS如何管理?
- 先描述再组织,所以共享内存不是我们想的那样,只要在内存中开辟空间即可,系统也要为了管理shm,构建对应的描述共享内存的结构体对象
- 共享内存=共享内存的内核数据结构+真正开辟的内存空间
- 通信的前提是看到相同的资源,通过传入相同的pathname和proj_id得到相同的key,从而找到同一块共享内存,实现进程间通信
- 深入理解key
-
-
-
-
- key和shmid的区别
- 类比于文件的fd和inode
- key的本质是在内核中使用的,对shm的所有操作在用户层都使用shmid
- 通过key和shmid的区分,能够面向系统层面和用户层面,这样能够更好的进行解耦合,以免内核中的变化影响到用户级
- key和shmid的区别
-
-
-
-
- 创建共享内存 shmget 用来创建共享内存
- 头文件: #include<sys/shm.h>
- 函数声明:int shmget(key_t key,size_t size,int shmflg)
- 参数:
- key->这个共享内存段的名字
- size->共享内存的大小
- shmflg->由9个权限标志组成,使用方法类似mode
- 返回值:成功返回一个非负整数,即该共享内存段的标识码,失败返回-1,并设置错误码errno
- shmflg:
- IPC_CREAT:创建一个共享内存,如果共享内存不存在,创建,如果共享内存已经存在,则获取已经存在的共享内存并返回
- IPC_EXCL:不能单独使用,配合IPC_CREAT使用,两者都存在时,如果共享内存不存在,创建,如果共享内存已经存在,则出错返回
- mode:标志结束后按位或上一个权限,一般为0666
- 创建共享内存 shmget 用来创建共享内存
-
-
-
- 关联共享内存 shmat 将共享内存链接到进程地址空间
- 头文件: #include<sys/shm.h>
- 函数声明: void* shmat(int shmid,const void* shmaddr,int shmflg)
- 参数:
- shmid:共享内存标志,shmget的返回值
- shmaddr:指定链接的地址,可设置成nullptr
- shmflg:这里给0默认可以读写
- 返回值:成功返回一个指针,指向共享内存的第一节(即共享内存的地址),失败返回-1,并设置错误码errno
- shmflg
- SHM_RDONLY:以只读方式附加共享内存段。如果省略此标志,则默认以读写方式附加
- SHM_RND:将附加地址舍入到最接近的 SHMLBA(共享内存锁定字节对齐)的倍数。
- SHM_REMAP:如果该区域已经附加到调用进程的地址空间,则重新附加它。
- SHM_EXEC:将共享内存段标记为可执行(在某些系统上可能不被支持)。
- 关联共享内存 shmat 将共享内存链接到进程地址空间
-
-
-
- 共享内存去关联 shmdt 将共享内存段与当前进程脱离
- 头文件: #include<sys/shm.h>
- 函数声明: int shmdt(const void* shmaddr)
- 参数:shmaddr:由shmat返回的指针
- 返回值:成功返回0,失败返回-1,并设置错误码errno
- 注意:将共享内存去关联不等于删除共享内存段。为什么?共享内存段的生命周期随OS,不随进程
- 共享内存去关联 shmdt 将共享内存段与当前进程脱离
-
-
-
- 控制共享内存 shmctl 用于控制共享进程
- 头文件: #include<sys/shm.h>
- 函数声明: int shmctl(int shmid,int cmd,struct shmid_ds* buf)
- 参数:
- shmid:共享内存标识符
- cmd:控制命令,用于指定要执行的操作
- IPC_RMID:删除共享内存段
- IPC_SET:设置共享内存的权限和所有权
- IPC_STAT:获取共享内存段的状态
- buf:用于存储或获取共享内存段的信息
- cmd==IPC_SET:用于设置共享内存的属性
- cmd==STAT:用于存储共享内存的属性
- 返回值:成功返回0,失败返回-1,并设置错误码errno
- 控制共享内存 shmctl 用于控制共享进程
-
-
- 利用共享内存实现进程间通信
- 云服务器:/home/fjq/linux/test20-shm
- gitee
- 利用共享内存实现进程间通信
https://gitee.com/qq--1444354226/linux/tree/master/test20-shm
-
- 共享内存知识点补充
- 共享内存的优缺点
- 优点:一旦共享内存映射到进程的地址空间,该共享内存就被所有的进程看到了,因为共享内存的这种特性,可以让进程通信的时候,减少拷贝的次数,所以共享内存是进程间通信中,速度最快的
- 共享内存的优缺点
- 共享内存知识点补充
-
-
-
- 缺点:
-
-
如果服务端读取速度较快,用户端发送数据较慢,就会产生同一段消息被服务器端读取多遍。共享内存是不进行同步和互斥的,没有对数据进行任何保护。为什么?因为像管道这种是调用系统接口通信,而共享内存是直接通信。
-
-
- 共享内存的大小
-
因为系统分配共享内存是以4KB为单位的,一般建议申请共享内存的大小为4KB的整数倍
- system V信号量(了解)
- 前提概念:
- 公共资源:大家都能看到的资源
- 互斥:任何一个时刻,都只允许一个执行流在进行共享资源的访问
- 临界资源:任何一个时刻,都只允许一个执行流访问的共享资源
- 临界区:临界区是一个程序片段,它访问那些无法同时被多个线程或进程访问的共用资源
- 原子性:要么不做,要么做完。只有两种确定状态的属性
- 感性理解
- 信号量的本质是一个计数器,描述资源数量的计数器
- 前提概念:
-
-
- 任何一个执行流想访问临界资源中的一个子资源的时候也不能直接访问
- (P操作, 预定资源) 我们首先要申请信号量资源 — count-- — 只要我申请信号量成功, 我就未来就一定能够拿到一个子资源, 一旦count减为0后, 有进程想要访问, 那么这个进程就会被挂起阻塞
- 接下来我们进入自己的临界区, 访问对应的临界资源
- (V操作,释放资源) 访问完后, 释放信号量资源 — count++ — 只要将计数器增加,就表示将我们对应的资源进行了归还
- 我们想让两个不同的进程看到同一个"计数器(count)"(资源), 所以信号量被归类到了进程间通信
- 信号量本身也是一个临界资源,它能保护其他共享资源的同时,也需要保护自己的安全,信号量内部的加加减减具有原子性。
- 二元信号量:信号量为1,说明共享资源时一整个整体,提供互斥功能。
-
- IPC资源管理方式
将内核中所有的ipc资源统一用struct ipc_perm* perms[]指针数组进行管理, 模拟了C++中多态的行为
- 关于信号的基本常识
- 在信号没有产生的时候,我们也应该知道怎么处理他。进程在没有收到信号的时候,其实他早就已经知道信号该怎么处理了,即他能够识别并处理一个信号,因为程序员设计进程的时候,早就已经设计了对信号的识别能力
- 信号可能随时产生,所以在产生信号前,进程可能正在做优先级更高的事情,可能不能马上处理这个信号,需要在后续合适的时间进行处理,哪么由信号产生到信号处理之间就会存在一个时间窗口,这段时间内进程没有处理这个信号,就需要进程具有记录信号的能力
- 处理信号的三种方式:(1).默认动作(2).忽略信号(3).用户自定义动作
- 信号的产生对进程来将是异步的
- 进程如何记录产生的信号?先描述再组织。如何描述一个信号呢?由0和1比特位来表示信号的有无。用什么数据结构来管理信号呢?位图。所以在task_struct内部必定要存在一个位图结构,用int表示,uint_32_t signal:0000 0000 0000 0000 0000 0000 0000 0000
- 所谓的发信号,本质其实是写入信号,直接修改特定进程的信号位图中特定的比特位,由0置1即可
- task_struct数据内核结构,只能由OS进行修改,即无论后面我们有多少种信号产生的方式,最终都必须让OS来完成最后的发送过程
- 信号的产生
键盘,系统调用,指令,软件条件,硬件异常
每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到
编号1-31我们称为普通信号,收到此类信号后只记录有无产生,编号34以上是实时信号,不做讨论
键盘知识补充:当我们在键盘按下对应键后,通过8259到cpu的某个针脚上,这个针脚会有对应的中断号,知道中断号后去OS内中断向量表中查找对应下标,执行对应方法即从键盘中读取对应的数据就可以判断键盘哪些位置被按下
- 系统调用
- signal:对信号进行自定义捕捉(9号信号和19号只能执行默认动作,不能更改)
- 头文件: #include<signal.h>
- 函数声明:sighandler_t signal(int signum,sighandler_t handler)
- 宏:sighandler_t : typedef void (*sighandler_t)(int) 这是一个函数指针
- 参数:
- signum:信号的编号
- handler:为SIG_DFL时表示信号默认处理方式,为SIG_ING设置为忽略处理,其他表示自定义处理方式,需要自己实现一个函数(该函数的返回值为void,参数为int,调用时传函数名即可)
- 注意:使用signal函数并未调用函数指针指向的函数,只是更改了signum信号的处理动作。只有在你指定的signum信号产生的时候函数指针指向的函数才会被调用
- kill:给任意进程发送任意的信号
- 头文件: #include<sys/types.h>
- signal:对信号进行自定义捕捉(9号信号和19号只能执行默认动作,不能更改)
#include<signal.h>
-
-
- 函数声明:int kill(pid_t pid,int sig)
- 给进程id为pid的进程发送sig号新号,成功返回0,失败返回-1,设置errno
- alarm:闹钟,发送14号信号(SIGALRM)(定时器)
- 头文件:#include<unistd.h>
- 函数声明:unsigned int alarm(unsigned int seconds)
- 返回值为剩余定时器的秒数(可提前唤醒闹钟),调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒后向当前进程发送SIGALRM信号。该信号的默认处理动作为终止当前进程。alarm(0) 表示取消之前设定的闹钟
-
- C语言库函数:
- raise:给自己发送由自己指定的任意信号
- 头文件:#include<signal.h>
- 函数声明:int raise(int sig)
- 给自己发送sig号新号,成功时返回0,失败时返回非0
- abort:给自己发送6号信号并执行对应的动作(终止)
- 头文件:#include<stdlib.h>
- 函数声明:void abort(void)
- raise:给自己发送由自己指定的任意信号
- 其他由硬件异常产生的信号
-
- ctrl+c属于2号信号(SIGINT)
- ctrl+\属于3号信号(SIGQUIT)
- 除零属于8号信号(SIGFPE)
-
-
-
-
- 运行如下代码
-
-
-
-
-
- 为什么会死循环打印一句话?
-
-
这个代码中有除0错误,溢出标志位由0置1,此进程并没有退出,而这个状态标志位也属于进程的上下文,出现异常后,OS会识别出异常并向此进程发送信号,但是此进程并未修复标志位且并未退出,对应被置1的溢出标志位一直存在(硬件异常一直存在),所以OS会一直给此进程发信号
-
-
-
- 此进程为什么没有退出?
-
-
执行了自定义的捕捉动作,进程的处理动作不再是终止进程,而是打印出一句话后向后运行,此进程在刚向后运行前,OS检测出硬件异常,阻止向后运行,并发送信号
-
-
- 野指针是段错误,属于11号信号(SIGEGV)
-
-
-
- 子进程退出时会向父进程发送17号信号(SIGCHLD)
-
- core dump(核心转储)和ulimit命令
- 什么是core dump?
当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常为core,这种行为叫core dump
-
- core dump的前提
- 信号旁边写着Core的信号,都可以使用core dump功能(可以使用man 7 signal 命令查看信号,其中Trem就是普通的终止,Core会先进行核心转储(如果能进行核心转储的话),再终止)
- core dump的前提
-
-
- 云服务器默认关闭了核心转储文件,在终端输入 ulimit -a能够显示操作系统各项资源的上限,使用ulimit -c 10240功能可开启核心转储
- core dump的意义?将程序异常的原因转储至磁盘,方便后续调试(在打开gdb后,支持通过 core-file 核心转储后生成的文件的文件名 来导出错误)
-
- 信号的保存
- 相关概念:
- 信号递达:实际执行信号的处理动作
- 信号未决:信号从产生到递达之间的状态
- 进程可以选择阻塞某个信号
- 信号被阻塞时将保持在未决状态,直到进程解除对该信号的阻塞,才会执行递达的动作
- 阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作
- 信号在内核中的表示
- 相关概念:
-
-
- struct_task:
- pending表,位图,位置表示哪一个信号,内容表示是否收到该信号(用于信号未决时保存信号),即使信号被block表屏蔽,pending表仍然能收到
- block表,位图,位置表示哪一个信号,内容表示是否阻塞,有些信号不允许屏蔽
- handler表,函数指针数组(void (*sighandler_t)(int)),位置表示哪一个信号,内容表示该信号的递达动作
- 每个信号都有两个标志位分别表示阻塞和未决,还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号抵达才清除该标志。所以常规信号在递达前产生多次只计入一次。
- struct_task:
- 认识sigset_t
- 每个信号只有一个bit的未决或阻塞标志,非0即1,不记录该信号产生了多少次
- 因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t被称为信号集,这个类型可以表示每个信号的有效或无效状态。阻塞信号集也叫做当前进程的信号屏蔽字,注意:这里的屏蔽是阻塞而不是忽略。
- 信号集操作函数
- 头文件:#include<signal.h>
- int sigemptyset(sigset_t* set); //全部置0
- int sigfillset(sigset_t* set); //全部置1
- int sigaddset(sigset_t* set,int signo); //指定信号置1
- int sigdelset(sigset_t* set,int signo); //指定信号置0
- int sigismember(const sigset_t* set , int signum); //判断是否收到该信号
- 前四个函数都是成功返回0,出错返回-1,sigismember成功收到该信号返回1,没收到返回0,失败返回-1
- 注意,在使用sigset_t类型的变量之前,一定要先调用sigemptyset或sigfillset进行初始化,是信号集处于确定的状态。
- 修改block表(信号屏蔽集)
- 函数声明:int sigprocmask(int how,sigset_t* set,sigset_t* oset)
- 读取或更改进程的信号屏蔽集,oset为输出型参数,目的为将原信号备份
- how的三个模式:
- SIG_BLOCK:相当于mask=mask|set,从当前信号屏蔽字中添加阻塞
- SIG_UNBLOCK:相当于mask=mask&(~set),从当前信号屏蔽字中解除阻塞
- SIG_SETMASK:相当于mask=set
- 查pending表(未决信号集)
- 函数声明:int sigpending(sigset_t* set)
- 获取pending位图,set为输出型参数,返回值表示是否成功,成功返回0,错误返回-1,错误码errno被设置
-
- 深入理解信号的保存(内核态与用户态)
- 什么是内核态?执行你写的代码的时候,进程所处于的状态
- 什么是用户态?执行OS的代码的时候,进程所处于的状态
- 上面说到,信号可以不是立即处理,而是在合适的时间处理,哪么啥时候是合适的时间呢?当进程从内核态切换成用户态时,会在OS的指导下,进行信号的检测与处理
- 每个进程的虚拟地址空间中有一块1G大小的内核空间,同时也存在一张内核级页表,每个进程都可以看到同一张内核级页表,所有的进程都可以通过统一的窗口看到同一个OS
- 为了不让用户可以随意的访问OS中的数据和代码,采用了软硬件结合的方案来进行用户态和内核态的切换,在进行用户态向内核态的切换过程中,首先通过CR3寄存器将进程状态由用户态修改为内核态(陷入内核),在本进程的内核空间中找到物理内存中的内核代码进行执行,执行完毕后返回结果给进程。
- 所以OS提供的所有系统调用,内部在执行调用逻辑的时候会修改执行级别
- 哪什么时候进程会由用户态切换为内核态呢?
- 进程的时间片到了,需要切换,就要执行切换的逻辑
- 系统调用
- 信号的捕捉
- 原理/流程
- 如果信号的处理动作是用户自定义函数在信号递达时就调用这个函数,这称为捕捉信号
- 由于信号的处理代码是在用户空间的,过程比较复杂,举例如下
- 用户程序注册SIGQUIT信号的处理函数sighanler。
- 当前正在执行的main函数由于中断或异常切换为内核态
- 在中断处理完毕后要返回用户态的main函数之前检查到有SIGQUIT信号递达
- 内核决定返回用户态后不是继续执行main函数的上下文,而是执行sighandler函数(由于sighandler函数和main函数使用了不同的堆栈空间,他们之间不存在调用和被调用的关系,而是两个独立的控制流程)
- sighandler函数执行完毕返回后自动执行特殊的系统调用sigreturn再次进入内核态。
- 此时如果没有新的信号要递达,这次返回用户态就是恢复main函数的上下文继续执行了。
- 原理/流程
-
-
- 简化为
-
-
- sigaction(查询或设置信号处理方式)
- 头文件:#include<signal.h>
- 函数声明:
- sigaction(查询或设置信号处理方式)
int sigaction(int signum, const struct sigaction* act, struct sigaction* oldact)
-
-
- struct sigaction{
-
void (*sa_handler)(int); //回调方法
void (*sa_sigaction)(int siginfo_t * , void*);
sigset_t sa_mask;//阻塞信号集
int sa_flags;
void (*sa_restorer)(void);//用于支持旧版本的sigaction的函数的信号处理函数地址,一般不使用
};
-
-
- signum:信号,act结构体对象,oldact:输出型参数,记录原来的act对象
- 成功时返回0,失败时返回-1,并设置错误码errno
-
-
- 云服务器:/home/fjq/linux/test21-signal/signal.cc
- gitee
https://gitee.com/qq--1444354226/linux/blob/master/test21-signal/signal.cc
- 认识可重入函数与不可重入函数,是否可重入是函数的一种特性,而不是优缺点,平时的函数大多是不可重入的
- 满足以下两点中的任意一点即为不可重入函数:
- 调用了malloc和free,因为malloc和free是用全局链表来管理的
- 调用了标准I/O库函数,标准I/O库函数很多都是用不可重入的方式来实现的
- 满足以下两点中的任意一点即为不可重入函数:
如上图:main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的 两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续 往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是, main函数和sighandler先后向链表中插入两个节点,而最后只有一个节点真正插入链表中了, 有一个节点就丢失找不到了,造成了内存泄露的问题。
像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为 不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。
- 关键字volatile:杜绝对变量做内存级的优化,保证内存可见性
我们在makefile文件中添加优化选项,对此代码做优化后重新编译运行后,结果如下
答案是 while(!quit); 这条语句,它一条对quit值判断的语句。
未优化前,每一次while循环检测都会把内存中quit的值load到cpu中的寄存器中, quit值修改后不满足条件,cpu中的pc指针向下移动执行后面的代码,直接循环结束
一旦优化后,在main执行流中,认为quit值没有被修改只是被检测,所以只有第一次load到cpu中的寄存器,往后判断不会load了,即使quit修改了寄存器中还是第一次的值,检测发现满足条件继续重复执行刚才的代码,继续死循环
引入volatile关键字就是告诉编译器,保证每次检测,都要尝试从内存中进行数据读取,不要用寄存器中的数据,让内存数据可见。
- SIGCHLD信号
事实上,由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调用sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用sigaction函数自定义的忽略 通常是没有区别的, 但这是一个特例。此方法对于Linux可用,但不保证在其它UNIX系统上都可用。
- 什么是线程
- 线程是进程内部的一个执行流
- 线程是一个执行分支,执行力度比进程更细,调度成本更低(不用切换cache)
- 线程是CPU调度的基本单位,进程是承担分配系统资源的基本实体
- Windows中存在真正的线程,即有线程的结构体,Linux中没有真正意义上的线程,而是用进程模拟的线程
- 线程共享进程数据,但是线程也有自己的一部分数据
- 线程ID
- 一组寄存器:说明线程需要切换,有自己的上下文数据
- 栈:说明线程有自己独立的数据
- errno,信号屏蔽字, 调度优先级
- 线程的优点:
- 创建一个新线程的代价要比创建一个新进程的代价小得多
- 线程切换OS要做的工作少
- 占用资源少
- 能充分利用多处理器的可并行数量
- 等待慢速IO操作结束同时,可执行其他的计算任务
- 计算密集型应用,能在多处理器系统上运行,将计算分解到多个线程
- IO密集型应用,为了提高性能,将IO操作重叠。线程可同时等待不同的IO
- 线程的缺点:
- 性能损失
- 健壮度降低
- 缺乏访问控制
- 编程难度高
- 线程异常
一个线程出现异常,如果不回收信号,会导致进程退出,即其他线程退出
- 页表
为什么修改字符常量区的数据程序会崩溃?
指针里面保存的是指向的字符的虚拟起始地址->进行修改时需要寻址->寻址需要由虚拟地址到物理地址的转化->MMU+查页表->对你的操作进行权限审查->能找到但是操作非法->MMU发生异常->异常转换成信号,发送给目标进程->在进程由内核态切换成用户态时进行信号处理->终止进程,程序崩溃。
我们实际在申请malloc内存时,OS只需要在虚拟地址空间上为我们申请,当我们真正访问空间时(执行自己的代码),)OS才会自动给我们申请或者填充页表(产生缺页中断)+申请具体的物理内存
- POSIX线程库
- 对下将Linux接口封装,对上给用户提供进行线程控制的接口
- 链接这些线程函数库时要使用编译器的-l选项指定库名(-lpthread)
- 使用ps -aL命令查看OS中的线程
- 深入理解线程
- 任何语言,在Linux中使用多线程编程,都要使用-lpthread进行链接
- C++的thread库,底层有条件编译判断当前运行环境,执行适用于Linux或Windows的多线程代码
- 在Linux中,C++的多线程,本质就是对pthread库的封装
- 深入理解线程id
所有线程都要有自己独立的栈结构,主线程用的是进程系统栈,新线程用的是库中提供的栈
-
- 线程局部存储
- 全局变量在已初始化数据段开辟空间,并不属于线程的私有数据,所以被所有线程共享,多个线程对全局变量做修改时,他们的地址相同
- 使用__thread可构建每个线程的局部存储,例如__thread int tmp=10;
- 线程局部存储
- 线程创建(pthread_create)
- 头文件 #include<pthread.h>
- 函数声明 int pthread_create(pthread_t* thread, const pthread_attr_t* attr,
void*(*start_routine)(void*), void* arg);
-
- 参数
- thread:输出型参数,线程创建成功后将通过此指针返回线程标识符
- attr:线程属性,包括线程的栈大小,调度策略,优先级等信息。使用NULL则为默认属性
- start_routine:函数指针,指的是线程启动后要执行的函数。
- arg:线程要执行的函数的参数
- 返回值:成功返回0,失败返回错误码,同上*thread中的内容未定义
- 参数
- 线程等待(pthread_join)
- 函数声明:int pthread_join(pthread_t thread,void** retval);
- 参数:
- thread:需要等待的线程
- retval:输出型参数,用以获取线程函数返回时的退出结果
- 返回值:成功返回0,失败返回错误码
- 线程终止(return/pthread_exit/pthread_cancel)
- 线程函数return。该方法对主线程不适用,从main函数退出相当于exit
- 线程可以使用pthread_exit终止自己
void pthread_exit(void* value_ptr)
value_ptr:线程函数结果,不要指向一个局部变量。相当于return的参数
-
- 一个线程可以调用pthread_cancel来终止同一进程下的其他一个线程。
int pthread_cancel(pthread_t thread);
thread:要取消哪一个进程
- 回去线程自身的id(pthread_self)
- 函数声明 pthread_t pthread_self(void);
- 返回调用线程的ID,该函数始终成功
- 线程分离(pthread_detach)
- 函数声明:int pthread_detach(pthread_t thread);
- 成功返回0,失败返回一个错误码
- 参数内调用pthread_self()函数可分离自己
- 一个线程被分离后无法join,调用join会报错
- 一般在新线程创建成功后,由主线程进行分离
-
- 云服务器:/home/fjq/linux/test22-thread/mythread.hpp
- gitee
https://gitee.com/qq--1444354226/linux/blob/master/test22-thread/mythread.hpp
- 相关概念
- 临界资源:多线程执行流中共享的资源就叫临界资源
- 临界区:每个线程内部,访问临界资源的代码,就叫临界区
- 互斥:任何时刻,互斥保证有且仅有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用。
- 原子性:不会被任何调度机制打断的操作,只有两态,要么完成,要么未完成
- 互斥量mutex
- 初始化互斥量
- 静态分配:
- 初始化互斥量
pthread_mutex_t mtx=PTHREAD_MUTEX_INITIALIZER;
-
-
- 动态分配
- 函数声明:int pthread_mutex_init(pthread_mutex_t* mutex, const pthread_mutexattr_t* attr);
- mutex:要初始化的互斥量
- attr:使用NULL即可
- 动态分配
- 销毁互斥量:
- 使用PTHREAD_MUTEX_INITIALIZER初始化的互斥量不需要销毁
- 不要销毁一个已经加锁的互斥量
- 已经销毁的互斥量,要确保后面不会有线程再尝试加锁
- int pthread_mutex_destory(pthread_mutex_t* mutex)
- 互斥量的加锁与解锁
- 加锁: int pthread_mutex_lock(pthread_mutex_t* mutex);
- 解锁: int pthread_mutex_unlock(pthread_mutex_t* mutex);
- 互斥量未处于加锁状态时,lock会将互斥量锁定,同时返回成功
- 发起lock时,若其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量加锁,但是没有竞争到,那么pthread_mutex_lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。
-
- 锁的原理:
- 为了实现互斥操作,大多数体系结构都提供了swap或exchange指令,该指令的作用时将寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性。
- 即使再多处理器平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期
- 概念
- 线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且再没有锁保护的情况下,会出现问题。
- 重入:同一个函数被不同的执行流调用,当前一个流还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或任何问题,则成该函数是可重入函数,否则就是不可重入函数。
- 常见线程不安全的情况
- 不保护共享变量的函数
- 函数状态随着被调用,状态发生变化的函数
- 返回指向静态变量指针的函数
- 调用线程不安全函数的函数
- 常见线程安全的情况
- 每个线程对全局变量或者静态变量只有读取的权限,没有写入的权限
- 多个线程之间的切换不会导致该接口的执行结果存在二义性
- 常见不可重入的情况
- 调用了malloc/free函数,因为malloc函数是用全局链表来管理的
- 调用了标准IO库函数,标准IO库的很多实现都是以不可重入的方式使用全局数据结构
- 可重入函数体内使用了静态的数据结构
- 常见可重入的情况
- 不使用全局变量或静态变量
- 不使用用malloc或则new开辟出的空间
- 不调用不可重入函数
- 不返回静态或全局数据,所有数据都由函数的调用者提供
- 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据
- 可重入与线程安全的联系
- 函数是可重入的,那就是线程安全的
- 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
- 如果一个函数中有全局变量,那么这个函数既不是线程安全的也不是可重入的
- 可重入与线程安全的区别
- 可重入函数是线程安全函数的一种
- 线程安全不一定是可重入的,而可重入函数一定是线程安全的
- 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的
- 死锁
死锁是指一组进程中各个进程均占有不会释放的资源,但因互相申请被其他进程所占用读的不会释放的资源而处于的一种永久等待状态
- 死锁的四个必要条件
- 互斥条件:一个资源每次只能被一个执行流使用
- 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
- 不剥夺条件:一个执行流已获得的资源,在未被使用前,不能强行剥夺
- 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系
- 如何避免死锁
- 核心思想:破坏死锁的4个必要条件中的任意一个即可
- 能不用锁就不要用锁
- 主动释放锁
- 按照顺序申请锁
- 控制线程统一释放锁
-
- 云服务器:/home/fjq/linux/test22-thread
- gitee
https://gitee.com/qq--1444354226/linux/blob/master/test22-thread
- 同步概念
- 为什么要有同步?
如果一个线程频繁的申请释放锁,释放锁后又迅速申请锁,就会造成其他线程饥饿的问题。所以我们要在安全的规则下,让多线程访问资源具有一定的顺序性,为了解决饥饿问题,我们提出了线程同步,让多线程协同工作。
-
- 什么是同步?
在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步。
- 条件变量
- 条件变量是一种同步机制,用于阻塞一个或多个线程,直到收到另一个线程的通知(通常是因为某个条件成为了真)。条件变量通常与互斥锁一起使用,以确保在访问共享资源或更新条件时的线程安全
- 初始化条件变量
- 静态分配
- pthread_cond_t cond=PTHREAD_COND_INITIALIZER;
- 动态分配
- int pthread_cond_init(pthread_cond_t* cond,
- 静态分配
const pthread_condattr_t* attr);
-
-
-
- 参数:
- cond:要初始化的条件变量
- attr:NULL
- 参数:
-
- 销毁条件变量
- int pthread_cond_destroy(pthread_cond_t* cond)
- 等待
- int pthread_cond_wait(pthread_cond_t* cond,pthread_mutex_t* mutex);
- 参数:
- cond:要在哪一个条件变量上等待
- mutex:互斥量
- 唤醒等待
- 唤醒一个线程
-
int pthread_cond_signal(pthread_cond_t* cond);
-
-
- 唤醒所有线程
-
int pthread_cond_broadcast(pthread_cond_t* cond);
- 321原则
- cp问题思路与深入理解
- 缓冲区必须被所有线程看到(生产者和消费者线程)
- 缓冲区一定是一个被多线程并发访问的临界资源
- 多线程一定要保护临界资源的安全
- 综上,程序员要自己维护线程互斥与同步的关系
- 为什么要使用生产者消费者模型与生产者消费者模型的优点
- 生产者消费者模型就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而是通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列中取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。
- 优点:
- 解耦
- 支持并发
- 支持忙闲不均
- 基于BlockingQueue的生产者消费者模型
- 什么是BlockingQueue(阻塞队列)
当队列为空时,从队列获取元素的操作将被阻塞,直到队列中被放入了元素;当队列满时,往队列中存放元素的操作也会被阻塞,直到有元素被从队列中取出
-
- 该生产者消费者模型为什么高效?
该模型将任务的工序拆开,一组线程分为生产者,另一组线程分为消费者。充分利用生产者的阻塞时间,用以提前准备好生产资源;也充分利用了消费者计算耗时的问题,让消费者线程将更多的时间花费在计算上,而不是抢不到锁造成线程干等。
生产者消费者模型的意义在于让生产者的重点放在生产数据的过程中,而不是将数据写入到阻塞队列;消费者的重点放在处理数据,而不是读取数据
-
- 云服务器:/home/fjq/linux/test23-thread_producer_and_consumer/BlockQueue.hpp
- gitee
https://gitee.com/qq--1444354226/linux/blob/master/test23-thread_producer_and_consumer/BlockQueue.hpp
- 概念
- 信号量是描述临界资源中资源数目的
- 每一个线程,在访问对应资源的时候,先申请信号量,申请成功,表示该线程允许使用该资源,申请不成功,目前无法使用该资源
- 信号量是一种对资源的预定机制
- 信号量已经是资源的计数器了,申请信号量成功,本身就表明资源可用,申请信号量失败则表明资源不可用----->把判断转换成信号量的申请行为
- 接口(头文件#include<semaphore.h>)
- 初始化
- 函数声明:int sem_init(sem_t* sem,int pshared ,unsigned int value)
- 参数:
- sem:输出型参数,要初始化的信号量
- pshared:0表示线程间共享,非零表示进程间共享
- value:信号量初始值,即初始化时该信号量表示的资源有几个
- 销毁
- 初始化
int sem_destroy(sem_t* sem)
-
- 等待信号量(P操作--),会将信号量的值-1
int sem_wait(sem_t* sem);
-
- 发布信号量(V操作++),表示资源使用完毕,可以归还,信号量值+1
int sem_post(sem_t* sem);
- 基于RingQueue的生产者消费者模型
- RingQUeue(环形队列):采用数组模拟,用模运算来模拟环装特性。多预留一个空的位置,用于满或空的状态判断
- 构建cp问题
- 生产者关心环形队列的空间,消费者关心环形队列的数据
- 只要信号量不为0,那么就表示资源可用,表示线程可以访问
- 生产和消费行为可以同时进行
- 在初始化后,队列为空,此时指向同一个位置,存在竞争,让生产者先运行
- 队列中数据满时,让消费者先运行
- 多生产者多消费者情况下,我们同一时间最多只允许一个生产者和一个消费者对资源进行访问,所以必须加锁,而且是现申请信号量,再加锁
-
- 云服务器:/home/fjq/linux/test23-thread_producer_and_consumer/RingQueue.hpp
- gitee
https://gitee.com/qq--1444354226/linux/blob/master/test23-thread_producer_and_consumer/RingQueue.hpp
- 概念
一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。
- 应用场景
- 需要大量的线程来完成任务,且完成任务的时间比较短。web服务器完成网页请求这样的任务,使用线程池技术是非常合适的。
- 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求
- 接受突发性的大量请求,但不至于是服务器因此产生大量线程的应用
- 示例
- 创建固定数量线程,循环从任务队列中获取任务对象
- 获取到任务对象后,执行任务对象中的任务接口
-
- 云服务器:/home/fjq/linux/test24-thread_pool
- gitee
https://gitee.com/qq--1444354226/linux/tree/master/test24-thread_pool
- STL,智能指针的线程安全性,其他锁,读者写者
- STL中的容器是否是线程安全的?
不是。STL的设计初衷是将性能挖掘到极致,而一旦涉及到加锁保证线程安全,会对性能造成巨大的影响。而且不同的容器,加锁方式的不同,性能也可能不同。因此STL默认不是线程安全的。如果需要在多线程环境下使用,往往需要调用者自行保证线程安全。
- 智能指针是否是线程安全的?
- unique_ptr:由于只在当前代码块生效且不允许拷贝赋值,不涉及线程安全(硬要说的话是线程安全的)。
- shared_ptr:shared_ptr自带的计数是线程安全的,但是他管理的数据不是线程安全的
- weak_ptr: 从weak_ptr转换回shared_ptr时,可能会设计对共享计数的检查,这一操作本身是线程安全的。但是指向的资源不是线程安全的
- 其他常见的锁
- 悲观锁
悲观锁是一种悲观思想的加锁机制,他假定当前环境时写多读少,即认为遇到并发写的可能性高。因此每次在读取或写入数据时都会加锁,以确保数据的一致性和完整性。悲观锁适用于写操作较多的场景,可以防止脏读,幻读,不可重复读等并发问题。然而,由于悲观锁会阻塞其他线程对数据的访问,可能会影响系统的并发性能。
-
- 乐观锁
乐观锁采用更加宽松的加锁机制,它假定当前环境是读多写少,即认为遇到并发写的概率比较低。乐观锁在读取数据时不会加锁,但在更新数据时会检查数据是否已被其他线程修改。如果数据自读取以来未被修改,则进行更新操作;如果数据已被修改,则更新操作会失败。乐观锁通常通过版本号或时间戳等机制实现。
-
- CAS锁
CAS锁是一种基于乐观锁的同步机制,他通过比较并替换的方式来实现线程间的同步。CAS操作包括三个步骤:读取内存值,比较内存值与预期值,如果相等则更新内存值。CAS锁利用了硬件提供的原子性高作来保证线程间的同步,适用于需要频繁进行原子性操作的场景。
-
- 自旋锁
自旋锁是一种避免线程切换的开销的锁机制。当线程尝试获取锁时,如果锁已被其他线程占用,则当前线程会进入自旋状态,即执行一个忙循环(自旋),并周期性的检查锁是否被释放。如果锁被释放,则当前线程会立即获取锁并继续执行;如果锁仍然被占用,则继续自旋等待。自旋锁适用于锁持有时间较短的场景,可以避免线程切换的开销。然而,如果锁持有时间较长,自旋锁会浪费处理器资源
- 读者写者问题
- 3种关系
- 写者与写者:互斥关系
- 读者与读者:无关系,谁都可以读
- 写者与读者:互斥与同步关系。
- 互斥:写者在写数据时,必须禁止所有读者读取数据,因为可能会读到不完整或错误的数据
- 同步:当写者写入完成时,可唤醒读者进行读取
- 2种角色
- 3种关系
读者和写者
-
- 1个交易场所
缓冲区