Linux系统编程总结

day2

vim的三种工作模式

  1. 命令模式 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)]

自动化格式程序 gg=G(命令模式)

大括号对应:%(命令模式)

删除单个字符: 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 history=1000

"开启相关插件"
"侦测文件类型"
filetype on
"载入文件类型插件"
filetype plugin on
"为特定文件类型载入相关缩进文件"
filetype indent on

"当文件在外部被修改时,自动更新该文件"
set autoread

"激活鼠标的使用"
set mouse=a
set selection=exclusive
set selectmode=mouse,key

"保存全局变量"
set viminfo+=!

"带有如下符号的单词不要被换行分割"
set iskeyword+=_,$,@,%,#,-

"通过使用: commands命令,告诉我们文件的哪一行被改变过"
set report=0

"被分割的窗口间显示空白,便于阅读"
set fillchars=vert:\ ,stl:\ ,stlnc:\


"""""""""""""""""""""""""""""""""
"""=>字体和颜色<="""
"""""""""""""""""""""""""""""""""
"自动开启语法高亮"
syntax enable

"设置字体"
"set guifont=dejaVu\ Sans\ MONO\ 10
set guifont=Courier_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 fdm=XX)"
"有6种折叠方法:
"manual   手工定义折叠"
"indent   更多的缩进表示更高级别的折叠"
"expr     用表达式来定义折叠"
"syntax   用语法高亮来定义折叠"
"diff     对没有更改的文本进行折叠"
"marker   对文中的标志进行折叠"
set foldmethod=manual
"set fdl=0(这个是不选用任何折叠方法)"

"设置折叠区域的宽度"
"如果不为0,则在屏幕左侧显示一个折叠标识列
"分别用“-”和“+”来表示打开和关闭的折叠
set foldcolumn=0

"设置折叠层数为3"
setlocal foldlevel=3

"设置为自动关闭折叠"
set foldclose=all

"用空格键来代替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 tabstop=4

"设定<<>>命令移动时的宽度为4"
set shiftwidth=4

"使得按退格键时可以一次删除4个空格"
set softtabstop=4
set smarttab

"缩进,自动缩进(继承前一行的缩进)"
"set autoindent 命令打开自动缩进,是下面配置的缩写
"可使用autoindent命令的简写,即“:set ai”和“:set noai”
"还可以使用“:set ai sw=4”在一个命令中打开缩进并设置缩进级别
set ai
set cindent

"智能缩进"
set si

"自动换行”
set wrap

"设置软宽度"
set sw=4

"行内替换"
set gdefault


""""""""""""""""""""""""""""""""""
"""=>Vim 界面<="""
""""""""""""""""""""""""""""""""""
"增强模式中的命令行自动完成操作"
set wildmenu

"显示标尺"
set ruler

"设置命令行的高度"
set cmdheight=1

"显示行数"
set nu

"不要图形按钮"
set go=

"在执行宏命令时,不进行显示重绘;在宏命令执行完成后,一次性重绘,以便提高性能"
set lz

"使回格键(backspace)正常处理indent, eol, start等"
set backspace=eol,start,indent

"允许空格键和光标键跨越行边界"
set whichwrap+=<,>,h,l

"设置魔术"
set magic

"关闭遇到错误时的声音提示"
"关闭错误信息响铃"
set noerrorbells

"关闭使用可视响铃代替呼叫"
set novisualbell

"高亮显示匹配的括号([{和}])"
set showmatch

"匹配括号高亮的时间(单位是十分之一秒)"
set mat=2

"光标移动到buffer的顶部和底部时保持3行距离"
set scrolloff=3

"搜索逐字符高亮"
set hlsearch
set incsearch

"搜索时不区分大小写"
"还可以使用简写(“:set ic”和“:set noic”)"
set ignorecase

"用浅色高亮显示当前行"
autocmd InsertLeave * se nocul
autocmd InsertEnter * se cul

"输入的命令显示出来,看的清楚"
set showcmd


""""""""""""""""""""""""""""""""""""
"""=>编码设置<="""
""""""""""""""""""""""""""""""""""""
"设置编码"
set encoding=utf-8
set fencs=utf-8,ucs-bom,shift-jis,gb18030,gbk,gb2312,cp936

"设置文件编码"
set fileencodings=utf-8

