Linux系统编程学习笔记

课程链接:https://www.bilibili.com/video/BV1KE411q7ee

课程视频资源和笔记: 链接:https://pan.baidu.com/s/150gSAFxTGBaBF-wb6yZfhQ 提取码:unix

练习代码:https://gitee.com/daniel187/Linux_System

001-Linux命令基础习惯

终端: 一系列输入输出设备的统称;

$ echo $SHELL			#查看当前正在使用的命令解析器
$ cat /etc/shells		#查看支持的所有shell
$ ls -l file			#查看file的详细信息

Ctrl+a-回到命令首;

Ctrl+e-回到命令尾;

Ctrl+u-删除所有内容;

002-类Unix系统目录

在这里插入图片描述

  • bin-存放可执行文件;
  • boot-存放OS启动例程;
  • dev-存放设备文件(Linux系统中所见皆文件);
  • etc-存放用户信息, 如密码等;
  • home-用户的家目录;
  • lib-库目录
  • media和mnt-挂载磁盘的设备文件;
  • opt和proc-进程相关;
  • usr-UnixSoftwareResource;
  • root-管理员宿主目录(家目录);

sudo su-切换到管理员

003-目录和文件操作1

cd --在两个目录之间来回切换;

ls -a-显示隐藏文件, 每个目录下面都有一个.和…的隐藏文件;

$ ls -l dir		#查看dir目录下文件的详细信息
$ ls -ld dir	#查看dir目录本身的详细信息
$ ls -R			#递龟进入子目录

Linux文件类型:

  • 普通-
  • 目录d
  • 字符设备c
  • 块设备b
  • 软连接l
  • 管道p
  • 套接字s
  • Unknown

which date-查看可执行文件的路径;

rmdir-删除空目录;

cp -a/-r srcdir dstdir-拷贝目录;

004-目录和文件操作2

more-分屏显示文件内容, 空格翻页;

less同理;

head -n file-查看file的前n行;

tail -n file-查看file的后n行;

sudo apt-get install tree-安装命令;

005-软连接和硬连接

ln -s hello.c hello.c.s-创建软连接;
Linux系统编程学习笔记_第1张图片
软连接中存的就是文件的路径, 路径有几个字符就占几个字节, 所以建议用绝对路径创建软连接;

另外注意文件的权限, 软连接的权限代表其本身的权限, 与指向的目的文件无关;

ln hello.c hello.c.h-创建硬链接;
在这里插入图片描述

创建硬链接会增加硬链接计数;

这些硬链接只想同一个文件, 修改一个其余的会同步变化;

查看文件状态:
Linux系统编程学习笔记_第2张图片
所有的硬链接有相同的Inode(文件统一id);

删除只是把硬链接计数-1;

006-创建修改用户和用户组

chmod:

文字设定法:

  • u-所有者;
  • g-同组用户;
  • o-其他用户;
  • a-上面所有;

chmod o+w file-给其他用户写权限;

chmod a+x file-给所有用户执行权限;

数字设定法:

rwx分别对应421

chmod 471 file r–rwx–x;

sudo adduser tom-添加用户;

chown tom file-改变文件的所有者;

su tom- 切换用户;

sudo addgroup g77-添加一个新组;

sudo chgrp g77 file-修改所属组;

sudo chown tom:tom file-同时修改用户和用户组;
Linux系统编程学习笔记_第3张图片
sudo deluser tom-删除用户;

sudo delgroup g77-删除用户组;

007-find命令1

目录紧跟在find之后

find ./ -type 'l'找当前目录下的软连接, 子目录会递龟进入;

find ./ -name '*.jpg'-找当前目录下的jpg文件, 子目录会递龟进入;

find ./ -maxdepth 3 -name '*.jpg'-指定目录层级深度为3层;

find ./ -size +20M -size -50M-指定大小范围;

ls -h-以人类可读的方式显示结果;

man手册中反斜杠**/**可以用于查找关键字;

关于size的单位(find默认用b):
Linux系统编程学习笔记_第4张图片
按时间查找:

  • -atime(access访问时间)
  • -ctime(change更改时间)
  • -mtime(modify改动时间)

find ./ -ctime 3查找三天内被改动的文件;

008-午后复习

None

009-find命令2

find /usr/ -name "\*temp\*" -exec ls -l {} \;

大括号表示前面命令返回的结果集, 对其指定**-exec**后面的命令, ;是转义后的;

find ~/ -type f -ok rm -r {} \;

exec的缓冲版, 操作前会询问, 保证安全性;

010-grep和xargs

grep:按文件内容搜索"return"关键字:

grep -r "return" ./ -n

ps:监控后台进程的工作情况;

ps aux

加个管道过滤内容

ps aux | grep "kernel"(搜索本身会占一个进程)

如果将管道的手法用在find上(用xargs):

find /usr/ -maxdepth 3 -type -f | xargs ls -l

-execxargs的区别:前者会将结果不论多少一股脑的交给-exec, 而xargs会做分片处理(效率更高);

创建名字中有空格的文件:

$ touch abc\ def
$ touch "abc def"

由于xargs会将文件名中的空格误认为是分隔符, 解决方式: 控制分隔符:

find /usr/ -maxdepth 3 -type f -print0 | xargs -0 ls -l

011-xargs和awk说明

awk是按行拆分;

sed是按列拆分;

通常用于shell的正则表达式中;

012-软件包的安装

更新源服务器列表:sudo vim /etc/apt/sources.list

更新源:sudo apt-get update

安装:sudo apt-get install package

卸载:sudo apt-get remove package

deb包安装(Ubuntu属于debain系列)
Linux系统编程学习笔记_第5张图片

源码安装:
Linux系统编程学习笔记_第6张图片

013-压缩命令gzip和bzip2

压缩文件:

tar -zcvf test.tar.gz 039_serverMultiProcess.c hello.c repository

系统中真正进行压缩的是gzip, 解压缩是gunzip, 但是gzip只能一对一压缩;

tar命令实际上是用于打包的, 参数z就是用gzip进行压缩, ccreate的意思, v表示可见, f表示生成文件;

file是文件照妖镜, 看文件属性;

bzip2gzip类似, 都是单个文件所用, 如果使用bzip2进行压缩:

tar -jcvf test2.tar.gz 039_serverMutiProcess.c hello.c repository

014-rar压缩和zip压缩

rar压缩:

rar a -r rartest.rar hello.c hello.cpp

rar解压缩:

rar x rartest.rar

sudo aptitude show tree可以查看安装软件的详细信息;

zip压缩:

zip -r ziptest.zip hello.c hello.cpp

zip解压缩:

unzip ziptest.zip

015-其他命令

vi是Visual Interface的简称;

who

whoami

cat &让cat去后台运行;

则用jobs可以将其拿出来查看(查看OS后面用户的作业);

fgbg前后台切换;

env查看环境变量;

env | grep SHELL

top是调出任务管理器;

sudo passwd daniel改密码;

ifconfig查看网卡信息;

man手册:
Linux系统编程学习笔记_第7张图片

clear=Ctrl+l, 操, 才知道;

alias起别名:

daniel@ubuntu:~/sys$ alias
alias alert='notify-send --urgency=low -i "$([ $? = 0 ] && echo terminal || echo error)" "$(history|tail -n1|sed -e '\''s/^\s*[0-9]\+\s*//;s/[;&|]\s*alert$//'\'')"'
alias egrep='egrep --color=auto'
alias fgrep='fgrep --color=auto'
alias grep='grep --color=auto'
alias l='ls -CF'
alias la='ls -A'
alias ll='ls -alF'
alias ls='ls --color=auto'

umask指定用户创建文件时的掩码, 其中的mode和chmod命令中的格式一样;

OS不认为你新touch出来的文件具有执行能力, 所以他会将umask的执行权限给去掉:

例如touch一个新文件的权限为rw-r–r--, 对应的数字表示法为644, 则对应的umask应为133, 但实际上时022, 默认把执行权限去掉了;

再例如你设置了一个umask为511, 则对应的文件权限为266, -w-rw-rw-, 本身没有执行权限, 操作系统认为合法, 不会改动你的设置;

但是你设置umask为522, 对应的文件权限为255, 对应的文件权限为-w-r-xr-x, 但是执行权限会被抹掉, 所以最终的权限只能得到 -w-r–r--;
Linux系统编程学习笔记_第8张图片

reboot重启;

free -m查看空闲内存;

016-总结

None(第一天结束)

017-复习

终端中Ctrl+h是BackSpace;

一个目录所占的磁盘大小为4K;

018-vim的三种工作模式

Linux系统编程学习笔记_第9张图片
命令模式下ZZ也可以保存退出哦;

019-vim基本操作-跳转和删除字符

命令模式下:

I->光标到行首, 插入;

A->光标到行尾, 插入;

S->直接干掉整行, 切换到文本模式书写;
Linux系统编程学习笔记_第10张图片

末行模式下直接输入行号就可以跳转到指定行;

命令模式下的%可以跳转到匹配的括号;

D->从当前一直删到本行结尾;

d0->删到行首;

X->删除光标前一个字符;

020-vim基本操作-删除

r->不改变工作模式的替换单个字符;

021-vim基本操作-复制粘贴

P->向光标所在行的上一行粘贴;

vim没有删除, 全是剪切操作;

022-vim基本操作-查找和替换

找设想内容: 命令模式下输入斜杠/, 然后输入查找的内容, 进行查找;

回车后按n找到下一个;

找看到的内容: 在一定范围内检索单词: 在单词名字上星号(向后)*, 或者井号(向前)#;

替换: 在本行的末行模式下:

:s /printf/println

通篇替换: 只会替换每一行的首个:

:%s /printf/println

如果想要把每行后面的也替换, 加参数g;

局部替换:

:30,37 s /int/unsigned int/g

023-vim基本操作-其他

Ctrl+r反撤销;

:sp水平分屏;

