株洲能建网站的有哪些,营销型网站的三大特点,wordpress程序如何降低版本,常州外贸建站day2
vim的三种工作模式
命令模式 vi hello.c zz 保存退出
2.编辑模式 i a o s #xff08;有大写#xff09;可以写东西
3.末行模式#xff1a; 文本和末行模式不能直接切换 要切换回命令模式 再到末行模式#xff0c;w:保存 q:退出 按两次esc回到命令模式
vim的基本…day2
vim的三种工作模式
命令模式 vi hello.c zz 保存退出
2.编辑模式 i a o s 有大写可以写东西
3.末行模式 文本和末行模式不能直接切换 要切换回命令模式 再到末行模式w:保存 q:退出 按两次esc回到命令模式
vim的基本操作-跳转和删除
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ub0TQGxh-1684633973964)(D:\Typora笔记\c\Linux系统编程.assets\image-20221112123423275.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AoxNPfnp-1684633973965)(D:\Typora笔记\c\Linux系统编程.assets\image-20221112123457163.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BCOgGldQ-1684633973966)(D:\Typora笔记\c\Linux系统编程.assets\image-20221112123511760.png)]
自动化格式程序 ggG命令模式
大括号对应%(命令模式)
删除单个字符 x(命令模式)执行结束工作模式不变。
删除整个单词dw(命令模式)光标置于首字母进行操作。
删除光标至行尾 D(命令模式)
光标移至行首$光标移至行尾
删除光标至行首d0(命令模式)
-vim删除操作
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ji9Gs8X0-1684633973966)(D:\Typora笔记\c\Linux系统编程.assets\image-20221112132216369.png)]
~/.vimrc
vim config file
date 2018-12-26
Created by bert
blog:http://blog.51cto.com/zpf666全局配置关闭vi兼容模式
set nocompatible设置历史记录步数
set history1000开启相关插件
侦测文件类型
filetype on
载入文件类型插件
filetype plugin on
为特定文件类型载入相关缩进文件
filetype indent on当文件在外部被修改时自动更新该文件
set autoread激活鼠标的使用
set mousea
set selectionexclusive
set selectmodemouse,key保存全局变量
set viminfo!带有如下符号的单词不要被换行分割
set iskeyword_,$,,%,#,-通过使用: commands命令告诉我们文件的哪一行被改变过
set report0被分割的窗口间显示空白便于阅读
set fillcharsvert:\ ,stl:\ ,stlnc:\
字体和颜色自动开启语法高亮
syntax enable设置字体
set guifontdejaVu\ Sans\ MONO\ 10
set guifontCourier_New:h10:cANSI设置颜色
colorscheme desert高亮显示当前行
set cursorline
hi cursorline guibg#00ff00
hi CursorColumn guibg#00ff00高亮显示普通txt文件需要txt.vim脚本
au BufRead,BufNewFile * setfiletype txt
代码折叠功能激活折叠功能
set foldenable
set nofen这个是关闭折叠功能设置按照语法方式折叠可简写set fdmXX
有6种折叠方法
manual 手工定义折叠
indent 更多的缩进表示更高级别的折叠
expr 用表达式来定义折叠
syntax 用语法高亮来定义折叠
diff 对没有更改的文本进行折叠
marker 对文中的标志进行折叠
set foldmethodmanual
set fdl0这个是不选用任何折叠方法设置折叠区域的宽度
如果不为0则在屏幕左侧显示一个折叠标识列
分别用“-”和“”来表示打开和关闭的折叠
set foldcolumn0设置折叠层数为3
setlocal foldlevel3设置为自动关闭折叠
set foldcloseall用空格键来代替zo和zc快捷键实现开关折叠
zo O-pen a fold (打开折叠)
zc C-lose a fold (关闭折叠)
zf F-old creation (创建折叠)
nnoremap space ((foldclosed(line(.)) 0) ? zc : zo)CR
文字处理使用空格来替换Tab
set expandtab设置所有的Tab和缩进为4个空格
set tabstop4设定和命令移动时的宽度为4
set shiftwidth4使得按退格键时可以一次删除4个空格
set softtabstop4
set smarttab缩进自动缩进继承前一行的缩进
set autoindent 命令打开自动缩进是下面配置的缩写
可使用autoindent命令的简写即“:set ai”和“:set noai”
还可以使用“:set ai sw4”在一个命令中打开缩进并设置缩进级别
set ai
set cindent智能缩进
set si自动换行”
set wrap设置软宽度
set sw4行内替换
set gdefault
Vim 界面增强模式中的命令行自动完成操作
set wildmenu显示标尺
set ruler设置命令行的高度
set cmdheight1显示行数
set nu不要图形按钮
set go在执行宏命令时不进行显示重绘在宏命令执行完成后一次性重绘以便提高性能
set lz使回格键backspace正常处理indent, eol, start等
set backspaceeol,start,indent允许空格键和光标键跨越行边界
set whichwrap,,h,l设置魔术
set magic关闭遇到错误时的声音提示
关闭错误信息响铃
set noerrorbells关闭使用可视响铃代替呼叫
set novisualbell高亮显示匹配的括号([{和}])
set showmatch匹配括号高亮的时间单位是十分之一秒
set mat2光标移动到buffer的顶部和底部时保持3行距离
set scrolloff3搜索逐字符高亮
set hlsearch
set incsearch搜索时不区分大小写
还可以使用简写“:set ic”和“:set noic”
set ignorecase用浅色高亮显示当前行
autocmd InsertLeave * se nocul
autocmd InsertEnter * se cul输入的命令显示出来看的清楚
set showcmd
编码设置设置编码
set encodingutf-8
set fencsutf-8,ucs-bom,shift-jis,gb18030,gbk,gb2312,cp936设置文件编码
set fileencodingsutf-8设置终端编码
set termencodingutf-8设置语言编码
set langmenuzh_CN.UTF-8
set helplangcn
其他设置开启新行时使用智能自动缩进
set smartindent
set cin
set showmatch在处理未保存或只读文件的时候弹出确认
set confirm隐藏工具栏
set guioptions-T隐藏菜单栏
set guioptions-m置空错误铃声的终端代码
set vb t_vb显示状态栏默认值为1表示无法显示状态栏
set laststatus2状态行显示的内容
set statusline%F%m%r%h%w\ [FORMAT%{ff}]\ [TYPE%Y]\ [POS%l,%v][%p%%]\ %{strftime(\%d/%m/%y\ -\ %H:%M\)}粘贴不换行问题的解决方法
set pastetoggleF9设置背景颜色
set backgrounddark文件类型自动检测代码智能补全
set completeoptlongest,preview,menu共享剪切板
set clipboardunnamed从不备份
set nobackup
set noswapfile自动保存
set autowrite显示中文帮助
if version 603set helplangcnset encodingutf-8
endif设置高亮相关项
highlight Search ctermbgblack ctermfgwhite guifgwhite guibgblack
在shell脚本开头自动增加解释器以及作者等版权信息新建.py,.cc,.sh,.java文件自动插入文件头
autocmd BufNewFile *.py,*.cc,*.sh,*.java exec :call SetTitle()
定义函数SetTitle自动插入文件头
func SetTitle()if expand (%:e) shcall setline(1, #!/bin/bash)call setline(2, #Author:bert)call setline(3, #Blog:http://blog.51cto.com/zpf666)call setline(4, #Time:.strftime(%F %T))call setline(5, #Name:.expand(%))call setline(6, #Version:V1.0)call setline(7, #Description:This is a production script.)endif
endfun静态库的制作 将.c 文件编译成.o文件 gcc -c fun1.c fun2.c
2.使用ar命令将.o文件打包成.a文件
ar rcs libtest1.c(库的名字) fun1.o fun2.o
静态库的使用
gcc -o main1 main.c -I./ -L./ -Itest1静态库和头文件再同一目录下
gcc -o main1 main.c -I./include -L./lib -Itest1(不在同一路径下)
动态库的制作: 1 将.c文件编译成.o文件 gcc -fpic -c fun1.c fun2.c 2 使用gcc将.o文件编译成库文件 gcc -shared fun1.o fun2.o -o libtest2.so
动态库的使用: gcc -o main2 main.c -I./include -L./lib -ltest2
动态库文件在编译的时候, 连接器需要使用参数-L找到库文件所在的路径; 在执行的时候, 是加载器ldd根据动态库的路径进行加载的, 与编译的时候用的-L 指定的路径无关.
最常用的解决办法: 将LD_LIBRARY_PATH环境变量加到用户级别的配置文件~/.bashrc中, 然后生效(. ~/.bashrc source ~/.bashrc 退出终端然后再登录)
GCC简介以及工作流程和常用选项
gcc简介
编辑器(如vi、记事本)是指我用它来写程序的编辑代码而我们写的代码语句电脑是不懂的我们需要把它转成电脑能懂的语句编译器就是这样的转化工具。就是说我们用编辑器编写程序由编译器编译后才可以运行
编译器是将易于编写、阅读和维护的高级计算机语言翻译为计算机能解读、运行的低级机器语言的程序。
gccGNU Compiler CollectionGNU 编译器套件是由 GNU 开发的编程语言编译器。gcc原本作为GNU操作系统的官方编译器现已被大多数类Unix操作系统如Linux、BSD、Mac OS X等采纳为标准的编译器gcc同样适用于微软的Windows。
工作流程
gcc工作的流程 ls 1hello.c第一步: 进行预处理$ gcc -E 1hello.c -o 1hello.i 第二步: 生成汇编文件$ gcc -S 1hello.i -o 1hello.s 第三步: 生成目标代码$ gcc -c 1hello.s -o 1hello.o 第四步: 生成可以执行文件$ gcc 1hello.o -o 1hello 第五步: 执行 $ ./1hello hello itcast直接将源文件生成一个可以执行文件
$ gcc 1hello.c -o 1hello dengitcast:~/share/3rd/1gcc$ ./1hello hello itcast如果不指定输出文件名字, gcc编译器会生成一个默认的可以执行a.out dengitcast:~/share/3rd/1gcc$ gcc 1hello.c
dengitcast:~/share/3rd/1gcc$ ls 1hello 1hello.c 1hello.i 1hello.o 1hello.s a.out dengitcast:~/share/3rd/1gcc$ ./a.out
hello itcast、gcc常用选项
选项作用-o file指定生成的输出文件名为file-E只进行预处理-S(大写)只进行预处理和编译-c(小写)只进行预处理、编译和汇编-v / --version查看gcc版本号-g包含调试信息-On n0~3编译优化n越大优化得越多-Wall提示更多警告信息-D编译时定义宏
显示所有的警告信息 gcc -Wall test.c 将警告信息当做错误处理 gcc -Wall -Werror test.c 静态库连接和动态库连接
链接分为两种静态链接和动态链接 静态链接 静态链接由链接器在链接时将库的内容加入到可执行程序中。 优点 对运行环境的依赖性较小具有较好的兼容性 缺点 生成的程序比较大需要更多的系统资源在装入内存时会消耗更多的时间库函数有了更新必须重新编译应用程序 动态链接 动态链接连接器在链接时仅仅建立与所需库函数的之间的链接关系在程序运行时才将所需资源调入可执行程序。 优点 在需要的时候才会调入对应的资源函数简化程序的升级有着较小的程序体积实现进程之间的资源共享避免重复拷贝 缺点 依赖动态库不能独立运行动态库依赖版本问题严重 3.静态、动态编译对比 前面我们编写的应用程序大量用到了标准库函数系统默认采用动态链接的方式进行编译程序若想采用静态编译加入-static参数。 可以看到大小不一样 如果是一个很庞大的代码 那静态库大小会变成很大 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rIPlYZXb-1684633973967)(D:\Typora笔记\c\Linux系统编程.assets\image-20230505163136717.png)]
静态库制作和使用
所谓“程序库”简单说就是包含了数据和执行码的文件。其不能单独执行可以作为其它执行程序的一部分来完成某些功能。
库的存在可以使得程序模块化可以加快程序的再编译可以实现代码重用,可以使得程序便于升级。
程序库可分静态库(static library)和共享库(shared library)。
静态库制作和使用
静态库可以认为是一些目标代码的集合是在可执行程序运行前就已经加入到执行码中成为执行程序的一部分。
按照习惯,一般以“.a”做为文件后缀名。静态库的命名一般分为三个部分
前缀lib库名称自己定义即可后缀.a
所以最终的静态库的名字应该为libxxx.a
1 静态库制作
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-77mO84MK-1684633973968)(D:\Typora笔记\c\Linux系统编程.assets\image-20230505164313804.png)]
步骤1将c源文件生成对应的.o文件
guojiahuiguojiahui-virtual-machine:~/Heima/day1$ gcc -c add.c -o add.步骤2使用打包工具ar将准备好的.o文件打包为.a文件 libtest.a
在使用ar工具是时候需要添加参数rcs
r更新c创建s建立索引
2静态库使用
静态库制作完成之后需要将.a文件和头文件一起发布给用户。
假设测试文件为main.c静态库文件为libtest.a头文件为head.h
编译命令 $ gcc test.c -L./ -I./ -ltest -o test 参数说明
-L表示要连接的库所在目录-I./: I(大写i) 表示指定头文件的目录为当前目录-l(小写L)指定链接时需要的库去掉前缀和后缀
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0qjybuUq-1684633973968)(D:\Typora笔记\c\Linux系统编程.assets\image-20230505164553292.png)]
动态库制作和使用
共享库在程序编译时并不会被连接到目标代码中而是在程序运行是才被载入。不同的应用程序如果调用相同的库那么在内存里只需要有一份该共享库的实例规避了空间浪费问题。
动态库在程序运行是才被载入也解决了静态库对程序的更新、部署和发布页会带来麻烦。用户只需要更新动态库即可增量更新。
按照习惯,一般以“.so”做为文件后缀名。共享库的命名一般分为三个部分
前缀lib库名称自己定义即可后缀.so
所以最终的动态库的名字应该为libxxx.so
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tRuIf56V-1684633973970)(D:\Typora笔记\c\Linux系统编程.assets\clip_image002-1527511145606.jpg)]
1动态库制作
步骤一生成目标文件此时要加编译选项-fPICfpic $ gcc -fPIC -c add.c 参数-fPIC 创建与地址无关的编译程序picposition independent code是为了能够在多个应用程序间共享。
步骤二生成共享库此时要加链接器选项: -shared指定生成动态链接库 dengitcast:~/test/5share_lib$ gcc -shared add.o sub.o mul.o div.o -o libtest.so 步骤三: 通过nm命令查看对应的函数 $ nm libtest.so | grep add 00000000000006b0 T add $ nm libtest.so | grep sub 00000000000006c4 T sub ldd查看可执行文件的依赖的动态库 $ ldd test linux-vdso.so.1 (0x00007ffcf89d4000) libtest.so /lib/libtest.so (0x00007f81b5612000) libc.so.6 /lib/x86_64-linux-gnu/libc.so.6 (0x00007f81b5248000) /lib64/ld-linux-x86-64.so.2 (0x00005562d0cff000) 2动态库测试
引用动态库编译成可执行文件跟静态库方式一样 $ gcc test.c -L. -I. -ltest (-I. 大写i -ltest 小写L) 报错了 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-no2kpmTK-1684633973970)(D:\Typora笔记\c\Linux系统编程.assets\image-20230505215355781.png)]
原因以及解决办法 当系统加载可执行代码时候能够知道其所依赖的库的名字但是还需要知道绝对路径。此时就需要系统动态载入器(dynamic linker/loader)。 对于elf格式的可执行程序是由ld-linux.so*来完成的它先后搜索elf文件的 DT_RPATH段 — 环境变量LD_LIBRARY_PATH — /etc/ld.so.cache文件列表 — /lib/, /usr/lib目录找到库文件后将其载入内存。 1.拷贝到标准库 不推荐 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bOiV2GNA-1684633973971)(D:\Typora笔记\c\Linux系统编程.assets\image-20230505215610439.png)] 3如何让系统找到动态库 拷贝自己制作的共享库到/lib或者/usr/lib(不能是/lib64目录)临时设置LD_LIBRARY_PATH export LD_LIBRARY_PATH$LD_LIBRARY_PATH:库路径 永久设置,把export LD_LIBRARY_PATH$LD_LIBRARY_PATH:库路径设置到~/.bashrc或者 /etc/profile文件中 $ vim ~/.bashrc 最后一行添加如下内容: export LD_LIBRARY_PATH$LD_LIBRARY_PATH:路径 将其添加到 /etc/ld.so.conf文件中 编辑/etc/ld.so.conf文件加入库文件所在目录的路径 运行sudo ldconfig -v该命令会重建/etc/ld.so.cache文件 dengitcast:~/share/3rd/2share_test$ sudo vim /etc/ld.so.conf 文件最后添加动态库路径(绝对路径) 使用符号链接 但是一定要使用绝对路径 $ sudo ln -s /home/deng/test/6share_test/libtest.so /lib/libtest.so
day3
Makefile简介
一个工程中的源文件不计其数其按类型、功能、模块分别放在若干个目录中makefile定义了一系列的规则来指定哪些文件需要先编译哪些文件需要后编译哪些文件需要重新编译甚至于进行更复杂的功能操作因为 makefile就像一个Shell脚本一样其中也可以执行操作系统的命令。
Makefile带来的好处就是——“自动化编译”一旦写好只需要一个make命令整个工程完全自动编译极大的提高了软件开发的效率。make是一个命令工具是一个解释makefile中指令的命令工具一般来说大多数的IDE都有这个命令比如Delphi的make[Visual C](https://baike.baidu.com/item/Visual C%2B%2B)的nmakeLinux下GNU的make。可见makefile都成为了一种在工程方面的编译方法。
make主要解决两个问题
*1) 大量代码的关系维护*
大项目中源代码比较多手工维护、编译时间长而且编译命令复杂难以记忆及维护
把代码维护命令及编译命令写在makefile文件中然后再用make工具解析此文件自动执行相应命令可实现代码的合理编译
*2) 减少重复编译时间*
n 在改动其中一个文件的时候能判断哪些文件被修改过可以只对该文件进行重新编译然后重新链接所有的目标文件节省编译时间
Makefile文件命名规则
makefile和Makefile都可以推荐使用Makefile。
make工具的安装 sudo apt install make Makefile语法规则
一条规则 目标依赖文件列表 命令列表 Makefile基本规则三要素
1目标
通常是要产生的文件名称目标可以是可执行文件或其它obj文件也可是一个动作的名称
2依赖文件
用来输入从而产生目标的文件一个目标通常有几个依赖文件可以没有
3命令
make执行的动作一个规则可以含几个命令可以没有有多个命令时每个命令占一行
举例说明
测试代码
test:echo hello world
test:test1 test2echo test
test1:echo test1
test2:echo test2make命令格式
make是一个命令工具它解释Makefile 中的指令应该说是规则。
make命令格式
make [ -f file ][ options ][ targets ]
1.[ -f file ]
make默认在工作目录中寻找名为GNUmakefile、makefile、Makefile的文件作为makefile输入文件-f 可以指定以上名字以外的文件作为makefile输入文件
l
2.[ options ]
-v 显示make工具的版本信息-w 在处理makefile之前和之后显示工作路径-C dir读取makefile之前改变工作路径至dir目录-n只打印要执行的命令但不执行-s执行但不显示执行的命令
3.[ targets ] 若使用make命令时没有指定目标则make工具默认会实现makefile文件内的第一个目标然后退出 指定了make工具要实现的目标目标可以是一个或多个多个目标间用空格隔开。
make命令格式
make是一个命令工具它解释Makefile 中的指令应该说是规则。
make命令格式
make [ -f file ][ options ][ targets ]
1.[ -f file ]
make默认在工作目录中寻找名为GNUmakefile、makefile、Makefile的文件作为makefile输入文件-f 可以指定以上名字以外的文件作为makefile输入文件
l
2.[ options ]
-v 显示make工具的版本信息-w 在处理makefile之前和之后显示工作路径-C dir读取makefile之前改变工作路径至dir目录-n只打印要执行的命令但不执行-s执行但不显示执行的命令
3.[ targets ]
若使用make命令时没有指定目标则make工具默认会实现makefile文件内的第一个目标然后退出指定了make工具要实现的目标目标可以是一个或多个多个目标间用空格隔开。
Makefile工作原理
1若想生成目标, 检查规则中的依赖条件是否存在,如不存在,则寻找是否有规则用来 生成该依赖文件
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xPzkbE1X-1684633973971)(D:\Typora笔记\c\Linux系统编程.assets\image-20230506122603448.png)]
2 检查规则中的目标是否需要更新必须先检查它的所有依赖,依赖中有任一个被更新,则目标必须更新
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Sd1cZ68U-1684633973972)(D:\Typora笔记\c\Linux系统编程.assets\clip_image002-1527647235911.jpg)]
总结
Ø 分析各个目标和依赖之间的关系
Ø 根据依赖关系自底向上执行命令
Ø 根据修改时间比目标新,确定更新
Ø 如果目标不依赖任何条件,则执行对应命令,以示更新
Makefile示例
测试程序 test.c add.c sub.c mul.c div.c
6.1 最简单的Makefile test:test.c add.c sub.c mul.c div.cgcc test.c add.c sub.c mul.c div.c -o test缺点效率低修改一个文件所有文件会被全部编译
6.2 第二个版本Makefile test:test.o add.o sub.o mul.o div.ogcc test.o add.o sub.o mul.o div.o -o testtest.o:test.cgcc -c test.c
add.o:add.cgcc -c add.c
sub.o:sub.cgcc -c sub.c
mul.o:mul.cgcc -c mul.c
div.o:div.cgcc -c div.c
Makefile中的变量
在Makefile中使用变量有点类似于C语言中的宏定义使用该变量相当于内容替换使用变量可以使Makefile易于维护,修改内容变得简单变量定义及使用。
7.1 自定义变量
1定义变量方法
变量名变量值
2引用变量
( 变量名 ) 或 (变量名)或 (变量名)或{变量名}
3makefile的变量名
makefile变量名可以以数字开头变量是大小写敏感的变量一般都在makefile的头部定义变量几乎可在makefile的任何地方使用
示例 #变量
OBJSadd.o sub.o mul.o div.o test.o
TARGETtest$(TARGET):$(OBJS)gcc $(OBJS) -o $(TARGET) add.o:add.cgcc -c add.c -o add.osub.o:sub.cgcc -c sub.c -o sub.omul.o:mul.cgcc -c mul.c -o mul.odiv.o:div.cgcc -c div.c -o div.otest.o:test.cgcc -c test.c -o test.oclean:rm -rf $(OBJS) $(TARGET)
除了使用用户自定义变量makefile中也提供了一些变量变量名大写供用户直接使用我们可以直接对其进行赋值。 CC gcc #arm-linux-gcc CPPFLAGS : C预处理的选项 如:-I CFLAGS: C编译器的选项 -Wall -g -c LDFLAGS : 链接器选项 -L -l 7.2 自动变量
$: 表示规则中的目标$: 表示规则中的第一个条件$^: 表示规则中的所有条件, 组成一个列表, 以空格隔开,如果这个列表中有重复的项则消除重复项。
注意自动变量只能在规则的命令中中使用
参考示例 #变量
OBJSadd.o sub.o mul.o div.o test.o add.o
TARGETtest
CCgcc#$: 表示目标
#$: 表示第一个依赖
#$^: 表示所有的依赖$(TARGET):$(OBJS)#$(CC) $(OBJS) -o $(TARGET) $(CC) $^ -o $echo $echo $echo $^add.o:add.c$(CC) -c $ -o $ sub.o:sub.c$(CC) -c $ -o $ mul.o:mul.c$(CC) -c $ -o $ div.o:div.c$(CC) -c $ -o $ test.o:test.c$(CC) -c $ -o $clean:rm -rf $(OBJS) $(TARGET)
7.3 模式规则
模式规则示例: %.o:%.c $(CC) -c $(CFLAGS) $(CPPFLAGS) $ -o $ Makefile第三个版本 OBJStest.o add.o sub.o mul.o div.o
TARGETtest
$(TARGET):$(OBJS)gcc $(OBJS) -o $(TARGET) %.o:%.cgcc -c $ -o $Makefile中的函数
makefile中的函数有很多在这里给大家介绍两个最常用的。 wildcard – 查找指定目录下的指定类型的文件 src $(wildcard *.c) //找到当前目录下所有后缀为.c的文件,赋值给src patsubst – 匹配替换 obj $(patsubst %.c,%.o, $(src)) //把src变量里所有后缀为.c的文件替换成.o 在makefile中所有的函数都是有返回值的。
Makefile第四个版本
SRC$(wildcard *.c)
OBJS$(patsubst %.c, %.o, $(SRC))
TARGETtest
$(TARGET):$(OBJS)gcc $(OBJS) -o $(TARGET) %.o:%.cgcc -c $ -o $Makefile中的伪目标
clean用途: 清除编译生成的中间.o文件和最终目标文件
make clean 如果当前目录下有同名clean文件则不执行clean对应的命令解决方案
Ø 伪目标声明: .PHONY:clean
声明目标为伪目标之后makefile将不会该判断目标是否存在或者该目标是否需要更新
clean命令中的特殊符号
“-”此条命令出错make也会继续执行后续的命令。如:“-rm main.o”“”不显示命令本身,只显示结果。如:“echo clean done”
Makefile第五个版本 SRC$(wildcard *.c)
OBJS$(patsubst %.c, %.o, $(SRC))
TARGETtest
$(TARGET):$(OBJS)gcc $(OBJS) -o $(TARGET) %.o:%.cgcc -c $ -o $
.PHONY:clean
clean:rm -rf $(OBJS) $(TARGET)总结 一条规则两个函数三个变量。
gdb调试
gdb调试 : gdb是在程序运行的结果与预期不符合的时候, 可以使用gdb进行调试, 特别注意的是: 使用gdb调试需要在编译的时候加-g参数.
gcc -g -c hello.c gcc -o hello hello.o
文件IO
#includestdio.h #includestdlib.h #includestring.h #includesys/types.h #includeunistd.h #includesys/stat.h #includefcntl.h
文件描述符
在 Linux 的世界里一切设备皆文件。我们可以系统调用中 I/O 的函数Iinput输入Ooutput输出对文件进行相应的操作 open()、close()、write() 、read() 等。
打开现存文件或新建文件时系统内核会返回一个文件描述符文件描述符用来指定已打开的文件。这个文件描述符相当于这个已打开文件的标号文件描述符是非负整数是文件的标识操作这个文件描述符相当于操作这个描述符所指定的文件。
程序运行起来后每个进程都有一张文件描述符的表标准输入、标准输出、标准错误输出设备文件被打开对应的文件描述符 0、1、2 记录在表中。程序运行起来后这三个文件描述符是默认打开的。 #define STDIN_FILENO 0 //标准输入的文件描述符
#define STDOUT_FILENO 1 //标准输出的文件描述符
#define STDERR_FILENO 2 //标准错误的文件描述符在程序运行起来后打开其他文件时系统会返回文件描述符表中最小可用的文件描述符并将此文件描述符记录在表中。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KENJPXJv-1684633973972)(D:\Typora笔记\c\Linux系统编程.assets\1527651181126.png)]
最大打开的文件个数
Linux 中一个进程最多只能打开 NR_OPEN_DEFAULT 即1024个文件故当文件不再使用时应及时调用 close() 函数关闭文件。 查看当前系统允许打开最大文件个数 cat /proc/sys/fs/file-max 当前默认设置最大打开文件个数1024 ulimit -a 修改默认设置最大打开文件个数为4096 ulimit -n 4096
常用文件IO函数
open函数 #include sys/types.h
#include sys/stat.h
#include fcntl.hint open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
功能打开文件如果文件不存在则可以选择创建。
参数pathname文件的路径及文件名flags打开文件的行为标志必选项 O_RDONLY, O_WRONLY, O_RDWRmode这个参数只有在文件不存在时有效指新建文件时指定文件的权限
返回值成功成功返回打开的文件描述符失败-1flags详细说明
必选项
取值含义O_RDONLY以只读的方式打开O_WRONLY以只写的方式打开O_RDWR以可读、可写的方式打开
可选项和必选项按位或起来
取值含义O_CREAT文件不存在则创建文件使用此选项时需使用mode说明文件的权限O_EXCL如果同时指定了O_CREAT且文件已经存在则出错O_TRUNC如果文件存在则清空文件内容O_APPEND写文件时数据添加到文件末尾O_NONBLOCK对于设备文件, 以O_NONBLOCK方式打开可以做非阻塞I/O
mode补充说明
文件最终权限mode ~umaskshell进程的umask掩码可以用umask命令查看
Ø umask查看掩码补码
Ø umask mode设置掩码mode为八进制数
Ø umask -S查看各组用户的默认操作权限
取值八进制含义S_IRWXU00700文件所有者的读、写、可执行权限S_IRUSR00400文件所有者的读权限S_IWUSR00200文件所有者的写权限S_IXUSR00100文件所有者的可执行权限S_IRWXG00070文件所有者同组用户的读、写、可执行权限S_IRGRP00040文件所有者同组用户的读权限S_IWGRP00020文件所有者同组用户的写权限S_IXGRP00010文件所有者同组用户的可执行权限S_IRWXO00007其他组用户的读、写、可执行权限S_IROTH00004其他组用户的读权限S_IWOTH00002其他组用户的写权限S_IXOTH00001其他组用户的可执行权限
close函数 #include unistd.hint close(int fd);
功能关闭已打开的文件
参数fd : 文件描述符open()的返回值
返回值成功0失败 -1, 并设置errno 需要说明的是当一个进程终止时内核对该进程所有尚未关闭的文件描述符调用close关闭所以即使用户程序不调用close在终止时内核也会自动关闭它打开的所有文件。
但是对于一个长年累月运行的程序(比如网络服务器)打开的文件描述符一定要记得关闭,否则随着打开的文件越来越多会占用大量文件描述符和系统资源。
write函数 #include unistd.h
ssize_t write(int fd, const void *buf, size_t count);
功能把指定数目的数据写到文件fd
参数fd : 文件描述符buf : 数据首地址count : 写入数据的长度字节
返回值成功实际写入数据的字节个数失败 - 1read函数 #include unistd.hssize_t read(int fd, void *buf, size_t count);
功能把指定数目的数据读到内存缓冲区
参数fd : 文件描述符buf : 内存首地址count : 读取的字节个数
返回值成功实际读取到的字节个数失败 - 1day4
文件描述符复制(重点)
概述
dup() 和 dup2() 是两个非常有用的系统调用都是用来复制一个文件的描述符使新的文件描述符也标识旧的文件描述符所标识的文件。
这个过程类似于现实生活中的配钥匙钥匙相当于文件描述符锁相当于文件本来一个钥匙开一把锁相当于一个文件描述符对应一个文件现在我们去配钥匙通过旧的钥匙复制了一把新的钥匙这样的话旧的钥匙和新的钥匙都能开启这把锁。
对比于 dup(), dup2() 也一样通过原来的文件描述符复制出一个新的文件描述符这样的话原来的文件描述符和新的文件描述符都指向同一个文件我们操作这两个文件描述符的任何一个都能操作它所对应的文件。
dup函数
#include unistd.hint dup(int oldfd);
功能通过 oldfd 复制出一个新的文件描述符新的文件描述符是调用进程文件描述符表中最小可用的文件描述符最终 oldfd 和新的文件描述符都指向同一个文件。
参数oldfd : 需要复制的文件描述符 oldfd
返回值成功新文件描述符失败 -1dup2函数
#include unistd.hint dup2(int oldfd, int newfd);
功能通过 oldfd 复制出一个新的文件描述符 newfd如果成功newfd 和函数返回值是同一个返回值最终 oldfd 和新的文件描述符 newfd 都指向同一个文件。
参数oldfd : 需要复制的文件描述符newfd : 新的文件描述符这个描述符可以人为指定一个合法数字0 - 1023如果指定的数字已经被占用和某个文件有关联此函数会自动关闭 close() 断开这个数字和某个文件的关联再来使用这个合法数字。
返回值成功返回 newfd失败返回 -1//测试dup函数复制文件描述符
#include stdio.h
#include stdlib.h
#include string.h
#include sys/types.h
#include unistd.h
#include sys/types.h
#include sys/stat.h
#include fcntl.hint main(int argc, char *argv[])
{//打开文件int fd open(argv[1], O_RDWR);if(fd0){perror(open error);return -1;}//调用dup函数复制fdint newfd dup(fd);printf(newfd:[%d], fd:[%d]\n, newfd, fd);//使用fd对文件进行写操作write(fd, hello world, strlen(hello world));//调用lseek函数移动文件指针到开始处lseek(fd, 0, SEEK_SET);//使用newfd读文件char buf[64];memset(buf, 0x00, sizeof(buf));int n read(newfd, buf, sizeof(buf));printf(read over: n[%d], buf[%s]\n, n, buf);//关闭文件close(fd);close(newfd);return 0;
}day5
进程相关概念
程序和进程 程序是指编译好的二进制文件在磁盘上占用磁盘空间, 是一个静态的概念. 进程一个启动的程序 进程占用的是系统资源如物理内存CPU终端等是一个动态的概念
程序 → 剧本(纸) 进程 → 戏(舞台、演员、灯光、道具…) 同一个剧本可以在多个舞台同时上演。同样同一个程序也可以加载为不同的进程(彼此之间互不影响)
进程和程序 (理解)
我们平时写的 C 语言代码通过编译器编译最终它会成为一个可执行程序当这个可执行程序运行起来后没有结束之前它就成为了一个进程。
程序是存放在存储介质上的一个可执行文件而进程是程序执行的过程。进程的状态是变化的其包括进程的创建、调度和消亡。程序是静态的进程是动态的。
示例
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0ch28ZeJ-1684633973973)(D:\Typora笔记\c\Linux系统编程.assets\1527992375886.png)]
程序就类似于剧本(纸)进程类似于戏(舞台、演员、灯光、道具…)同一个剧本可以在多个舞台同时上演。同样同一个程序也可以加载为不同的进程(彼此之间互不影响)。
在 Linux 系统中操作系统是通过进程去完成一个一个的任务进程是管理事务的基本单元。
进程拥有自己独立的处理环境如当前需要用到哪些环境变量程序运行的目录在哪当前是哪个用户在运行此程序等和系统资源如处理器 CPU 占用率、存储器、I/O设备、数据、程序。
我们可以这么理解公司相当于操作系统部门相当于进程公司通过部门来管理系统通过进程管理对于各个部门每个部门有各自的资源如人员、电脑设备、打印机等。
并行和并发
并发在一个时间段内, 是在同一个cpu上, 同时运行多个程序。
如若将CPU的1S的时间分成1000个时间片每个进程执行完一个时间片必须无条件让出CPU的使用权这样1S中就可以执行1000个进程。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uOaPb69Z-1684633973973)(D:\Typora笔记\c\Linux系统编程.assets\wps1.jpg)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iYksEmr4-1684633973974)(D:\Typora笔记\c\Linux系统编程.assets\wps2.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-58DBIrz6-1684633973974)(D:\Typora笔记\c\Linux系统编程.assets\wps3.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NcY3h0X5-1684633973975)(D:\Typora笔记\c\Linux系统编程.assets\wps4.jpg)]
并行性指两个或两个以上的程序在同一时刻发生(需要有多颗)。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2PbPndq0-1684633973975)(D:\Typora笔记\c\Linux系统编程.assets\wps5.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5JuQdi5M-1684633973975)(D:\Typora笔记\c\Linux系统编程.assets\wps6.jpg)]
总结
并行是两个队列同时使用两台咖啡机并发是两个队列交替使用一台咖啡机
PCB-进程控制块
每个进程在内核中都有一个进程控制块PCB来维护进程相关的信息Linux内核的进程控制块是task_struct结构体。
/usr/src/linux-headers-4.4.0-96/include/linux/sched.h文件的1390行处可以查看struct task_struct 结构体定义。其内部成员有很多我们重点掌握以下部分即可
]进程id。系统中每个进程有唯一的id在C语言中用pid_t类型表示其实就是一个非负整数。进程的状态有就绪、运行、挂起、停止等状态。进程切换时需要保存和恢复的一些CPU寄存器。描述虚拟地址空间的信息。描述控制终端的信息。当前工作目录Current Working Directory。getcwd --pwdumask掩码。文件描述符表包含很多指向file结构体的指针。和信号相关的信息。用户id和组id。会话Session和进程组。进程可以使用的资源上限Resource Limit。n ulimit -a
进程状态(面试考)
进程基本的状态有5种。分别为**初始态就绪态运行态挂起态与终止态。**其中初始态为进程准备阶段常与就绪态结合来看。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Zq5ft5Lu-1684633973975)(D:\Typora笔记\c\Linux系统编程.assets\wps7.jpg)] 处于就绪态的进程有执行资格但是没有cpu的时间片 但是处于挂起态的进程既没有执行资格也没有cpu的时间片; 从挂起态不能直接回到运行态必须回到就绪态 只有就绪态才能回到运行态 如何查看进程状态 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bSZunqj1-1684633973976)(D:\Typora笔记\c\Linux系统编程.assets\1527994562159.png)] stat中的参数意义如下 参数含义D不可中断 Uninterruptibleusually IOR正在运行或在队列中的进程S(大写)处于休眠状态T停止或被追踪Z僵尸进程W进入内存交换从内核2.6开始无效X死掉的进程高优先级N低优先级s包含子进程位于前台的进程组
ps
进程是一个具有一定独立功能的程序它是操作系统动态执行的基本单元。
ps命令可以查看进程的详细状况常用选项(选项可以不加“-”)如下
选项含义-a显示终端上的所有进程包括其他用户的进程-u显示进程的详细状态-x显示没有控制终端的进程-w显示加宽以便显示更多的信息-r只显示正在运行的进程
ps aux
ps ef
ps -a
top
top命令用来动态显示运行中的进程。top命令能够在运行后在指定的时间间隔更新显示信息。可以在使用top命令时加上-d 来指定显示信息更新的时间间隔。
在top命令执行后可以按下按键得到对显示的结果进行排序
按键含义M根据内存使用量来排序P根据CPU占有率来排序T根据进程运行时间的长短来排序U可以根据后面输入的用户名来筛选进程K可以根据后面输入的PID来杀死进程。q退出h获得帮助
kill
kill命令指定进程号的进程需要配合 ps 使用。
使用格式
kill [-signal] pid
信号值从0到15其中9为绝对终止可以处理一般信号无法终止的进程。
killall
通过进程名字杀死进程
创建进程
fork函数 函数作用创建子进程 原型: pid_t fork(void);
函数参数无
返回值调用成功:父进程返回子进程的PID子进程返回0
调用失败:返回-1设置errno值。
fork函数讲解
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h1XfZrc5-1684633973976)(D:\Typora笔记\c\Linux系统编程.assets\image-20221117094111216.png)]
● fork函数代码片段实例
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Bo2nSRtr-1684633973977)(D:\Typora笔记\c\Linux系统编程.assets\wps8.jpg)]
#includestdio.h
#includestdlib.h
#includestring.h
#includesys/types.h
#includeunistd.h
#includesys/stat.h
#includefcntl.h
int main()
{printf(before fork.pid:[%d]\n,getpid());pid_t pid fork();if (pid 0)// fork 失败{perror(fork error);return -1;}else if (pid 0)// 父进程{printf(father:pid [% d], fpid[%d]\n, getpid(),getppid());//sleep(1);}else if (pid 0)//子进程{printf(child:pid [%d], fpid[%d]\n, getpid(),getppid());}printf(after fork.pid:[%d]\n,getpid());return 0;
}例 循环创建n个子进程
#includestdio.h
#includestdlib.h
#includestring.h
#includesys/types.h
#includeunistd.h
#includesys/stat.h
#includefcntl.h
int main()
{ int i 0;for(i 0;i3;i){ pid_t pid fork();if (pid 0)// fork 失败{perror(fork error);return -1;}else if (pid 0)// 父进程{printf(father:pid [% d], fpid[%d]\n, getpid(),getppid());//sleep(1);}else if (pid 0)//子进程{printf(child:pid [%d], fpid[%d]\n, getpid(),getppid());}}sleep(10);return 0;} father:pid [ 88267], fpid[77978] father:pid [ 88267], fpid[77978] child:pid [88268], fpid[88267] child:pid [88270], fpid[88267] father:pid [ 88268], fpid[88267] father:pid [ 88268], fpid[88267] child:pid [88271], fpid[88268] child:pid [88269], fpid[88267] father:pid [ 88269], fpid[88267] father:pid [ 88271], fpid[88268] child:pid [88273], fpid[88271] child:pid [88274], fpid[88269] child:pid [88272], fpid[88268]
子进程也在创建子进程 一共七个子进程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aQchz8BB-1684633973977)(D:\Typora笔记\c\Linux系统编程.assets\image-20221118160236878.png)]
测试父子进程是否能够共享全局变量不可以
#includestdio.h
#includestdlib.h
#includestring.h
#includesys/types.h
#includeunistd.h
#includesys/stat.h
#includefcntl.h
int g_var 99;
int main()
{pid_t pid fork();if (pid 0)// fork 失败{ perror(fork error);return -1;}else if (pid 0)// 父进程{printf(father:pid [% d], fpid[%d]\n, getpid(),getppid());g_var;//sleep(1);}else if (pid 0)//子进程{sleep(1);//为了避免父进程还没有执行子进程就结束了printf(child:pid [%d], fpid[%d]\n, getpid(),getppid());printf(child:g_var [%d]\n,g_var);}return 0;}[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zsiInzdT-1684633973977)(D:\Typora笔记\c\Linux系统编程.assets\image-20221118165319619.png)]
exce函数族
函数作用和函数介绍
有的时候需要在一个进程里面执行其他的命令或者是用户自定义的应用程序此时就用到了exec函数族当中的函数。
使用方法一般都是在父进程里面调用fork创建处子进程然后在子进程里面调用exec函数。
excle函数介绍
函数原型: int execl(const char *path, const char arg, … / (char *) NULL */);
参数介绍 path: 要执行的程序的绝对路径 变参arg: 要执行的程序的需要的参数 arg:占位通常写应用程序的名字 arg后面的: 命令的参数 参数写完之后: NULL
返回值若是成功则不返回不会再执行exec函数后面的代码若是失败会执行execl后面的代码可以用perror打印错误原因。
execl函数一般执行自己写的程序。
exce函数族原理介绍
exec族函数的实现原理图
如execlp(“ls”, “ls”, “-l”, NULL);
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GpW3H6PV-1684633973978)(D:\Typora笔记\c\Linux系统编程.assets\image-20221119105126533.png)]
总结
exce函数是用一个新程序替换了当前进程的代码段数据段堆和栈原有进程空间没有发生变化并没有创建新的进程进程PID没有发生改变。
进程回收
为什么要进行进程资源的回收
当一个进程退出之后进程能够回收自己的用户区的资源但是不能回收内核空间的PCB资源必须由它的父进程调用wait或者waitpid函数完成对子进程的回收避免造成系统资源的浪费。
孤儿进程
孤儿进程的概念
若子进程的父进程已经死掉而子进程还存活着这个进程就成了孤儿进程。 为了保证每个进程都有一个父进程孤儿进程会被init进程领养init进程成为了孤儿进程的养父进程当孤儿进程退出之后由init进程完成对孤儿进程的回收。 模拟孤儿进程的案例 编写模拟孤儿进程的代码讲解孤儿进程验证孤儿进程的父进程是否由原来的父进程变成了init进程。 #include stdio.h
#include stdlib.h
#include string.h
#include sys/types.h
#include unistd.hint main()
{//创建子进程pid_t pid fork();if(pid0) //fork失败的情况{perror(fork error);return -1;}else if(pid0)//父进程{sleep(5);printf(father: [%d], pid[%d], fpid[%d]\n, pid, getpid(),getppid());}else if(pid0) //子进程{printf(child: pid[%d], fpid[%d]\n, getpid(), getppid());sleep(20);printf(child: pid[%d], fpid[%d]\n, getpid(), getppid());}return 0;
}[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nJnQZZI8-1684633973978)(D:\Typora笔记\c\Linux系统编程.assets\image-20221119110651517.png)]
僵尸进程进程
僵尸进程的概念:
若子进程死了父进程还活着 但是父进程没有调用wait或waitpid函数完成对子进程的回收则该子进程就成了僵尸进程。
//僵尸进程
#include stdio.h
#include stdlib.h
#include string.h
#include sys/types.h
#include unistd.hint main()
{//创建子进程pid_t pid fork();if(pid0) //fork失败的情况{perror(fork error);return -1;}else if(pid0)//父进程{sleep(100);printf(father: [%d], pid[%d], fpid[%d]\n, pid, getpid(),getppid());}else if(pid0) //子进程{printf(child: pid[%d], fpid[%d]\n, getpid(), getppid());}return 0;
}如何解决僵尸进程 由于僵尸进程是一个已经死亡的进程所以不能使用kill命令将其杀死通过杀死其父进程的方法可以消除僵尸进程。
杀死其父进程后这个僵尸进程会被init进程领养由init进程完成对僵尸进程的回收。
模拟僵尸进程的案例
编写模拟僵尸进程的代码讲解僵尸进程, 验证若子进程先于父进程退出, 而父进程没有调用wait或者waitpid函数进行回收, 从而使子进程成为了僵尸进程. #include stdio.h
#include stdlib.h
#include string.h
#include sys/types.h
#include unistd.h
#include sys/wait.hint main()
{pid_t pid fork();if(pid0) {perror(fork error);return -1;}else if(pid0){printf(father: [%d], pid[%d], fpid[%d]\n, pid, getpid(),getppid());int status;pid_t wpid wait(status);printf(wpid[%d]\n, wpid);if(WIFEXITED(status)){printf(child normal exit, status[%d]\n, WEXITSTATUS(status));}else if(WIFSIGNALED(status)) {printf(child killed by signal, signo[%d]\n, WTERMSIG(status));}}else if(pid0) {printf(child: pid[%d], fpid[%d]\n, getpid(), getppid());sleep(20);return 9;}return 0;
}总结
解决僵尸进程不能使用kill -9 杀死僵尸进程原因是僵尸进程是一个死掉的进程
应该使用杀死僵尸进父进程的方法来解决僵尸进程
原因是杀死其父进程可以让init进程领养僵尸进程最后init进程回收僵尸进程。
进程回收函数
wait函数 函数原型
pid_t wait(int *status);
函数作用
阻塞并等待子进程退出回收子进程残留资源获取子进程结束状态(退出原因)。
返回值
成功清理掉的子进程ID失败-1 (没有子进程)
status参数子进程的退出状态 – 传出参数
WIFEXITED(status)为非0 → 进程正常结束
WEXITSTATUS(status)获取进程退出状态
WIFSIGNALED(status)为非0 → 进程异常终止
day6
进程间通信相关概念
什么是进程间的通信
Linux环境下进程地址空间相互独立每个进程各自有不同的用户地址空间。任何一个进程的全局变量在另一个进程中都看不到所以进程和进程之间不能相互访问要交换数据必须通过内核在内核中开辟一块缓冲区进程1把数据从用户空间拷到内核缓冲区进程2再从内核缓冲区把数据读走内核提供的这种机制称为进程间通信IPCInterProcess Communication。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3NODDdI8-1684633973978)(D:\Typora笔记\c\Linux系统编程.assets\wps9.jpg)]
进程间的通信方式
在进程间完成数据传递需要借助操作系统提供特殊的方法如文件、管道、信号、共享内存、消息队列、套接字、命名管道等。随着计算机的蓬勃发展一些方法由于自身设计缺陷被淘汰或者弃用。现今常用的进程间通信方式有 管道 (使用最简单) 信号 (开销最小) 共享映射区 (无血缘关系) 本地套接字 (最稳定)
管道
管道的概念
管道是一种最基本的IPC机制也称匿名管道应用于有血缘关系的进程之间完成数据传递。调用pipe函数即可创建一个管道。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JgxFmaIl-1684633973979)(D:\Typora笔记\c\Linux系统编程.assets\wps10.jpg)]
有如下特质
管道的本质是一块内核缓冲区由两个文件描述符引用一个表示读端一个表示写端。规定数据从管道的写端流入管道从读端流出。当两个进程都终结的时候管道也自动消失。管道的读端和写端默认都是阻塞的。
管道的原理
管道的实质是内核缓冲区内部使用环形队列实现。缓刑管道效率高默认缓冲区大小为4K可以使用ulimit -a命令获取大小。实际操作过程中缓冲区会根据数据压力做适当调整。
管道的局限性
数据一旦被读走便不在管道中存在不可反复读取。数据只能在一个方向上流动若要实现双向流动必须使用两个管道只能在有血缘关系的进程间使用管道。
创建管道-pipe函数
函数作用:
创建一个管道
函数原型:
int pipe(int fd[2]);
函数参数:
若函数调用成功fd[0]存放管道的读端fd[1]存放管道的写端
返回值:
成功返回0
失败返回-1并设置errno值。
函数调用成功返回读端和写端的文件描述符其中****fd[0]是读端 fd[1]是写端**向管道读写数据是通过使用这两个文件描述符进行的读写管道的实质是操作内核缓冲区。****
管道创建成功以后创建该管道的进程父进程同时掌握着管道的读端和写端。如何实现父子进程间通信呢
父子进程使用管道通信
一个进程在由pipe()创建管道后一般再fork一个子进程然后通过管道实现父子进程间的通信因此也不难推出只要两个进程中存在血缘关系这里的血缘关系指的是具有共同的祖先都可以采用管道方式来进行通信。*父子进程间具有相同的文件描述符且指向同一个管道pipe*其他没有关系的进程不能获得pipe产生的两个文件描述符也就不能利用同一个管道进行通信。
*第一步父进程创建管道*
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zJwTaX18-1684633973979)(D:\Typora笔记\c\Linux系统编程.assets\wps11.jpg)]
*第二步父进程fork出子进程*
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pH8FVZWa-1684633973979)(D:\Typora笔记\c\Linux系统编程.assets\wps12.jpg)]
*第三步父进程关闭fd[0]子进程关闭fd[1]*
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wS7i3J2O-1684633973980)(D:\Typora笔记\c\Linux系统编程.assets\wps13.jpg)]
*创建步骤总结*
父进程调用pipe函数创建管道得到两个文件描述符fd[0]和fd[1]分别指向管道的读端和写端。父进程调用fork创建子进程那么子进程也有两个文件描述符指向同一管。父进程关闭管道读端子进程关闭管道写端。父进程可以向管道中写入数据子进程将管道中的数据读出这样就实现了父子进程间通信。
管道练习
使用管道完成父子进程间通信
#include stdio.h
#include stdlib.h
#include string.h
#include sys/types.h
#include unistd.h
#include sys/wait.h
int main()
{int fd[2];int ret pipe(fd);if(ret0){ perror(pipe error);return -1;}//创建子进程pid_t pid fork();if(pid0){perror(fork error);return -1;}else if(pid0){//关闭读端close(fd[0]);write(fd[1],hello world,strlen(hello world));wait(NULL);}else{//关闭写段close(fd[1]);char buf[64];memset(buf,0x00,sizeof(buf));int n read(fd[0],buf,sizeof(buf));printf(read over, n[%d],buf[%s]\n,n,buf);
} return 0;
}
管道的读写行为
读操作
有数据
read正常读返回读出的字节数
无数据
写端全部关闭
read解除阻塞返回0, 相当于读文件读到了尾部
没有全部关闭
read阻塞
写操作
读端全部关闭
管道破裂进程终止, 内核给当前进程发SIGPIPE信号
读端没全部关闭
缓冲区写满了
write阻塞
缓冲区没有满
继续write
如何设置管道为非阻塞
默认情况下管道的读写两端都是阻塞的若要设置读或者写端为非阻塞则可参
考下列三个步骤进行
第1步 int flags fcntl(fd[0], F_GETFL, 0);
第2步 flag | O_NONBLOCK;
第3步 fcntl(fd[0], F_SETFL, flags);
若是读端设置为非阻塞
Ø 写端没有关闭管道中没有数据可读则read返回-1
Ø 写端没有关闭管道中有数据可读则read返回实际读到的字节数
Ø 写端已经关闭管道中有数据可读则read返回实际读到的字节数
Ø 写端已经关闭管道中没有数据可读则read返回0
如何查看管道缓冲区大小
命令
ulimit -a
函数
long fpathconf(int fd, int name);
printf(“pipe size[%ld]\n”, fpathconf(fd[0], _PC_PIPE_BUF));
printf(“pipe size[%ld]\n”, fpathconf(fd[1], _PC_PIPE_BUF));
FIFO
FIFO介绍
FIFO常被称为命名管道以区分管道(pipe)。管道(pipe)只能用于“有血缘关系”的进程间通信。但通过FIFO不相关的进程也能交换数据。
FIFO是Linux基础文件类型中的一种文件类型为p可通过ls -l查看文件类型。但FIFO文件在磁盘上没有数据块文件大小为0仅仅用来标识内核中一条通道。进程可以打开这个文件进行read/write实际上是在读写内核缓冲区这样就实现了进程间通信。
创建管道
方式1-使用命令 mkfifo
命令格式 mkfifo 管道名
例如mkfifo myfifo
方式2-使用函数
int mkfifo(const char *pathname, mode_t mode);
参数说明和返回值可以查看man 3 mkfifo
当创建了一个FIFO就可以使用open函数打开它常见的文件I/O函数都可用于FIFO。如close、read、write、unlink等。
FIFO严格遵循先进先出first in first out对FIFO的读总是从开始处返回数据对它们的写则把数据添加到末尾。*它们不支持诸如*lseek*()等文件定位操作。*
使用FIFO完成两个进程通信
使用FIFO完成两个进程通信的示意图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Kt0wmEbQ-1684633973980)(D:\Typora笔记\c\Linux系统编程.assets\wps1-1670200792026-1.jpg)]
思路
进程A
创建一个fifo文件myfifo 调用open函数打开myfifo文件 调用write函数写入一个字符串如“hello world”其实是将数据写入到了内核缓冲区 调用close函数关闭myfifo文件
进程B 调用open函数打开myfifo文件 调用read函数读取文件内容其实就是从内核中读取数据 打印显示读取的内容 调用close函数关闭myfifo文件
内存映射区
储存映射区介绍
存储映射I/O (Memory-mapped I/O) 使一个磁盘文件与存储空间中的一个缓冲区相映射。从缓冲区中取数据就相当于读文件中的相应字节将数据写入缓冲区则会将数据写入文件。这样就可在不使用read和write函数的情况下使用地址指针完成I/O操作。
使用存储映射这种方法首先应通知内核将一个指定文件映射到存储区域中。这个映射工作可以通过mmap函数来实现。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O7rJJ0zM-1684633973981)(D:\Typora笔记\c\Linux系统编程.assets\wps2-1670200963607-3.jpg)]
mmap函数
函数作用:
建立存储映射区
函数原型 void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);函数返回值
成功返回创建的映射区首地址
失败MAP_FAILED宏
参数
addr: 指定映射的起始地址, 通常设为NULL, 由系统指定
length映射到内存的文件长度
prot 映射区的保护方式, 最常用的:
读PROT_READ
写PROT_WRITE
读写PROT_READ | PROT_WRITE
flags 映射区的特性, 可以是
MAP_SHARED: 写入映射区的数据会写回文件, 且允许其他映射该文件的进程共享。
MAP_PRIVATE: 对映射区的写入操作会产生一个映射区的复制(copy-on-write), 对此区域所做的修改不会写回原文件。
fd由open返回的文件描述符, 代表要映射的文件。
offset以文件开始处的偏移量, 必须是4k的整数倍, 通常为0, 表示从文件头开始映射。
munmap函数
函数作用:
释放由mmap函数建立的存储映射区
函数原型:
int munmap(void *addr, size_t length);
返回值
成功返回0
失败返回-1设置errno值
函数参数:
addr调用mmap函数成功返回的映射区首地址
length映射区大小mmap函数的第二个参数
mmap函数注意事项 创建映射区的过程中隐含着一次对映射文件得读操作将文件内容读取到映射区 当MAP_SHARED时要求映射区的权限文件打开的权限出于对映射区的保护。而MAP_PRIVATE则无所谓因为mmap中的权限是对内存的限制 映射区的释放与文件关闭无关只要映射建立成功文件可以立即关闭。 特别注意当映射文件大小为0时不能创建映射区。所以用于映射的文件必须要有实际大小mmap使用时常常会出现总线错误。通常是由于共享文件储存空间大小引起的。 munmap传入的地址一定是mmap的返回地址。坚决杜绝指针操作。 文件偏移量必须为0或者4k的整数倍 mmap创建映射区出现错误概率非常高一定要检查返回值确保映射区建立成功再进行后续操作。
有关mmap函数使用的总结
第一个参数写成NULL第二个参数要映射的大小0第三个参数PROT_READ 、PROT_WRITE第四个参数MAP_SHARED 或者 MAP_PRIVATE第五个参数打开的文件对应的文件描述符第六个参数4k的整数倍
day7
信号介绍
信号的概念
信号是信息的载体Linux/UNIX 环境下古老、经典的通信方式 现下依然是主要的通信手段。
信号在我们的生活中随处可见例如
² 古代战争中摔杯为号
² 现代战争中的信号弹
² 体育比赛中使用的信号枪…
信号的特点
² 简单
² 不能携带大量信息
² 满足某个特点条件才会产生
信号的机制
进程A给进程B发送信号进程B收到信号之前执行自己的代码收到信号后不管执行到程序的什么位置都要暂停运行去处理信号处理完毕后再继续执行。与硬件中断类似——异步模式。但信号是软件层面上实现的中断早期常被称为“软中断”。每个进程收到的所有信号都是由内核负责发送的。
进程A给进程B发送信号示意图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nRm9DyRr-1684633973981)(D:\Typora笔记\c\Linux系统编程.assets\wps1-1670315130080-1.jpg)]
信号四要素
每个信号必备4要素分别是
1编号
2名称
3事件
4默认处理动作
可通过man 7 signal查看帮助文档获取
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Cc3y4Svo-1684633973981)(D:\Typora笔记\c\Linux系统编程.assets\1527928967909.png)]
在标准信号中有一些信号是有三个“Value”第一个值通常对alpha和sparc架构有效中间值针对x86、arm和其他架构最后一个应用于mips架构。一个‘-’表示在对应架构上尚未定义该信号。
不同的操作系统定义了不同的系统信号。因此有些信号出现在Unix系统内也出现在Linux中而有的信号出现在FreeBSD或Mac OS中却没有出现在Linux下。这里我们只研究Linux系统中的信号。
Action为默认动作
Term终止进程Ign 忽略信号 (默认即时对该种信号忽略操作)Core终止进程生成Core文件。(查验死亡原因用于gdb调试)Stop停止暂停进程Cont继续运行进程
注意通过man 7 signal命令查看帮助文档其中可看到 : The signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored.
这里特别强调了9) SIGKILL 和19) SIGSTOP信号不允许忽略和捕捉只能执行默认动作。甚至不能将其设置为阻塞。
另外需清楚只有每个信号所对应的事件发生了该信号才会被递送(但不一定递达)不应乱发信号
信号的状态
1) 产生
a) 当用户按某些终端键时将产生信号。
终端上按“Ctrlc”组合键通常产生中断信号 SIGINT
终端上按“Ctrl\”键通常产生中断信号 SIGQUIT
终端上按“Ctrlz”键通常产生中断信号 SIGSTOP 等。
b) 硬件异常将产生信号。
除数为 0无效的内存访问等。这些情况通常由硬件检测到并通知内核然后内核产生适当的信号发送给相应的进程。
c) 软件异常将产生信号。
当检测到某种软件条件已发生(如定时器alarm)并将其通知有关进程时产生信号。
d) 调用系统函数(如kill、raise、abort)将发送信号。
注意接收信号进程和发送信号进程的所有者必须相同或发送信号进程的所有者必须是超级用户。
e) 运行 kill /killall命令将发送信号。
此程序实际上是使用 kill 函数来发送信号。也常用此命令终止一个失控的后台进程。
2) 未决状态没有被处理
3) 递达状态信号被处理了
阻塞信号集和未决信号集
信号的实现手段导致信号有很强的延时性但对于用户来说时间非常短不易察觉。
Linux内核的进程控制块PCB是一个结构体task_struct, 除了包含进程id状态工作目录用户id组id文件描述符表还包含了信号相关的信息主要指阻塞信号集和未决信号集。
6.1 阻塞信号集(信号屏蔽字)
将某些信号加入集合对他们设置屏蔽当屏蔽x信号后再收到该信号该信号的处理将推后(处理发生在解除屏蔽后)。
6.2 未决信号集
信号产生未决信号集中描述该信号的位立刻翻转为1表示信号处于未决状态。当信号被处理对应位翻转回为0。这一时刻往往非常短暂。
信号产生后由于某些原因(主要是阻塞)不能抵达。这类信号的集合称之为未决信号集。在屏蔽解除前信号一直处于未决状态。
信号集概述
在PCB中有两个非常重要的信号集。一个称之为“阻塞信号集”另一个称之为“未决信号集”。
这两个信号集都是内核使用位图机制来实现的。但操作系统不允许我们直接对其进行位操作。而需自定义另外一个集合借助信号集操作函数来对PCB中的这两个信号集进行修改。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u7fYLX78-1684633973981)(D:\Typora笔记\c\Linux系统编程.assets\1527930344052.png)]
信号捕捉
信号处理方式
一个进程收到一个信号的时候可以用如下方法进行处理
1执行系统默认动作
对大多数信号来说系统默认动作是用来终止该进程。
2忽略此信号(丢弃)
接收到此信号后没有任何动作。
3执行自定义信号处理函数(捕获)
用用户定义的信号处理函数处理该信号。
【注意】SIGKILL 和 SIGSTOP 不能更改信号的处理方式因为它们向用户提供了一种使进程终止的可靠方法。
内核实现信号捕捉过程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2Kqhf6Ym-1684633973981)(D:\Typora笔记\c\Linux系统编程.assets\1527931072795.png)]
如何避免僵尸进程
最简单的方法父进程通过 wait() 和 waitpid() 等函数等待子进程结束但是这会导致父进程挂起。如果父进程要处理的事情很多不能够挂起通过 signal() 函数人为处理信号 SIGCHLD 只要有子进程退出自动调用指定好的回调函数因为子进程结束后 父进程会收到该信号 SIGCHLD 可以在其回调函数里调用 wait() 或 waitpid() 回收。
示例程序: void sig_child(int signo)
{pid_t pid;//处理僵尸进程, -1 代表等待任意一个子进程, WNOHANG代表不阻塞while ((pid waitpid(-1, NULL, WNOHANG)) 0){printf(child %d terminated.\n, pid);}
}int main()
{pid_t pid;// 创建捕捉子进程退出信号// 只要子进程退出触发SIGCHLD自动调用sig_child()signal(SIGCHLD, sig_child);pid fork(); // 创建进程if (pid 0){ // 出错perror(fork error:);exit(1);}else if (pid 0){ // 子进程printf(I am child process,pid id %d.I am exiting.\n, getpid());exit(0);}else if (pid 0){ // 父进程sleep(2); // 保证子进程先运行printf(I am father, i am exited\n\n);system(ps -ef | grep defunct); // 查看有没有僵尸进程}return 0;
}
3如果父进程不关心子进程什么时候结束那么可以用signalSIGCHLD, SIG_IGN通知内核自己对子进程的结束不感兴趣父进程忽略此信号那么子进程结束后内核会回收 并不再给父进程发送信号。
示例程序: int main()
{pid_t pid;// 忽略子进程退出信号的信号// 那么子进程结束后内核会回收 并不再给父进程发送信号signal(SIGCHLD, SIG_IGN);pid fork(); // 创建进程if (pid 0){ // 出错perror(fork error:);exit(1);}else if (pid 0){ // 子进程printf(I am child process,pid id %d.I am exiting.\n, getpid());exit(0);}else if (pid 0){ // 父进程sleep(2); // 保证子进程先运行printf(I am father, i am exited\n\n);system(ps -ef | grep defunct); // 查看有没有僵尸进程}return 0;
}
day8
终端的概念(了解)
在UNIX系统中用户通过终端登录系统后得到一个Shell进程这个终端成为Shell进程的控制终端Controlling Terminal进程中控制终端是保存在PCB中的信息而fork会复制PCB中的信息因此由Shell进程启动的其它进程的控制终端也是这个终端。
默认情况下没有重定向每个进程的标准输入、标准输出和标准错误输出都指向控制终端进程从标准输入读也就是读用户的键盘输入进程往标准输出或标准错误输出写也就是输出到显示器上。
信号中还讲过在控制终端输入一些特殊的控制键可以给前台进程发信号例如CtrlC表示SIGINTCtrl\表示SIGQUIT。
函数说明: #include unistd.hchar *ttyname(int fd);
功能由文件描述符查出对应的文件名
参数fd文件描述符
返回值成功终端名失败NULL下面我们借助ttyname函数通过实验看一下各种不同的终端所对应的设备文件名 int main()
{printf(fd 0: %s\n, ttyname(0));printf(fd 1: %s\n, ttyname(1));printf(fd 2: %s\n, ttyname(2));return 0;
}进程组概念(理解)
进程组概述
进程组也称之为作业。BSD于1980年前后向Unix中增加的一个新特性。代表一个或多个进程的集合。
每个进程都属于一个进程组。在waitpid函数和kill函数的参数中都曾使用到。操作系统设计的进程组的概念是为了简化对多个进程的管理。当父进程创建子进程的时候默认子进程与父进程属于同一进程组。进程组ID为第一个进程ID(组长进程)。所以组长进程标识其进程组ID为其进程ID 可以使用kill -SIGKILL -进程组ID(负的)来将整个进程组内的进程全部杀死
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cpZN54Zg-1684633973982)(D:\Typora笔记\c\Linux系统编程.assets\1528119946594.png)]
组长进程可以创建一个进程组创建该进程组中的进程然后终止。只要进程组中有一个进程存在进程组就存在与组长进程是否终止无关。
进程组生存期进程组创建到最后一个进程离开(终止或转移到另一个进程组)。 一个进程可以为自己或子进程设置进程组ID。
守护进程
守护进程介绍
守护进程Daemon Process也就是通常说的 Daemon 进程精灵进程是 Linux 中的后台服务进程。它是一个生存期较长的进程通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。一般采用以d结尾的名字。
守护进程是个特殊的孤儿进程这种进程脱离终端为什么要脱离终端呢之所以脱离于终端是为了避免进程被任何终端所产生的信息所打断其在执行过程中的信息也不在任何终端上显示。由于在 Linux 中每一个系统与用户进行交流的界面称为终端每一个从此终端开始运行的进程都会依附于这个终端这个终端就称为这些进程的控制终端当控制终端被关闭时相应的进程都会自动关闭。
Linux 的大多数服务器就是用守护进程实现的。比如Internet 服务器 inetdWeb 服务器 httpd 等。守护进程模型
1.创建子进程父进程退出(必须)
所有工作在子进程中进行形式上脱离了控制终端
2.在子进程中创建新会话(必须)
setsid()函数使子进程完全独立出来脱离控制
3.改变当前目录为根目录(不是必须)
chdir()函数防止占用可卸载的文件系统也可以换成其它路径
4.重设文件权限掩码(不是必须)
umask()函数防止继承的文件创建屏蔽字拒绝某些权限增加守护进程灵活性
5.关闭文件描述符(不是必须)
继承的打开文件不会用到浪费系统资源无法卸载
6.开始执行守护进程核心工作(必须)
守护进程退出处理程序模型
守护进程参考代码
写一个守护进程, 每隔2s获取一次系统时间, 将这个时间写入到磁盘文件
void write_time(int num){time_t rawtime;struct tm * timeinfo;// 获取时间time(rawtime);
#if 0// 转为本地时间timeinfo localtime(rawtime);// 转为标准ASCII时间格式char *cur asctime(timeinfo);
#elsechar* cur ctime(rawtime);
#endif// 将得到的时间写入文件中int fd open(/home/edu/timelog.txt, O_RDWR | O_CREAT | O_APPEND, 0664);if (fd -1){perror(open error);exit(1);}// 写文件int ret write(fd, cur, strlen(cur) 1);if (ret -1){perror(write error);exit(1);}// 关闭文件close(fd);
}
int main(int argc, const char* argv[])
{pid_t pid fork();if (pid -1){perror(fork error);exit(1);}if (pid 0){// 父进程退出exit(1);}else if (pid 0){// 子进程// 提升为会长同时也是新进程组的组长setsid();// 更改进程的执行目录chdir(/home/edu);// 更改掩码umask(0022);// 关闭文件描述符close(STDIN_FILENO);close(STDOUT_FILENO);close(STDERR_FILENO);// 注册信号捕捉函数//先注册再定时struct sigaction sigact;sigact.sa_flags 0;sigemptyset(sigact.sa_mask);sigact.sa_handler write_time;sigaction(SIGALRM, sigact, NULL);// 设置定时器struct itimerval act;// 定时周期act.it_interval.tv_sec 2;act.it_interval.tv_usec 0;// 设置第一次触发定时器时间act.it_value.tv_sec 2;act.it_value.tv_usec 0;// 开始计时setitimer(ITIMER_REAL, act, NULL);// 防止子进程退出while (1);}return 0;
}线程简介
线程概念
在许多经典的操作系统教科书中总是把进程定义为程序的执行实例它并不执行什么, 只是维护应用程序所需的各种资源而线程则是真正的执行实体。
所以线程是轻量级的进程LWPlight weight process在Linux环境下线程的本质仍是进程。 为了让进程完成一定的工作进程必须至少包含一个线程。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZG7bVU24-1684633973982)(D:\Typora笔记\c\Linux系统编程.assets\1528121100232.png)]
进程直观点说保存在硬盘上的程序运行以后会在内存空间里形成一个独立的内存体这个内存体有自己的地址空间有自己的堆上级挂靠单位是操作系统。操作系统会以进程为单位分配系统资源所以我们也说进程是CPU分配资源的最小单位。
线程存在与进程当中(进程可以认为是线程的容器)是操作系统调度执行的最小单位。说通俗点线程就是干活的。
进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动进程是系统进行资源分配和调度的一个独立单位。
线程是进程的一个实体是 CPU 调度和分派的基本单位它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源只拥有一点在运行中必不可少的资源如程序计数器一组寄存器和栈但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
如果说进程是一个资源管家负责从主人那里要资源的话那么线程就是干活的苦力。一个管家必须完成一项工作就需要最少一个苦力也就是说一个进程最少包含一个线程也可以包含多个线程。苦力要干活就需要依托于管家所以说一个线程必须属于某一个进程。
进程有自己的地址空间线程使用进程的地址空间也就是说进程里的资源线程都是有权访问的比如说堆啊栈啊静态存储区什么的。 进程是操作系统分配资源的最小单位 线程是操作系统调度的最小单位 线程的特点
类Unix系统中早期是没有“线程”概念的80年代才引入借助进程机制实现出了线程的概念。
因此在这类系统中进程和线程关系密切
线程是轻量级进程(light-weight process)也有PCB创建线程使用的底层函数和进程一样都是clone从内核里看进程和线程是一样的都有各自不同的PCB.进程可以蜕变成线程在linux下线程最是小的执行单位进程是最小的分配资源单位
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KqOcyvoO-1684633973982)(D:\Typora笔记\c\Linux系统编程.assets\1528121496711.png)]
查看指定进程的LWP号 ps -Lf pid 实际上无论是创建进程的fork还是创建线程的pthread_create底层实现都是调用同一个内核函数 clone 。
Ø 如果复制对方的地址空间那么就产出一个“进程”
Ø 如果共享对方的地址空间就产生一个“线程”。
Linux内核是不区分进程和线程的, 只在用户层面上进行区分。所以线程所有操作函数 pthread_* 是库函数而非系统调用。
线程共享资源
文件描述符表每种信号的处理方式当前工作目录用户ID和组ID
内存地址空间 (.text/.data/.bss/heap/共享库)
线程非共享资源 线程id 处理器现场和栈指针(内核栈) 独立的栈空间(用户空间栈) errno变量 信号屏蔽字 调度优先级 线程的优缺点 优点 Ø 提高程序并发性 Ø 开销小 Ø 数据通信、共享数据方便 缺点 Ø 库函数不稳定 Ø 调试、编写困难、gdb不支持 Ø 对信号支持不好 优点相对突出缺点均不是硬伤。Linux下由于实现方法导致进程、线程差别不是很大。
线程常用操作
线程号
就像每个进程都有一个进程号一样每个线程也有一个线程号。进程号在整个系统中是唯一的但线程号不同线程号只在它所属的进程环境中有效。
进程号用 pid_t 数据类型表示是一个非负整数。线程号则用 pthread_t 数据类型来表示Linux 使用无符号长整数表示。
有的系统在实现pthread_t 的时候用一个结构体来表示所以在可移植的操作系统实现不能把它做为整数处理。
pthread_self函数
#include pthread.h
pthread_t pthread_self(void);
功能获取线程号。
参数无
返回值调用线程的线程 ID 。线程的创建
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失败非 0在一个线程中调用pthread_create()创建新的线程后当前线程从pthread_create()返回继续往下执行而新的线程所执行的代码由我们传给pthread_create的函数指针start_routine决定。
由于pthread_create的错误码不保存在errno中因此不能直接用perror()打印错误信息可以先用strerror()把错误码转换成错误信息再打印。
参考程序
// 回调函数
void *thread_fun(void * arg)
{sleep(1);int num *((int *)arg);printf(int the new thread: num %d\n, num);return NULL;
}int main()
{pthread_t tid;int test 100;// 返回错误号int ret pthread_create(tid, NULL, thread_fun, (void *)test);if (ret ! 0){printf(error number: %d\n, ret);// 根据错误号打印错误信息printf(error information: %s\n, strerror(ret));}while (1);return 0;
}
线程资源回收
pthread_join函数
#include pthread.hint pthread_join(pthread_t thread, void **retval);
功能等待线程结束此函数会阻塞并回收线程资源类似进程的 wait() 函数。如果线程已经结束那么该函数会立即返回。
参数thread被等待的线程号。retval用来存储线程退出状态的指针的地址。
返回值成功0失败非 0参考程序
void *thead(void *arg)
{static int num 123; //静态变量printf(after 2 seceonds, thread will return\n);sleep(2);return num;
}int main()
{pthread_t tid;int ret 0;void *value NULL;// 创建线程pthread_create(tid, NULL, thead, NULL);// 等待线程号为 tid 的线程如果此线程结束就回收其资源// value保存线程退出的返回值pthread_join(tid, value);printf(value %d\n, *((int *)value));return 0;
}调用该函数的线程将挂起等待直到id为thread的线程终止。thread线程以不同的方法终止通过pthread_join得到的终止状态是不同的总结如下
如果thread线程通过return返回retval所指向的单元里存放的是thread线程函数的返回值。如果thread线程被别的线程调用pthread_cancel异常终止掉retval所指向的单元里存放的是常数PTHREAD_CANCELED。如果thread线程是自己调用pthread_exit终止的retval所指向的单元存放的是传给pthread_exit的参数。
线程分离
一般情况下线程终止后其终止状态一直保留到其它线程调用pthread_join获取它的状态为止。但是线程也可以被置为detach状态这样的线程一旦终止就立刻回收它占用的所有资源而不保留终止状态。
不能对一个已经处于detach状态的线程调用pthread_join这样的调用将返回EINVAL错误。也就是说如果已经对一个线程调用了pthread_detach就不能再调用pthread_join了。
pthread_detach函数 #include pthread.hint pthread_detach(pthread_t thread);
功能使调用线程与当前进程分离分离后不代表此线程不依赖与当前进程线程分离的目的是将线程资源的回收工作交由系统自动来完成也就是说当被分离的线程结束之后系统会自动回收它的资源。所以此函数不会阻塞。
参数thread线程号。
返回值成功0失败非0线程退出
在进程中我们可以调用exit函数或_exit函数来结束进程在一个线程中我们可以通过以下三种在不终止整个进程的情况下停止它的控制流。
线程从执行函数中返回。线程调用pthread_exit退出线程。线程可以被同一进程中的其它线程取消。
pthread_exit函数
#include pthread.hvoid pthread_exit(void *retval);
功能退出调用线程。一个进程中的多个线程是共享该进程的数据段因此通常线程退出后所占用的资源并不会释放。
参数retval存储线程退出状态的指针。
返回值无 参考程序:
void *thread(void *arg)
{static int num 123; //静态变量int i 0;while (1){printf(I am runing\n);sleep(1);i;if (i 3){pthread_exit((void *)num);// return num;}}return NULL;
}int main(int argc, char *argv[])
{int ret 0;pthread_t tid;void *value NULL;pthread_create(tid, NULL, thread, NULL);pthread_join(tid, value);printf(value %d\n, *(int *)value);return 0;
}线程取消 #include pthread.hint pthread_cancel(pthread_t thread);
功能杀死(取消)线程
参数thread : 目标线程ID。
返回值成功0失败出错编号
注意线程的取消并不是实时的而又一定的延时。需要等待线程到达某个取消点(检查点)。
类似于玩游戏存档必须到达指定的场所(存档点如客栈、仓库、城里等)才能存储进度。
杀死线程也不是立刻就能完成必须要到达取消点。
取消点是线程检查是否被取消并按请求进行动作的一个位置。通常是一些系统调用creatopenpauseclosereadwrite… 执行命令man 7 pthreads可以查看具备这些取消点的系统调用列表。
可粗略认为一个系统调用(进入内核)即为一个取消点。
参考程序: void *thread_cancel(void *arg)
{while (1){pthread_testcancel(); //设置取消点}return NULL;
}int main()
{pthread_t tid;pthread_create(tid, NULL, thread_cancel, NULL); //创建线程sleep(3); //3秒后pthread_cancel(tid); //取消tid线程pthread_join(tid, NULL);return 0;
}
线程取消 #include pthread.hint pthread_cancel(pthread_t thread);
功能杀死(取消)线程
参数thread : 目标线程ID。
返回值成功0失败出错编号
注意线程的取消并不是实时的而又一定的延时。需要等待线程到达某个取消点(检查点)。
类似于玩游戏存档必须到达指定的场所(存档点如客栈、仓库、城里等)才能存储进度。
杀死线程也不是立刻就能完成必须要到达取消点。
取消点是线程检查是否被取消并按请求进行动作的一个位置。通常是一些系统调用creatopenpauseclosereadwrite… 执行命令man 7 pthreads可以查看具备这些取消点的系统调用列表。
可粗略认为一个系统调用(进入内核)即为一个取消点。
参考程序: void *thread_cancel(void *arg)
{while (1){pthread_testcancel(); //设置取消点}return NULL;
}int main()
{pthread_t tid;pthread_create(tid, NULL, thread_cancel, NULL); //创建线程sleep(3); //3秒后pthread_cancel(tid); //取消tid线程pthread_join(tid, NULL);return 0;
}
如果thread线程是自己调用pthread_exit终止的retval所指向的单元存放的是传给pthread_exit的参数。
线程分离
一般情况下线程终止后其终止状态一直保留到其它线程调用pthread_join获取它的状态为止。但是线程也可以被置为detach状态这样的线程一旦终止就立刻回收它占用的所有资源而不保留终止状态。
不能对一个已经处于detach状态的线程调用pthread_join这样的调用将返回EINVAL错误。也就是说如果已经对一个线程调用了pthread_detach就不能再调用pthread_join了。
pthread_detach函数 #include pthread.hint pthread_detach(pthread_t thread);
功能使调用线程与当前进程分离分离后不代表此线程不依赖与当前进程线程分离的目的是将线程资源的回收工作交由系统自动来完成也就是说当被分离的线程结束之后系统会自动回收它的资源。所以此函数不会阻塞。
参数thread线程号。
返回值成功0失败非0线程退出
在进程中我们可以调用exit函数或_exit函数来结束进程在一个线程中我们可以通过以下三种在不终止整个进程的情况下停止它的控制流。
线程从执行函数中返回。线程调用pthread_exit退出线程。线程可以被同一进程中的其它线程取消。
pthread_exit函数
#include pthread.hvoid pthread_exit(void *retval);
功能退出调用线程。一个进程中的多个线程是共享该进程的数据段因此通常线程退出后所占用的资源并不会释放。
参数retval存储线程退出状态的指针。
返回值无 参考程序:
void *thread(void *arg)
{static int num 123; //静态变量int i 0;while (1){printf(I am runing\n);sleep(1);i;if (i 3){pthread_exit((void *)num);// return num;}}return NULL;
}int main(int argc, char *argv[])
{int ret 0;pthread_t tid;void *value NULL;pthread_create(tid, NULL, thread, NULL);pthread_join(tid, value);printf(value %d\n, *(int *)value);return 0;
}线程取消 #include pthread.hint pthread_cancel(pthread_t thread);
功能杀死(取消)线程
参数thread : 目标线程ID。
返回值成功0失败出错编号
注意线程的取消并不是实时的而又一定的延时。需要等待线程到达某个取消点(检查点)。
类似于玩游戏存档必须到达指定的场所(存档点如客栈、仓库、城里等)才能存储进度。
杀死线程也不是立刻就能完成必须要到达取消点。
取消点是线程检查是否被取消并按请求进行动作的一个位置。通常是一些系统调用creatopenpauseclosereadwrite… 执行命令man 7 pthreads可以查看具备这些取消点的系统调用列表。
可粗略认为一个系统调用(进入内核)即为一个取消点。
参考程序: void *thread_cancel(void *arg)
{while (1){pthread_testcancel(); //设置取消点}return NULL;
}int main()
{pthread_t tid;pthread_create(tid, NULL, thread_cancel, NULL); //创建线程sleep(3); //3秒后pthread_cancel(tid); //取消tid线程pthread_join(tid, NULL);return 0;
}
线程取消 #include pthread.hint pthread_cancel(pthread_t thread);
功能杀死(取消)线程
参数thread : 目标线程ID。
返回值成功0失败出错编号
注意线程的取消并不是实时的而又一定的延时。需要等待线程到达某个取消点(检查点)。
类似于玩游戏存档必须到达指定的场所(存档点如客栈、仓库、城里等)才能存储进度。
杀死线程也不是立刻就能完成必须要到达取消点。
取消点是线程检查是否被取消并按请求进行动作的一个位置。通常是一些系统调用creatopenpauseclosereadwrite… 执行命令man 7 pthreads可以查看具备这些取消点的系统调用列表。
可粗略认为一个系统调用(进入内核)即为一个取消点。
参考程序: void *thread_cancel(void *arg)
{while (1){pthread_testcancel(); //设置取消点}return NULL;
}int main()
{pthread_t tid;pthread_create(tid, NULL, thread_cancel, NULL); //创建线程sleep(3); //3秒后pthread_cancel(tid); //取消tid线程pthread_join(tid, NULL);return 0;
}