"设置终端编码"
set termencoding=utf-8

"设置语言编码"
set langmenu=zh_CN.UTF-8
set helplang=cn


"""""""""""""""""""""""""""""
"""=>其他设置<="""
"""""""""""""""""""""""""""""
"开启新行时使用智能自动缩进"
set smartindent
set cin
set showmatch

"在处理未保存或只读文件的时候,弹出确认"
set confirm

"隐藏工具栏"
set guioptions-=T

"隐藏菜单栏"
set guioptions-=m

"置空错误铃声的终端代码"
set vb t_vb=

"显示状态栏(默认值为1,表示无法显示状态栏)"
set laststatus=2

"状态行显示的内容"
set statusline=%F%m%r%h%w\ [FORMAT=%{&ff}]\ [TYPE=%Y]\ [POS=%l,%v][%p%%]\ %{strftime(\"%d/%m/%y\ -\ %H:%M\")}

"粘贴不换行问题的解决方法"
set pastetoggle=<F9>

"设置背景颜色"
set background=dark

"文件类型自动检测,代码智能补全"
set completeopt=longest,preview,menu

"共享剪切板"
set clipboard+=unnamed

"从不备份"
set nobackup
set noswapfile

"自动保存"
set autowrite

"显示中文帮助"
if version >= 603
        set helplang=cn
            set encoding=utf-8
endif

"设置高亮相关项"
highlight Search ctermbg=black ctermfg=white guifg=white guibg=black


""""""""""""""""""""""""""""""""
"""=>在shell脚本开头自动增加解释器以及作者等版权信息<="""
""""""""""""""""""""""""""""""""
"新建.py,.cc,.sh,.java文件,自动插入文件头"
autocmd BufNewFile *.py,*.cc,*.sh,*.java exec ":call SetTitle()"
"定义函数SetTitle,自动插入文件头"
func SetTitle()
    if expand ("%:e") == 'sh'
        call 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

静态库的制作

  1. 将.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、记事本)是指我用它来写程序的(编辑代码),而我们写的代码语句,电脑是不懂的,我们需要把它转成电脑能懂的语句,编译器就是这样的转化工具。就是说,我们用编辑器编写程序,由编译器编译后才可以运行!

编译器是将易于编写、阅读和维护的高级计算机语言翻译为计算机能解读、运行的低级机器语言的程序。

gcc(GNU Compiler Collection,GNU 编译器套件),是由 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 deng@itcast:~/share/3rd/1gcc​$ ./1hello hello itcast

如果不指定输出文件名字, gcc编译器会生成一个默认的可以执行a.out

deng@itcast:~/share/3rd/1gcc$ gcc 1hello.c
deng@itcast:~/share/3rd/1gcc​$ ls 1hello  1hello.c  1hello.i  1hello.o  1hello.s  a.out deng@itcast:~/share/3rd/1gcc​$ ./a.out
hello itcast、

gcc常用选项

选项 作用
-o file 指定生成的输出文件名为file
-E 只进行预处理
-S(大写) 只进行预处理和编译
-c(小写) 只进行预处理、编译和汇编
-v / --version 查看gcc版本号
-g 包含调试信息
-On n=0~3 编译优化,n越大优化得越多
-Wall 提示更多警告信息
-D 编译时定义宏

显示所有的警告信息

gcc -Wall test.c

将警告信息当做错误处理

gcc -Wall -Werror test.c

静态库连接和动态库连接

链接分为两种:静态链接和动态链接

  1. 静态链接

    静态链接:由链接器在链接时将库的内容加入到可执行程序中。

    优点:

    • 对运行环境的依赖性较小,具有较好的兼容性

    缺点:

    • 生成的程序比较大,需要更多的系统资源,在装入内存时会消耗更多的时间
    • 库函数有了更新,必须重新编译应用程序
  2. 动态链接

    动态链接:连接器在链接时仅仅建立与所需库函数的之间的链接关系,在程序运行时才将所需资源调入可执行程序。

    优点:

    • 在需要的时候才会调入对应的资源函数
    • 简化程序的升级;有着较小的程序体积
    • 实现进程之间的资源共享(避免重复拷贝)

    缺点:

    • 依赖动态库,不能独立运行
    • 动态库依赖版本问题严重

    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文件

guojiahui@guojiahui-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)动态库制作