[d查看宏定义;

! gcc hello.c -o hello在文件中执行命令;

024-vim配置思路

vim配置文件路径:

~/.vimrc

025-gcc编译4步骤

Linux系统编程学习笔记_第11张图片

优化编译速度主要集中则编译阶段;

026-gcc编译常用参数

指定头文件位置-I:gcc hello.c -I ./headers -o hello

只做预处理, 编译, 汇编, 不做连接, 拿到二进制文件-c;

编译时添加调试信息, 支持gdb调试-g:gcc hello.c -I ./headers -o hello2 -g
Linux系统编程学习笔记_第12张图片

显示所有警告信息-Wall;

向程序中动态注册一个宏-D:gcc hello.c -o hello -D HELLO

#ifdef HELLO
	#define HI 20
#endif

#include "hello.h"
int main(int argc,char* argv[]){
     
	printf("----------------\n");
	printf("%d\n",HI);
	return 0;
}

这种宏定义常可以做开关使用, 用于输出调试信息;

027-午后复习

None

028-静态库和动态库理论对比

Linux系统编程学习笔记_第13张图片
静态库会大量占用存储空间:

Linux系统编程学习笔记_第14张图片

如果使用动态库情况则大不相同:

Linux系统编程学习笔记_第15张图片

动态库不需要编译入程序, 运行时动态加载, 导致速度慢了一些;

二者的适合场景:

  • 静态库: 对空间要求较低, 对时间要求较高;
  • 动态库: 对时间要求较低, 对空间要求较高;

029-静态库制作

ar rcs libMyLib.a add.o sub.o div1.o

Linux系统编程学习笔记_第16张图片

先用gcc的-c参数将源文件编译成二进制文件, 再用ar命令封装静态库;

如果直接编译, collect2是链接器, 报错了

Linux系统编程学习笔记_第17张图片
说明链接阶段出错;

将库直接加入编译的源文件中就可以了:gcc test.c libMyMath.a -o test1

030-静态库使用及头文件对应

隐式声明: 编译过程中没有遇到函数定义和函数声明, 编译器会帮你做隐式声明;

但是这种隐式声明只能对于返回值为int型的;

Linux系统编程学习笔记_第18张图片
解决方法: 添加头文件;

/*添加头文件守卫,防止头文件重复包含,一旦头文件被展开过一次,_MYMATH_H_就被定义过了,后面就不会再展开*/
#ifndef _MYMATH_H_
#define _MYMATH_H_

int add(int,int);
int sub(int,int);
int div1(int,int);

#endif

然后将源文件和库联编即可, 注意源文件在前;

工程化一点的话, 考虑目录的组织结构:

Linux系统编程学习笔记_第19张图片

gcc test.c ./lib/libMyMath.a -o test -I ./inc

031-动态库制作-生成与位置无关代码

将源文件.c编译为目标文件.o, 生成与位置无关的代码, 借助参数-fPIC;

Linux系统编程学习笔记_第20张图片

编译生成hello.o的时候, 各个函数的地址还是相对于main的地址, 链接阶段填入main的地址;

由于动态库的函数在库里, 不能像程序内部的函数一样直接填入main的地址, 动态函数在a.out中没有位置, 依赖于@plt, 进行延迟绑定;

查看二进制文件的反汇编代码:objdump -dS test

输出重定向:objdump -dS test > test.s

032-动态库制作演示

  1. 将.c文件生成.o文件(生成与位置无关的代码-fPIC):gcc -c add.c -o add.o -fPIC
  2. 使用gcc -shared制作动态库:gcc -shared add.o sub.o div1.o -o libMyMath.so
  3. 编译可执行程序时, 指定所使用的动态库, -l 指定库名, -L 指定库路径:gcc test.c -o test -l MyMath -L ./lib
  4. 运行可执行程序
    在这里插入图片描述
    Linux系统编程学习笔记_第21张图片
    编译过了, 执行出错: 找不到文件;

033- 动态库加载错误原因及解决方式

上面的错误原因:

  • 链接器:工作于链接阶段, 工作时需要指定-l和-L参数, 上面已经指定了;
  • 动态链接器:工作于程序运行阶段, 工作时需要提供动态库所在目录;

上面两者没有任何关系

动态链接器要根据环境变量寻找动态库:LD_LIBRARY_PATH

export LD_LIBRARY_PATH=./lib

指定后就可以执行了(但是上面指定的只是临时的, 环境变量是进程的概念)

要想永久指定, 需要更改配置文件, 加入环境变量, 重启终端使之生效:
在这里插入图片描述

034- 动态库加载错误原因及解决方式2

像标准C库这种本身就在系统的环境变量里, 所以他能找到;

滥竽充数法:将库文件放到系统根目录下的lib里就可以了;

ldd test可以查看程序运行所需要的动态库
Linux系统编程学习笔记_第22张图片

最后一种方法:修改配置文件法;

sudo vim /etc/ld.so.conf

写入动态库绝对路径, 保存;

sudo ldconfig -v, 使配置文件生效;

035-扩展讲解-数据段合并

Linux系统编程学习笔记_第23张图片

为了节省内存, 将只读的.rodata和只读的.text段合并到一页内存;

同样的也将.bss和.data合并到一页内存;

036-总结

None

037-复习

动态库和静态库共存时, 编译器优先使用动态库;

038-gdb调试基础指令

Linux系统编程学习笔记_第24张图片

gdb a.out:开始调试

list 1:从第一行开始显示源码, 后面再展开用l;

break 52:在第52行设置断点;

run:开始执行, 到断点暂停;

next:下一个, 转到下一条语句或函数;

step:单步, 进入函数, 单步执行, 注意系统函数只能用n, 不要用s进入;

print i:打印变量i的值;

continue:继续执行断点后续指令;

quit:退出gdb调试工具;

039-gdb调试其他指令

用gdb调试段错误: 直接run, 程序停止的位置就是出段错误的位置;

start:单步执行;

finish:结束当前函数调用, 返回调用点;

set args aa bb cc:给函数添加参数, 或者run aa bb cc;

info b:查看断点信息;

b 20 if i=5:设置条件断点;

ptype arr:查看变量类型;

栈帧:随着函数调用而在stack上开辟的一块内存空间, 用于存放函数调用时产生的局部变量和临时值;

backtrace:简称bt查看函数调用的栈帧和层级关系;

frame 1:切换函数栈帧;

display j:一直显示j变量;

undisplay num:取消监视;

delete:删除断点;

040-gdb常见错误说明

file a.out:不退出gdb打开另一个文件进行调试;

041-makefile基础规则

makefile的名字只能是makefile或Makefile;

用途:
Linux系统编程学习笔记_第25张图片

  • 1个规则:

    目标:依赖条件

    ​ 命令(前面是一个Tab缩进);

  • 2个函数:

  • 3个自动变量:

若想生成目标, 检查规则中的依赖条件是否存在, 如果不存在, 则寻找是否有规则用来生成该依赖文件;

检查规则中的目标是否需要被更新, 必须先检查他的所有依赖, 依赖中有任何一个被更新, 则目标必须被更新;

  • 分析各个目标和依赖之间的关系;
  • 根据依赖关系自底向上执行命令;
  • 根据修改时间比目标新旧与否确定更新;
  • 如果目标不依赖任何条件, 则执行对应命令, 以示更新;
/*一个最简单的makefile*/
hello:hello.c
    gcc hello.c -o hello

考虑中间步骤:

hello:hello.o
	gcc hello.o -o hello
hello.o:hello.c
	gcc -c hello.c -o hello.o

042-makefile的1个规则

多文件联编:

hello:hello.c
	gcc hello.c add.c sub.c div1.c -o hello

考虑到多文件编译的时间成本, 应该先将各个模块编译成.o目标文件, 由目标文件链接成可执行文件;

这样, 只有改动了的模块会被再次编译, 其他的保持不变;

hello:hello.o add.o sub.o div1.o
	gcc hello.o add.o sub.o div1.o -o hello

hello.o:hello.c
	gcc -c hello.c -o hello.o

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

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

div1.o:div1.c
	gcc -c div1.c -o div1.o

当依赖条件的时间比目标的时间还晚, 说明目标该更新了;

依赖条件如果不存在, 找寻新的规则去产生依赖;

make只会认为第一行是自己的最终目标, 如果最终目标没有写在第一行, 通过ALL来指定;

ALL:hello

hello.o:hello.c
	gcc -c hello.c -o hello.o
add.o:add.c
	gcc -c add.c -o add.o
sub.o:sub.c
	gcc -c sub.c -o sub.o	
div1.o:div1.c
	gcc -c div1.c -o div1.o

hello:hello.o add.o sub.o div1.o
	gcc hello.o add.o sub.o div1.o -o hello

043-午后回顾

None

044-makefile的2个函数和clean

2个函数:

src=$(wildcard ./*.c):匹配当前目录下的所有.c源文件, 赋值给变量src(与shell类似, 变量只有字符串类型);

obj=$(patsubst %.c,%.o,$(src)):将参数3中包含参数1的部分替换为参数2;

src=$(wildcard ./*.c)
obj=$(patsubst %.c,%.o,$(src))

ALL:hello
hello:$(obj)
	gcc $(obj) -o hello

hello.o:hello.c
	gcc -c hello.c -o hello.o
add.o:add.c
	gcc -c add.c -o add.o
sub.o:sub.c
	gcc -c sub.c -o sub.o	
div1.o:div1.c
	gcc -c div1.c -o div1.o

clean:
	-rm -rf $(obj) hello

执行make clean时必须加上-n参数检查, 否则一迷糊把源码删掉就烷基八氮了;

clean相当于一个没有依赖条件的规则;

rm前面的横杠表示出错(文件不存在)仍然执行;

045-makefile的3个自动变量和模式规则

三个自动变量:

  • $@:在规则的命令中, 表示规则中的目标;
  • $^:在规则的命令中, 表示所有依赖条件;
  • $<:在规则的命令中, 表示第一个依赖条件;
src=$(wildcard ./*.c)
obj=$(patsubst %.c,%.o,$(src))

ALL:hello
hello:$(obj)
	gcc $^ -o $@			#目标依赖于所有依赖条件

hello.o:hello.c
	gcc -c $< -o $@			#目标依赖于第一个(唯一一个)依赖条件
add.o:add.c
	gcc -c $< -o $@			#目标依赖于第一个(唯一一个)依赖条件
sub.o:sub.c
	gcc -c $< -o $@			#目标依赖于第一个(唯一一个)依赖条件
div1.o:div1.c
	gcc -c $< -o $@			#目标依赖于第一个(唯一一个)依赖条件

clean:
	-rm -rf $(obj) hello

模式规则:

鉴于上面的都是某个.o文件依赖于某个.c文件的形式, 可以将其总结为一个模式规则:

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

关于$<:如果将该变量应用在模式规则中, 它可将依赖条件列表中的依赖项依次取出, 套用模式规则;

src=$(wildcard ./*.c)
obj=$(patsubst %.c,%.o,$(src))

ALL:hello
hello:$(obj)
	gcc $^ -o $@

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

clean:
	-rm -rf $(obj) hello

加入了模式规则后, 当再加入新的模块, 比如mul模块, 不需要改动makefile就可以实现自动编译链接, 非常的方便;

扩展:

(1)静态模式规则(制定了模式规则给谁用):

$(obj)%.o:%.c	gcc -c $< -o $@

(2)加入伪目标(为了防止目录下的与clean和ALL的同名文件的干扰):

.PHONY:clean ALL

(3)加入常用参数(-Wall, -I, -l, -L, -g), 形成了最终版本:

src=$(wildcard ./*.c)
obj=$(patsubst %.c,%.o,$(src))

myArgs=-Wall -g

ALL:hello
hello:$(obj)
	gcc $^ -o $@ $(myArgs)

%.o:%.c
	gcc -c $< -o $@ $(myArgs)
clean:
	-rm -rf $(obj) hello

.PHONY:clean ALL

046-习题和作业

考虑工程目录结构:
Linux系统编程学习笔记_第26张图片

makefile文件:

src=$(wildcard ./src/*.c)
obj=$(patsubst ./src/%.c,./obj/%.o,$(src))			#注意百分号的匹配和锁定作用

myArgs=-Wall -g
inc_path=./inc									#头文件所在目录

ALL:hello
hello:$(obj)
	gcc  $^ -o $@ $(myArgs)

$(obj):./obj/%.o:./src/%.c							#目标和依赖都需要改变
	gcc -c $< -o $@ $(myArgs) -I $(inc_path)

.PHONY: ALL clean

clean:
	-rm -rf ./obj/*.o hello

当你的文件名不叫makefile:

make -f m6make -f m6 clean

047-系统编程阶段说在前面的话

系统调用: 内核提供的函数: 由操作系统实现并提供给外部应用程序的编程接口, 是应用程序同操作系统之间交互数据的桥梁;

为了保证系统的安全性, manPage中的系统调用都是对系统调用的一次浅封装, 比如open对应的是sys_open…;
Linux系统编程学习笔记_第27张图片
逼逼叨: 不要一味地去追求底层原理, 底层永远无穷无尽, 适度的抽象有助于对整体的把握, 这也正是任何东西工程化的关键不是吗?

048-open函数

#include 
#include 
#include 

int open(const char* pathname, int flags);
int open(const char* pathname, int flags, mode_t mode);	//mode_t是一个8进制整型,指定文件权限,只有当参2指定了CREAT才有用

参数:

  • O_RDONLY
  • O_WRONLY
  • O_RDWR
  • O_APPEND
  • O_CREAT
  • O_EXCL
  • O_TRUNC
  • O_NONBLOCK

成功返回文件描述符, 失败返回-1并设置errno;

int main(int argc,char* argv[]){
     
	int fd1=0;
	int fd2=0;

	fd1=open("./dirt.txt",O_RDONLY|O_CREAT|O_TRUNC,0644);
    /*打开的文件不存在*/
	fd2=open("./dirt2.txt",O_RDONLY);

	printf("fd1=%d\n",fd1);
	printf("fd2=%d,errno=%d:%s\n",fd2,errno,strerror(errno));

	close(fd1);
	close(fd2);
	return 0;
}

创建文件权限时, 指定文件访问权限, 权限同时受umask影响:文件权限=mode&(~umask)
Linux系统编程学习笔记_第28张图片

049-总结

None(第3天结束)

050-复习

None

051-makefile作业

将当前目录下的所有C程序编译成可执行文件:

src=$(wildcard ./*.c)
target=$(patsubst %.c,%,$(src))
ALL:$(target)
myArgs=-Wall -g

%:%.c
	gcc $< -o $@ $(myArgs)

clean:
	-rm -rf $(target)
.PHONY:ALL clean

052-read和write实现cp

read:从文件中读数据到缓冲区

#include 
ssize_t read(int fd, void* buf, size_t count);

参3是缓冲区的大小;

成功返回实际读到的字节数, 返回0时意味着读到了文件末尾, 失败返回-1并设置errno;

wirte:从缓冲区中读数据到文件

#include 
ssize_t write(int fd, const void* buf, size_t count);

参3是数据的大小(字节数);

成功返回实际写入的字节数, 失败返回-1, 并设置errno;

int main(int argc,char* argv[]){
     
	char buf[1024];
	int n=0;

	int fd1=open(argv[1],O_RDONLY);
	if(fd1==-1){
     
		perror("open argv1 error");
		exit(1);
	}
	int fd2=open(argv[2],O_RDWR|O_CREAT|O_TRUNC,0644);
	if(fd2==-1){
     
		perror("open argv2 error");
		exit(1);
	}

	while((n=read(fd1,buf,sizeof(buf)))!=0){
     
		if(n<0){
     
			perror("open argv2 error");
			break;
		}
		write(fd2,buf,n);
	}

	close(fd1);
	close(fd2);
	return 0;
}

053-系统调用和库函数比较-预读入和缓输出

strace-跟踪一个程序执行时所需要的系统调用;

如果规定逐字节的进行拷贝, 用库函数会比用系统调用快很多, 因为有预读入和缓输出机制:
Linux系统编程学习笔记_第29张图片
OS绝不会让你逐字节的向Disk上写数据, 实际上它维护了一个系统级缓冲, 只有当从用户空间过来的数据在该缓冲上写满时, 他才会一次性将数据冲刷到Disk上;

当使用系统调用的方法时, 要不断的在用户空间和内核空间进行来回切换, 这会消耗大量时间;

而使用fputc(库函数)时, 他在设计之初自己在用户空间维护了一个缓冲, 这样在用户空间把自己的缓冲写满, 再一次性写入内核缓冲(写入了内核缓冲就认为写到了磁盘上了), 可见这样大大减少了在用户空间和内核空间来回切换的次数;

readwrite函数常被称为UnbufferedIO, 指无用户级缓冲区, 但不保证不使用内核缓冲区;
Linux系统编程学习笔记_第30张图片

综上所述-少造轮子;

054-文件描述符

Linux系统编程学习笔记_第31张图片

PCB中有一根指针, 指向了该进程的文件描述符表, 每个表项都是一个键值对, 其中的value是指向文件结构体的指针, 其中的索引是fd, OS暴露给用户的唯一操作文件的依据;

新打开的文件描述符一定是所有文件描述符表中可用的, 最小的那个文件描述符;

文件描述符最大1023, 说明一个进程最多能打开1024个文件;

文件结构体:
Linux系统编程学习笔记_第32张图片

055-阻塞和非阻塞

Linux系统编程学习笔记_第33张图片
一个自己的echo程序:

int main(int argc,char* argv[]){
     
	int n=0;
	char buf[10];

	n=read(STDIN_FILENO,buf,sizeof(buf));
	if(n==-1){
     
		perror("read error");
		exit(1);
	}

	write(STDOUT_FILENO,buf,n);
	return 0;
}

当不敲入换行符时,read会一直阻塞等待用户输入;

阻塞是设备文件, 网络文件的属性;

当然也可以设置以非阻塞方式从tty中读数据:

int main(int argc,char* argv[]){
     
	int fd=0;
	char buf[10];
	int n=0;
	/*以非阻塞方式打开终端文件*/
	fd=open("/dev/tty",O_RDONLY|O_NONBLOCK);
	if(fd<0){
     
		perror("open /dev/tty error");
		exit(1);
	}

tryagain:
	n=read(fd,buf,sizeof(buf));
    /*当read的返回值小于0*/
	if(n<0){
     
        /*errno不是EWOULDBLOCK,说明出现了其他问题*/
		if(errno!=EWOULDBLOCK){
     
			perror("read /dev/tty error");
			exit(1);
		}else{
     
            /*errno是EWOULDBLOCK,说明读到为空,则打印提示信息,并再次尝试*/
			write(STDOUT_FILENO,"try again\n",strlen("try again\n"));
			sleep(2);
			goto tryagain;
		}
	}
	/*当read的返回值大于0,说明读到了数据,写到标准输出上*/
	write(STDOUT_FILENO,buf,n);
	close(fd);
	return 0;
}

read函数返回-1, 并且errno=EAGAINEWOULDBLOCK, 说明不是read失败, 而是read在以非阻塞方式读一个设备文件网络文件, 而文件中无数据;

阻塞方式存在的问题也正是后来网络IO中select, pollepoll函数存在的原因;

056-fcntl改文件属性

fcntl函数:改变一个已经打开文件的访问控制属性

#include 
#include 
int fcntl(int fd, int cmd, ... /* arg */ );

fcntl改写上面的程序, 不用重新打开文件:

int main(int argc,char* argv[]){
     
	int fd=0;
	char buf[10];
	int n=0;
	int ret=0;
	int flags=0;
    /*获取原来的flags*/
	flags=fcntl(STDIN_FILENO,F_GETFL);
	if(flags==-1){
     
		perror("fcntl error");
		exit(1);
	}
    /*位或上新的属性*/
	flags|=O_NONBLOCK;
    /*将新的flags设置回去*/
	ret=fcntl(STDIN_FILENO,F_SETFL,flags);
	if(ret==-1){
     
		perror("fcntl error");
		exit(1);
	}
/*与上面的相同*/
tryagain:
	n=read(STDIN_FILENO,buf,sizeof(buf));
	if(n<0){
     
		if(errno!=EWOULDBLOCK){
     
			perror("read /dev/tty error");
			exit(1);
		}else{
     
			write(STDOUT_FILENO,"try again\n",strlen("try again\n"));
			sleep(2);
			goto tryagain;
		}
	}

	write(STDOUT_FILENO,buf,n);
	close(fd);
	return 0;
}

文件的flags是一个位图, 每一位代表不同属性的真假值;

057-午后回顾

None

058-lseek函数

Linux系统编程学习笔记_第34张图片

#include 
#include 
off_t lseek(int fd, off_t offset, int whence);

举个栗子:

int main(int argc,char* argv[]){
     
	int fd=0;
	int n=0;
	char msg[]="It's a lseek test\n";
	char c;

	fd=open("./lseek.txt",O_CREAT|O_RDWR,0644);
	if(fd==-1){
     
		perror("open error");
		exit(1);
	}

	write(fd,msg,strlen(msg));
    
    /*如果这里不进行lseek,由于读写共用同一个偏移位置,下面的读会从文件末尾开始读,读不到任何数据*/
	lseek(fd,0,SEEK_SET);
    
	while((n=read(fd,&c,1))){
     
		if(n==-1){
     
			perror("read error");
			exit(1);
		}
		write(STDOUT_FILENO,&c,n);
	}
	close(fd);
	return 0;
}

lseek获取文件大小:

int main(int argc,char* argv[]){
     
	int fd=open(argv[1],O_RDWR);
	if(fd==-1){
     
		perror("open error");
		exit(1);
	}
	/*从0开始向后偏移到结尾,返回值表示偏移量,即为文件大小*/
	int size=lseek(fd,0,SEEK_END);
	printf("The file's size:%d\n",size);

	close(fd);
	return 0;
}

使用lseek拓展文件大小: 要想使文件大小真正拓展, 必须引起IO操作;

int main(int argc,char* argv[]){
     
	int fd=open(argv[1],O_RDWR);
	if(fd==-1){
     
		perror("open error");
		exit(1);
	}
	/*从文件的结束位置开始,向后偏移110*/
	int size=lseek(fd,110,SEEK_END);
	printf("The file's size:%d\n",size);
	/*然后写入一个空字符*/
	write(fd,"\0",1);
	close(fd);
	return 0;
}

被填入的是文件空洞:
在这里插入图片描述

以HEX查看文件:od -tcx filename;

也可以使用truncate拓展文件大小:

int ret=truncate("dict.cp",250);

059-传入传出参数

传入参数:

  1. 指针作为函数参数;
  2. 同时有const关键字修饰;
  3. 指针指向有效区域, 在函数内部做读操作;

传出参数:

  1. 指针作为函数参数;
  2. 在函数调用前, 指针指向的空间可以无意义, 但必须有效;
  3. 在函数内部做写操作;
  4. 函数调用结束后充当函数返回值;

传入传出参数:

  1. 指针作为函数参数;
  2. 在函数调用前, 指针指向的空间有实际意义;
  3. 在函数内部, 先做读操作, 再做写操作;
  4. 函数调用结束后, 充当函数返回值;

060-目录项和inode

Linux系统编程学习笔记_第35张图片
Linux系统编程学习笔记_第36张图片

增加文件的硬链接只是增加dentry, 指向相同的inode;

同样, 删除硬链接也只是删除dentry, 要注意删除文件并不会让数据在磁盘消失, 只是OS丢失了inode, 磁盘只能覆盖, 不能擦除;

061-stat函数

stat函数作用:获取文件属性(从inode中获取);

#include 
#include 
#include 
int stat(const char* pathname, struct stat* statbuf);
/*结构体信息*/
struct stat {
     
	dev_t     st_dev;         /* ID of device containing file */
	ino_t     st_ino;         /* Inode number */
	mode_t    st_mode;        /* File type and mode */
	nlink_t   st_nlink;       /* Number of hard links */
	uid_t     st_uid;         /* User ID of owner */
	gid_t     st_gid;         /* Group ID of owner */
	dev_t     st_rdev;        /* Device ID (if special file) */
	off_t     st_size;        /* Total size, in bytes */
	blksize_t st_blksize;     /* Block size for filesystem I/O */
	blkcnt_t  st_blocks;      /* Number of 512B blocks allocated */

/* Since Linux 2.6, the kernel supports nanosecond precision for the following timestamp fields.For the details before Linux 2.6, see NOTES. */

	struct timespec st_atim;  /* Time of last access */
	struct timespec st_mtim;  /* Time of last modification */
	struct timespec st_ctim;  /* Time of last status change */

	#define st_atime st_atim.tv_sec      /* Backward compatibility */
	#define st_mtime st_mtim.tv_sec
	#define st_ctime st_ctim.tv_sec
};

参数:

  • path:文件路径;
  • buf(传出参数)存放文件属性;

返回值: 成功返回0, 失败返回-1并设置errno;

利用stat获取文件大小:

int main(int argc,char* argv[]){
     
	struct stat sbuf;
	int ret=0;
	ret=stat(argv[1],&sbuf);
	if(ret==-1){
     
		perror("stat error");
		exit(1);
	}
	printf("file size:%ld\n",sbuf.st_size);
	return 0;
}

062-lstat函数和stat

使用宏函数获取文件属性:

int main(int argc,char* argv[]){
     
	struct stat sbuf;
	int ret=0;
	ret=stat(argv[1],&sbuf);
	if(ret==-1){
     
		perror("stat error");
		exit(1);
	}
    /*宏函数一般返回布尔值*/
	if(S_ISREG(sbuf.st_mode))
		printf("It's a regular\n");
	else if(S_ISDIR(sbuf.st_mode))
		printf("It's a dir\n");
	else if(S_ISFIFO(sbuf.st_mode))
		printf("It's a pipe\n");
	else if(S_ISLNK(sbuf.st_mode))
		printf("It's a symbol");
	/*and so on...*/
	return 0;
}

ln -s makefile makefile.soft:创建软连接;

mkfifo f1:创建管道文件;

stat穿透: 当用stat获取软连接的文件属性时, 会穿透符号连接直接返回软连接指向的本尊的文件属性;

解决方法: 换lstat函数;

vim,cat命令也有穿透作用;

S_IFMT是一个文件类型掩码(文件类型那四位全1), st_mode与它位与后就可以提取出文件类型(后面的权限位被归零);
Linux系统编程学习笔记_第37张图片
在这里插入图片描述
Linux系统编程学习笔记_第38张图片

063-传出参数重充当返回值

Linux系统编程学习笔记_第39张图片

064-link和unlink的隐式回收

关于特殊权限位:
Linux系统编程学习笔记_第40张图片

link函数:可以为已经存在的文件创建目录项(硬链接);

ln makefile makefile.hard:为makefile创建硬连接;

int link(const char *oldpath, const char *newpath);

使用linkunlink函数实现mv命令:

int main(int argc,char* argv[]){
     
	int ret=0;
	ret=link(argv[1],argv[2]);
	if(ret==-1){
     
		perror("link error");
		exit(1);
	}

	ret=unlink(argv[1]);
	if(ret==-1){
     
		perror("unlink error");
		exit(1);
	}
	return 0;
}

Linux下的文件删除机制: 不断的将文件的st_nlink-1, 直到减到0为止. 无目录项对应的文件, 会被操作系统择机释放;

因此我们删除文件, 从某种意义上来说只是让文件具备了被删除的条件;

unlink函数的特征:清楚文件时, 如果文件的硬连接计数减到了0, 没有dentry与之对应, 但该文件仍不会马上被释放掉. 要等到所有打开该文件的进程关闭该文件, 系统才会择机将文件释放;

一个demo:

int main(int argc,char* argv[]){
     
	int fd=0;
	int ret=0;
	char* p="test of unlink\n";
	char* p2="after write something\n";

	fd=open("temp.txt",O_RDWR|O_TRUNC|O_CREAT,0644);
	if(fd<0)
		perr_exit("open file error");

	ret=write(fd,p,strlen(p));
	if(ret==-1)
		perr_exit("write error");

	printf("hello,I'm printf\n");
	ret=write(fd,p2,strlen(p2));
	if(ret==-1)
		perr_exit("write error");
	printf("Entry key to continue\n");
    /*程序在此阻塞等待用户输入*/
	getchar();
	close(fd);
	/*删除该文件*/
	ret=unlink("temp.txt");
	if(ret==-1)
		perr_exit("unlink error");
	return 0;
}

但是如果在unlink之前诱发段错误, 程序崩溃, temp.txt就会存活下来. 所以将unlink这一步放到打开文件之后紧接着就unlink掉;

虽然文件被unlink掉了, 用户用cat查看不到磁盘上的对应文件, 但是write函数拿到fd写文件是向内核的buffer中写, 仍可正常写入;

隐式回收:

当进程运行结束时, 所有该进程打开的文件会被关闭, 申请的内存空间会被释放, 系统的这一特性称为隐式回收系统资源;

065-文件目录rwx权限差异

readlink m1.soft:查看软连接的内容;

在这里插入图片描述
Linux下所见皆文件, 如果用vim打开一个目录:

Linux系统编程学习笔记_第41张图片
Linux系统编程学习笔记_第42张图片

066-目录操作函数

文件名不能超过255个字符, 因为dirent中的d_name长度为256, 再算上\0, 有255个字符可用;

#include 
DIR* opendir(const char* name);	/*返回的是一个目录结构体指针*/
int closedir(DIR* dirp);

struct dirent* readdir(DIR* dirp);
struct dirent {
     
	ino_t          d_ino;       /* Inode number */
	off_t          d_off;       /* Not an offset; see below */
	unsigned short d_reclen;    /* Length of this record */
	unsigned char  d_type;      /* Type of file; not supported by all filesystem types */
	char           d_name[256]; /* Null-terminated filename */
};

用目录操作函数实现ls的功能:

int main(int argc,char* argv[]){
     
	DIR* dp;
	struct dirent* sdp;
    /*根据输入的内容打开一个目录文件,拿到一个类似文件描述符的东西dp*/
	dp=opendir(argv[1]);
	if(dp==NULL)
		perr_exit("opendir error");
	/*循环从dirent流中读取数据*/
	while((sdp=readdir(dp))!=NULL){
     
        /*跳过当前目录和上一级目录*/
		if(!strcmp(sdp->d_name,"."))
			continue;
		if(!strcmp(sdp->d_name,".."))
			continue;
        /*打印文件名*/
		printf("%s\n",sdp->d_name);
	}
	printf("\n");
	/*关闭文件*/
	closedir(dp);
	return 0;
}

067-总结

None

068-复习

Linux下文件存储原理:
Linux系统编程学习笔记_第43张图片

069-递归遍历目录思路分析

1.判断命令行参数, 获取用户要查询的目录名-argv[1];

注意如果argc==1, 说明要查询的是当前目录./;

2.判断用户指定的是否是目录: stat S_ISDIR()->封装函数isFile();

3.读目录:

opendir(dir);

while(readdir()){

​ 普通文件:直接打印;

​ 目录文件:拼接目录访问绝对路径:sprintf(path,"%s%s",dir,d_name);

​ 递归调用自己:opendir(path), readdir, closedir;

}

closedir();

070-递归遍历目录代码预览

/*参2是回调函数名*/
void fetchdir(const char* dir,void(*fcn)(char*)){
     
	char name[PATH_LEN];
	struct dirent* sdp;
	DIR* dp;
	/*打开目录失败*/
	if((dp=opendir(dir))==NULL){
     
		fprintf(stderr,"fetchdir:can't open %s\n",dir);
		return;
	}
	/*循环读取内容*/
	while((sdp=readdir(dp))!=NULL){
     
        /*遇到当前目录和上一级目录,跳过,否则会陷入死循环*/
		if((strcmp(sdp->d_name,".")==0)||(strcmp(sdp->d_name,"..")==0))
			continue;
		/*路径名是否越界*/
		if(strlen(dir)+strlen(sdp->d_name)+2>sizeof(name)){
     
			fprintf(stderr,"fetchdir:name %s %s is too long\n",dir,sdp->d_name);
		}else{
     
            /*拼接为一个路径,传给isFile函数*/
			sprintf(name,"%s/%s",dir,sdp->d_name);
			(*fcn)(name);
		}
	}
	closedir(dp);
}

void isFile(char* name){
     
	struct stat sbuf;
    /*获取文件属性失败*/
	if(stat(name,&sbuf)==-1){
     
		fprintf(stderr,"isFile:can't access %s\n",name);
		exit(1);
	}
    /*这是一个目录文件:调用函数fetchdir*/
	if((sbuf.st_mode&S_IFMT)==S_IFDIR){
     
		fetchdir(name,isFile);
	}
    /*不是目录文件:是一个普通文件,打印文件信息*/
	printf("%ld\t\t%s\n",sbuf.st_size,name);
}

int main(int argc,char* argv[]){
     
	/*不指定命令行参数*/
	if(argc==1)
		isFile(".");
	else{
     
		while(--argc>0)
			isFile(*++argv);
	}
	return 0;
}

071-递归遍历目录实现

None

072-递归遍历目录总结

None(把上面的代码真正掌握);

073-dup和dup2

duplicate:复制, 副本;

cat makefile > m1:将cat的结果重定向到m1(此时m1与makefile内容相同);

cat makefile >> m1:将cat的结果重定向并追加到m1后面(此时m1是双份的makefile);

#include 
int dup(int oldfd);
int dup2(int oldfd, int newfd);

The dup() system call creates a copy of the file descriptor oldfd, using the lowest-numbered unused file descriptor for the new descriptor.

传入已有的文件描述符, 返回一个新的文件描述符;

int main(int argc,char* argv[]){
     
    /*open或创建一个文件,拿到文件描述符fd1*/
	int fd1=open(argv[1],O_RDWR|O_CREAT|O_TRUNC,0644);
	if(fd1==-1)
		perr_exit("open error");
	/*fd2作为fd1的副本,拿着fd2也可以向被open的文件写入*/
	int fd2=dup(fd1);
	if(fd2==-1)
		perr_exit("dup error");

	printf("fd1=%d	fd2=%d\n",fd1,fd2);
	/*向fd2(fd1)中写入一句话*/
	write(fd2,"fuckyou\n",8);
	return 0;
}

dup的返回值fd2相当于fd1的副本, 拿着它也可以操作fd1;

(这里视频好像讲错了, 凭什么瞧不起dup)

int main(int argc,char* argv[]){
     

	int fd1=open(argv[1],O_RDWR|O_CREAT|O_TRUNC,0644);

	int fd2=open(argv[2],O_RDWR|O_CREAT|O_TRUNC,0644);
	/*dup2后fd2也指向了fd1的文件*/
	int fdret=dup2(fd1,fd2);
	printf("fdret=%d\n",fdret);

	int ret=write(fd2,"fuckyou\n",8);
	printf("ret=%d\n",ret);
	/*现在标准输出也指向了fd1*/
	dup2(fd1,STDOUT_FILENO);
	printf("--------fuckyou--------\n");

	return 0;
}

Linux系统编程学习笔记_第44张图片
总之, dup2是后面的指向前面的;

074-fcntl实现dup描述符

int main(int argc,char* argv[]){
     
	int fd1=open(argv[1],O_RDWR|O_CREAT,0644);
	printf("fd1=%d\n",fd1);

    int newfd1=fcntl(fd1,F_DUPFD,0);
	printf("newfd1=%d\n",newfd1);

	int newfd2=fcntl(fd1,F_DUPFD,8);
	printf("newfd2=%d\n",newfd2);

	int ret=write(newfd2,"fuckyou\n",8);
	printf("ret=%d\n",ret);

	return 0;
}

参3传0, 则从0开始向下寻找可用的文件描述符返回给newfd1;

参3传8, 则从8开始向下寻找可用的文件描述符返回给newfd2;

075-复习

dup2的newfd比dup的灵活一些: 他能打破可用最小的文件描述符限制;

076-进程和程序以及CPU相关

Linux系统编程学习笔记_第45张图片

程序: 死的, 只占用磁盘空间(剧本);

进程: 活得, 运行起来的程序, 占用内存和CPU等系统资源(演出);

关于并发:
Linux系统编程学习笔记_第46张图片
关于CPU和MMU相关:
Linux系统编程学习笔记_第47张图片

MMU(内存管理单元), 位于CPU内部;

077-虚拟内存和物理内存映射关系

Linux系统编程学习笔记_第48张图片

对于一个32位的机器来说, 每个进程都能看到4GB的虚拟地址空间, 且他们的3G~4G的位置都是kernel(每个进程都有kernel区);

从虚拟内存到物理内存的映射由MMU完成, 不同进程的用户空间被映射到物理内存的不同位置, 而不同进程的kernel空间被映射到物理内存的相同位置, 对于物理内存来用户空间和内核空间有不同的特权级, 从用户空间到内核空间的转换实质上是特权级的切换;

078-PCB进程控制块

每个进程在内核中都有一个PCB来维护进程相关信息, Linux内核的进程控制块是task_struct类型的结构体;

主要内容:
Linux系统编程学习笔记_第49张图片

着重掌握的:

  • 进程id;
  • 文件描述符表;
  • 信号相关的信息资源;
  • 进程状态:初始态, 就绪态, 运行态, 挂起态, 终止态;
  • 进程工作目录位置;
  • 用户id和组id;

079-环境变量

  • PAHT:存放可执行程序的目录位置;
  • SHELL:当前使用的命令解析器;
  • TERM:查看终端类型;
  • LANG:语言和编码;
  • env:查看所有环境变量;
    Linux系统编程学习笔记_第50张图片

080-fork函数原理

fork函数原型:

pid_t fork();		/*函数原型相当简单:空参,返回一个整数pid*/

“On success, the PID of the child process is returned in the parent, and 0 is returned in the child. On failure, -1 is returned in the parent, no child process is created,and errno is set appropriately.”

成功fork后, 在子进程中返回0, 在父进程中返回子进程的pid;

失败返回-1并设置errno;

081-fork创建子进程

int main(int argc,char* argv[]){
     
	printf("---before fork 1---\n");
	printf("---before fork 2---\n");
	printf("---before fork 3---\n");
	printf("---before fork 4---\n");

	pid_t pid=0;
	pid=fork();
	
	if(pid==-1)
		perr_exit("---fork error---\n");
	else if(pid==0)
		printf("---I'm child,my pid is %d,my parent's pid is %d---\n",getpid(),getppid());
	else if(pid>0)
		printf("---I'm parent,my pid is %d,my child's pid is %d---\n---My parent's pid is %d---\n",getpid(),pid,getppid());

	printf("---end of file---\n\n");
	return 0;
}

执行结果:
Linux系统编程学习笔记_第51张图片

082-getpid和getppid

父进程的父进程是bash:
在这里插入图片描述
思考如何循环创建n个子进程(我给你八分钟!飞哥震怒):

Linux系统编程学习笔记_第52张图片

083-循环创建多个子进程

int main(int argc,char* argv[]){
     
	int i=0;
    /*如果是子进程则跳出循环*/
	for(i=0;i<5;++i){
     
		if(fork()==0)
			break;
	}
	/*子进程从i==5跳出for循环*/
	if(i==5){
     
		sleep(5);
		printf("I'm parent\n");
	}
	else{
     
		sleep(i);
		printf("I'm %dth child\n",i+1);
	}
	sleep(1);
	return 0;
}

printf前不加sleep的执行效果:
在这里插入图片描述

乱序输出反映了了操作系统对进程调度的无序性;

加上了sleep后就能控制输出顺序;

084-父子进程共享哪些内容

刚fork之后:

父子的相同之处: 全局变量, .data段, .text段, 栈, 堆, 环境变量, 用户ID, 宿主目录, 进程工作目录, 信号处理方式…;

父子进程的不同之处: 进程ID, fork返回值, 父进程ID, 进程运行时间, 闹钟(定时器), 未决信号集;

但是子进程并不是把父进程0~3G地址空间完全cpoy一份, 然后映射到物理内存;

父子进程间遵循读时共享, 写时复制的原则, 这样设计, 无论子进程执行父进程的逻辑还是执行自己的逻辑都能节省内存开销;

int var=100;
int main(int argc,char* argv[]){
     
	pid_t pid=fork();
	if(pid==-1)
		perr_exit("fork error");
	/*读时共享,写时复制*/
	if(pid==0){
     
		var=288;
		printf("I'm child,my pid=%d,my parent's pid=%d\n",getpid(),getppid());
		printf("My var=%d\n\n",var);
	}else if(pid>0){
     
		var=200;
		printf("I'm parent,my pid=%d,my parent's pid=%d\n",getpid(),getppid());
		printf("My var=%d\n\n",var);

	}
	return 0;
}

输出结果:

在这里插入图片描述

躲避父子进程共享全局变量的误区(线程之间是共享全局变量的);

重点要掌握的共享的内容: 文件描述符, mmap建立的映射区;

085-父子进程共享

Linux系统编程学习笔记_第53张图片

086-总结

None;

087-复习

多道程序设计模型: 宏观并行, 围观串行;

088-父子进程gdb调试

使用gdb调试的时候, gdb只能跟踪一个进程, 可以在fork函数调用之前通过指令设置gdb跟踪父进程还是子进程:

set follow-fork-mode child

set follow-fork-mode parent

089-exec函数族原理

fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支), 子进程往往要执行一种exec函数以执行另一个程序;