步骤一:生成目标文件,此时要加编译选项:-fPIC(fpic)

$ gcc -fPIC -c add.c

参数:-fPIC 创建与地址无关的编译程序(pic,position independent code),是为了能够在多个应用程序间共享。

步骤二:生成共享库,此时要加链接器选项: -shared(指定生成动态链接库)

deng@itcast:~/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文件

      deng@itcast:~/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)的nmake,Linux下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 test2
    echo "test"
test1:
    echo "test1"
test2:
    echo "test2"

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工具要实现的目标,目标可以是一个或多个(多个目标间用空格隔开)。

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.c
    gcc 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.o
    gcc test.o add.o sub.o mul.o div.o -o test

test.o:test.c
    gcc -c test.c
add.o:add.c
    gcc -c add.c
sub.o:sub.c
    gcc -c sub.c
mul.o:mul.c
    gcc -c mul.c
div.o:div.c
    gcc -c div.c

Makefile中的变量

在Makefile中使用变量有点类似于C语言中的宏定义,使用该变量相当于内容替换,使用变量可以使Makefile易于维护,修改内容变得简单变量定义及使用。

7.1 自定义变量

1)定义变量方法:

​ 变量名=变量值

2)引用变量:

( 变量名 ) 或 (变量名)或 (变量名){变量名}

3)makefile的变量名:

  • makefile变量名可以以数字开头
  • 变量是大小写敏感的
  • 变量一般都在makefile的头部定义
  • 变量几乎可在makefile的任何地方使用

示例:


#变量
OBJS=add.o sub.o mul.o div.o test.o
TARGET=test

$(TARGET):$(OBJS)
    gcc $(OBJS) -o $(TARGET) 

add.o:add.c
    gcc -c add.c -o add.o

sub.o:sub.c
    gcc -c sub.c -o sub.o

mul.o:mul.c
    gcc -c mul.c -o mul.o

div.o:div.c
    gcc -c div.c -o div.o

test.o:test.c
    gcc -c test.c -o test.o

clean:
    rm -rf $(OBJS) $(TARGET)

除了使用用户自定义变量,makefile中也提供了一些变量(变量名大写)供用户直接使用,我们可以直接对其进行赋值。

CC = gcc #arm-linux-gcc

CPPFLAGS : C预处理的选项 如:-I

CFLAGS: C编译器的选项 -Wall -g -c

LDFLAGS : 链接器选项 -L -l

7.2 自动变量

  • $@: 表示规则中的目标
  • $<: 表示规则中的第一个条件
  • $^: 表示规则中的所有条件, 组成一个列表, 以空格隔开,如果这个列表中有重复的项则消除重复项。

注意:自动变量只能在规则的命令中中使用

参考示例:


#变量
OBJS=add.o sub.o mul.o div.o test.o add.o
TARGET=test
CC=gcc

#$@: 表示目标
#$<: 表示第一个依赖
#$^: 表示所有的依赖

$(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第三个版本:


OBJS=test.o add.o sub.o mul.o div.o
TARGET=test
$(TARGET):$(OBJS)
    gcc $(OBJS) -o $(TARGET) 

%.o:%.c
    gcc -c $< -o $@

Makefile中的函数

makefile中的函数有很多,在这里给大家介绍两个最常用的。

  1. wildcard – 查找指定目录下的指定类型的文件

src = $(wildcard *.c) //找到当前目录下所有后缀为.c的文件,赋值给src

  1. patsubst – 匹配替换

obj = $(patsubst %.c,%.o, $(src)) //把src变量里所有后缀为.c的文件替换成.o

在makefile中所有的函数都是有返回值的。

Makefile第四个版本:

SRC=$(wildcard *.c)
OBJS=$(patsubst %.c, %.o, $(SRC))
TARGET=test
$(TARGET):$(OBJS)
    gcc $(OBJS) -o $(TARGET) 

%.o:%.c
    gcc -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))
TARGET=test
$(TARGET):$(OBJS)
    gcc $(OBJS) -o $(TARGET) 

%.o:%.c
    gcc -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

#include
#include
#include
#include
#include
#include
#include

文件描述符

​ 在 Linux 的世界里,一切设备皆文件。我们可以系统调用中 I/O 的函数(I:input,输入;O:output,输出),对文件进行相应的操作( 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 
#include 
#include 

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
功能:
    打开文件,如果文件不存在则可以选择创建。
参数:
    pathname:文件的路径及文件名
    flags:打开文件的行为标志,必选项 O_RDONLY, O_WRONLY, O_RDWR
    mode:这个参数,只有在文件不存在时有效,指新建文件时指定文件的权限
返回值:
    成功:成功返回打开的文件描述符
    失败:-1

flags详细说明

必选项:

取值 含义
O_RDONLY 以只读的方式打开
O_WRONLY 以只写的方式打开
O_RDWR 以可读、可写的方式打开

可选项,和必选项按位或起来

取值 含义
O_CREAT 文件不存在则创建文件,使用此选项时需使用mode说明文件的权限
O_EXCL 如果同时指定了O_CREAT,且文件已经存在,则出错
O_TRUNC 如果文件存在,则清空文件内容
O_APPEND 写文件时,数据添加到文件末尾
O_NONBLOCK 对于设备文件, 以O_NONBLOCK方式打开可以做非阻塞I/O

mode补充说明

  1. 文件最终权限:mode & ~umask
  2. shell进程的umask掩码可以用umask命令查看

​ Ø umask:查看掩码(补码)

​ Ø umask mode:设置掩码,mode为八进制数

​ Ø umask -S:查看各组用户的默认操作权限

取值 八进制 含义
S_IRWXU 00700 文件所有者的读、写、可执行权限
S_IRUSR 00400 文件所有者的读权限
S_IWUSR 00200 文件所有者的写权限
S_IXUSR 00100 文件所有者的可执行权限
S_IRWXG 00070 文件所有者同组用户的读、写、可执行权限
S_IRGRP 00040 文件所有者同组用户的读权限
S_IWGRP 00020 文件所有者同组用户的写权限
S_IXGRP 00010 文件所有者同组用户的可执行权限
S_IRWXO 00007 其他组用户的读、写、可执行权限
S_IROTH 00004 其他组用户的读权限
S_IWOTH 00002 其他组用户的写权限
S_IXOTH 00001 其他组用户的可执行权限

close函数


#include 

int close(int fd);
功能:
    关闭已打开的文件
参数:
    fd : 文件描述符,open()的返回值
返回值:
    成功:0
    失败: -1, 并设置errno

​ 需要说明的是,当一个进程终止时,内核对该进程所有尚未关闭的文件描述符调用close关闭,所以即使用户程序不调用close,在终止时内核也会自动关闭它打开的所有文件。

​ 但是对于一个长年累月运行的程序(比如网络服务器),打开的文件描述符一定要记得关闭,否则随着打开的文件越来越多,会占用大量文件描述符和系统资源。

write函数


#include 
ssize_t write(int fd, const void *buf, size_t count);
功能:
    把指定数目的数据写到文件(fd)
参数:
    fd :  文件描述符
    buf : 数据首地址
    count : 写入数据的长度(字节)
返回值:
    成功:实际写入数据的字节个数
    失败: - 1

read函数


#include 

ssize_t read(int fd, void *buf, size_t count);
功能:
    把指定数目的数据读到内存(缓冲区)
参数:
    fd : 文件描述符
    buf : 内存首地址
    count : 读取的字节个数
返回值:
    成功:实际读取到的字节个数
    失败: - 1

day4

文件描述符复制(重点)

概述

​ dup() 和 dup2() 是两个非常有用的系统调用,都是用来复制一个文件的描述符,使新的文件描述符也标识旧的文件描述符所标识的文件。

​ 这个过程类似于现实生活中的配钥匙,钥匙相当于文件描述符,锁相当于文件,本来一个钥匙开一把锁,相当于,一个文件描述符对应一个文件,现在,我们去配钥匙,通过旧的钥匙复制了一把新的钥匙,这样的话,旧的钥匙和新的钥匙都能开启这把锁。

​ 对比于 dup(), dup2() 也一样,通过原来的文件描述符复制出一个新的文件描述符,这样的话,原来的文件描述符和新的文件描述符都指向同一个文件,我们操作这两个文件描述符的任何一个,都能操作它所对应的文件。

dup函数

#include 

int dup(int oldfd);
功能:
    通过 oldfd 复制出一个新的文件描述符,新的文件描述符是调用进程文件描述符表中最小可用的文件描述符,最终 oldfd 和新的文件描述符都指向同一个文件。
参数:
    oldfd : 需要复制的文件描述符 oldfd
返回值:
        成功:新文件描述符
        失败: -1

dup2函数

#include 

int dup2(int oldfd, int newfd);
功能:
    通过 oldfd 复制出一个新的文件描述符 newfd,如果成功,newfd 和函数返回值是同一个返回值,最终 oldfd 和新的文件描述符 newfd 都指向同一个文件。
参数:
    oldfd : 需要复制的文件描述符
    newfd : 新的文件描述符,这个描述符可以人为指定一个合法数字(0 - 1023),如果指定的数字已经被占用(和某个文件有关联),此函数会自动关闭 close() 断开这个数字和某个文件的关联,再来使用这个合法数字。
返回值:
    成功:返回 newfd
    失败:返回 -1
//测试dup函数复制文件描述符
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
 
int main(int argc, char *argv[])
{
    //打开文件
    int fd = open(argv[1], O_RDWR);
    if(fd<0)
    {
        perror("open error");
        return -1;
    }
 
    //调用dup函数复制fd
    int 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 --pwd
  • umask掩码。
  • 文件描述符表,包含很多指向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 不可中断 Uninterruptible(usually IO)
    R 正在运行,或在队列中的进程
    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)]