当进程调用一种exec函数族时, 该进程的用户空间代码和数据完全被新程序替换, 从新程序的启动例程开始执行;

调用exec函数并不会创建新的进程, 所以调用exec前后该进程的id并未改变;

将当前进程的.text和.data替换为所加载程序的.text和.data, 然后进程从新的.text的第一条指令开始执行, 但进程id不变, 换核不换壳;

exec函数不会返回任何值给任何人;

090-execlp和execl函数

execlp中的p表示环境变量, 所以该函数通常用来调用系统程序;

int execlp(const char* file, const char* arg, ... /* (char  *) NULL */);

注意结尾加上NULL指定变参结束,printf函数也是变参, 结尾也要加上NULL作为哨兵;

int main(int argc,char* argv[]){
     

	pid_t pid=fork();
	if(pid==-1)
		perr_exit("fork error");

	if(pid==0){
     
		/*好家伙,参数从argv[0]开始算*/
		execlp("ls","ls","-l","-R","-h",NULL);
		/*正常情况下是不会执行到这里的,只有当出错时才会返回到这里执行*/
		perror("execlp error");
		exit(1);
	}else if(pid>0){
     
		printf("I'm parent:%d\n",getpid());
		sleep(1);
	}
	return 0;
}

先fork, 再exec, 这就是bash的大概原理;

如果要执行自己的可执行文件:

execl("./test","./test",NULL);

091-exec函数族特性

ps aux的输出打印到文件当中:

int main(int argc,char* argv[]){
     

	int ret=0;
	int fd1=0;
    /*打开或创建一个文件*/
	fd1=open("ps.log",O_RDWR|O_CREAT|O_TRUNC,0644);
	if(fd1==-1)
		perr_exit("open error");
	/*将STDOUT_FILENO指向fd1*/
	ret=dup2(fd1,STDOUT_FILENO);
	if(ret==-1)
		perr_exit("dup2 error");
	/*执行命令*/
	execlp("ps","ps","aux",NULL);
	perror("execlp error");
	exit(1);
	return 0;
}

execvp(v是vector的意思):

int execvp(const char* file, char* const argv[]);

就是将execlp中的参数组织成字符串传入(或许你也可以传入从main函数中传来的参数)

/*execlp("ls","ls","-l","-R","-h",NULL)的等效形式*/
char* argv[]={
     "ls","-l","-R","-h",NULL};
execvp("ls",argv);

exec函数族的一般规律:调用成功立即执行新的程序, 不会返回, 只有调用失败才会返回-1;

092-孤儿进程和僵尸进程

孤儿进程: 父进程先于子进程结束, 子进程的父进程变为init进程, init进程又称为进程孤儿院, 专门收养孤儿进程(为了回收);

僵尸进程: 进程终止, 父进程尚未回收子进程残留在内核的资源(PCB), 变为僵尸(defunct)进程(每一个进程都会经历僵尸态);

ps ajx:查看进程ID和父进程ID;

kill -9 pid:杀死进程, 但是杀不死僵尸进程, 杀僵尸只能杀死他父亲;

093-wait回收子进程

Linux系统编程学习笔记_第54张图片

父进程调用wait函数可以回收子进程终止信息, 该函数有三个功能:

  • 阻塞等待子进程退出;
  • 回收子进程残留资源;
  • 获取子进程结束状态(退出原因);
pid_t wait(int* wstatus);

成功返回清理掉的子进程ID, 失败返回-1;

094-获取子进程退出值和异常终止信号

通过调用宏函数获取子进程退出状态:

int main(int argc,char* argv[]){
     
	pid_t pid,wpid;
	int status=0;

	pid=fork();
	if(pid==-1)
		perr_exit("fork error");

	if(pid==0){
     
		printf("I'm child:%d,my parent is %d,I'm going to sleep 10s\n",getpid(),getppid());
		sleep(10);
		printf("I'm child,I'm going to die\n");
		return 73;
	}else if(pid>0){
     
		//wpid=wait(NULL);		//不关心子进程退出原因
		wpid=wait(&status);
		if(wpid==-1)
			perr_exit("wait error");
		/*如果子进程正常终止,则可获取它的退出值*/
		if(WIFEXITED(status))
			printf("My child exited with:%d\n",WEXITSTATUS(status));
        /*如果子进程被信号终止,可获取结束它的信号*/
		else if(WIFSIGNALED(status))
			printf("My child killed by:%d\n",WTERMSIG(status));
		/*提示回收完成*/
		printf("I'm parent,wait %d finish\n",wpid);
	}
	return 0;
}

子进程被信号杀死:

在这里插入图片描述

子进程正常终止:

在这里插入图片描述

各种信号的宏值:

Linux系统编程学习笔记_第55张图片

程序所有异常终止的原因都是因为信号;

095-waitpid回收子进程

waitpid可以指定某一个子进程回收;

一次waitwaitpid函数调用, 只能回收一个子进程, 如果你循环创建了多个子进程, 那么就碰到哪个算哪个;

pid_t waitpid(pid_t pid, int* wstatus, int options);

参1传要回收的pid, 传-1表示回收任意子进程, 传0表示回收同一组的所有子进程;

参2传进程结束状态, 如果不关心直接传NULL(传出参数);

参3传回收方式:WNOHANG(非阻塞);

一个有bug的版本:

int main(int argc,char* argv[]){
     
	int i=0;
	int wpid=0;
	int pid=0;
	for(i=0;i<5;++i){
     
		if(fork()==0){
     
			if(i==2)
				pid=getpid();
			break;
		}
	}

	if(i==5){
     
		//wpid=waitpid(-1,NULL,WNOHANG);		//以非阻塞的方式回收任意子进程
		sleep(5);
		wpid=waitpid(pid,NULL,WNOHANG);
		if(wpid==-1)
			perr_exit("waitpid error");
		printf("I'm parent,wait a child finish:%d\n",wpid);
	}
	else{
     
		sleep(i);
		printf("I'm %dth child,my pid=%d\n",i+1,getpid());
	}
	sleep(1);
	return 0;
}

bug的原因:在fork()==0时是在子进程的执行逻辑中保存了pid, 但是子进程执行结束后直接返回, 用户空间的地址空间被回收, 当然也就没有了pid这个变量, 所以后面父进程waitpid时拿到的pid一直是0;

096-中午回顾

None;

097-错误解析

waitpid的参1传0表示回收同组的所有子进程, 一般以父进程的id号为组号, 可通过系统调用将子进程分离出去;

waitpid的参1传进程组号取反, 表示回收指定进程组的任意子进程;

bug改掉后:

int main(int argc,char* argv[]){
     
	int i=0;
	int wpid=0;
	int pid=0;
	int temppid=0;

	for(i=0;i<5;++i){
     
		pid=fork();
		if(pid==0)
			break;
		/*在父进程中,如果i==2,将fork的返回值存入temppid*/
		if(i==2)
			temppid=pid;
	}

	if(i==5){
     
		//wpid=waitpid(-1,NULL,WNOHANG);		//以非阻塞的方式回收任意子进程
		sleep(5);
		wpid=waitpid(temppid,NULL,WNOHANG);
		if(wpid==-1)
			perr_exit("waitpid error");
		printf("I'm parent,wait a child finish:%d\n",wpid);
	}
	else{
     
		sleep(i);
		printf("I'm %dth child,my pid=%d\n",i+1,getpid());
	}

	sleep(1);
	return 0;
}

098-waitpid回收多个子进程

回收多个: 用while循环;

int main(int argc,char* argv[]){
     
	int i=0;
	int wpid=0;
	int pid=0;

	for(i=0;i<5;++i){
     
		pid=fork();
		if(pid==0)
			break;
	}

	if(i==5){
     
        /*以非阻塞忙轮询的方式回收子进程*/
		while((wpid=waitpid(-1,NULL,WNOHANG))!=-1){
     
			if(wpid>0)
				printf("wait chile:%d\n",wpid);
			else if(wpid==0)
				sleep(1);
		}
	}else{
     
		sleep(i);
		printf("I'm %dth child,my pid=%d\n",i+1,getpid());
	}

	sleep(1);
	return 0;
}

099-wait和waitpid总结

Linux系统编程学习笔记_第56张图片
waitpid(-1,&status,0)==wait(&status);