#include
#include
#include
#include
#include
#include
#include
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个子进程:

#include
#include
#include
#include
#include
#include
#include
int main()
{       int i = 0;
        for(i = 0;i<3;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)]

测试父子进程是否能够共享全局变量(不可以)

#include
#include
#include
#include
#include
#include
#include
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 
    #include 
    #include 
    #include 
    #include 
     
    int main()
    {
        //创建子进程
        pid_t pid = fork();
        if(pid<0) //fork失败的情况
        {
            perror("fork error");
            return -1;
        }
        else if(pid>0)//父进程
        {
            sleep(5);
            printf("father: [%d], pid==[%d], fpid==[%d]\n", pid, getpid(),getppid());
        }
        else if(pid==0) //子进程
        {
             
            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 
#include 
#include 
#include 
#include 
 
int main()
{
    //创建子进程
    pid_t pid = fork();
    if(pid<0) //fork失败的情况
    {
        perror("fork error");
        return -1;
    }
    else if(pid>0)//父进程
    {
        sleep(100);
        printf("father: [%d], pid==[%d], fpid==[%d]\n", pid, getpid(),getppid());
    }
    else if(pid==0) //子进程
    {
         
        printf("child: pid==[%d], fpid==[%d]\n", getpid(), getppid());
    }
     
 
    return 0;
}
  • 如何解决僵尸进程
    • 由于僵尸进程是一个已经死亡的进程,所以不能使用kill命令将其杀死
    • 通过杀死其父进程的方法可以消除僵尸进程。

​ 杀死其父进程后,这个僵尸进程会被init进程领养,由init进程完成对僵尸进程的回收。

  • 模拟僵尸进程的案例

编写模拟僵尸进程的代码讲解僵尸进程, 验证若子进程先于父进程退出, 而父进程没有调用wait或者waitpid函数进行回收, 从而使子进程成为了僵尸进程.


#include 
#include 
#include 
#include 
#include 
#include 
 
int main()
{

    pid_t pid = fork();
    if(pid<0) 
    {
        perror("fork error");
        return -1;
    }
    else if(pid>0)
    {
        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(pid==0) 
    {
         
        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);

  • 函数作用
  1. 阻塞并等待子进程退出
  2. 回收子进程残留资源
  3. 获取子进程结束状态(退出原因)。
  • 返回值:
  1. 成功:清理掉的子进程ID;
  2. 失败:-1 (没有子进程)
  • status参数:子进程的退出状态 – 传出参数

WIFEXITED(status):为非0 → 进程正常结束

WEXITSTATUS(status):获取进程退出状态

WIFSIGNALED(status):为非0 → 进程异常终止

day6

进程间通信相关概念

什么是进程间的通信

​ Linux环境下,进程地址空间相互独立,每个进程各自有不同的用户地址空间。任何一个进程的全局变量在另一个进程中都看不到,所以进程和进程之间不能相互访问,要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC,InterProcess Communication)。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3NODDdI8-1684633973978)(D:\Typora笔记\c++\Linux系统编程.assets\wps9.jpg)]

进程间的通信方式

​ 在进程间完成数据传递需要借助操作系统提供特殊的方法,如:文件、管道、信号、共享内存、消息队列、套接字、命名管道等。随着计算机的蓬勃发展,一些方法由于自身设计缺陷被淘汰或者弃用。现今常用的进程间通信方式有:

  • 管道 (使用最简单)

  • 信号 (开销最小)

  • 共享映射区 (无血缘关系)

  • 本地套接字 (最稳定)

管道

管道的概念

​ 管道是一种最基本的IPC机制,也称匿名管道,应用于有血缘关系的进程之间,完成数据传递。调用pipe函数即可创建一个管道。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JgxFmaIl-1684633973979)(D:\Typora笔记\c++\Linux系统编程.assets\wps10.jpg)]