100-进程间通信常见方式

Linux系统编程学习笔记_第57张图片

常见的进程间通信方式:

  1. 管道(使用最简单);
  2. 信号(开销最小);
  3. 共享映射区(可以用于无血缘关系的进程之间);
  4. 本地套接字(最稳定);

101-管道的特性

Linux系统编程学习笔记_第58张图片

管道是一种最基本的IPC机制, 作用于有血缘关系的进程之间, 完成数据传递. 调用pipe系统函数即可创建一个管道, 有如下特质:

  1. 其本质是一个伪文件(实为内核缓冲区);
  2. 有两个文件描述符引用, 一个表示读端, 一个表示写端;
  3. 规定数据从管道的写端流入管道, 从读端流出, 只能单向流动;

管道的原理: 管道实际为内核使用环形队列机制, 借助内核缓冲区(4k)实现;

管道的局限性:

  • 数据不能进程自己写, 自己读;
  • 管道中的数据不可反复读取, 一旦读走, 管道中不再存在;
  • 采用半双工通信方式, 数据只能在单方向上流动;
  • 只能在有公共祖先的进程之间使用管道;

102-管道的基本用法

pipe函数: 创建并打开管道

int pipe(int pipefd[2]);

pipefd[0]-读端;

pipefd[1]-写端;

成功返回0, 失败返回-1并设置errno;

fork完成时:
Linux系统编程学习笔记_第59张图片

则父进程关闭读端, 子进程关闭写端, 数据就能在pipe中单向流动, 父子进程能够完成通信;

int main(int argc,char* argv[]){
     
	int ret=0;
	int pipefd[2];
	pid_t pid=0;
	char* str="fuckyou\n";
	char buf[1024];
	/*创建管道,文件描述符保存在数组里*/
	ret=pipe(pipefd);
	if(ret==-1)
		perr_exit("pipe error");

	pid=fork();
	if(pid>0){
     
		close(pipefd[0]);						//父进程关闭读端
		write(pipefd[1],str,sizeof(str));		//向管道中写入数据
		close(pipefd[1]);						//父进程关闭写端
	}else if(pid==0){
     
		close(pipefd[1]);						//子进程关闭写端
		ret=read(pipefd[0],buf,sizeof(buf));	//从管道中读取数据
		write(STDOUT_FILENO,buf,ret);			//写到标准输出打印
		close(pipefd[0]);
	}
	return 0;
}

103-管道读写行为

Linux系统编程学习笔记_第60张图片

读管道:

1.管道中有数据, read返回实际读到的字节数;

2.管道中无数据:

​ 管道写端被全部关闭, read返回0;

​ 写端没有被全部关闭,read阻塞等待(不久的将来可能会有数据抵达, 此时会让出CPU);

写管道:

1.管道读端全部被关闭, 进程异常终止(也可以捕捉SIGPIPE信号, 使进程不终止);

2.管道读端没有全部关闭:

​ 管道已满, write阻塞;

​ 管道未满, write将数据写入, 并返回实际读到的字节数;

104-父子进程通信练习分析

ls | wc -l:统计行数;

需要使用的函数:

  • exec();
  • dup2();
  • pipe();

105-总结

Linux系统编程学习笔记_第61张图片
在这里插入图片描述
Linux系统编程学习笔记_第62张图片

106-复习

wait/waitpid只能回收进程, 爷孙的也不行;

107-父子进程lswc-l

int main(int argc,char* argv[]){
     
	int fd[2];
	pid_t pid;
	int ret=0;

	ret=pipe(fd);
	if(ret==-1)
		perr_exit("pipe error");

	pid=fork();
	if(pid==-1)
		perr_exit("fork error");
	if(pid>0){
     
        /*父进程先读管道,如果子进程还没起来,他就会阻塞,这样子进程就会先于父进程结束*/
		close(fd[1]);
		dup2(fd[0],STDIN_FILENO);
		execlp("wc","wc","-l",NULL);
		perr_exit("execlp wc error");
	}else if(pid==0){
     
        /*子进程写管道*/
		close(fd[0]);
		dup2(fd[1],STDOUT_FILENO);
		execlp("ls","ls",NULL);
		perr_exit("execlp ls error");
	}

	return 0;
}

108-兄弟进程间通信

上面的内容如果用兄弟进程间通信来做:

int main(int argc,char* argv[]){
     
	int fd[2];
	pid_t pid;
	int ret=0;
	int i=0;
	/*创建管道*/
	ret=pipe(fd);
	if(ret==-1)
		perr_exit("pipe error");
	/*循环创建2个子进程*/
	for(i=0;i<2;++i){
     
		pid=fork();
		if(pid==-1)
			perr_exit("fork error");
		if(pid==0)
			break;
	}
	/*父进程关闭管道读写两端*/
	if(i==2){
     
		close(fd[0]);
		close(fd[1]);
		wait(NULL);
		wait(NULL);
	}else if(i==0){
     
        /*兄进程关闭读端,将STDOUT指向fd[1]*/
		close(fd[0]);
		dup2(fd[1],STDOUT_FILENO);
		execlp("ls","ls",NULL);
		perr_exit("execlp ls error");
	}else if(i==1){
     
        /*弟进程关闭写端,将STDIN指向fd[0]*/
		close(fd[1]);
		dup2(fd[0],STDIN_FILENO);
		execlp("wc","wc","-l",NULL);
		perr_exit("execlp wc error");
	}
	return 0;
}

注意创建完进程后, 父进程要将管道的读写两端全部关闭;

109-多个读写端操作管道和管道缓冲区大小

管道可以一个读端, 多个写端, 但是不建议这样做;

默认管道的大小是4k;
Linux系统编程学习笔记_第63张图片

110-命名管道FIFO的创建和原理

匿名管道pipe的优缺点:

在这里插入图片描述

为区分pipe, 将FIFO称为命名管道;

FIFO可以用于不相关的进程间交换数据;

FIFO是Linux基础文件类型中的一种, 但是FIFO文件在磁盘上没有数据块, 仅仅用来标识内核中的一条通道, 各进程可以打开这个文件进行read/write, 实际上是在读写内核通道, 这样就实现了进程间通信;

创建方式:

int mkfifo(const char* pathname,mode_t mode);

成功返回0, 失败返回-1并设置errno;

111-FIFO实现非血缘关系进程间通信

FIFO进行通信几乎只有文件读写操作, 比较简单;

写进程

int main(int argc,char* argv[]){
     
	int fd=0;
	int i=0;
	char buf[4096];
	/*靠已经创建好的FIFO,如果命令行参数没给指定,报错*/
	if(argc<2){
     
		printf("Enter like this:./a.out fifoname\n");
		return -1;
	}
	/*以只写方式打开FIFO文件,拿到fd*/
	fd=open(argv[1],O_WRONLY);
	if(fd==-1)
		perr_exit("open error");

	while(1){
     
        /*将数据写到buf中*/
		sprintf(buf,"fuckyou:%d\n",i++);
		write(fd,buf,strlen(buf));
		sleep(1);
	}
	close(fd);
	return 0;
}

读进程:

int main(int argc,char* argv[]){
     
	int fd=0;
	int i=0;
	int len=0;
	char buf[4096];
	/*同样要依靠已经创建好的FIFO,从命令行参数中指定*/
	if(argc<2){
     
		printf("Enter like this:./a.out fifoname\n");
		return -1;
	}
	/*以只读方式打开FIFO文件,拿到fd*/
	fd=open(argv[1],O_RDONLY);
	if(fd==-1)
		perr_exit("open error");
	/*从fd中读取数据,并写到标准输出上*/
	while(1){
     
		len=read(fd,buf,sizeof(buf));
		write(STDOUT_FILENO,buf,len);
		sleep(1);
	}
	close(fd);
	return 0;
}

112-文件用于进程间通信

读普通文件不会造成read阻塞, 如果子进程睡1秒再写, 父进程由于刚开始读不到数据read直接返回0;

没有血缘关系的进程也可以用文件进行进程间通信;

113-MMAP函数原型

存储映射I/O使一个磁盘文件与存储空间中的一个缓冲区相映射, 于是当从缓冲区中取数据, 就相当于读文件中的相应字节;

于此类似, 将数据存入缓冲区, 则相应的字节就自动写入文件, 这样就可以在不使用read和write函数的情况下, 使用指针完成I/O操作;

使用这种方法, 首先应通知内核, 将一个文件映射到存储区域中, 这个映射工作可以通过mmap函数来实现;
Linux系统编程学习笔记_第64张图片

void* mmap(void* addr, size_t length, int prot, int flags,int fd, off_t offset);

参数:

  • ​ addr:指定映射区的首地址, 通常传NULL, 表示让系统自动分配;
  • ​ length:共享内存映射区大小(<=文件的实际大小);
  • ​ prot:共享内存映射区的读写属性, PROT_READ, PROT_WRITE及PROT_READ|PROT_WRITE;
  • ​ flags:标注共享内存的共享属性, MAP_SHARED或MAP_PRIVATE(shared内存的变化会反映到文件上, private不会反映到文件上);
  • ​ fd:用于创建共享内存映射区的那个文件描述符;
  • ​ offset:偏移位置, 需是4k的整数倍. 默认0, 表示映射文件全部;

返回值:

  • ​ 成功返回映射区首地址;
  • ​ 失败返回MAP_FAILED((void*)-1), 设置errno;

114-复习

None;

115-MMAP建立映射区