有如下特质:

  1. 管道的本质是一块内核缓冲区
  2. 由两个文件描述符引用,一个表示读端,一个表示写端。
  3. 规定数据从管道的写端流入管道,从读端流出。
  4. 当两个进程都终结的时候,管道也自动消失。
  5. 管道的读端和写端默认都是阻塞的。

管道的原理

  • 管道的实质是内核缓冲区,内部使用环形队列实现。(缓刑管道效率高)
  • 默认缓冲区大小为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 
#include 
#include 
#include 
#include 
#include 
int main()
{
        int fd[2];
        int ret = pipe(fd);
        if(ret<0)
        {       
                perror("pipe error");
                return -1;
        }
        //创建子进程
        pid_t pid = fork();
        if(pid<0)
        {
                 perror("fork error");
                return -1;

        }
        else if(pid>0)
        {
                //关闭读端
                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函数使用的总结

  1. 第一个参数写成NULL
  2. 第二个参数要映射的大小>0
  3. 第三个参数:PROT_READ 、PROT_WRITE
  4. 第四个参数:MAP_SHARED 或者 MAP_PRIVATE
  5. 第五个参数:打开的文件对应的文件描述符
  6. 第六个参数: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) 当用户按某些终端键时,将产生信号。

​ 终端上按“Ctrl+c”组合键通常产生中断信号 SIGINT

​ 终端上按“Ctrl+\”键通常产生中断信号 SIGQUIT

​ 终端上按“Ctrl+z”键通常产生中断信号 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)]

如何避免僵尸进程

  1. 最简单的方法,父进程通过 wait() 和 waitpid() 等函数等待子进程结束,但是,这会导致父进程挂起。
  2. 如果父进程要处理的事情很多,不能够挂起,通过 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)如果父进程不关心子进程什么时候结束,那么可以用signal(SIGCHLD, 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进程启动的其它进程的控制终端也是这个终端。

​ 默认情况下(没有重定向),每个进程的标准输入、标准输出和标准错误输出都指向控制终端,进程从标准输入读也就是读用户的键盘输入,进程往标准输出或标准错误输出写也就是输出到显示器上。

​ 信号中还讲过,在控制终端输入一些特殊的控制键可以给前台进程发信号,例如Ctrl+C表示SIGINT,Ctrl+\表示SIGQUIT。

函数说明:


#include 

char *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 服务器 inetd,Web 服务器 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);
#else
    char* 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;
}

线程简介

线程概念

在许多经典的操作系统教科书中,总是把进程定义为程序的执行实例,它并不执行什么, 只是维护应用程序所需的各种资源,而线程则是真正的执行实体。

所以,线程是轻量级的进程(LWP:light weight process),在Linux环境下线程的本质仍是进程。

​ 为了让进程完成一定的工作,进程必须至少包含一个线程。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZG7bVU24-1684633973982)(D:\Typora笔记\c++\Linux系统编程.assets\1528121100232.png)]

​ 进程,直观点说,保存在硬盘上的程序运行以后,会在内存空间里形成一个独立的内存体,这个内存体有自己的地址空间,有自己的堆,上级挂靠单位是操作系统。操作系统会以进程为单位,分配系统资源,所以我们也说,进程是CPU分配资源的最小单位