int munmap(void* addr, size_t length);
int main(int argc,char* argv[]){
     
	char* p=NULL;
	int fd=0;
	int len=0;
	int ret=0;

	fd=open("./testmap",O_RDWR|O_CREAT|O_TRUNC,0644);
	if(fd==-1)
		perr_exit("open error");
	/*
	lseek(fd,10,SEEK_END);
	write(fd,"\0",1);
	*/
	ret=ftruncate(fd,20);
	if(ret==-1)
		perr_exit("ftruncate error");

	len=lseek(fd,0,SEEK_END);

	p=mmap(NULL,len,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
	if(p==MAP_FAILED)
		perr_exit("mmap error");
	/*内存的写操作,会被映射到文件的写操作*/
	strcpy(p,"fuckyou\n");
	/*内存的读操作,会被映射到文件的读操作*/
	printf("----%s\n",p);
	/*与malloc一样,申请的内存要还回去*/
	ret=munmap(p,len);
	if(ret==-1)
		perr_exit("munmap error");
	return 0;
}

od -tcx filename:以16进制查看文件;

116-MMAP使用注意事项1

Linux系统编程学习笔记_第65张图片

1.可以, 但是要拓展文件大小, 否则会出现总线错误. 当然, 如果破罐子破摔, mmap时指定size=0, mmap会报错;

2.mmap会报错: 无效参数(注意ftruncte()函数需要写权限, 否则无法拓展文件大小). 如果都用只读权限, 不会出错. 要创建映射区, 文件必须有读权限;

3.没有影响, 建立完映射区后fd就能关闭;

4.mmap报错: 无效参数, 偏移量必须是4k的整数倍(因为MMU映射的最小单位就是4k);

5.小范围的越界问题不大, 但是最好不要这么做(操纵不安全的内存, 操作系统不给你保障);

6.不能成功. 与malloc一样, 释放的内存的指针必须是申请得来的初始的指针, 如果要改变指针的值, 拷贝一份用;

7.除了第一个参数, 后面的参数都可能导致失败;

8.会死的很难看;

117-MMAP使用注意事项2

总结使用注意事项:
Linux系统编程学习笔记_第66张图片

所以MMAP的保险调用方式:

fd=open("filename",O_RDWR);

mmap(NULL,有效文件大小,PROT_READ|PROT_WRITE,MAX_SHARED,fd,0);

118-MMAP总结

Linux系统编程学习笔记_第67张图片

119-父子进程间MMAP通信

必须指定内存映射区为shared属性, 如果指定了private属性, 内核只会给子进程mmap的拷贝, 不会给他真正的mmap;

int main(int argc,char* argv[]){
     
	int* p;
	pid_t pid;
	int fd=0;
	int ret=0;
	/*先打开(创建)一个文件*/
	fd=open("temp",O_RDWR|O_CREAT|O_TRUNC,0644);
	if(fd==-1)
		perr_exit("open error");
	ftruncate(fd,4);
	/*创建映射区*/
	p=(int*)mmap(NULL,4,PROT_WRITE|PROT_READ,MAP_SHARED,fd,0);
	if(p==MAP_FAILED)
		perr_exit("mmap error");
	/*创建完就可以关闭文件了*/
	close(fd);
	/*创建子进程*/
	pid=fork();
	if(pid==-1)
		perr_exit("fork error");
	
	if(pid==0){
     
        /*子进程写一个int*/
		*p=2000;
		var=1000;
		printf("child:*p=%d,var=%d\n",*p,var);
	}else if(pid>0){
     
        /*父进程读出这个int*/
		sleep(1);
		printf("parent:*p=%d,var=%d\n",*p,var);
        /*回收子进程*/
		wait(NULL);
		/*归还映射区给内存池*/
		ret=munmap(p,4);
		if(ret==-1)
			perr_exit("munmap error");
	}
	return 0;
}

120-无血缘关系进程间MMAP通信

先来认识一个内存操作函数:

void* memcpy(void* dest, const void* src, size_t n);

写进程:

int main(int argc,char* argv[]){
     
	struct student stu={
     1,"xiaming",18};
	struct student* p;
	int fd=0;
    /*打开或创建一个文件*/
	fd=open("temp",O_RDWR|O_CREAT|O_TRUNC,0644);
	if(fd==-1)
		perr_exit("open error");
	ftruncate(fd,sizeof(stu));
	/*建立内存映射区*/
	p=(struct student*)mmap(NULL,sizeof(stu),PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
	if(p==MAP_FAILED)
		perr_exit("mmap error");
	/*循环使用memcpy向内存映射区中写入数据,并修改stu的id值*/
	while(1){
     
		memcpy(p,&stu,sizeof(stu));
		stu.id++;
		sleep(1);
	}
	/*归还内存映射区给内存池*/
	munmap(p,sizeof(stu));
	close(fd);
	return 0;
}

读进程:

int main(int argc,char* argv[]){
     
	struct student stu;
	struct student* p;
	int fd=0;
	fd=open("temp",O_RDONLY);
	if(fd==-1)
		perr_exit("open error");
	/*建立内存映射区*/
	p=(struct student*)mmap(NULL,sizeof(stu),PROT_READ,MAP_SHARED,fd,0);
	if(p==MAP_FAILED)
		perr_exit("mmap error");
	/*循环读出内存映射区中的数据*/
	while(1){
     
		printf("id=%d,name=%s,age=%d\n",p->id,p->name,p->age);
		sleep(1);
	}
	/*归还内存映射区给内存池*/
	munmap(p,sizeof(stu));
	close(fd);
	return 0;
}

121-MMAP总结

mmap相当于文件, 所以可以反复读取, 不像FIFO;
Linux系统编程学习笔记_第68张图片

122-MMAP匿名映射区

Linux系统编程学习笔记_第69张图片

int main(int argc,char* argv[]){
     
	pid_t pid=0;
	int* p=NULL;
	int ret=0;
	/*创建匿名映射区*/
	p=(int*)mmap(NULL,4,PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANON,-1,0);
	if(p==MAP_FAILED)
		perr_exit("mmap error");

	pid=fork();
	if(pid==-1)
		perr_exit("fork error");

	if(pid==0){
     
        /*子进程向映射区中写*/
		*p=9527;
		var=200;
		printf("I'm child,*p=%d,var=%d\n",*p,var);
	}else if(pid>0){
     
        /*父进程从映射区中读,然后回收子进程*/
		sleep(1);
		printf("I'm parent,*p=%d,var=%d\n",*p,var);
		wait(NULL);
		/*父进程归还映射区*/
		ret=munmap(p,4);
		if(ret==-1)
			perr_exit("munmap error");
	}
	return 0;
}

/dev/zero-文件白洞, 里面有无限量的’\0’, 要多少有多少;

/dev/null-文件黑洞, 可以写入任意量的数据;

所以在创建映射区时可以用zero文件, 就不用自己创建文件然后拓展大小了;

无血缘关系进程间通信, 不能用匿名映射;

123-总结

Linux系统编程学习笔记_第70张图片

124-复习

Linux系统编程学习笔记_第71张图片
/dev/zero文件也不能用于无血缘关系进程间通信;

125-信号的概念和机制

信号的共性:

  • 简单;
  • 不能携带大量信息;
  • 满足特性条件才能发送;

信号的特质:
Linux系统编程学习笔记_第72张图片

所有信号的产生和处理, 都是由内核完成的;

126-与信号相关的概念

产生信号:

  1. 按键产生:Ctrl+c, Ctrl+z, Ctrl+;
  2. 系统调用产生:kill, raise, abort;
  3. 软件条件产生:定时器alarm;
  4. 硬件异常产生:非法访问内存(段错误), 除0(浮点数例外), 内存对齐错误(总线错误);
  5. 命令产生:kill命令;

递达:内核发出的信号递送并且到达进程;

未决:产生和递达之间的状态, 主要由于阻塞(屏蔽)导致该状态;

信号的处理方式:

  1. 执行默认动作;
  2. 丢弃(忽略);
  3. 捕捉(调用户处理函数);

127-信号屏蔽字和未决信号集

两者都是位图;

阻塞信号集(信号屏蔽字):将某些信号加入集合, 对他们设置屏蔽, 当屏蔽x信号后, 再收到该信号, 该信号的处理将推后(直到解除屏蔽后);

未决信号集:

  1. 信号产生后, 未决信号集中描述该信号的位立刻翻转为1, 表示信号处于未决状态, 当信号被处理后, 对应位翻转回0, 这一时刻非常短暂;
  2. 信号产生后由于某些原因(主要是阻塞)不能抵达, 这类信号的集合称为未决信号集, 在屏蔽解除前, 信号一直处于未决状态;

128-信号四要素和常规信号一览

Linux系统编程学习笔记_第73张图片

前31个位常规信号, 有默认事件和处理动作. 后面的是实时信号, 没有默认事件和处理动作;

信号四要素:

  • 编号;
  • 名称;
  • 事件;
  • 默认处理动作
    Linux系统编程学习笔记_第74张图片

后面有多个值的信号是因为不同的操作系统(处理器架构不同);

常规信号一览:
Linux系统编程学习笔记_第75张图片
Linux系统编程学习笔记_第76张图片

信号的默认处理动作:

  • Term: 终止进程;
  • Ign: 忽略信号(默认即时对该种信号忽略操作)
  • Core: 终止进程, 生成Core文件(查验进程死亡原因, 用于gdb调试);
  • Stop: 暂停进程;
  • Cont: 继续运行进程;

SIGKILL(9)SIGSTOP(19), 不允许忽略和捕捉, 只能执行默认动作, 甚至不能将其设置为阻塞;

只有每个信号所对应的事件发生了, 该信号才会被递送(但不一定递达), 不应该乱发信号;

129-kill函数和kill命令

int kill(pid_t pid, int sig);		//send signal to a process

一个弑父的例子:

int main(int argc,char* argv[]){
     
	pid_t pid=fork();

	if(pid>0){
     
		printf("I'm parent,pid=%d\n",getpid());
		while(1);

	}else if(pid==0){
     
		printf("I'm child,pid=%d,ppid=%d\n",getpid(),getppid());
		sleep(2);
		kill(getppid(),SIGKILL);
	}
	return 0;
}

pid的不同取值:

Linux系统编程学习笔记_第77张图片

kill -9 -10698-杀死10698进程组的所有进程;

关于发送权限:发送者实际有效的用户ID==接收者实际有效的用户ID;
Linux系统编程学习笔记_第78张图片

如果你想杀死1号进程, 是不允许的;

130-alarm函数

unsigned int alarm(unsigned int seconds);

测试一秒钟数多少个数:

int main(int argc,char* argv[]){
     
	int i=0;
	int j=0;
    /*设置闹钟为1s,1s后自动结束进程*/
	alarm(1);
	/*死循环打印计数*/
	while(1){
     
		printf("i=%d,j=%d\n",i,j);
		++i;
		j++;
	}
	return 0;
}

使用time命令查看程序执行时间占用情况:

程 序 实 际 执 行 时 间 = 系 统 时 间 + 用 户 时 间 + 等 待 时 间 程序实际执行时间=系统时间+用户时间+等待时间 =++

向屏幕打印的情况下进程的时间占用情况:
在这里插入图片描述

重定向到文件, 进程的时间占用情况:
Linux系统编程学习笔记_第79张图片
程序运行的瓶颈在于IO, 要优化程序, 首选优化IO;

Linux系统编程学习笔记_第80张图片

131-settimer函数

int setitimer(int which, const struct itimerval* new_value, struct itimerval* old_value);

struct itimerval {
     
	struct timeval it_interval; 	/* Interval for periodic timer */
	struct timeval it_value;    	/* Time until next expiration */
};

/*精确到us的时间结构体*/
struct timeval {
     
	time_t	tv_sec;        		 /* seconds */
	suseconds_t	tv_usec;        /* microseconds */
};

成功返回0, 失败返回-1并设置errno;

参1which指定定时方式:

  • 自然定时:ITIMER_REAL->SIGALRM;
  • 用户空间计时(只计算进程占用CPU的时间):ITIMER_VIRTUAL->SIGVTALARM;
  • 运行时计时(用户+内核):ITIMER_PROF->SIGPROF;

参2是传入参数;

参3是传出参数;

it_interval:设定两次定时任务之间的时间间隔;

it_value:定时的时长(等it_value秒后触发闹钟, 以后每隔it_interval触发一次);

/*信号捕捉回调函数*/
void myfunc(int signo){
     
	printf("fuckyou\n");
	return;
}

int main(int argc,char* argv[]){
     
    /*注册信号捕捉函数*/
	signal(SIGALRM,myfunc);
	int ret=0;
	/*这是一个传出参数*/
	struct itimerval oldit;
	/*传入参数,进行初始化*/
	struct itimerval it;
	it.it_value.tv_sec=2;
	it.it_value.tv_usec=0;
	it.it_interval.tv_sec=5;
	it.it_interval.tv_usec=0;
	/*设置闹钟*/
	ret=setitimer(ITIMER_REAL,&it,&oldit);
	if(ret==-1)
		perr_exit("setitimer error");
	/*手动让程序阻塞*/
	while(1);
	return 0;
}

132-午后回顾

Linux系统编程学习笔记_第81张图片

在这里插入图片描述

133-信号集操作函数

Linux系统编程学习笔记_第82张图片

/*自定义信号集*/
sigset_t set;

/*全部清空*/
int sigemptyset(sigset_t* set);
/*全部置1*/
int sigfillset(sigset_t* set);
/*将一个信号添加到集合当中*/
int sigaddset(sigset_t* set, int signum);
/*将一个信号从集合中移除*/
int sigdelset(sigset_t* set, int signum);
/*判断某一信号是否在集合当中*/
int sigismember(const sigset_t* set, int signum);

sigpromask函数:

int sigprocmask(int how, const sigset_t* set, sigset_t* oldset);

用于屏蔽信号解除屏蔽, 本质是读取或修改进程PCB中的信号屏蔽字;

屏蔽信号, 只是将信号处理延后执行(延至解除屏蔽);而忽略表示将该信号丢弃处理;

how:

  • SIG_BLOCK:设置阻塞, set表示需要屏蔽的信号;
  • SIG_UNBLOCK:设置非阻塞, set表示需要解除屏蔽的信号;
  • SIG_SETMASK:用set替换原始屏蔽集;

set:传入参数, 是一个位图, set中哪位置1, 就表示当前进程屏蔽哪个信号;

oldset:传出参数, 保存旧的信号屏蔽集;

sigpending函数:读取当前进程的未决信号集;

int sigpending(sigset_t* set);

set传出参数;

返回值: 成功返回0, 失败返回-1并设置errno;

134-信号集操作函数使用原理分析

操作信号集的若干步骤:

/*创建一个自定义信号集*/
sigset_t set;
/*清空自定义信号集*/
sigemptyset(&set);
/*向自定义信号集添加信号*/
sigaddset(&set,SIGINT);
/*用自定义信号集操作内核信号集*/
sigprocmask(SIG_BLOCK,&set);
/*查看未决信号集*/
sigpending(&myset);

135-信号集操作函数练习

Ctrl+D是向终端中写入一个EOF;

void print_set(sigset_t* set){
     
	int i=0;
	for(i=1;i<32;++i){
     
		if(sigismember(set,i))
			putchar('1');
		else
			putchar('0');
	}
	printf("\n");
}

int main(int argc,char* argv[]){
     
	sigset_t set,oldset,pedset;
	int ret=0;

	sigemptyset(&set);
	/*屏蔽掉Ctrl+c*/
	sigaddset(&set,SIGINT);
	/*屏蔽掉Ctrl+\*/
	sigaddset(&set,SIGQUIT);
    /*屏蔽掉SIGBUS*/
	sigaddset(&set,SIGBUS);
    /*屏蔽掉SIGKILL-不灵*/
	sigaddset(&set,SIGKILL);
	/*设置到内核*/
	sigprocmask(SIG_BLOCK,&set,&oldset);
	if(ret==-1)
		perr_exit("sigpending error");
	
	while(1){
     
		ret=sigpending(&pedset);
		if(ret==-1)
			perr_exit("sigpending error");
		
		print_set(&pedset);
		sleep(1);
	}
	return 0;
}

注意点: 对于SIGKILL信号, 即使设置了信号屏蔽, 依然能kill;

136-signal实现信号捕捉

/*定义回调函数类型,很不幸,函数类型限制死了*/
typedef void (*sighandler_t)(int);
/*注册信号捕捉函数*/
sighandler_t signal(int signum, sighandler_t handler);

该函数由ANSI定义, 由于历史原因在不同版本的Unix和不同版本的Linux中可能有不同的行为, 因此应尽量避免使用它, 取而代之使用sigaction函数;

void catch_signal(int signum){
     
	printf("Catch you!!!%d\n",signum);
}

int main(int argc,char* argv[]){
     
	/*注册信号捕捉函数*/
	signal(SIGINT,catch_signal);
	/*让程序阻塞*/
	while(1);
	return 0;
}

137-sigaction实现信号捕捉

int sigaction(int signum, const struct sigaction* act, struct sigaction* oldact);

struct sigaction {
     
	void     (*sa_handler)(int);
	void     (*sa_sigaction)(int, siginfo_t *, void *);			//不用
	sigset_t   sa_mask;										//只工作于信号捕捉函数执行期间,相当于中断屏蔽
	int        sa_flags;									//本信号默认屏蔽
	void     (*sa_restorer)(void);							//废弃
};

一个Demo:

void catch_signal(int signum){
     
	if(signum==SIGINT)
		printf("Catch you SIGINT:%d\n",signum);
	else if(signum==SIGQUIT)
		printf("Catch you SIGQUIT:%d\n",signum);
}

int main(int argc,char* argv[]){
     
	int ret=0;
	/*前者传入,后者传出*/
	struct sigaction act,oldact;
    /*设置回调函数*/
	act.sa_handler=catch_signal;
    /*设置回调函数的信号屏蔽字*/
	sigemptyset(&act.sa_mask);
	act.sa_flags=0;
	/*捕捉SIGINT信号*/
	ret=sigaction(SIGINT,&act,&oldact);
	if(ret==-1)
		perr_exit("sigaction error");
	/*捕捉SIGQUIT信号*/
	ret=sigaction(SIGQUIT,&act,&oldact);
	if(ret==-1)
		perr_exit("sigaction error");

	while(1);
	return 0;
}

138-信号捕捉的特性

  1. 捕捉函数执行期间, 信号屏蔽字由mask变为sigaction结构体中的sa_mask, 捕捉函数执行结束后, 恢复回mask;
  2. 捕捉函数执行期间, 本信号自动被屏蔽(sa_flags=0);
  3. 捕捉函数执行期间, 若被屏蔽信号多次发送, 解除屏蔽后只响应一次;
    Linux系统编程学习笔记_第83张图片

139-内核实现信号捕捉简析

Linux系统编程学习笔记_第84张图片

为什么执行完信号处理函数后要再次进入内核?

因为信号处理函数是内核调用的, 函数执行完毕后要返回给调用者;

140-借助信号捕捉回收子进程

void catch_child(int signum){
     
	pid_t wpid;
	/*
		*这里要用循环回收多个子进程:
		*如果只简单的调用一次wait,会造成当多个子进程同时发信号时,父进程只能响应一次回收过程,
		*而设置了while相当于自动提醒父进程要检查还有没有未回收的子进程,知道都回收完了,返回-1.
	*/
	while((wpid=wait(NULL))!=-1){
     
		printf("Catch child:%d\n",wpid);
	}
	return;
}

int main(int argc,char* argv[]){
     
	pid_t pid;
	int i=0;
	/*
		*那么如何解决父进程注册信号捕捉函数时子进程无法回收的问题呢(下面sleep(1)的问题)?
		*先设置信号屏蔽,将子进程发来的信号屏蔽在未决信号集上,
		*后面一旦信号集被打开,父进程就会处理未决信号集中的信号,
		*进入到信号捕捉函数,while(wait(NULL))自动提醒父进程检查回收子进程
	*/
	sigset_t set;
	sigemptyset(&set);
	sigaddset(&set,SIGCHLD);
	sigprocmask(SIG_BLOCK,&set,NULL);
	/*循环创建15个子进程*/
	for(i=0;i<15;++i)
		if((pid=fork())==0)
			break;
		
	if(i==15){
     
		struct sigaction act,oldact;
		act.sa_handler=catch_child;
		sigemptyset(&(act.sa_mask));
		act.sa_flags=0;
		/*	
			*这里设置sleep(1)只是为了模拟如果父进程注册信号的时间很长,
			*在这期间子进程已经死亡而发送的信号没有被父进程响应.
		*/
		sleep(1);
		sigaction(SIGCHLD,&act,&oldact);
		/*此处将阻塞解除,父进程能拿到SIGCHLD信号*/
		sigprocmask(SIG_UNBLOCK,&set,NULL);

		printf("I'm parent,pid=%d\n",getpid());
		while(1);
	}
	else
		printf("I'm child,pid=%d\n",getpid());

	return 0;
}

要注意的点已经写在注释里了, 如果有的地方不小心有纰漏, 很可能会造成产生僵尸进程;

141-慢速系统调用中断

Linux系统编程学习笔记_第85张图片

142-总结

setitimer可以实现高精度定时;

Linux系统编程学习笔记_第86张图片

Linux系统编程学习笔记_第87张图片

143-复习子进程借助信号回收

None;

144-会话

会话的概念: 多个进程组的集合;

Linux系统编程学习笔记_第88张图片

setsid函数:

创建一个会话, 并以自己的ID设置进程组ID, 同时也是新会话的ID;

pid_t setsid(void);

成功返回调用进程的会话ID, 失败返回-1并设置errno;

调用了setsid函数的进程, 既是新的会长, 也是新的组长;

145-守护进程创建步骤分析

Linux系统编程学习笔记_第89张图片

  1. 创建子进程, 父进程退出: 所有工作在子进程中形式上脱离了控制终端;
  2. 在子进程中创建新会话: setsid()函数, 使子进程完全独立出来, 脱离控制;
  3. 改变当前工作目录位置: chdir()函数, 防止占用可卸载的文件系统;
  4. 重设文件权限掩码: umask()函数, 防止继承的文件创建屏蔽字拒绝某些权限;
  5. 关闭文件描述符: 继承的打开文件不会用到, 浪费系统资源, 无法卸载;
  6. 开始执行守护进程核心工作;

146-守护进程创建

int main(int argc,char* argv[]){
     
	pid_t pid=0;
	int ret=0;
	int fd=0;

	pid=fork();
	if(pid>0)
		exit(0);
	
	/*创建新会话*/
	pid=setsid();
	if(pid==-1)
		perr_exit("setsid error");

	ret=chdir("/home/daniel");
	if(ret==-1)
		perr_exit("chdir error");

	/*重设文件权限掩码*/
	umask(0022);

	/*关闭标准输入*/
	close(STDIN_FILENO);

	/*将标准输出和标准出错重定向到文件黑洞*/
	fd=open("/dev/null",O_RDWR);
	if(fd==-1)
		perr_exit("open error");
	dup2(fd,STDOUT_FILENO);
	dup2(fd,STDERR_FILENO);

	/*模拟业务逻辑*/
	while(1);

	return 0;
}

147-线程概念

什么是线程:

LWP:轻量级进程, 本质仍然是进程;

进程: 有独立的地址空间, 有PCB;

线程: 有独立的PCB, 但是没有独立的地址空间(共享);

所以二者区别就在于是否共享地址空间;

线程: 最小的执行单位;

进程: 最小分配资源的单位, 可以看作是只有一个线程的进程;

ps -Lf pid查看一个进程开的线程个数;

148-三级映射

Linux系统编程学习笔记_第90张图片
PCB中持有当前进程的页目录表的指针, 页目录表中每一项指向一个个页表, 用页表检索物理内存页面;

149-线程共享和非共享

线程之间共享的资源:

  1. 文件描述符表;
  2. 每种信号的处理方式;
  3. 当前工作目录位置;
  4. 用户ID和组ID;
  5. 内存地址空间(.text/.data/.bss/.heap/共享库);

线程非共享资源:

  1. 线程id;
  2. 处理器现场和栈指针(内核栈);
  3. 独立的栈空间(用户空间栈);
  4. errno变量;
  5. 信号屏蔽字;
  6. 调度优先级;
    Linux系统编程学习笔记_第91张图片

150-中午复习

在这里插入图片描述

线程号不是线程ID;

151-创建线程

获取线程id:

pthread_t pthread_self(void);

创建线程:

int pthread_create(pthread_t* thread,const pthread_attr_t* attr,void* (*start_routine)(void* ),void* arg);

成功返回0, 失败返回errno;

一个例子:

void* tfn(void* arg){
     
	printf("tfn:pid=%d,tid=%lu\n",getpid(),pthread_self());
	return NULL;
}

int main(int argc,char* argv[]){
     
	int ret=0;
	pthread_t tid=0;

	printf("main:pid=%d,tid=%lu\n",getpid(),pthread_self());

	ret=pthread_create(&tid,NULL,tfn,NULL);
	if(ret!=0)
		perr_exit("pthread_create error");

	/*父进程等待1秒,否则父进程一旦退出,地址空间被释放,子线程没机会执行*/
	sleep(1);
	return 0;
}

152-循环创建多个子线程

void* tfn(void* arg){
     
	int i=(int)arg;
	printf("I'm %dth thread,pid=%d,tid=%lu\n",i+1,getpid(),pthread_self());
	sleep(i);
	return NULL;
}

int main(int argc,char* argv[]){
     
	int i=0;
	int ret=0;
	pthread_t tid=0;
	/*循环创建多个子线程*/
	for(i=0;i<5;++i){
     
		ret=pthread_create(&tid,NULL,tfn,(void*)i);
		if(ret!=0)
			perr_exit("pthread_create error");
	}
	sleep(i);
	return 0;
}

注意参数传递方式, 先将int型的i强转成void*传入, 用到时再强转回int型;

153-错误分析

如果不用强转, 看似规规矩矩的传地址再解引用, 会出现问题:

/*这是一个出错的版本*/
void* tfn(void* arg){
     
	int i=*((int*)arg);
	printf("I'm %dth thread,pid=%d,tid=%lu\n",i+1,getpid(),pthread_self());
	sleep(i);
	return NULL;
}

int main(int argc,char* argv[]){
     
	int i=0;
	int ret=0;
	pthread_t tid=0;

	for(i=0;i<5;++i){
     
		ret=pthread_create(&tid,NULL,tfn,(void*)&i);
		if(ret!=0)
			perr_exit("pthread_create error");
	}
	sleep(i);
	return 0;
}

在这里插入图片描述

错误分析:
Linux系统编程学习笔记_第92张图片

main中给tfn传入的是他的函数栈帧中局部变量i的地址, 这样tfn能随时访问到i的值, 考虑到线程之间是并发执行的, 每次中main中固定的地址中拿数据, 相当于各个线程共享了这块地址, 由于访问时刻随机, 所以访问到的各个值也是很随机的;

使用强转可以保证变量i的实时性(C语言值传递的特性);

154-线程间全局变量共享

线程默认共享数据段, 代码段等地址空间, 常用的是全局变量, 而进程不共享全局变量, 只能借助mmap;

155-pthrea_exit退出

void pthread_exit(void* retval);

retval表示退出状态, 通常传NULL;

exit()函数用来退出当前进程, 不可以用在线程中, 否则直接一锅端了;

pthread_exit()函数才是用来将单个的线程退出;

pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者malloc分配的, 不能在线程函数的栈上分配, 因为其他线程得到这个返回指针时线程函数已经退出了;

void* tfn(void* arg){
     
	int i=(int)arg;
    /*当i循环到2,退出当前线程*/
	if(i==2)
		pthread_exit(NULL);
	printf("I'm %dth thread,pid=%d,tid=%lu\n",i+1,getpid(),pthread_self());
	sleep(i);
	return NULL;
}

156-pthread_join函数

int pthread_join(pthread_t thread, void** retval);

成功返回0, 失败返回errno

线程的退出状态是void*, 回收时传的就是void**;

struct thrd{
     
	int var;
	char str[256];
};

void* tfn(void* arg){
     
    /*在堆区创建一个结构体*/
	struct thrd* tval;
	tval=(struct thrd*)malloc(sizeof(struct thrd));
	/*给结构体赋值*/
	tval->var=100;
	strcpy(tval->str,"fuckyou");

	return (void*)tval;
}

int main(int argc,char* argv[]){
     
	pthread_t tid=0;
	struct thrd* retval;
	int ret=0;

	ret=pthread_create(&tid,NULL,tfn,NULL);
	if(ret==-1)
		perr_exit("pthread_create error");
	/*pthread_join回收子线程*/
	ret=pthread_join(tid,(void**)&retval);
	if(ret==-1)
		perr_exit("pthread_join error");
	/*打印结构体中的信息*/
	printf("child thread exit with var=%d,str=%s\n",retval->var,retval->str);

	pthread_exit(NULL);
	return 0;
}

注意一个错误的写法:

void* tfn(void* arg){
     
    /*在堆区创建一个结构体*/
	struct thrd tval;
	/*给结构体赋值*/
	tval.var=100;
	strcpy(tval.str,"fuckyou");

	return (void*)&tval;
}

不能将子线程的回调函数的局部变量返回, 由于该函数执行完毕返回后, 其栈帧消失, 栈上的局部变量也就消失, 返回的是无意义的;

当然, 可以在main函数中创建局部变量;

157-pthread_join作业

使用pthread_join函数将循环创建的多个子线程回收;

定义一个tid数组, 保存不同子线程的tid;

158-线程分离pthread_detach

int pthread_detach(pthread_t thread);

子线程分离后不能再调用join回收了:

void* tfn(void* arg){
     
	printf("tfn:pid=%d,tid=%lu\n",getpid(),pthread_self());
	return NULL;
}

int main(int argc,char* argv[]){
     
	int ret=0;
	pthread_t tid=0;

	ret=pthread_create(&tid,NULL,tfn,NULL);
	if(ret!=0){
     
		fprintf(stderr,"pthread_create error%s\n",strerror(ret));
		exit(1);
	}
	/*设置线程分离*/
	ret=pthread_detach(tid);
	if(ret!=0){
     
		fprintf(stderr,"pthread_detach error%s\n",strerror(ret));
		exit(1);
	}

	sleep(1);
	/*这里回出错:不能对一个已经分离出去的子线程回收*/
	ret=pthread_join(tid,NULL);
	if(ret!=0){
     
		fprintf(stderr,"pthread_join error%s\n",strerror(ret));
		exit(1);
	}

	printf("main:pid=%d,tid=%lu\n",getpid(),pthread_self());
	pthread_exit(NULL);
	return 0;
}

Linux系统编程学习笔记_第93张图片

159-检查出错返回

detach: 设置线程分离, 线程终止会自动清理pcb, 无需回收;

detach相当于自动回收, join相当于手动回收;

注意检查出错方式的变化(失败会直接返回errno);

if(ret!=0){
     
		fprintf(stderr,"pthread_detach error%s\n",strerror(ret));
		exit(1);
}

160-pthread-cancel函数

函数原型:

 int pthread_cancel(pthread_t thread);
DESCRIPTION
       The  pthread_cancel() function sends a cancellation request to the thread thread.  Whether and when the target thread reacts to the cancellation request depends on two attributes
       that are under the control of that thread: its cancelability state and type.

       A thread's cancelability state, determined by pthread_setcancelstate(3), can be enabled (the default for new threads) or disabled.  If a thread has disabled cancellation, then  a
       cancellation request remains queued until the thread enables cancellation.  If a thread has enabled cancellation, then its cancelability type determines when cancellation occurs.

       A thread's cancellation type, determined by pthread_setcanceltype(3), may be either asynchronous or deferred (the default for new threads).  Asynchronous cancelability means that
       the thread can be canceled at any time (usually immediately, but the system does not guarantee this).  Deferred cancelability means that cancellation will be  delayed  until  the
       thread next calls a function that is a cancellation point.  A list of functions that are or may be cancellation points is provided in pthreads(7).

       When a cancellation requested is acted on, the following steps occur for thread (in this order):

       1. Cancellation clean-up handlers are popped (in the reverse of the order in which they were pushed) and called.  (See pthread_cleanup_push(3).)

       2. Thread-specific data destructors are called, in an unspecified order.  (See pthread_key_create(3).)

       3. The thread is terminated.  (See pthread_exit(3).)

       The  above steps happen asynchronously with respect to the pthread_cancel() call; the return status of pthread_cancel() merely informs the caller whether the cancellation request
       was successfully queued.

       After a canceled thread has terminated, a join with that thread using pthread_join(3) obtains PTHREAD_CANCELED as the thread's exit status.  (Joining with a thread  is  the  only
       way to know that cancellation has completed.)

RETURN VALUE
       On success, pthread_cancel() returns 0; on error, it returns a nonzero error number.

ERRORS
       ESRCH  No thread with the ID thread could be found.

ATTRIBUTES
       For an explanation of the terms used in this section, see attributes(7).

       ┌─────────────────┬───────────────┬─────────┐
       │Interface        │ Attribute     │ Value   │
       ├─────────────────┼───────────────┼─────────┤
       │pthread_cancel() │ Thread safety │ MT-Safe │
       └─────────────────┴───────────────┴─────────┘


应用:

void* tfn(void* arg){
     
	while(1){
     
		printf("tfn:pid=%d,tid=%lu\n",getpid(),pthread_self());
		sleep(1);
	}
	return NULL;
}

int main(int argc,char* argv[]){
     
	int ret=0;
	pthread_t tid=0;

	printf("main:pid=%d,tid=%lu\n",getpid(),pthread_self());

	ret=pthread_create(&tid,NULL,tfn,NULL);
	if(ret!=0)
		perr_exit("pthread_create error",ret);
	/*等待5秒之后杀死子线程*/
	sleep(5);
	ret=pthread_cancel(tid);
	if(ret!=0)
		perr_exit("pthread_cancel error",ret);
	while(1);
	pthread_exit(NULL);
	return 0;
}

cancel必须要等待取消点(进入内核的契机), 所以如果一个线程一直使用系统调用(一直不进内核), cancel就无法杀死该线程;

可以手动添加一个取消点:

pthread_testcancel();

Linux系统编程学习笔记_第94张图片

161-进程和线程控制原语对比

Linux系统编程学习笔记_第95张图片

162-线程属性设置分离线程

先初始化线程属性, 再pthread_create创建线程;

/*初始化线程属性:成功返回0,失败返回errno*/
int pthread_attr_init(pthread_attr_t* attr);
/*销毁线程属性所占用的资源:成功返回0,失败返回errno*/
int pthread_attr_destroy(pthread_attr_t* attr);

Linux系统编程学习笔记_第96张图片
线程分离状态函数:

/*设置线程属性:分离或非分离*/
int pthread_attr_setdetachstate(pthread_attr_t* attr, int detachstate);
/*获取线程属性*/
int pthread_attr_getdetachstate(const pthread_attr_t* attr, int* detachstate);

detachstate取值:

PTHREAD_CREATE_DETACHED

PTHREAD_CREATE_JOINABLE
Linux系统编程学习笔记_第97张图片

一个例子:

int main(int argc,char* argv[]){
     
	int ret=0;
	pthread_t tid=0;
	pthread_attr_t attr;
	/*初始化属性结构体*/
	ret=pthread_attr_init(&attr);
	if(ret!=0)
		perr_exit("pthread_attr_init error",ret);
	/*给属性结构体添加分离属性*/
	pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);
	if(ret!=0)
		perr_exit("pthread_attr_setdetachstate error",ret);

	printf("main:pid=%d,tid=%lu\n",getpid(),pthread_self());
	/*创建子线程*/
	ret=pthread_create(&tid,&attr,tfn,NULL);
	if(ret!=0)
		perr_exit("pthread_create error",ret);
	/*join一下试试,由于线程已经分离了,会出错*/
	ret=pthread_join(tid,NULL);
	if(ret!=0)
		perr_exit("pthread_join error",ret);
	/*销毁线程属性结构体*/
	ret=pthread_attr_destroy(&attr);
	if(ret!=0)
		perr_exit("pthread_attr_destory error",ret);
	pthread_exit(NULL);
	return 0;
}

如果为了突出逻辑, 将所有错误检查都去掉, 还是比较简单的:

int main(int argc,char* argv[]){
     
	int ret=0;
	pthread_t tid=0;
	pthread_attr_t attr;
    
	/*初始化属性结构体*/
	ret=pthread_attr_init(&attr);	
	/*给属性结构体添加分离属性*/
	pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);
	printf("main:pid=%d,tid=%lu\n",getpid(),pthread_self());
	/*创建子线程*/
	ret=pthread_create(&tid,&attr,tfn,NULL);	
	/*销毁线程属性结构体*/
	ret=pthread_attr_destroy(&attr);	
	pthread_exit(NULL);
	return 0;
}

各个子线程会均分进程的栈空间, 但是线程的栈空间大小是可以调整的;

Linux系统编程学习笔记_第98张图片

163-线程使用注意事项

Linux系统编程学习笔记_第99张图片

164-总结

Linux系统编程学习笔记_第100张图片
Linux系统编程学习笔记_第101张图片

165-线程同步概念

Linux系统编程学习笔记_第102张图片

线程同步: 一个线程发出某一功能调用时, 再没有得到结果之前, 该调用不返回. 同时其他线程为保证数据的一致性, 不能调用该功能;

避免产生与时间有关的错误;

166-锁使用注意事项