​ 线程存在与进程当中(进程可以认为是线程的容器),是操作系统调度执行的最小单位。说通俗点,线程就是干活的。

​ 进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。

​ 线程是进程的一个实体,是 CPU 调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。

​ 如果说进程是一个资源管家,负责从主人那里要资源的话,那么线程就是干活的苦力。一个管家必须完成一项工作,就需要最少一个苦力,也就是说,一个进程最少包含一个线程,也可以包含多个线程。苦力要干活,就需要依托于管家,所以说一个线程,必须属于某一个进程。

​ 进程有自己的地址空间,线程使用进程的地址空间,也就是说,进程里的资源,线程都是有权访问的,比如说堆啊,栈啊,静态存储区什么的。

进程是操作系统分配资源的最小单位

线程是操作系统调度的最小单位

线程的特点

​ 类Unix系统中,早期是没有“线程”概念的,80年代才引入,借助进程机制实现出了线程的概念。

因此在这类系统中,进程和线程关系密切:

  1. 线程是轻量级进程(light-weight process),也有PCB,创建线程使用的底层函数和进程一样,都是clone
  2. 从内核里看进程和线程是一样的,都有各自不同的PCB.
  3. 进程可以蜕变成线程
  4. 在linux下,线程最是小的执行单位;进程是最小的分配资源单位

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KqOcyvoO-1684633973982)(D:\Typora笔记\c++\Linux系统编程.assets\1528121496711.png)]

查看指定进程的LWP号:

ps -Lf pid

实际上,无论是创建进程的fork,还是创建线程的pthread_create,底层实现都是调用同一个内核函数 clone 。

Ø 如果复制对方的地址空间,那么就产出一个“进程”;

Ø 如果共享对方的地址空间,就产生一个“线程”。

​ Linux内核是不区分进程和线程的, 只在用户层面上进行区分。所以,线程所有操作函数 pthread_* 是库函数,而非系统调用。

线程共享资源

  1. 文件描述符表
  2. 每种信号的处理方式
  3. 当前工作目录
  4. 用户ID和组ID

内存地址空间 (.text/.data/.bss/heap/共享库)

线程非共享资源

  1. 线程id

  2. 处理器现场和栈指针(内核栈)

  3. 独立的栈空间(用户空间栈)

  4. errno变量

  5. 信号屏蔽字

  6. 调度优先级

  7. 线程的优缺点

    优点:

    Ø 提高程序并发性

    Ø 开销小

    Ø 数据通信、共享数据方便

    缺点:

    Ø 库函数,不稳定

    Ø 调试、编写困难、gdb不支持

    Ø 对信号支持不好

    优点相对突出,缺点均不是硬伤。Linux下由于实现方法导致进程、线程差别不是很大。

线程常用操作

线程号

​ 就像每个进程都有一个进程号一样,每个线程也有一个线程号。进程号在整个系统中是唯一的,但线程号不同,线程号只在它所属的进程环境中有效。

​ 进程号用 pid_t 数据类型表示,是一个非负整数。线程号则用 pthread_t 数据类型来表示,Linux 使用无符号长整数表示。

​ 有的系统在实现pthread_t 的时候,用一个结构体来表示,所以在可移植的操作系统实现不能把它做为整数处理。

pthread_self函数:

#include 
pthread_t pthread_self(void);
功能:
    获取线程号。
参数
    无
返回值:
    调用线程的线程 ID 。

线程的创建

pthread_create函数:

#include 
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 

int 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 #
}

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得到的终止状态是不同的,总结如下:

  1. 如果thread线程通过return返回,retval所指向的单元里存放的是thread线程函数的返回值。
  2. 如果thread线程被别的线程调用pthread_cancel异常终止掉,retval所指向的单元里存放的是常数PTHREAD_CANCELED。
  3. 如果thread线程是自己调用pthread_exit终止的,retval所指向的单元存放的是传给pthread_exit的参数。

线程分离

一般情况下,线程终止后,其终止状态一直保留到其它线程调用pthread_join获取它的状态为止。但是线程也可以被置为detach状态,这样的线程一旦终止就立刻回收它占用的所有资源,而不保留终止状态。

​ 不能对一个已经处于detach状态的线程调用pthread_join,这样的调用将返回EINVAL错误。也就是说,如果已经对一个线程调用了pthread_detach就不能再调用pthread_join了。

pthread_detach函数:

 
#include 

int pthread_detach(pthread_t thread);
功能:
    使调用线程与当前进程分离,分离后不代表此线程不依赖与当前进程,线程分离的目的是将线程资源的回收工作交由系统自动来完成,也就是说当被分离的线程结束之后,系统会自动回收它的资源。所以,此函数不会阻塞。
参数:
    thread:线程号。
返回值:
    成功:0
    失败:非0

线程退出

​ 在进程中我们可以调用exit函数或_exit函数来结束进程,在一个线程中我们可以通过以下三种在不终止整个进程的情况下停止它的控制流。

  • 线程从执行函数中返回。
  • 线程调用pthread_exit退出线程。
  • 线程可以被同一进程中的其它线程取消。

pthread_exit函数:

#include 

void 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 #
        }
    }

    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 

int pthread_cancel(pthread_t thread);
功能:
    杀死(取消)线程
参数:
    thread : 目标线程ID。
返回值:
    成功:0
    失败:出错编号

注意:线程的取消并不是实时的,而又一定的延时。需要等待线程到达某个取消点(检查点)。

类似于玩游戏存档,必须到达指定的场所(存档点,如:客栈、仓库、城里等)才能存储进度。

杀死线程也不是立刻就能完成,必须要到达取消点。

取消点:是线程检查是否被取消,并按请求进行动作的一个位置。通常是一些系统调用creat,open,pause,close,read,write… 执行命令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 

int pthread_cancel(pthread_t thread);
功能:
    杀死(取消)线程
参数:
    thread : 目标线程ID。
返回值:
    成功:0
    失败:出错编号

注意:线程的取消并不是实时的,而又一定的延时。需要等待线程到达某个取消点(检查点)。

类似于玩游戏存档,必须到达指定的场所(存档点,如:客栈、仓库、城里等)才能存储进度。

杀死线程也不是立刻就能完成,必须要到达取消点。

取消点:是线程检查是否被取消,并按请求进行动作的一个位置。通常是一些系统调用creat,open,pause,close,read,write… 执行命令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 

int pthread_detach(pthread_t thread);
功能:
    使调用线程与当前进程分离,分离后不代表此线程不依赖与当前进程,线程分离的目的是将线程资源的回收工作交由系统自动来完成,也就是说当被分离的线程结束之后,系统会自动回收它的资源。所以,此函数不会阻塞。
参数:
    thread:线程号。
返回值:
    成功:0
    失败:非0

线程退出

​ 在进程中我们可以调用exit函数或_exit函数来结束进程,在一个线程中我们可以通过以下三种在不终止整个进程的情况下停止它的控制流。

  • 线程从执行函数中返回。
  • 线程调用pthread_exit退出线程。
  • 线程可以被同一进程中的其它线程取消。

pthread_exit函数:

#include 

void 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 #
        }
    }

    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 

int pthread_cancel(pthread_t thread);
功能:
    杀死(取消)线程
参数:
    thread : 目标线程ID。
返回值:
    成功:0
    失败:出错编号

注意:线程的取消并不是实时的,而又一定的延时。需要等待线程到达某个取消点(检查点)。

类似于玩游戏存档,必须到达指定的场所(存档点,如:客栈、仓库、城里等)才能存储进度。

杀死线程也不是立刻就能完成,必须要到达取消点。

取消点:是线程检查是否被取消,并按请求进行动作的一个位置。通常是一些系统调用creat,open,pause,close,read,write… 执行命令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 

int pthread_cancel(pthread_t thread);
功能:
    杀死(取消)线程
参数:
    thread : 目标线程ID。
返回值:
    成功:0
    失败:出错编号

注意:线程的取消并不是实时的,而又一定的延时。需要等待线程到达某个取消点(检查点)。

类似于玩游戏存档,必须到达指定的场所(存档点,如:客栈、仓库、城里等)才能存储进度。

杀死线程也不是立刻就能完成,必须要到达取消点。

取消点:是线程检查是否被取消,并按请求进行动作的一个位置。通常是一些系统调用creat,open,pause,close,read,write… 执行命令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;
}

你可能感兴趣的:(linux,vim,运维)