Linux提供的都是建议锁, 不具有强制性, 对于不守规矩的线程无能为力;

Linux系统编程学习笔记_第103张图片
Linux系统编程学习笔记_第104张图片

167-借助互斥锁管理共享数据实现同步

先来看一个C的关键字:

restrict:用来限定指针变量, 被该关键字限定的指针变量所指向的内存操作, 必须由本指针完成;
Linux系统编程学习笔记_第105张图片

/*创建一把全局锁*/
pthread_mutex_t mutex;

void* tfn(void* arg){
     
	srand(time(NULL));
	while(1){
     
		/*子线程先加锁再访问*/
		pthread_mutex_lock(&mutex);
		printf("hello ");
		sleep(rand()%3);
		printf("world\n");
		/*访问完立即解锁*/
		pthread_mutex_unlock(&mutex);
		sleep(rand()%3);
	}
	return NULL;
}

int main(int argc,char* argv[]){
     
	pthread_t tid;
	int ret=0;
	srand(time(NULL));
	/*在main函数中将锁初始化*/
	ret=pthread_mutex_init(&mutex,NULL);
	if(ret!=0)
		perr_exit("pthread_mutex_init error",ret);

	ret=pthread_create(&tid,NULL,tfn,NULL);
	if(ret!=0)
		perr_exit("pthread_create error",ret);

	while(1){
     
		/*主线程先加锁再访问*/
		pthread_mutex_lock(&mutex);
		printf("HELLO ");
		sleep(rand()%3);
		printf("WORLD\n");
		/*访问完立即解锁*/
		pthread_mutex_unlock(&mutex);
		sleep(rand()%3);
	}
	/*销毁锁*/
	pthread_mutex_destroy(&mutex);
	pthread_join(tid,NULL);
	return 0;
}

168-互斥锁使用技巧

锁的粒度越小越好: 访问前加锁, 访问结束后立即解锁;

可以将mutex想象为一个整数:

  • 初始化好了之后可以认为mutex=1;
  • lock操作可以想象成mutex–;
  • unlock操作可以想象成mutex++;

169-try锁

try会尝试加锁, 成功mutex--, 失败返回错误号;

而lock如果加锁失败会阻塞, 等待锁释放;

170-死锁两种

  1. 对一个mutex反复加锁;
  2. 线程1有A锁, 请求B锁. 线程2有B锁, 请求A锁, 造成二者循环等待;
    Linux系统编程学习笔记_第106张图片

171-读写锁原理

读写锁与互斥量类似, 但读写锁允许更高的并行性, 其特性为

  1. 写独占, 读共享;
  2. 写锁优先级高(读写锁一起过来时);
  3. 锁只有一把;

读写锁只有一把, 但其具备两种状态:

  • 读模式下加锁状态(读锁);
  • 写模式下加锁状态(写锁);

172-读写锁操作函数原型

Linux系统编程学习笔记_第107张图片
当读线程远大于写线程, 可以调高访问效率;

读写锁操作函数:

/*定义一个读写锁变量*/
pthread_rwlock_t rwlock;

int pthread_rwlock_init(&rwlock,NULL);
int pthread_rwlock_destory(&rwlock);
int pthread_rwlock_rdlock(&rwlock);
int pthread_rwlock_wrlock(&rwlock);
int pthread_rwlock_tryrdlock(&rwlock);
int pthread_rwlock_trywrlock(&rwlock);
int pthread_rwlock_unlock(&rwlock);

都是成功返回0, 失败直接返回错误号;

173-读写锁

int counter;
/*创建一把全局锁*/
pthread_rwlock_t rwlock;

/*写线程回调函数*/
void* th_write(void* arg){
     
	int t=0;
	int i=(int)arg;
	while(1){
     
		/*加写锁*/
		pthread_rwlock_wrlock(&rwlock);
		t=counter;
		usleep(1000);
		printf("----Write:i=%d,tid=%lu,conter=%d,++conter=%d\n",i,pthread_self(),t,++counter);
		pthread_rwlock_unlock(&rwlock);
		usleep(10000);
	}
	return NULL;
}

/*读线程回调函数*/
void* th_read(void* arg){
     
	int i=(int)arg;
	while(1){
     
		/*加读锁*/
		pthread_rwlock_rdlock(&rwlock);
		printf("Read:i=%d,tid=%lu,conter=%d\n",i,pthread_self(),counter);
		pthread_rwlock_unlock(&rwlock);
		usleep(2000);
	}
	return NULL;
}

int main(int argc,char* argv[]){
     
	int i=0;
	pthread_t tid[8];

	/*在主线程中初始化读写锁*/
	pthread_rwlock_init(&rwlock,NULL);
	/*创建3个写线程*/
	for(i=0;i<3;++i)
		pthread_create(tid+i,NULL,th_write,(void*)i);
	/*创建5个读线程*/
	for(i=0;i<5;++i)
		pthread_create(tid+3+i,NULL,th_read,(void*)i);
	/*回收8个子线程*/
	for(i=0;i<8;++i)
		pthread_join(tid[i],NULL);
	/*销毁读写锁*/
	pthread_rwlock_destroy(&rwlock);
	return 0;
}

现象:
Linux系统编程学习笔记_第108张图片

174-午后复习

Linux系统编程学习笔记_第109张图片

175-静态初始化条件变量和互斥量

条件变量本身不是锁, 但它也可以造成线程阻塞. 通常与互斥锁配合使用, 给多线程提供一个会和的场所;

主要应用函数:

/*定义一个条件变量:静态初始化*/
pthread_cond_t cond=PTHREAD_COND_INITIALIZER;

int pthread_cond_init(&cond,NULL);
int pthread_cond_destory();
int pthread_cond_wait(&cond,&mutex);
int pthread_cond_timewait();
int pthread_cond_signal();
int pthread_cond_broadcast();

互斥量也可以进行静态初始化:

pthread_mutex_t cond=PTHREAD_MUTEX_INITIALIZER;

176-条件变量相关函数wait

int pthread_cond_wait(pthread_cond_t* restrict cond,pthread_mutex_t* restrict mutex);

阻塞等待一个条件变量;

函数作用:

  1. 阻塞等待条件变量cond(参1)满足;
  2. 释放已经掌握的互斥锁(解锁互斥量), 相当于pthread_mutex_unlock(&mutex)(与第一步在一块形成一个原子操作);
  3. 当被唤醒, pthread_cond_wait函数返回时, 解除阻塞并重新申请互斥锁pthread_mutex_lock(&mutex);
    Linux系统编程学习笔记_第110张图片

177-条件变量的生产者消费者模型分析

Linux系统编程学习笔记_第111张图片

生产者:

  1. 生产数据;
  2. 加锁pthread_mutex_lock(&mutex);
  3. 将数据放置到公共区域;
  4. 解锁pthread_mutex_unlock(&mutex);
  5. 通知阻塞在条件变量上的线程: pthread_cond_signal()或pthread_cond_broadcast();
  6. 循环生产后续数据;

消费者:

  1. 创建锁: pthread_mutex_t mutex;
  2. 初始化: pthread_mutex_init(&mutex,NULL);
  3. 加锁: pthread_mutex_lock(&mutex);
  4. 等待条件满足: pthread_cond_wait(&cond,&mutex): 首先解锁并阻塞等待条件变量, 然后加锁;
  5. 访问共享数据;
  6. 解锁, 释放条件变量, 释放锁;

178-条件变量实现生产者消费者模型代码预览

/*链表节点:产品*/
struct msg{
     
	int num;
	struct msg* next;
};
/*全局的一个头指针*/
struct msg* head;
/*创建全局锁和信号量*/
pthread_mutex_t lock=PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t has_product=PTHREAD_COND_INITIALIZER;

/*消费者回调函数*/
void* consumer(void* arg){
     
	struct msg* mp;
	while(1){
     
		/*先加锁*/
		pthread_mutex_lock(&lock);
		/*当条件变量不满足时,解锁,并死循环等待*/
		while(head==NULL)
			pthread_cond_wait(&has_product,&lock);
		/*条件满足,被唤醒,加锁,进入临界区,从链表上摘下节点*/
		mp=head;
		head=mp->next;
		/*解锁*/
		pthread_mutex_unlock(&lock);

		printf("----Consumer=%lu,num=%d\n",pthread_self(),mp->num);
		free(mp);
		sleep(rand()%3);
	}
	return NULL;
}

/*生产者回调函数*/
void* producter(void* arg){
     
	struct msg* mp;
	while(1){
     
		/*生产一个节点*/
		mp=(struct msg*)malloc(sizeof(struct msg));
		mp->num=rand()%1000+1;
		printf("====Producter=%lu,num=%d\n",pthread_self(),mp->num);
		/*加锁,然后将产品放入临界区*/
		pthread_mutex_lock(&lock);
		mp->next=head;
		head=mp;
		/*解锁*/
		pthread_mutex_unlock(&lock);
		/*唤醒阻塞在条件变量上的线程*/
		pthread_cond_signal(&has_product);
		sleep(rand()%3);
	}
	return NULL;
}

int main(int argc,char* argv[]){
     

	srand(time(NULL));
	pthread_t pid,cid;
	/*创建两个子线程*/
	pthread_create(&pid,NULL,producter,NULL);
	pthread_create(&cid,NULL,consumer,NULL);
	/*回收两个子线程*/
	pthread_join(pid,NULL);
	pthread_join(cid,NULL);

	return 0;
}

179-条件变量实现生产者消费者代码实现

None;

180-多个消费者使用while做

/*当条件变量不满足时,解锁,并死循环等待*/
while(head==NULL)
	pthread_cond_wait(&has_product,&lock);

注意这里不能用if, 否则当多个消费者其中一个抢到了锁把数据读走后, 其他消费者由于阻塞在了锁上, 会尝试去缓冲区拿数据, 而此时缓冲区并没有数据, 所以应该用while循环回来, 重新检查条件变量;

181-条件变量signal注意事项

pthread_cond_signal()唤醒阻塞在条件变量上的至少一个线程;

pthread_cond_broadcast()唤醒阻塞在条件变量上的所有线程;

182-信号量概念以及相关操作函数

相当于初始化值为N的互斥量, N值表示可以同时访问共享区域的线程数;

Linux系统编程学习笔记_第112张图片

信号量操作函数:

/*定义一个信号量*/
sem_t sem;
/*信号量操作函数*/
int sem_init(sem_t* sem, int pshared, unsigned int value);
int sem_destroy(sem_t* sem);
int sem_wait(sem_t* sem);
int sem_trywait(sem_t* sem);
int sem_timedwait(sem_t* sem, const struct timespec* abs_timeout);
int sem_post(sem_t *sem);

sem_wait(): 如果信号量>0, 则信号量–. 如果信号量=0, 造成线程阻塞;

sem_post(): 将信号量++, 同时唤醒阻塞在信号量上的线程;

sem_t的实现对用户隐藏, 所以所谓的++--只能通过函数来实现, 不能直接用++--符号;

信号量的初值, 决定了占用信号量的线程个数;

183-信号量实现的生产者消费者

#define NUM 5

int queue[NUM];
sem_t blank_num,product_num;

void* producter(void* arg){
     
	int i=0;
	while(1){
     
		sem_wait(&blank_num);
			queue[i]=rand()%1000+1;
			printf("====Producter:%d\n",queue[i]);
		sem_post(&product_num);

		i=(i+1)%NUM;
		sleep(rand()%3);
	}
	return NULL;
}

void* consumer(void* arg){
     
	int i=0;
	while(1){
     
		sem_wait(&product_num);
			printf("----Consumer:%d\n",queue[i]);
			queue[i]=0;
		sem_post(&blank_num);

		i=(i+1)%NUM;
		sleep(rand()%3);
	}
	return NULL;
}

int main(int argc,char* argv[]){
     

	srand(time(NULL));
	pthread_t pid,cid;

	sem_init(&blank_num,0,NUM);
	sem_init(&product_num,0,0);

	/*创建两个子线程*/
	pthread_create(&pid,NULL,producter,NULL);
	pthread_create(&cid,NULL,consumer,NULL);

	/*回收两个子线程*/
	pthread_join(pid,NULL);
	pthread_join(cid,NULL);

	sem_destroy(&blank_num);
	sem_destroy(&product_num);

	return 0;
}

184-总结

Linux系统编程学习笔记_第113张图片

好了, 184P的Linux系统编程二刷完毕, 感谢飞哥!

你可能感兴趣的:(Linux,linux,操作系统,c语言,多线程,多进程)