LInux课堂笔记

第一章 Linux命令基础

1.1 常用命令

1.1.1 ls命令和相对路径绝对路径

history指令

  • 可以查看历史命令,可以方向键查看之前的

UNIX系统的目录结构

  • linux一切皆文件
/根目录
/bin系统可执行程序存放根目录
/boot内核和启动程序相关文件都在此目录下
/lib库目录、主要存放系统最基本的动态共享库
/media挂载设备媒体,u盘,光驱等
/mnt该目录是为了让用户挂载别的文件系统
/usr庞大和复杂的目录,许多引用都会安装到此目录
/sbin超级管理员的执行程序
/proc这个目录是系统内存的映射,会保留进程运行的一些信息
/etc系统软件的启动和配置目录
/dev设备文件所在目录
/home/user用户家目录

ls 查看文件信息

  • 可以使用通配符*ls组合使用
    • *–代表任意多个字符
    • –代表任意一个字符
ls -l显示详细信息
ls -a显示隐藏的文件或目录
ls -R递归显示子目录的内容
ls -lrt所有文件安装时间顺序排序显示
ll等价与ls -alF
  • 相对路径和绝对路径

    • “/”开头的路径为绝对路径

    • 不是以“/”开头的路径为相对路径

1.1.2 目录相关操作

命令说明
cd --Change dir更改目录回到家目录的方式 cd /home/cmxiao cd ~ cd cd $HOME
pwd显示当前工作目录路径
mkdir [option] dirname dirname ..创建目录 -p递归创建目录
rmdir删除目录,但不能删除非空目录 -p递归删除目录参数
which 命令显示对应命令所在地路径(cd命令不在任何一个目录下,是shell自带)
  • 使用tree命令可以查看目录的树状结构

1.1.3 文件相关操作命令

命令说明
touch [option] filename1 filename2 ..创建文件,如果文件存在,则修改文件袋最后修改时间
rm -rf *删除文件或目录,其中
-r 递归删除子目录
-f 强制删除
-rm-rf * 删除当前目录全部内容(强制删除,慎用)
cp [option] srcpah despath拷贝文件或者目录
-despath是一个目录,将文件拷贝到该目录下
-despath不是一个目录,在 despath上级目录(…/xxx),在…/下创建一个xxx文件,并将源文件内容拷贝进来
mv [option] srcpah despath移动文件或者目录

1.1.4 文件内容查看

命令说明
cat [OPTION]... [FILE]...直接显示文件内容
more 分屏显示文件信息
-回车 逐行显示
-空格 一页一页显示
less分屏显示文件信息
-回车或者上下方向键可以反复查看文件内容
head [OPTTON] FILE 查看文件头,默认显示10行,可以通过-n指定显示行数
tail [OPTTON] FILE查看文件尾,默认显示10行,可以通过-n指定显示行数,-f可以一直跟踪文件尾

1.1.5 统计相关信息

命令说明
tree [dir]树形显示目录结构
wc --l --w --c file统计文件袋行数、单词数、字节数
du -h显示当前目录占用空间
df -h显示当前系统的磁盘空间信息

1.1.6 文件权限

drwxr-xr-x  6 cmxiao cmxiao 4096 43 17:26 Downloads
#文件权限标志   #用户	#组	  #大小
-rwxrwxr-x  1 cmxiao cmxiao  154 43 13:22 panda.desktop
  		 #硬连接计数

利用 ls -l 显示的文件属性中,第一个字段是档案的权限,共有十个位,第一个位是文件类型(- 普通文件、 d 目录文件、), 接下来三个为一组共三组,为使用者、群组、其他人的权限,权限有 r(读),w(写),x(执行) 三种;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ToYvTFZ7-1659943933353)(image/image-20220403223207860.png)]

1.1.7 软硬连接的建立和删除

命令说明
ln src des建立硬连接
ln -s src des建立软连接,目录也可以创建软连接
unlink删除软硬连接,如果连接数为0,文件也将被自动删除

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sA3xIFBl-1659943933371)(image/image-20220403215656639.png)]

1.1.8 修改文件的用户和组

  • 更改档案的群组支持可用 chgrp 组 文件名|目录
  • 修改档案的拥有者可用 chown 用户:组 文件名|目录
  • 修改档案的权限可用 chmod

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LRAdyp7r-1659943933372)(image/image-20220403220545171.png)]

  • 用数字表示发更改权限: r-4、w-2、x-1 -> chmod 777 helo

  • 注意目录需要可执行权限,才能进入目录

1.1.9 查找方法find_grep_xargs命令

命令说明
find dir [option] n内容在指定文件夹下查找指定文件
-name 按名字查找
-type按类型查找,文件是f
-size +-2M按大小查找
-maxdepth指定路径深度: find ./ -maxdepth 1 -size +1G当前路径
-execfind ./ -maxdepth 2 -size +1G -exec ls -l {} \;使用OK会相对安全
xargs find 命令的好伴侣将find命令查找的结果分成若干块输出给后面的指令,避免溢出
`find ./ -type f
grep1、按文件内容查找:grep -rn 查找内容 [对应文件] 递归查找,显示行号
`tail -f helo

1.1.10 压缩文件zip、tar、rar

命令说明
zip/unzip 压缩和解压zip格式文件
- zip -r dir.zip 源文件 递归压缩文件
-unzip dir.zip 解压到当前目录
gzip/gunzip压缩和解压.gz格式文件
tar [option] 压缩包名 源文件
tar zcvf file.tar.gz files ..添加压缩
tar -zxvf file.tar.gz解压缩
常用的解压命令,用于解压tar.gz或tar.bazip格式文件
-c压缩文件
-v显示信息
-f指向压缩包名
-zgz格式压缩
-x解压缩
-jbzip2格式压缩
rar
rar a -r 压缩包名(可以无后) 源文件
rar x 压缩包名
压缩和解压rar格式文件
a代表压缩
-r递归子目录
x代表解压缩

1.1.11 用户管理

指令说明
-s指定shell
-g指定组
-d用户家目录
-m家目录不存在是,自动创建
sudo useradd -s /bin/bash -g cmixao -d /home/cmxiao -m cmxiao
创建用户
sudo groupadd cmxiao增加用户组设置用户组
sudo passwd cmxiao设置密码
su cmixao切换到cmxiao用户
su -cmixao带有环境变量切换
切换用户
sudo suroot用户
sudo userdel cmxiao删除用户
sudo userdel -r cmxiao删除用户连带删除家目录
删除用户

1.1.10 进程管理

who查看登录设备
ps查看进程信息
-ps aux
-ps ajx内容更全,可以查看进之间的父子关系
kill给进程发送一个信号可以使用kill -l查看所有信号
SIGKILL 9号信息
杀死进程:kill -9 pid
env环境变量:echo $HELL
top查看系统信息

1.1.11 网络管理

ifconfig查看ip信息
eth0代表本地第一块网卡
sudo ifconfig eth0 ip
ping-c 4只ping四次
ping 域名
nslookup通过域名得到ip
netstat查看网络连接状态
`netstat -an

1.1.12 其他常用命令(date、umask、echo、man、alias)

datedate获取系统时间
date +"%Y%m%d"获取当前日期
umask文件权限补码,用户创建文件满权限是666,创建文件夹是77
umask显示8进制文件掩码
umask -S显示掩码对应的读写
echo输出变量或者字符串
man帮助手册
alias命令重命名
shutdown -h now,init 0,poweroff,reboot关机重启

1.1.13 源码安装与卸载软件

#解压缩源代码包
cd dir
./configure	#检测文件是否缺失,创建Makefile检测编译环境
make		#编译源码,生成库和可执行程序
sudo make install	#把库和可执行程序,安装到系统路径下
sudo make distclean #删除和卸载软件

1.2 vim-gcc-library相关

1.2.1 vim编辑器相关

模式功能指令
命令模式移动光标hj下、k上、l
0行首
$行尾
gg文件开头
G文件袋末尾
nG到文件袋第n行
删除内容x删除光标所在内容
X删除光标字母
dw删除单词
d0删除光标到行首
d$(D)删除光标到行尾
dd删除光标所在行
ndd删除光标所在行开始n行
撤销操作u撤销操作
ctrl + r反撤销
复制粘贴yy赋值一行内容
dd剪切一行内容
nyy复制n行内容
p/P粘贴(注意粘贴位置不一样)
r替换,输入r之后,再输入一个字母
可视模式v进入可视模式
移动光标选中内容
y复制内容
移动光标到目的地
p/P将内容粘贴
查找操作/查找内容
n/N进行遍历,注意向前还是向后
?查找内容
n/N方向与上面相反
光标移动到字符串上,按#也可以查找
格式调整gg = G文件整体自动调整
>>当前行向右移动一个Tab
<<当前行向左移动一个Tab
n>>当前行开始n行,整体向右移动一个Tab
n<<当前行开始n行,整体向左移动一个Tab
man帮助查看k或者n+K光标移动到函数位置,按K切换到man帮助页,如果是n+K,则指定章节
编辑模式命令模式变为编辑模式i在光标前插入
a在光标后插入
I在行首插入
A在行尾插入
o在下一行插入
O在上一行插入
s删除当前字母,变为插入模式
S删除当前行,进入插入模式
末行模式命令模式变为末行模式进入末行模式
Esc*2退回到命令模式
执行shell命令:! + 命令
:! ls - l
代码替换:s/src/des/只会替换当前行第一个匹配的src为des
:s/src/des/g只会替换当前行所有的src为des
:%s/src/des/只会替换所有行的第一个匹配src为des
:%s/src/des/g替换文件内容所有的src为des
保存和退出:wq保存退出
:w只保存
:q只退出,文件不能修改
q!强制退出
x保存退出
ZZ保存退出(命令模式)
vi分屏:sp filename横向分屏
:vsp filename纵向分屏
:qall全部文件退出
:wqall全部保存退出
Ctrl +ww切换光标所在窗口
vim的配置文件/home/user/.vimrc
/etc/vim/vimrc
用户的配置文件(user要替换为对应的用户名)
系统vim配置文件
set -o vi在系统环境设置此项,可以在当前shell环境下使用vi快捷键

1.2.2 gcc编译器相关

  • 编译的流程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4k3ZlTUA-1659943933373)(image/image-20220406202819224.png)]

  • gcc参数
参数说明
-I包含头文件路径(可以绝对路径或者相对路径)
-O优化选项,1-3越高优先级越高
-L包含库路径
-l指定库名(格式通常是libxxx.so或者libxxx.a,-lxxx)
-o指定输出文件
-c编译成.o二进制文件
-E输出到表这输出,宏替换,头文件展开
-S编译汇编
-g用于gdb调试,不加此选项不能gdb调试
-Wall显示更多警告
-D指定宏编译,debug调试
-lstdc++编译c++代码

1.2.3 静态库和共享库

1、静态库

  • 静态库的命名:libxxx.a对应windos下的.lib文件

  • 静态库制作流程

    1. 编译为.o文件:gcc -c *.c -I./include
    2. .o文件打包: ar rcs libxxx.a file1.o file2.o ...
    3. 将头文件与库一起发布
  • 静态库的使用:编译时,需要加静态库名(记得路径), -I包含头文件

  • 静态库查看:使用nm + 静态库名查看静态库的一些信息

  • 静态库的优缺点:

    • 优点:
      1. 执行快
      2. 发布应用时不需要发布库
    • 缺点:
      1. 执行程序体积会比较大
      2. 库编更时需要重新编译应用

2、动态库

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WynhsLZJ-1659943933374)(image/image-20220406214557690.png)]

  • 动态库的制作步骤:
    1. 编译与位置无关的代码,生成.o,关键参数 -fPIC表示代码与位置无关: gcc -fPIC -c *.c -I ../include/
    2. .o文件打包:关键参数 -sharedgcc -shared -o lbCalc.so *.o
    3. 将库文件与头文件一起发布
  • 动态库的使用:
    • -L指定动态库的路径 ,-l指定库名: gcc -o newapp main.c -L /lib -lmvcakc -I /include/
    • 解决不能加载动态库的问题(保证ld链接器能够加载):
      1. 拷贝到系统/lib下—不允许
      2. 将库路径增加到环境变量LD_LIBRARY_PATH,不是特别推荐: export LD_LIBRARY_PATH=/home/user/lib/:$LD_LIBRARY_PATH,要永久设置可以修改.bashrc
      3. 配置/etc/ld.so.conf文件,增加/home/user/cmd/Calc/lib路径,执行sudo ldconfig -v
  • 优缺点总结:
    • 缺点:
      1. 执行时需要加载动态库,相对而言,比静态库慢
      2. 发布应用时需要同时发布动态库
    • 优点:
      1. 执行程序体积小
      2. 库变更时,一般不需要重新编译应用

1.3 makefile-gdb-IO相关

1.3.1 makefile文件

  • makefile的好处:一次编写,终身受益

  • makefile的命名:

    • makefile
    • Makefile
  • makefile的三要素:

    • 目标
    • 依赖
    • 规则命令
  • makefile写法:

    • 目标:依赖
    • tab键 规则命令
  • 第一版makefile

app:main.c add.c div.c
	gcc -o app -I ./include main.c add.c div.c

​ 如果更改其中一个文件,所有的源码都重新编译。可以考虑编译过程分解,先生层.o文件,然后使用.o文件得到结果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PlN8S8Yx-1659943933374)(image/image-20220408235025205.png)]

  • 第二版makefile :变量的定义和使用
  2 ObjFiles=main.o add.o div.o #定义目标文件变量
  3 app:$(ObjFiles) #使用目标文件变量$(遍历名)
  4     gcc -o app -I ./include main.o add.o div.o
  5 main.o:main.c
  6     gcc -c main.c -I ./include  
  7 div.o:div.c
  8     gcc -c div.c -I ./include
  9 add.o:add.c
 10     gcc -c add.c -I ./include

​ makefile的隐含规则,默认处理第一个目标

  • 第三版:使用函数wildcard与pastubst

    • 函数
      • wildcard可以进行文件匹配
      • patsubst内容的替换
    • makefile的变量
      • $@代表目标
      • $^代表全部依赖
      • $<第一个依赖
      • $?第一个变化的依赖
#get all .c file
SrcFiles=$(wildcard *.c)
#all .c file --> .o file
ObjFiles = $(patsubst %.c,%.o,$(SrcFiles))

app:$(ObjFiles) #使用目标文件变量$(遍历名)
    gcc -o app -I ./include $(ObjFiles)

#模式匹配规则,$@,$< 这样的变量,只能在规则中出现
%.o:%.c
    gcc -o $@ -c $<  -I ./include 

test:
    echo $(SrcFiles)
    echo $(ObjFiles1)
  • 第四版makefile:

    • 可以指定编译目标make test;

    • 增加清理目标clean; 使用make clean清理工程

    • @在规则前代表不输出该条规则的命

    • 规则前的-,代表该条规则报错,任然继续执行

#get all .c file
SrcFiles=$(wildcard *.c)
#all .c file --> .o file
ObjFiles = $(patsubst %.c,%.o,$(SrcFiles))

app:$(ObjFiles) #使用目标文件变量$(遍历名)
    gcc -o app -I ./include $(ObjFiles)

#模式匹配规则,$@,$< 这样的变量,只能在规则中出现
%.o:%.c
    gcc -o $@ -c $<  -I ./include 

test:
    @echo $(SrcFiles)
    @echo $(ObjFiles)
clean:
    -@rm -f *.o
    rm -f app         
  • 终极版makefile
    • 定义伪目标:all
    • 使用.PHONY防止目标有歧义
#get all .c file
SrcFiles=$(wildcard *.c)
#all .c file --> .o file
ObjFiles = $(patsubst %.c,%.o,$(SrcFiles))

all:app app1

app:$(ObjFiles) #使用目标文件变量$(遍历名)
    gcc -o $@ -I ./include $(ObjFiles)
app1:$(ObjFiles) #使用目标文件变量$(遍历名)
    gcc -o $@ -I ./include $(ObjFiles)

#模式匹配规则,$@,$< 这样的变量,只能在规则中出现
%.o:%.c
    gcc -o $@ -c $<  -I ./include 

test:
    @echo $(SrcFiles)
    @echo $(ObjFiles)

#定义伪目标,防止有歧义
.PHONY:clean all 
clean:
    -@rm -f *.o

1.3.2 gdb调试

  • 使用gdb: 编译的时候加上-g参数: gcc -o app -g main.c
  • 启动gdb: gdb app(对应可执行程序名)
  • 在gdb中启动程序
功能说明
启动程序r(un)启动整个程序
start启动-停留在main函数,分部调试
执行下一条指令n(ext)下一条指令
s(tep)下一条指令,可以进入函数内部,当库函数不能进
退出gdbquit
设置启动参数set args 10 6
查看源代码,默认显示10行l(ist) 显示主函数对应的文件
l 文件名:行号
设置断点b(reak) 行号主函数所在行
b 函数名
b 文件名:行号
查看断点i(nfo) b得到编号
删除编号d(el) 编号
跳到下一个断点c(ontinue)
打印变量的值和类型p(rint)打印上文变量的值
ptype打印变量的类型
set设置变量的值set argc = 4
set argv[1]=12
set argv[2]=7
显示变量的值,用于追踪变量变化时机display
删除显示 变量info dosplay显示编号
undisplay 编号
设置条件断点b line if i == 5
  • gdb跟踪core: 核心已转储!
    • 设置生成core: ulimit -c unlimited(或者大小)
    • 取消生产core: ulimit -c 0
    • 设置core文件格式:/proc/sys/kernel/core_pattern文件不能vi,可以用后面的套路 echo "/corefile/core-%e-%p-%t" > core_pattern

1.3.3文件IO相关

  • 系统api和库函数的关系,下图是系统函数调用的一般流程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-s2HZKPCB-1659943933375)(image/image-20220410213729345.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tbZ65FJg-1659943933375)(image/image-20220410214805753.png)]

#include <stdio.h>
#include <fcntl.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
    close(1);
    int fd = open("1.log",O_CREAT|O_TRUNC|O_WRONLY,0644);

    printf("hello word\n");
    fflush(stdout);//使buff刷新
    close(fd);
    return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jNigSBjh-1659943933376)(image/image-20220414172422464.png)]

open函数
  • 函数原型

    int open(const char *pathname, int flags);
    int open(const char *pathname, int flags, mode_t mode);
    
  • pathname文件名

  • flags

    • 必选项
      • O_RDONLY 只读
      • O_WRONLY只写
      • or O_RDWR读写
    • 可选项
      • O_APPEND追加
      • O_CREAT文件不存在创建
        • O_EXCLO_CREAT一起使用,如果文件存在,则报错
        • mode权限位,最终(mode & ~umask)
      • O_NONBLOCK非阻塞
  • 返回值:返回最小的可用文件描述符,失败返回-1,设置errno

close函数
  • 函数原型:int close(int fd);
  • fd open打开的文件描述符
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

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

    if(argc != 2){ 
        printf("./a.out filename\n");
        return -1; 
    }   
    int fd = open(argv[1], O_RDONLY|O_CREAT,0666);
    close(fd);
    return 0;
}
#实现了touch
read函数
  • 函数原型:ssize_t read(int fd, void *buf, size_t count);
  • fd文件描述符
  • buf缓冲区
  • count缓冲区大小
  • 返回值:
    • 失败返回:-1
    • 成功返回读到的大小
    • 0代表读到文件袋末尾
    • 非阻塞的情况下read返回-1,但是此时要判断err number的值
write函数
  • 函数原型:ssize_t write(int fd, const void *buf, size_t count);
  • fd文件描述符
  • buf缓冲区
  • count缓冲区大小
  • 返回值
    • 失败返回:-1
    • 成功返回写入的的字节数
    • 0代表未写入
#实现cat功能,读文件,输出到标准输出
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>

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

    if(argc != 2){ 
        printf("./a.out filename\n");
        return -1; 
    }   
    int fd = open(argv[1], O_RDONLY);
    //读,输出到屏幕
    char buf[256];
    int  ret = read(fd,buf,sizeof(buf));
    
    //循环读取,读到0阶数
    while(write(STDOUT_FILENO,buf,ret)){
      ret = read(fd,buf,sizeof(buf));
    }   
    close(fd);
    return 0;
}
lseek实现文件读写位置改变
  • 函数原型:off_t lseek(int fd, off_t offset, int whence);
    • fd文件描述符
    • offset偏移量
    • whence
      • SEEK_SET文件开始位置
      • SEEK_CUR文件当前位置
      • SEEK_END结尾
    • 返回值:返回当前位置到开始的长度,失败返回-1
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>

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

    if(argc != 2){ 
        printf("./a.out filename\n");
        return -1; 
    }   
    int fd = open(argv[1], O_RDWR|O_CREAT, 0666);
    //写
    write(fd,"heelo word!",12);
    //文件读写位置此时到末尾了
    //需要移动读写位置
    lseek(fd,0,SEEK_SET);
    //读,输出到屏幕
    char buf[256];
    int  ret = read(fd,buf,sizeof(buf));
    
    //循环读取,读到0阶数
    while(write(STDOUT_FILENO,buf,ret)){
      ret = read(fd,buf,sizeof(buf));
    }
lseek计算文件大小
  • 返回文件末尾到开头的长度大小
  • stat也可以计算文件大小
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>

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

    if(argc != 2){ 
        printf("./a.out filename\n");
        return -1; 
    }   
    int fd = open(argv[1], O_RDONLY);
    int ret = lseek(fd,0,SEEK_END);
    printf("file size is %d\n",ret);
    close(fd);
    return 0;
}

lseek拓展文件
int main(int argc, char *argv[])
{   

    if(argc != 2){ 
        printf("./a.out filename\n");
        return -1; 
    }   
    //1 open
    int fd = open(argv[1], O_RDONLY|O_CREAT,0666);
    //2 lseek ,拓展文件
    int ret = lseek(fd,1024,SEEK_END);
    //3 需要至少写一次
    write(fd,"a",1);

    printf("file size is %d\n",ret);
    close(fd);
    return 0;
}

1.3.4阻塞和非阻塞

  • 阻塞的概念:read函数在读设备或者读管道,或者读网络的时候
  • 输入输出设备对应/dev/tty
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>

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

	//1 open
	int fd = open("/dev/tty", O_RDWR|O_NONBLOCK); //非阻塞,可以变为阻塞
    //fcntl()函数, 设置非阻塞
	/*int flags = fcntl(fd,F_GETFL);
	flags |= O_NONBLOCK;
	fcntl(fd,F_SETFL,flags);*/
    
    
	char buf[256];
	int ret = 0;
	while(1){
		ret = read(fd,buf,sizeof(buf));
		if(ret < 0){
			perror("read err: "); //打印err number
			printf("ret is %d\n",ret);
		}
		if(ret)
		{
			printf("buf is %s\n",buf);
		}
		printf("okok\n");
	}
	close(fd);
	return 0;
}

1.4 第一次作业

1、将静态库与动态库的制作写成makefile, wildcard通配时也可以处理路径M

# 制作静态库和动态库
#导入源文件.c  
#get all src/.c file
SrcFiles=$(wildcard src/*.c)
ObjFiles=$(patsubst %.c,%.o,$(SrcFiles))
#静态库
#1、编译.o文件
#%.o:%.c
#	gcc -c $< -o $@ -I ./includes
#2、将.o文件打包
#libtest.a:$(ObjFiles)
#	ar rcs $@ $^

#动态库
#1、编译生成与位置无关的代码.o
%.o:%.c
	gcc -fPIC -c $< -o $@ -I ./includes
#2、将.o文件打包
libtest.so:$(ObjFiles)
	gcc -shared -o $@ $^

2、在一个目录创建两个c代码文件:hello.c与world.c,分别输出hello与world,要求自动生成hello和world可执行文件

  • 将两个目标执行文件统一作为源文件传递给另外一个总的入口目标文件。实际并不会生成all这个目标文件
#导入源文件.c  
#get all src/.c file
SrcFiles=$(wildcard *.c)
ObjFiles=$(patsubst %.c,%.o,$(SrcFiles))
TarGets = $(SrcFiles:%.c=%)

all:$(TarGets) #定义一个目标,会主动去生成依赖所需的文件
%:%.c
	gcc -o $@ $<

3、实现mycp功能,拷贝一个文件(够大的文件, .map3文件)

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
	if(argc !=3 ){
		printf("./a.out srcfile tarfile\n");
		return -1;
	}
	int fd1 = open(argv[1], O_RDONLY); //读源文件
	if(fd1 < 0){//判断文件是否打开成功
		printf("file name 1 open fail");
		return -2;
	}

	//O_TRUNC:若文件存在,直接覆盖
	int fd2 = open(argv[2], O_RDWR|O_CREAT|O_TRUNC, 0666);
	
	int lenth = lseek(fd1,0,SEEK_END); //获取源问件末尾到开头的长度大小
	printf("%d\n",lenth);
	lseek(fd2,lenth,SEEK_END);//扩展目标文件的大小
    //移动文件指针到开始??
	lseek(fd1,0,SEEK_SET);
	lseek(fd2,0,SEEK_SET);
	char buf[256];
	int ret; //读取文件内容长度

	//循环读取
	while(1){
		ret = read(fd1,buf,sizeof(buf));
		write(fd2,buf,ret);
		printf("%d\n",ret);	
		//判断是否文件读取结束
		if(ret != 256)break;
	}
	close(fd1);
	close(fd2);

	return 0;

}

1.5 stat-Readir-dup2相关

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LvT633DR-1659943933376)(image/image-20220425124630800.png)]

  • 查看打开最大文件数,根据文件描述符应该最大打开1024
  • 也可以使用ulimit -n查看
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
	int num = 3;
	char filename[128] = {0};
	while(1){
		sprintf(filename,"temp_%4d",num);
		if(open(filename,O_RDONLY|O_CREAT,0666) < 0){
			perror("open err");
			break;
		}
		num++;
	}
	printf("num == %d\n",num);
	return 0;
}

1.5.1 文件属性相关函数使用

1、stat/lstat函数的使用

1、stat函数:获得文件信息

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IyoNXcDr-1659943933378)(image/image-20220429132641191.png)]

  • 函数原型
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
           };

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-S9JrNFMd-1659943933378)(image/image-20220429132739744.png)]

struct timespec {
     __kernel_old_time_t tv_sec;     /* seconds */当前时间到1970.1.1 000的秒数
    long            tv_nsec;    /* nanoseconds */纳秒数
 };
  • stat函数参数
    • pathname:文件名
    • struct stat *buf:传出参数,定义struct stat sb; &sb
  • 返回值:
    • 成功返回0,失败返回-1

2、实现ls-l命令

  • 使用struct passwd *getpwuid(uid_t uid)获得用户名,需要传入uid
struct passwd {
               char   *pw_name;       /* username */ //用户名
               char   *pw_passwd;     /* user password */
               uid_t   pw_uid;        /* user ID */
               gid_t   pw_gid;        /* group ID */
               char   *pw_gecos;      /* user information */
               char   *pw_dir;        /* home directory */
               char   *pw_shell;      /* shell program */
           };
  • 使用 struct group *getgrgid(gid_t gid);获得组名
struct group {
               char   *gr_name;        /* group name *///组名
               char   *gr_passwd;      /* group password */
               gid_t   gr_gid;         /* group ID */
               char  **gr_mem;         /* NULL-terminated array of pointers
                                          to names of group members */
           };

  • 获得本地实际时间 struct tm *localtime(const time_t *timep);
    • 传入参数timep, 对应stat函数得到结构体的秒数(time_t)
           struct tm {
               int tm_sec;    /* Seconds (0-60) */
               int tm_min;    /* Minutes (0-59) */
               int tm_hour;   /* Hours (0-23) */
               int tm_mday;   /* Day of the month (1-31) */
               int tm_mon;    /* Month (0-11) */
               int tm_year;   /* Year - 1900 */
               int tm_wday;   /* Day of the week (0-6, Sunday = 0) */
               int tm_yday;   /* Day in the year (0-365, 1 Jan = 0) */
               int tm_isdst;  /* Daylight saving time */
           };

  • 完整代码
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
#include <grp.h>
#include <pwd.h>
#include <string.h>
#include <time.h>
int main(int argc,char *argv[])
{
	if(argc != 2){
		printf("./a.out filename\n");
	}
	//调用stat 得到文件属性信息
	struct stat sb;
	stat(argv[1],&sb);
	//解析属性信息  st_mode  uid  gid   time
	char stmode[11] ={0};
	memset(stmode,'-',sizeof(stmode) -1);//全部初始化为-
	if(S_ISREG(sb.st_mode)) stmode[0] = '-';//普通文件
	if(S_ISDIR(sb.st_mode)) stmode[0] = 'd';
	if(S_ISBLK(sb.st_mode)) stmode[0] = 'b';
	if(S_ISCHR(sb.st_mode)) stmode[0] = 'c';
	if(S_ISFIFO(sb.st_mode)) stmode[0] = 'p';
	if(S_ISLNK(sb.st_mode)) stmode[0] = 'l';
	if(S_ISSOCK(sb.st_mode)) stmode[0] = 's';

	//解析权限
	if(sb.st_mode & S_IRUSR) stmode[1] = 'r';
	if(sb.st_mode & S_IWUSR) stmode[2] = 'w';
	if(sb.st_mode & S_IXUSR) stmode[3] = 'x';

	if(sb.st_mode & S_IRGRP) stmode[4] = 'r';
	if(sb.st_mode & S_IWGRP) stmode[5] = 'w';
	if(sb.st_mode & S_IXGRP) stmode[6] = 'x';

	if(sb.st_mode & S_IROTH) stmode[7] = 'r';
	if(sb.st_mode & S_IWOTH) stmode[8] = 'w';
	if(sb.st_mode & S_IXOTH) stmode[9] = 'x';

	//用户名,组名可以通过函数获得
	//时间获取
	struct tm *filetm = localtime(&sb.st_atim.tv_sec);
	char timebuf[20] = {0};
	sprintf(timebuf, "%d月   %d  %02d:%02d",filetm->tm_mon+1, filetm->tm_mday, filetm->tm_hour, filetm->tm_min);
	printf("%s %ld %s %s %ld %s %s\n",stmode,sb.st_nlink,getpwuid(sb.st_uid)->pw_name,
			getgrgid(sb.st_gid)->gr_name,sb.st_size,timebuf,argv[1]);
	return 0;
}

3、stat与lstat区别

  • 用法基本一致,当要注意的是:

    • stat碰到连接,会追溯到源文件,穿透!!
    • lstat不会穿透
  • 也可以用了计算文件大小

2、access与truncate函数

1、access判断文件的权限和是否存在

  • 函数原型: int access(const char *pathname, int mode);
    • pathname:文件
    • mode: 具体权限
      • R_OK
      • W_OK
      • X_OK
      • F_OK 文件存在与否
    • 返回值
      • 如果有权限或者文件存在,对应返回0
      • 失败返回-1,设置errno
#include <stdio.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
	if(argc != 2)
	{
		printf("./a.out filename\n");
	}
	if(access(argv[1], R_OK) == 0) printf("%s read ok!\n",argv[1]);
	if(access(argv[1], W_OK) == 0) printf("%s write ok!\n",argv[1]);
	if(access(argv[1], X_OK) == 0) printf("%s exe ok!\n",argv[1]);
	if(access(argv[1], F_OK) == 0) printf("%s file exists!\n",argv[1]);

	return 0;
}

2、truncate 截断文件

  • 函数原型:int truncate(const char *path, off_t length);
    • path:文件名
    • lenth:长度如果大于源文件,直接拓展;小于源文件,截断为长度
    • 返回值
      • 如果有权限或者文件存在,对应返回0
      • 失败返回-1,设置errno
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
	truncate("01_openmax.c",10);
	return 0;
}
3、link系列函数

1、创建硬链接

  • 函数原型:int link(const char *oldpath, const char *newpath);
    • oldpath:源文件
    • newpath:硬链接文件

2、创建软链接

  • 函数原型 :int symlink(const char *target, const char *linkpath);
    • target:源文件
    • linkpath:硬链接文件

3、读取软符号链接本身内容,得到链接指向的文件名

  • 函数原型:ssize_t readlink(const char *pathname, char *buf, size_t bufsiz);
    • pathname:链接名
    • buf:缓冲区
    • bufsiz:缓冲区大小
    • 返回值:成功返回buf填充大小,失败返回-1

4、删除软硬符号链接

  • 函数原型: int unlink(const char *pathname);
    • pathname:对应的链接的名字,文件也可以
    • 返回值:成功返回0,失败返回-1
#include <stdio.h>
#include <unistd.h>

int main()
{
	char buf[100] = {0};
	readlink("01_openmax.soft",buf,sizeof(buf));
	printf("buf is %s\n",buf);
	
	unlink("01_openmax.soft");//删除链接
    unlink("01_openmax.c");//删除文件
	return 0;
}
  • 有对文件操作时,会让操作完成后,再执行unlink操作
#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, int argv[])
{
	int fd = open("world", O_WRONLY|O_CREAT,0666);
	unlink("world");

	int ret = write(fd,"hello",5);
	if(ret > 0){
		printf("write ok!%d\n",ret);
	}
	if(ret < 0){
		printf("write err!\n");
	}
	close(fd);
	return 0;
}
4、改变用户和组
  • 函数原型:int chown(const char *pathname, uid_t owner, gid_t group);
    • pathname:文件名
    • owner:用户ID,参考 /etc/passwd
    • group:组ID,/etc/group
5、修改文件权限
  • 函数原型:
       int chmod(const char *pathname, mode_t mode);
       int fchmod(int fd, mode_t mode);
6、重命名文件
  • 函数原型:int rename(const char *oldpath, const char *newpath);
    • oldpath:旧文件
    • newpath:新文件

1.5.2 目录操作相关函数的使用

1、获得当前工作路径
  • 函数原型: char *getcwd(char *buf, size_t size);
    • buf:传出参数,路径
    • size:缓冲区大小
    • 返回值:
      • 成功返回路径的指针
      • 失败返回NULL
2、改变进程的工作路径
  • 函数原型: int chdir(const char *path);
    • path:对应的目标工作路径
    • 返回值,成功返回0,失败返回-1
  • 工作目录是每个进程独有的
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
	//先切换工作目录
	chdir("TestDir");
	//验证一下
	int fd = open("temp",O_WRONLY|O_CREAT,0666);
	write(fd,"daociyiyou",10);
	close(fd);

	//显示当前工作目录
	char buf[256];
	getcwd(buf,sizeof(buf));

	printf("buf is [%s]\n",buf);
	return 0;
}
3、创建目录
  • 函数原型:int mkdir(const char *pathname, mode_t mode);
    • pathname:路径
    • mode:mode & ~umask &0777注意权限,如果目录没有可执行权限,不可进入
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
int main(int argc, char *argv[])
{
	if(argc != 2){
		printf("./a.out dirname\n");
	}
	mkdir(argv[1],0777);
	return 0;
}
4、删除空目录
  • 函数原型:int rmdir(const char *pathname);
5、打开目录
  • 函数原型: DIR *opendir(const char *name);
    • name:目录名
    • 返回值:返回DIR*的指针,指向目录项的信息;失败返回NULL
6、读目录
  • 函数原型:struct dirent *readdir(DIR *dirp);
    • dirp:传入参数,opendir返回的指针
    • 返回值:
      • NULL:代表读到末尾或者有错误
      • 读到目录项的内容
          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 */
           };
7、关闭目录
  • 函数原型: int closedir(DIR *dirp);
    • dirp:传入参数,opendir返回的指针
8、其它的一些目录相关的函数
函数说明
void rewinddir(DIR *dirp);把目录指针恢复到起始位置
lon telldir(DIR* dirp)获取目录读写位置
返回值:成功返回当前在目录中的位置;失败返回-1
void seekdir(DIR drip,long loc)修改目录读写位置
9、统计指定目录下普通文件个数,要求子目录递归
  • 命令行:find ./ -type f | wc -l

  • 代码实现

#include <stdio.h>
#include <unistd.h>
#include <dirent.h>
#include <string.h>
#include <sys/types.h>
int count = 0;//定义全局计数

int DirCount(char *dirname)
{
	printf("%s\n",dirname);
	//打开目录
	DIR * dirp = opendir(dirname);
	if(dirp == NULL){
		perror("opendir err");
		return -1;
	}
	//循环读目录,如果是普通文件,计数;如果是目录,递归调用
	struct dirent * dentp = NULL;
	while((dentp = readdir(dirp)) !=NULL){
	//如果为NULL,代表读到目录末尾
		//printf("dirname:%s,dtype:%d\n",dentp->d_name,dentp->d_type);
		if(dentp->d_type == DT_DIR){//如果是目录
			if(strcmp(".",dentp->d_name)==0 || strcmp("..",dentp->d_name)==0){
				continue;//如果是.或者..,直接跳过
			}

			//注意进程的工作路径,不能直接打开子目录
			//使用dirname拼接下一级目录
			char newdirname[256] = {0};
			sprintf(newdirname,"%s/%s",dirname,dentp->d_name);
			DirCount(newdirname);
			//
		}
		if(dentp->d_type == DT_REG){
			//普通文件,计数
			count++;
			printf("name:%s\n",dentp->d_name);
		}
	}

	//关闭目录
	closedir(dirp);
	return 0;
}


int main(int argc, char *argv[])
{
	if(argc != 2)
	{
		printf("./exe dirname\n");
	}

	DirCount(argv[1]);
	printf("files counts:%d\n",count);
	return 0;
}

1.5.3 errno说明

  • errno输出函数: char *strerror(int errnum);
    • return string describing error number

1.5.4 dup、dup2文件重定向函数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K2gOCWYQ-1659943933379)(image/image-20220510150802657.png)]

1、dup2

  • 函数原型:int dup2(int oldfd, int newfd);
  • 关闭newfd对应的文件描述符,并将newfd重新指向尾oldfd对应的文件

2、dup

  • 函数原型:int dup(int oldfd);
  • 新返回一个文件描述符,指向oldfd对应的文件

3、dup2和dup的使用

  • 描述:在代码中执行两次printf,依次输出到hello文件中,后一次输出到屏幕上
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <unistd.h>
int main()
{
	//先备份现场
	int outfd = dup(1);

	//重定向
	int fd = open("world",O_WRONLY|O_CREAT,0666);
	dup2(fd,1);//标准输出重定向到fd对应的文件
	printf("hello world1!\n");

	//需要来一次刷新,缓冲区
	fflush(stdout);
	
	//恢复 1对应标准输出
	dup2(outfd,1);
	printf("hello world2\n");

	close(fd);
	return 0;
}

1.5.5 fcntl函数的使用

  • 也可以实现重定向

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rxlOliQU-1659943933380)(image/image-20220510152939701.png)]

1.6 第二次作业

1、实现ls -l dirname,不用递归子目录

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
#include <grp.h>
#include <pwd.h>
#include <string.h>
#include <time.h>
#include <dirent.h>

void LsPrint(char* filename)
{
	//调用stat 得到文件属性信息
	struct stat sb;
	stat(filename,&sb);
	//解析属性信息  st_mode  uid  gid   time
	char stmode[11] ={0};
	memset(stmode,'-',sizeof(stmode) -1);//全部初始化为-
	if(S_ISREG(sb.st_mode)) stmode[0] = '-';//普通文件
	if(S_ISDIR(sb.st_mode)) stmode[0] = 'd';
	if(S_ISBLK(sb.st_mode)) stmode[0] = 'b';
	if(S_ISCHR(sb.st_mode)) stmode[0] = 'c';
	if(S_ISFIFO(sb.st_mode)) stmode[0] = 'p';
	if(S_ISLNK(sb.st_mode)) stmode[0] = 'l';
	if(S_ISSOCK(sb.st_mode)) stmode[0] = 's';

	//解析权限
	if(sb.st_mode & S_IRUSR) stmode[1] = 'r';
	if(sb.st_mode & S_IWUSR) stmode[2] = 'w';
	if(sb.st_mode & S_IXUSR) stmode[3] = 'x';

	if(sb.st_mode & S_IRGRP) stmode[4] = 'r';
	if(sb.st_mode & S_IWGRP) stmode[5] = 'w';
	if(sb.st_mode & S_IXGRP) stmode[6] = 'x';

	if(sb.st_mode & S_IROTH) stmode[7] = 'r';
	if(sb.st_mode & S_IWOTH) stmode[8] = 'w';
	if(sb.st_mode & S_IXOTH) stmode[9] = 'x';

	//用户名,组名可以通过函数获得
	//时间获取
	struct tm *filetm = localtime(&sb.st_atim.tv_sec);
	char timebuf[20] = {0};
	sprintf(timebuf, "%d月   %d  %02d:%02d",filetm->tm_mon+1, filetm->tm_mday, filetm->tm_hour, filetm->tm_min);
	printf("%s\t %ld\t %s\t %s\t %ld\t %s\t %s\n",stmode,sb.st_nlink,getpwuid(sb.st_uid)->pw_name,
			getgrgid(sb.st_gid)->gr_name,sb.st_size,timebuf,filename);
}

int main(int argc, char *argv[])
{
	if(argc != 2)
	{
		printf("./exe pathname\n");
	}
	//打开目录
	DIR* dirp = opendir(argv[1]);
	if(dirp == NULL)
	{
		perror("opendir err\n");
		return -1;
	}
	//循环读目录
	struct dirent* dentp = NULL;
	while((dentp = readdir(dirp)) != NULL){//如果为NULL代表读到了目录末尾
		LsPrint(dentp->d_name);
	}
	return 0;
}

第二章 Linux系统课程

2.1 进程控制

2.1.1 了解进程相关的概念

程序和进程
  • 什么是程序?

    • 编译好的二进制文件
  • 什么是进程?

    • 运行着的程序
  • 从程序员的角度:运行一系列指令的过程

  • 从操作系统的角度:分配系统资源的基本单位

  • 区别:

    • 程序占用磁盘空间,不占用系统资源
    • 内存占用系统资源
    • 一个程序对应多个进程,一个进程对应一个程序
    • 程序没有生命周期,进程有生命周期
单道和多道程序设计

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GWybWtr4-1659943933380)(image/image-20220515153638980.png)]

进程状态切换

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6sWucru9-1659943933380)(image/image-20220515155418694.png)]

CPU和MMU(了解)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6U3Y4Z5b-1659943933381)(image/image-20220515154155729.png)]

MMU(内存管理单元):包括从逻辑地址到虚拟地址(线性地址)再到内存地址的变换过程、页式存储管理、段式存储管理、段页式存储管理、虚拟存储管理(请求分页、请求分段、请求段页)。 MMU位于CPU内部,可以假想为一个进程的所需要的资源都放在虚拟地址空间里面,而CPU在取指令时,机器指令中的地址码部分为虚拟地址(线性地址),需要经过MMU转换成为内存地址,才能进行取指令。MMU完成两大功能:1.虚拟地址到内存地址的地址变换;2.设置修改CPU对内存的访问级别。比如在Linux的虚拟地址空间中,3-4G为内核空间,访问级别最高,可以访问整个内存;而0-3G的用户空间只能访问用户空间的内容。其实这也是由MMU的地址变换机制所决定的。对于Inter(英特尔)CPU架构,CPU对内存的访问设置了4个访问级别:0、1、2、3(如上图所示),0最高,4最低。而Linux下,只是使用了CPU的两种级别:0、3。CPU的状态属于程序状态字PSW的一位,系统模式(0),用户模式(1),CPU交替执行操作系统程序和用户程序。0级对应CPU的内核态(特权态、管态、系统态),而3级对应用户态(普通态或目态),这其实是对内核的一种保护机制。例如,在执行printf函数的时候,其本身是在用户空间执行,然后发生系统调用,调用系统函数write将用户空间的数据写入到内核空间,最后把内核的数据刷到(fsync)磁盘上,在这个过程中,CPU的状态发生了变化,从0级(用户态)到3级(内核态)。

综上,MMU只是在读内存和写内存完成地址变换,以及更改CPU的访问级别。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pGKXRoji-1659943933381)(image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzODgzMDg1,size_16,color_FFFFFF,t_70.jpeg)]

MMU(内存管理单元)的作用:

  • 虚拟内存和物理内存的映射
  • 修改内存访问级别

用户空间映射到物理内存是独立

内核空间映射到物理内存是一起的

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

  • /usr/src/linux-hwe-5.13-headers-5.13.0-41/include/linux/sched.h文件可以查看struct task_struct结构体的定义。

  • 可以通过将光标移动到{}上实现括号间的跳跃。其内部成员很多重点掌握以下部分即可。

    1. 进程id:系统中每个进程有唯一的id,在c语言中用pid_t类型表示,其实就是一个非负整数
    2. 进程的状态。有新建、就绪、运行、挂起、终止
    3. 当前工作目录(Current Working Directory)。
    4. umask掩码。
    5. 文件描述符表。
    6. 和信号相关的信息。
    7. 用户id和组id。
    8. 进程切换时需要保存和恢复的一些CPU寄存器。
    9. 描述虚拟地址空间的信息。
    10. 描述控制终端的信息。
    11. 会话(Session)和进程组。
    12. 进程可以使用的资源上限(Resource Limit):ulimit -a查看所有资源上限

以上重要性依次递减,重要理解前七个即可。下面对上面的每一点进行分析。

2.1.2 环境变量(了解)

  • 环境变量写法:key=value等号两边不能有空格
  • 查看所以环境变量:env
  • 查看指定环境变量:echo $变量名,比如echo $HOME
常见环境变量
  • 1**.PATH:指定命令的搜索路径**
  • 2.HOME:指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)
  • 3.HISTSIZE:指保存历史命令记录的条数。
  • 4.LOGNAME:指当前用户的登录名。
  • 5.HOSTNAME:指主机的名称,许多应用程序如果要用到主机名的话,通常是从这个环境变量中来取得的。
  • 6**.SHELL:指当前用户用的是哪种Shell。**
  • 7.LANG/LANGUGE:和语言相关的环境变量,使用多种语言的用户可以修改此环境变量。
  • 8.MAIL:指当前用户的邮件存放目录。
  • 9.PS1:命令基本提示符,对于root用户是#,对于普通用户是$。
  • 10.PS2:附属提示符,默认是“>”。
  • 11.TERM:当前终端类型,图形界面通常是xterm,终端类型决定了一些程序的输出显示方式

备注:可以通过修改此环境变量来修改当前的命令符,比如下列命令会将提示符修改成字符串“Hello,My NewPrompt ”。

# PS1=“Hello,My NewPrompt”

注意:上述变量的名字并不固定,如HOSTNAME在某些Linux系统中可能设置成HOST

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jjXwC7iN-1659943933382)(image/image-20220515210611098.png)]

获取环境变量函数
  • 函数原型:char * getenv(const char *name);
#include <stdio.h>
#include <stdlib.h>

int main()
{
	printf("homepath is [%s]\n",getenv("HOME"));
	return 0;
}
配置环境变量
  • 修改文件.bashrc

    • export key=val
  • 使用函数setenv函数设置环境变量

    • int setenv(const char*name , const char* value, int overwrite)
      • overwrite取值

        • 1:覆盖原环境变量
        • 0:不覆盖。(该参数通常用来设置新的环境变量)
      • 返回值,成功返回0,失败返回-1

  • 使用unsetenv函数删除环境变量name的定义

    • int unsetenv(const char* name)
    • 成功返回0,失败返回-1
    • 注意事项:name不存在仍然返回0,当name命名为abc=时会报错

2.1.3 进程控制

fork创建子进程函数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9HuD1szd-1659943933382)(image/image-20220517103120736.png)]

  • 创建一个新的进程:pid_t fork(void);
    • 返回值:
      • 失败返回-1
      • 成功,两次返回
        • 父进程返回,子进程id
        • 进程返回0
getpid与getppid获取进程id函数
  • 获得pid,进程id,获得当前进程:pid_t get_pid(void);
  • 获得当前进程父进程的pid:pid_t get_ppid(void);
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
	printf("Begin ....\n");
    pid_t pid = fork();
    if(pid < 0){
        perror("fork err!\n");
    }
    if(pid == 0){
        //子进程
        printf("I am child,pid = %d, ppid = %d\n",getpid(),getppid());
    }
    else if(pid > 0){
        //父进程
        printf("childpid = %d, selfpid = %d, ppid = %d\n",pid,getpid(),getppid());
    	sleep(1);//让父进程晚点死,让子进程可以顺利结束,不要变成孤儿进程
    }
    printf("End ...\n");
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9dJlYBbV-1659943933383)(image/image-20220517110202936.png)]

创建n个子进程
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main()
{
	int n = 5;
	int i = 0;
	pid_t pid = 0;
	for(i=0; i< 5; i++){
		pid = fork();
		if(pid == 0){
			//son
			printf("i am child, pid = %d\t ppid=%d\n",getpid(),getppid());
			break;//子进程退出循环结构
		}
		else if(pid >0){
			//father	
			printf("i am father, pid = %d\t ppid=%d\n",getpid(),getppid());
		}
	}
	while(1)
	{
		sleep(1);
	}
	return 0;
}
循环创建n个子进程控制顺序
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main()
{
	int n = 5;
	int i = 0;
	pid_t pid = 0;
	for(i=0; i< 5; i++){
		pid = fork();
		if(pid == 0){
			//son
			printf("i am child, pid = %d\t ppid=%d\n",getpid(),getppid());
			break;//子进程退出循环结构
		}
		else if(pid >0){
			//father	
			//printf("i am father, pid = %d\t ppid=%d\n",getpid(),getppid());
		}
	}
	sleep(i);
	if( i < 5)
	{
		printf("i am child, will exit,pid = %d\t ppid=%d\n",getpid(),getppid());
	}
	else{
		printf("i am father,will exit,pid = %d\t ppid=%d\n",getpid(),getppid());
	}
	return 0;
}
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main()
{
	int n = 5;
	int i = 0;
	pid_t pid = 0;
	for(i=0; i< 5; i++){
		pid = fork();
		if(pid == 0){
			//son
			printf("i am child, pid = %d\t ppid=%d\n",getpid(),getppid());
			break;//子进程退出循环结构
		}
		else if(pid >0){
		}
		sleep(1);//控制顺序
	}
	if(i > 4)
	{
		printf("i am father, pid = %d\t ppid=%d\n",getpid(),getppid());
		
	}
	return 0;
}
进程共享

父子进程在fork后。有哪些相同,那些相异之处呢?

刚fork之后:

父子相同处: 全局变量、.data数据、.text代码段、stack栈、heap堆、环境变量、用户ID、宿主目录、进程工作目录、信号处理方式…

父子不同处: 1.进程ID 2.fork返回值 3.父进程ID 4.进程运行时间 5.闹钟(定时器) 6.未决信号集 似乎,子进程复制了父进程0-3G用户空间内容,以及父进程的PCB,但pid不同。真的每fork一个子进程都要将父进程的0-3G地址空间完全拷贝一份,然后在映射至物理内存吗? 当然不是!父子进程间遵循读时共享写时复制的原则。这样设计,无论子进程执行父进程的逻辑还是执行自己的逻辑都能节省内存开销。

父子进程共享:1. 文件描述符(打开文件的结构体) 2. mmap建立的映射区

子进程从父进程继承的主要有:用户号和用户组号;堆栈;共享内存;目录(当前目录、根目录);打开文件的描述符;但父进程和子进程拥有独立的地址空间和PID参数、不同的父进程号、自己的文件描述符。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yBxk3jnK-1659943933383)(image/image-20220517214611193.png)]

#include <stdio.h>
#include <unistd.h>

int var = 100; //修改全局变量测试
int main()
{
	pid_t pid = fork();
	if(pid == 0){
		//son
		printf("var = %d, child, pid = %d, ppid = %d\n",var,getpid(),getppid());
		var = 1001;
		printf("var = %d, child, pid = %d, ppid = %d\n",var,getpid(),getppid());
	}
	else if(pid > 0){
		//parent
		sleep(1);//保证子进程能够修改var的值得成功
		printf("var = %d, prent, pid = %d, ppid = %d\n",var,getpid(),getppid());
	}	
	return 0;
}

2.1.4 exec执行函数族

fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程玩玩啦啦啦要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exel并不创建新进程,所以调用exec前后该进程的id并未改变

将当前进程的.text,.data替换为所要加载的程序的.text、.data,然后让进程从新的.text第一条指令开始执行力,但进程ID不变。换核不换壳。

其实有6种exec开头的函数。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xfQfSzmS-1659943933383)(image/image-20220518201955826.png)]

函数原型:

#include <unistd.h>
extern char **environ;

int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg,..., char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[],char *const envp[]);

返回值:

  • exec函数族的函数执行成功后不会返回,调用失败时,会设置errno并返回-1,然后从原程序的调用点接着往下执行。

参数说明:

  • path:可执行文件的路径名字
  • arg:可执行程序所带的参数,第一个参数为可执行文件名字,没有带路径且arg必须以NULL结束
  • file:如果参数file中包含/,则就将其视为路径名,否则就按 PATH环境变量,在它所指定的各目录中搜寻可执行文件。

exec族函数参数极难记忆和分辨,函数名中的字符会给我们一些帮助:

  • l : 使用参数列表

  • p:使用文件名,并从PATH环境进行寻找可执行文件

  • v:应先构造一个指向各参数的指针数组,然后将该数组的地址作为这些函数的参数。

  • e:多了envp[]数组,使用新的环境变量代替调用进程的环境变量

执行其他程序int execl(const char *pathname, const char *arg, .../* (char *) NULL */);

  • pathname:执行程序的路径

执行其他程序的时候,使用PATH环境变量,执行的程序可以不用加路径int execlp(const char *file, const char *arg, .../* (char *) NULL */);

  • file:要执行的程序
  • arg:参数列表,【参1: 程序名、 参2: argv0 、参3: argv1 、哨兵:NULL】
    • 参数列表最后需要一个NULL作为结尾,哨兵
  • 返回值:只有失败才返回
#include <unistd.h>
#include <stdio.h>

int main()
{
	//int execlp(const char *file, const char *arg, .../* (char  *) NULL */);
	//execlp("ls","ls","-l","--color=auto",NULL);
	execl("/bin/ls","ls","-l","--color=auto",NULL);
	perror("exec err");
	return 0;
}

带v不带l的一类exac函数,包括execv、execvp、execve,应先构造一个指向各参数的指针数组,然后将该数组的地址作为这些函数的参数。

//文件execvp.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
//函数原型:int execvp(const char *file, char *const argv[]);

int main(void)
{
    printf("before execlp****\n");
    char *argv[] = {"ps","-l",NULL};
    if(execvp("ps",argv) == -1) 
    {
        printf("execvp failed!\n");     
    }
    printf("after execlp*****\n");
    return 0;
}

带e的一类exac函数,包括execle、execvpe,可以传递一个指向环境字符串指针数组的指针。 参数例如char *env_init[] = {“AA=aa”,”BB=bb”,NULL}; 带e表示该函数取envp[]数组,而不使用当前环境。

//文件execle.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
//函数原型:int execle(const char *path, const char *arg,..., char * const envp[]);

char *env_init[] = {"AA=aa","BB=bb",NULL};
int main(void)
{
    printf("before execle****\n");
        if(execle("./bin/echoenv","echoenv",NULL,env_init) == -1)
        {
                printf("execle failed!\n");
        }       
    printf("after execle*****\n");
    return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cpRWCXNg-1659943933384)(image/image-20220518201209550.png)]

2.1.5 回收子进程

孤儿进程

定义:父亲死了,子进程被init进程领养

#include <stdio.h>
#include <unistd.h>

int main()
{
	pid_t pid = fork();
	if(pid == 0){
		while(1){
			sleep(1);
			printf("child, pid = %d, ppid = %d\n",getpid(),getppid());
		}
	}
	else if(pid > 0){
			printf("father, pid = %d, ppid = %d\n",getpid(),getppid());
			sleep(5);
			printf("father, will die\n");//父进程先死
	}
	return 0;
		
}
僵尸进程

定义:子进程死了,父进程没有回收进程的资源(PCB)

如何回收僵尸进程:杀死父亲,init领养,负责回收

#include <stdio.h>
#include <unistd.h>

int main()
{

	pid_t pid = fork();
	if(pid == 0){
		printf("child, pid = %d, ppid = %d\n",getpid(),getppid());
		sleep(2);
		printf("child will die\n");//子进程先死
	}
	else if(pid > 0){
		while(1){
			printf("father, pid = %d\n",getpid());
			sleep(1);
		}

	}

	return 0;
}
  • 僵尸进程状况

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rd3VJfEA-1659943933384)(image/image-20220519105737790.png)]

wait函数

​ 一个进程在终止时会关闭所有文件描述符,释放在用户空间分配的内存,但它的PCB还保留着,内核在其中保存了一些信息:如果是正常终止则保存着退出状态,如果是异常终止则保留着导致该进程终止的信号是哪个。这个进程的父进程可以调用wait或waitpid获取这些信息,然后彻底清除掉这个进程。

​ 我们知道一个进程的退出状态在Shell中用特殊变量$?查看,因为Sheel是它的父进程,当它终止时Shell调用wait或者waitpid得到它的退出状态,同时彻底清除掉这个进程

#include <sys/types.h>
#include <sys/wait.h>

回收子进程,知道子进程的死亡原因

作用:

  • 阻塞等待
  • 回收子进程资源
  • 查看死亡原因

pid_t wait(int *wstatus);

  • wstatus:传出参数,
  • 返回值:成功返回终止的子进程ID;失败返回-1

子进程的死亡原因:

  • 正常死亡 WIFEXITED(wstatus)
    • 如果 WIFEXITED为真,使用 WEXITSTATUS(wstatus)得到退出状态
  • 非正常死亡WIFSIGNALED(wstatus)
    • 如果WIFSIGNALED为真,使用WTERMSIG(wstatus)得到信号

回收子进程

#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
int main()
{
	pid_t pid = fork();
	if(pid ==0 ){
		printf("i am child, will die!\n");
		sleep(2);
		while(1){
			printf("i am child, guo lai da wo!\n");
			sleep(1);
		}
		printf("child died\n");
		//return 101;
	    exit(102);
	}
	else if(pid > 0){
		printf("i am parent,wait for child die!\n");
		int status;
		pid_t wpid = wait(&status);
		printf("wait ok, wpid = %d,pid=%d\n",wpid,pid);
		if(WIFEXITED(status)){printf("child exit with %d\n",WEXITSTATUS(status));}
		if(WIFSIGNALED(status)){printf("child killed by %d\n",WTERMSIG(status));}
		while(1){
			sleep(1);
		}
	}
	return 0;
}
waitpid函数

作用与wait相同,但可以指定pid进程清理,可以不阻塞

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

  • pid
    • < -1:传递值是负的组ID
    • -1:回收任意
    • 0:回收和调用进程组id相同组内的子进程
    • > 0:回收指定的pid
  • options
    • 0 与wait相同,也会阻塞
    • WNOHANG,如果当前没有子进程退出,会立刻返回
  • 返回值:
    • 如果设置了WNOANG,那么如果没有子进程退出,返回0
      • 如果有子进程退出,返回退出的pid
    • 失败返回-1(没有子进程)

回收子进程

#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
int main()
{
	pid_t pid = fork();
	if(pid ==0 ){
		printf("i am child, will die!\n");
		sleep(2);
		printf("child died\n");
		//return 101;
	    exit(102);
	}
	else if(pid > 0){
		printf("i am parent,wait for child die!\n");
		int status;
		int ret;
		while((ret = waitpid(-1,&status,WNOHANG)) == 0){sleep(1);}
		printf("wait ok, ret = %d,pid = %d,childpid=%d\n",ret,getpid(),pid);
		ret = waitpid(-1,NULL,WNOHANG);
		if(ret < 0){
			perror("wait err");
		}
		if(WIFEXITED(status)){printf("child exit with %d\n",WEXITSTATUS(status));}
		if(WIFSIGNALED(status)){printf("child killed by %d\n",WTERMSIG(status));}
		while(1){
			sleep(1);
		}
	}
	return 0;
}

回收多个子进程

  • 死等,阻塞等待
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
	int n = 5;
	int i = 0;
	pid_t pid;
	for(i = 0; i < 5; i++){
		pid = fork();
		if(pid == 0){
			printf("i am child, pid = %d\n",getpid());
			break;
		}
	}
	sleep(i);
	if( i == 5){
		for(i = 0; i < 5; i++){
			pid_t wpid = wait(NULL);
			printf("wpid = %d\n",wpid);
		}
		while(1){
			sleep(1);
		}
	}
	return 0;


}
  • 非阻塞实现
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
	int n = 5;
	int i = 0;
	pid_t pid;
	for(i = 0; i < 5; i++){
		pid = fork();
		if(pid == 0){
			break;
		}
	}
	if( i == 5){
		printf("i am father\n");

		//如何使用waitpid回收,返回-1 代表子进程都死了
		while(1){
			pid_t wpid = waitpid(-1,NULL,WNOHANG);
			if(wpid == -1){
				break;
			}
			else if(wpid > 0){
				printf("waitpid wpid=%d\n", wpid);
			}
		}
		while(1){
			sleep(1);
		}
	}
	if(i < 5){
		printf("i am child, i = %d, pid = %d\n",i,getpid());
	}
	return 0;


}

2.2 第三次作业

1、创建子进程,调用fork之后,在子进程调用自定义程序(段错误,浮点型错误),用waitpid回收,查看退出状态

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>


int main()
{
	pid_t pid = fork();
	if(pid == 0)
	{
		//子进程,调用其他程序
		execl("/home/cmxiao/LinuxTest/day5/fpe","fpe",NULL);
	}
	else if(pid > 0){
		printf("childpid = %d, selfpid = %d, ppid = %d\n",pid,getpid(),getppid());
		int status;
		int ret;
		while((ret = waitpid(-1,&status,WNOHANG)) == 0){sleep(1);}
		printf("wait ok!, ret = %d\n",ret);
		if(WIFEXITED(status)){printf("child exit with %d\n",WEXITSTATUS(status));}
		if(WIFSIGNALED(status)){printf("child killed by %d\n",WTERMSIG(status));}
		while(1){sleep(1);}
	}

	return 0;
}

2、验证子进程是否共享文件描述符表,子进程负责写入数据,父进程负责读数据

先打开文件,再fork。

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/wait.h>
int main(int argc , char *argv[])
{
	if(argc != 2){
		printf("./a.out filename\n");
		return -1;
	}
	int fd = open(argv[1],O_RDWR);//先打开文件,再fork
	pid_t pid = fork();
	if(pid < 0){
		perror("fork err!\n");
	}
	if(pid == 0){
		//子进程
		printf("I am child,pid = %d, ppid = %d\n",getpid(),getppid());
		//子进程写文件
		write(fd,"hello\n",6);
	}
	else if(pid > 0){
	
		sleep(1);//让父进程晚点死,让子进程可以顺利结束,不要变成孤儿进程
		//父进程
		printf("childpid = %d, selfpid = %d, ppid = %d\n",pid,getpid(),getppid());
		//读,输出到屏幕
		write(fd,"world\n",6);
		wait(NULL);
		close(fd);
	}
	return 0;
}

2.3 进程间通信

2.3.1 IPC概念

IPC:InterProcess Communication进程间通信,通过内核提供的缓冲区进行数据交换的机制

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VHjq4j1c-1659943933385)(image/image-20220625151356081.png)]

IPC通信的方式有几种:

  • 管道(使用最简单)

    • pipe管道——最简单

    • fifo有名管道

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

    • mmap文件映射共享IO——速度最快
    • 匿名映射(可以不开辟文件)
  • 信号——携带信息量最小

  • 本地socket——最稳定

  • 共享内存

  • 消息队列

2.3.2 PIPE(管道)通信

管道的概念:

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

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

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

管道的局限性:

  • ① 数据一旦被读走,便不在管道中存在,不可反复读取。
  • ② 由于管道采用半双工通信方式。因此,数据只能在一个方向上流动。
  • ③ 只能在有公共祖先的进程间使用管道。

常见的通信方式

  • 单工:数据单向通讯(广播)
  • 半双工:双向通讯,但同一时刻数据只能单向传播(对讲机)
  • 全双工:双向通讯,同一时刻数据双向传播(电话)

管道:半双工通信,通信的进程必须有血缘关系

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-itW21xys-1659943933385)(image/image-20220619221921202.png)]

管道函数:int pipe(int pipefd[2]);

  • pipefd用来返回两个文件描述符,0代表读,1代表写
  • 返回值:成功返回0,失败返回-1;
    • 函数调用成功返回r/w两个文件描述符。无需open,但需手动close。规定:fd[0] -> r; fd[1] -> w,就像0对应标准输入,1对应标准输出一样。向管道文件读写数据其实是在读写内核缓冲区。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
	int fd[2];
	pipe(fd);
	pid_t pid = fork();//创建子进程
	if(pid == 0){
		//son
		sleep(1);
		write(fd[1],"hello",5);
	}
	else if(pid > 0)
	{
		//parent
		char buf[256];
		int ret = read(fd[0],buf,sizeof(buf));
		if(ret >0)
		{
			write(STDOUT_FILENO,buf,ret);//输出到屏幕
		}

	}
	return 0;
}

案例:父子进程实现pipe通信,实现ps aux|grep bash功能

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PgFQZBZu-1659943933386)(image/image-20220625153654526.png)]

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
	int fd[2];
	pipe(fd);
	pid_t pid = fork();//创建子进程
	if(pid == 0){
		//son
		// son --> ps
		close(fd[0]);//关闭读端
		//1、重定向,将标准输出重定向到管道写端
		dup2(fd[1],STDOUT_FILENO);
		//2、execlp
		execlp("ps","ps","aux",NULL);
	}
	else if(pid > 0)
	{
		//parent
		//关闭写端
		close(fd[1]);
		//1、先重定向,将标准输入重定向到管道读端
		dup2(fd[0],STDIN_FILENO);
		//2、execlp
		execlp("grep","grep","bash",NULL);
	}
	return 0;
}

代码问题:父进程认为还有写端存在,就有可能还有人发数据,继续等待

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6nII0zlx-1659943933386)(image/image-20220619231324284.png)]

读管道

  • 写端全部关闭——read读到0,相当于读到文件末尾
  • 写端没有全部关闭
    • 有数据——read读到数据
    • 没有数据——read阻塞
      • 可以用fcntl函数更改非阻塞
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
	int fd[2];
	pipe(fd);
	pid_t pid = fork();//创建子进程
	if(pid == 0){
		//son
		sleep(1);
		close(fd[0]);//关闭读端
		write(fd[1],"hello",5);
		close(fd[1]);//关闭写端
		while(1){
			sleep(1);
		}
	}
	else if(pid > 0)
	{
		//parent
		close(fd[1]);//关闭写端
		char buf[256];
	    while(1){

		int ret = read(fd[0],buf,sizeof(buf));
		if(ret >0)
		{
			write(STDOUT_FILENO,buf,ret);//输出到屏幕
		}
		if(ret == 0){
			printf("read ovver!");
			break;
		}

		}
	}
	return 0;
}

写管道

  • 读端全部关闭——?产生一个信号SIGPIPE,程序异常终止
  • 读端未全部关闭
    • 管道已满——write阻塞——如果要显示现象,读端一致不读,写端狂写
    • 管道未满——write正常写入
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
	int fd[2];
	pipe(fd);
	pid_t pid = fork();//创建子进程
	if(pid == 0){
		//son
		sleep(1);
		close(fd[0]);//关闭读端
		write(fd[1],"hello",5);
		close(fd[1]);//关闭写端
		while(1){
			sleep(1);
		}
	}
	else if(pid > 0)
	{
		//parent
		close(fd[1]);//关闭写端
		close(fd[0]);
		int status;
		wait(&status);
		if(WIFSIGNALED(status)){
			printf("killed by %d\n",WTERMSIG(status));
		}

		while(1){
			sleep(1);
		}
	}
	return 0;
}

管道缓冲区大小

  • 使用ulimit -a命令查看当前系统中创建管道文件所对应的内核缓冲区大小:pipe size (512 bytes, -p) 8
  • 也可以使用long fpathconf(int fd, int name);函数——512*8
    • fd:文件描述符,即创建管道时,生成的管道两端读写fd
    • name:宏定义,要返回哪个的大小,管道是_PC_PIPE_BUF

管道的优劣

  • 优点:简单,相比信号,套接字实现进程间通信,简单很多
  • 缺点:
    • 1、只能单向通信,双向通信需要建立两个管道
    • 2、只能用于父子,兄弟进程(有共同祖先)间通信。该问题后来使用fifo有名管道解决。

实现兄弟进程间通信ps aux | grep bash——父进程负责回收子进程

2.3.3 FIFO通信(有名管道)

实现无血缘关系进程通信

  • 创建一个管道的伪文件

    • mkfifo filename命令创建[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ze98UoJG-1659943933387)(image/image-20220622224159294.png)]
    • 也可以用函数int mkfifo(const char *pathname, mode_t mode);
  • 内核会针对fifo文件开辟一个缓冲区,操作fifo文件,可以操作缓冲区,实现进程间通信——实际上就是文件读写

  • open注意事项:打开fifo文件时,read端会阻塞等待write端open,write端open,write端同理,也会阻塞等待另一端打开

写端

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main(int argc, char* argv[])
{
	if(argc != 2){
		printf("./a.out fifoname\n");
	}
	//当前目录有一个myfifo文件
	//先打开fifo文件
	printf("begin open ...\n");
	int fd = open(argv[1],O_WRONLY);
	printf("end open ...\n");
	//写
	char buf[256];
	int num = 1;
	while(1){//循环写
		memset(buf,0x00,sizeof(buf));//清缓冲区
		sprintf(buf,"xiaoming%04d",num++);
		write(fd,buf,strlen(buf));
		sleep(1);
	}
	//关闭文件描述符
	close(fd);
	return 0;
}

读端

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>


int main(int argc, char* argv[])
{
	if(argc != 2){
		perror("input err!\n");
		return -1;
	}
	printf("begin open ...\n");
	int fd = open(argv[1],O_RDONLY);
	printf("end open ...\n");
	char buf[256];
	int ret;
	while(1){
		//循环读
		memset(buf,0x00,sizeof(buf));//清缓存
		ret = read(fd,buf,sizeof(buf));
		if(ret > 0){
			printf("read:%s\n",buf);
		}

	}
	close(fd);
	return 0;
}

2.3.4 mmap共享映射区

映射到内存,直接操作内存修改文件

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k0NCwIED-1659943933387)(image/image-20220623224329922.png)]

创建映射区函数void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

  • addr:传入NULL

  • length:映射区的长度

  • prot:

    • PROT_EXEC Pages may be executed.
    • PROT_READ Pages may be read.
    • PROT_WRITE Pages may be written.
    • PROT_NONE Pages may not be accessed.
  • flags:

    • MAP_SHARED共享的,对内存的修改会影响到源文件
    • MAP_PRIVATE私有的
  • fd:文件描述符,open打开一个文件

  • offset:偏移量

  • 返回值

    • 成功返回映射区地址
    • 失败返回MAP_FAILED

释放映射区函数int munmap(void *addr, size_t length);

  • addr传入mmap地址
  • length:mmap创建的长度
  • 返回值,成功返回0,失败返回-1

简单例子

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <sys/mman.h>
int main()
{
	int fd = open("mem.txt",O_RDWR);
	//创建映射区
	//char* mem = mmap(NULL,8,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
	char* mem = mmap(NULL,8,PROT_READ|PROT_WRITE,MAP_PRIVATE,fd,0);
	if(mem == MAP_FAILED)
	{
		printf("mmap err!\n");
		return -1;
	}
	
	//strcpy(mem,"hello");
	strcpy(mem,"world");
	//释放mmap
	if(munmap(mem,8) < 0){
		perror("munmap err\n");
	}
	close(fd);
	return 0;
}

mmap九问

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TN2z8KxE-1659943933388)(image/image-20220623230618632.png)]

  1. 不能,可以使用++或–改变地址
  2. 文件大小对映射区操作有影响,应尽量避免
  3. offset必须是4K的整数倍
  4. 没有影响
  5. 不可以用大小为0的文件,文件大小会影响到写入后的数据
    int fd = open("mem.txt",O_RDWR|O_CREAT|O_TRUNC,0664);ftruncate(fd,8);
  6. 不可以:权限错误,没权限
  7. 不可以:权限错误,没权限。SHARED的时候,映射区的权限 <= open文件的权限
  8. 很多情况
  9. 会死的很难看

mmap实现父子进程之间通信

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <sys/wait.h>
int main()
{
	//先创建映射区
	int fd = open("mem.txt",O_RDWR);
	int *mem = mmap(NULL,4,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
	if(mem == MAP_FAILED){
		perror("mmap err\n");
		return -1;
	}

	//fork子进程
	int pid = fork();
	
	//父子进程交替修改数据
	if( pid == 0){
		//son
		*mem = 100;//修改内存区域的值
		printf("child,*mem = %d\n",*mem);
		sleep(3);
		printf("child,*mem = %d\n",*mem);
		
	}
	else if(pid > 0)
	{
		//father
		sleep(1);
		printf("father,*mem = %d\n",*mem);
		*mem = 1001;
		printf("father,*mem = %d\n",*mem);
		wait(NULL);//回收
	}

	//释放mmap
	munmap(mem,4);
	close(fd);
	return -1;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wsCkpF9M-1659943933388)(image/image-20220624214348838.png)]

结论:父子进程共享:1、打开文件 2、mmap映射建立的映射区(但必须使用MAP_SHARED)

2.3.5 匿名映射

无需依赖一个文件即可创建映射区,同样需要借助标志位参数flags来指定,不在需要传入文件描述符fd

int *mem = mmap(NULL,4,PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANONYMOUS,-1,0);

注意:MAP_ANONMAP_ANONYMOUSlinux系统独有的

unix中没有这两个宏,可以使用下面的方式

  • /dev/zero 聚宝盆,可以随意映射
  • /dev/null 无底洞,一般错误信息重定向到这个文件中

mmap支持无血缘关系进程通信

	//1、打开一个空文件
	int fd = open(argv[1],O_RDWR|O_CREAT|O_TRUNC,0666);
	int lenth = sizeof(Student);
	ftruncate(fd,lenth);
	//2、创建映射区  
	Student *stu = mmap(NULL,lenth,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);

	if(stu == MAP_FAILED){
		perror("mmap err\n");
		return -1;
	}

	int num = 1;
	//3、修改内存
	while(1)
	{
		stu->id = num;
		sprintf(stu->sname,"xiaoming-%03d",num++);
		sleep(1);//每隔一秒修改一次映射区
	}
	//3、读数据
	while(1)
	{
		printf("sid = %d\tsname = %s\n",stu->id,stu->sname);
		sleep(1);//每隔一秒读一次映射区
	}

如果进程要通信,flags必须设为MAP_SHARED

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C9CiGOCK-1659943933389)(image/image-20220624225723956.png)]

2.4 第四次作业

1、通过命名管道传输数据,进程A和进程B,进程A将一个文件(mp3)发送给进程B

程序设计思路大概是这样的.

  • 1、先在当前目录下创建一个FIFO文件;
  • 2、复制一个MP3文件到当前路径下。
  • 3、write.c中读取该MP3文件并写入到FIFO中。与此同时,read.c中读取FIFO并写到新建的mp3文件中(用open函数打开或创建的方式)

读取mp3文件并写入到FIFO文件中

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>

//通过命名管道传输数据,进程A和进程B,进程A将一个文件(MP3)发送给进程B。
int main(int argc,char *argv[])
{
    if(argc!=3)
    {
        printf("%s mp3_filepath fifo_filepath",argv[0]);
        return -1;
    }

    //读取mp3文件内容然后写入FIFO中,边读边写。
    //当前目录有一个fifo文件,否则还要创建

    //以只读方式打开mp3文件
    int fd_mp3 = open(argv[1],O_RDONLY);
    //以只写方式打开fifo文件
    int fd_fifo = open(argv[2],O_WRONLY);

    //写到FIFO中
    char buf[4096];
    int ret=0;

    do
    {
        //从fd_mp3中读,读到buf中,最多读sizeof(buf)个字节。
        //fd_mp3会递增的,最终指向文件末尾
        ret=read(fd_mp3,buf,sizeof(buf));
        //测试是否不管文件中还剩多少字节,每次都读count
        write(fd_fifo,buf,ret);
    }while(ret>0&&ret==sizeof(buf));//代表读到末尾了

    //关闭fifo
    close(fd_fifo);
    //关闭mp3
    close(fd_mp3);

    return 0;
}

读取FIFO文件并写入到新建的mp3文件中

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc,char *argv[])
{
    if(argc!=3)
    {
        printf("%s mp3_filename fifo_filename.\n",argv[0]);
        return -1;
    }

    int fd_mp3 = open(argv[1],O_RDWR|O_CREAT,0666);

    int fd_fifo = open(argv[2],O_RDONLY);

    char buf[4096];
    int ret=0;
    do
    {
        ret=read(fd_fifo,buf,sizeof(buf));
        write(fd_mp3,buf,ret);
    }while(ret>0);//代表读到末尾了

    close(fd_fifo);
    close(fd_mp3);

    return 0;
}

2、实现多进程拷贝文件

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bEFQofa4-1659943933390)(image/image-20220624230352885.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UcBJQSoi-1659943933392)(image/image-20220625215414586.png)]

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
int main(int argc, char* argv[])
{
    int n = 5;//进程数
    //输入参数至少是3,第四个参数可以是进程个数
    if(argc < 3){
        printf("./a.out src dst [n]\n");
        return 0;
    }
    if(argc == 4){
        n = atoi(argv[3]);//Convert a string to an integer.
    }

    //打开源文件
    int srcfd = open(argv[1], O_RDWR);
    if(srcfd < 0){
        perror("open src file err");
        exit(1);
    }
    //打开目标文件
    int dstfd = open(argv[2],O_RDWR|O_CREAT|O_TRUNC,0664);
    if(dstfd < 0){
        perror("open dst file err");
        exit(1);
    }    

    //目标扩展,从原文获得文件大小,stat
    struct stat sb;
    stat(argv[1],&sb);//为了计算大小
    int len = sb.st_size;
    truncate(argv[2],len);//将目标文件大小扩展到与原文件一致

    //将源文件映射到缓冲区
    char *psrc = mmap(NULL,len,PROT_READ|PROT_WRITE,MAP_SHARED,srcfd,0);
    if(psrc == MAP_FAILED){
        perror("mmap src err");
        exit(1);
    }
    char *pdst = mmap(NULL,len,PROT_READ|PROT_WRITE,MAP_SHARED,dstfd,0);
    if(pdst == MAP_FAILED){
        perror("mmap dst err");
        exit(1);
    }    

    //创建多个子进程
    int i = 0;
    for( i = 0; i < n; i++){
        if(fork() == 0){
            break;
        }
    }

    //计算子进程需要拷贝到起点和大小
    int copysize = len/n;
    int mod = len % n;

    //数据拷贝,memcpy
    if(i < n){
        //子进程
        if(i == n-1){//最后的一个子进程
            memcpy(pdst+i*copysize, psrc+i*copysize, copysize+mod);
        }
        else{
            memcpy(pdst+i*copysize, psrc+i*copysize, copysize);
        }
    }else{
        for(i = 0; i < n; i++){
            wait(NULL);//回收所以进程
        }
    }

    //释放映射区1111
    if(munmap(psrc,len) < 0){
        perror("munmap src err");
        exit(1);
    }
    if(munmap(pdst,len) < 0){
        perror("munmap dst err");
        exit(1);
    }    

    //关闭文件
    close(srcfd);close(dstfd);
    return 0;
}

2.5 信号

2.5.1 信号的概念

​ 信号在我们的生活中随处可见, 如:古代战争中摔杯为号;现代战争中的信号弹;体育比赛中使用的信号枪…他们都有共性1、简单,2、不能带大量信息,3、满足特定条件发生

信号的机制

​ A给B发送信号,B收到信号之前执行自己的代码,收到信号后,不管执行到程序的什么位置,都要暂停运行,去处理信号,处理完毕再继续执行。与硬件中断类似——异步模式。但信号是软件层面上实现的中断,早期常被称为“软中断”。

信号的特质:由于信号是通过软件方法实现,其实现手段导致信号有很强的延时性。但对于用户来说,这个延迟时间非常短,不易察觉。

每个进程收到的所有信号,都是由内核负责发送的,内核处理。

与信号相关的事件和状态

产生信号:

  1. 按键产生 ctrl+c ctrl+z ctrl+\
  2. 调用函数 kill raise abort
  3. 定时器alarm setitimer
  4. 命令产生kill
  5. 硬件异常:非法访问内存(段错误)、除0(浮点数例外)、内存对齐出错(总线错误)、SIGPIPE

信号的状态

  • 产生
  • 递达:信号到达并处理完
  • 未决:产生和递达之间的状态,信号被阻塞了

信号的处理方式

  • 忽略(丢弃)
  • 执行默认动作
  • 捕获(调用用户处理函数)

这里特别强调了9) SIGKILL 和19) SIGSTOP信号,不允许忽略和捕捉,只能执行默认动作。甚至不能将其设置为阻塞

​ Linux内核的进程控制块PCB是一个结构体,task_struct, 除了包含进程id,状态,工作目录,用户id,组id,文件描述符表,还包含了信号相关的信息,主要指阻塞信号集和未决信号集。

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

未决信号集:

  1. 信号产生,未决信号集中描述该信号的位立刻翻转为1,表信号处于未决状态。当信号被处理对应位翻转回为0。这一时刻往往非常短暂。

  2. 信号产生后由于某些原因(主要是阻塞)不能抵达。这类信号的集合称之为未决信号集。在屏蔽解除前,信号一直处于未决状态。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kGJ3lU6Z-1659943933393)(image/image-20220626144604729.png)]

信号的编号

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JNodN3Mk-1659943933394)(image/image-20220625203410653.png)]

不存在编号为0的信号。其中1-31号信号称之为常规信号(也叫普通信号或标准信号),34-64称之为实时信号,驱动编程与硬件相关。名字上区别不大。而前32个名字各不相同。

信号的四要素
  1. 编号
  2. 名称
  3. 事件
  4. 默认处理动作

​ 可通过man 7 signal查看帮助文档获取。也可查看/usr/src/linux-headers-3.16.0-30/arch/s390/include/uapi/asm/signal.h

标准信号量:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RR6Bxd3o-1659943933395)(image/image-20220625165830142.png)]

信号在不同系统下的值:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1R4MZOX1-1659943933396)(image/image-20220626143541262.png)]

信号的默认处理动作:

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

这里特别强调了9) SIGKILL 和19) SIGSTOP信号,不允许忽略和捕捉,只能执行默认动作。甚至不能将其设置为阻塞

另外需清楚**,只有每个信号所对应的事件发生了,该信号才会被递送(但不一定递达),不应乱发信号!!**

Linux常规信号一览表
  1. SIGHUP: 当用户退出shell时,由该shell启动的所有进程将收到这个信号,默认动作为终止进程

  2. SIGINT:当用户按下了<Ctrl+C>组合键时,用户终端向正在运行中的由该终端启动的程序发出此信号。默认动

作为终止进程。

  1. SIGQUIT:当用户按下<ctrl+>组合键时产生该信号,用户终端向正在运行中的由该终端启动的程序发出些信

号。默认动作为终止进程。

  1. SIGILL:CPU检测到某进程执行了非法指令。默认动作为终止进程并产生core文件

  2. SIGTRAP:该信号由断点指令或其他 trap指令产生。默认动作为终止里程 并产生core文件。

  3. SIGABRT: 调用abort函数时产生该信号。默认动作为终止进程并产生core文件。

  4. SIGBUS:非法访问内存地址,包括内存对齐出错,默认动作为终止进程并产生core文件。

  5. SIGFPE:在发生致命的运算错误时发出。不仅包括浮点运算错误,还包括溢出及除数为0等所有的算法错误。默认动作为终止进程并产生core文件。

  6. SIGKILL:无条件终止进程。本信号不能被忽略,处理和阻塞。默认动作为终止进程。它向系统管理员提供了可以杀死任何进程的方法。

  7. SIGUSE1:用户定义 的信号。即程序员可以在程序中定义并使用该信号。默认动作为终止进程。

  8. SIGSEGV:指示进程进行了无效内存访问。默认动作为终止进程并产生core文件。

  9. SIGUSR2:另外一个用户自定义信号,程序员可以在程序中定义并使用该信号。默认动作为终止进程。

  10. SIGPIPE:Broken pipe向一个没有读端的管道写数据。默认动作为终止进程。

  11. SIGALRM: 定时器超时,超时的时间 由系统调用alarm设置。默认动作为终止进程。

  12. SIGTERM:程序结束信号,与SIGKILL不同的是,该信号可以被阻塞和终止。通常用来要示程序正常退出。执行shell命令Kill时,缺省产生这个信号。默认动作为终止进程。

  13. SIGSTKFLT:Linux早期版本出现的信号,现仍保留向后兼容。默认动作为终止进程。

  14. SIGCHLD:子进程结束时,父进程会收到这个信号。默认动作为忽略这个信号。

  15. SIGCONT:如果进程已停止,则使其继续运行。默认动作为继续/忽略。

  16. SIGSTOP:停止进程的执行。信号不能被忽略,处理和阻塞。默认动作为暂停进程。

  17. SIGTSTP:停止终端交互进程的运行。按下<ctrl+z>组合键时发出这个信号。默认动作为暂停进程。

  18. SIGTTIN:后台进程读终端控制台。默认动作为暂停进程。

  19. SIGTTOU: 该信号类似于SIGTTIN,在后台进程要向终端输出数据时发生。默认动作为暂停进程。

  20. SIGURG:套接字上有紧急数据时,向当前正在运行的进程发出些信号,报告有紧急数据到达。如网络带外数据到达,默认动作为忽略该信号。

  21. SIGXCPU:进程执行时间超过了分配给该进程的CPU时间 ,系统产生该信号并发送给该进程。默认动作为终止进程。

  22. SIGXFSZ:超过文件的最大长度设置。默认动作为终止进程。

  23. SIGVTALRM:虚拟时钟超时时产生该信号。类似于SIGALRM,但是该信号只计算该进程占用CPU的使用时间。默认动作为终止进程。

  24. SGIPROF:类似于SIGVTALRM,它不公包括该进程占用CPU时间还包括执行系统调用时间。默认动作为终止进程。

  25. SIGWINCH:窗口变化大小时发出。默认动作为忽略该信号。

  26. SIGIO:此信号向进程指示发出了一个异步IO事件。默认动作为忽略。

  27. SIGPWR:关机。默认动作为终止进程。

  28. SIGSYS:无效的系统调用。默认动作为终止进程并产生core文件。

  29. SIGRTMIN ~ (64) SIGRTMAX:LINUX的实时信号,它们没有固定的含义(可以由用户自定义)。所有的实时信号的默认动作都为终止进程。

2.5.2 信号的产生

终端按键产生信号
  • Ctrl + c → 2) SIGINT(终止/中断) “INT” ----Interrupt
  • Ctrl + z → 20) SIGTSTP(暂停/停止) “T” ----Terminal 终端。
  • Ctrl + \ → 3) SIGQUIT(退出)
硬件异常产生信号
  • 除0操作 → 8) SIGFPE (浮点数例外) “F” -----float 浮点数。
  • 非法访问内存 → 11) SIGSEGV (段错误)
  • 总线错误 → 7) SIGBUS
kill函数/命令产生信号

kill命令产生信号:kill -SIGKILL pid

kill函数:给指定进程发送指定信号(不一定杀死) int kill(pid_t pid, int sig);

  • 成功:0;失败:-1 (ID非法,信号非法,普通用户杀init进程等权级问题),设置errno
  • sig:不推荐直接使用数字,应使用宏名,因为不同操作系统信号编号可能不同,但名称一致。
  • pid > 0: 发送信号给指定的进程。
  • pid = 0: 发送信号给 与调用kill函数进程属于同一进程组的所有进程。
  • pid < 0: 取|pid|发给对应进程组。
  • pid = -1:发送给进程有权限发送的系统中所有进程。

进程组:每个进程都属于一个进程组,进程组是一个或多个进程集合,他们相互关联,共同完成一个实体任务,每个进程组都有一个进程组长,默认进程组ID与进程组长ID相同。

权限保护:super用户(root)可以发送信号给任意用户,普通用户是不能向系统用户发送信号的。 kill -9 (root用户的pid) 是不可以的。同样,普通用户也不能向其他普通用户发送信号,终止其进程。 只能向自己创建的进程发送信号。普通用户基本规则是:发送者实际或有效用户ID == 接收者实际或有效用户ID

练习:循环创建5个子进程,任一子进程用kill函数终止其父进程。

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <signal.h>

int main()
{
    int i;
    for (i = 0; i < 5; i++){
        if(fork() == 0){
            break;
        }
    }
    if( i == 2)
    {
        printf("i will kill father process after 5s\n");
        sleep(1);
        kill(getppid(),SIGKILL);//杀死父进程
        while(1){
            sleep(1);
        }
    }
    else if( i == 5){
        //father
        while(1){
            printf("i am father process\n");
            sleep(1);
        }
    }

    return 0;
}

raise和abort函数

raise 函数:给当前进程发送指定信号(自己给自己发) raise(signo) == kill(getpid(), signo);

  • int raise(int sig); 成功:0,失败非0值

abort 函数:给自己发送异常终止信号 6) SIGABRT 信号,终止并产生core文件

  • void abort(void); 该函数无返回
时钟产生信号
alarm函数

​ 设置定时器(闹钟)。在指定seconds后,内核会给当前进程发送14)SIGALRM信号。进程收到该信号,默认动作终止。

每个进程都有且只有唯一个定时器。

  • unsigned int alarm(unsigned int seconds); 返回0或剩余的秒数,无失败。

常用:取消定时器alarm(0),返回旧闹钟余下秒数。

例:alarm(5) → 3sec → alarm(4) → 5sec → alarm(5) → alarm(0)

#include <unistd.h>
#include <stdio.h>

int main(){
    int ret = alarm(6);
    printf("ret = %d\n",ret);
    sleep(2);
    ret = alarm(4);
    printf("ret = %d\n",ret);
    while(1){
        printf("lai da wo!\n");
        sleep(1);
    }
    return 0;
}

定时,与进程状态无关(自然定时法)!就绪、运行、挂起(阻塞、暂停)、终止、僵尸…无论进程处于何种状态,alarm都计时。

练习:编写程序,测试你使用的计算机1秒钟能数多少个数。

使用time命令查看程序执行的时间。 程序运行的瓶颈在于IO,优化程序,首选优化IO。

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

setitimer函数

设置定时器(闹钟)。 可代替alarm函数。精度微秒us,可以实现周期定时。

int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value); 成功:0;失败:-1,设置errno

  • which:指定定时方式

    • ① 自然定时:ITIMER_REAL → 14)SIGLARM 计算自然时间

      • ② 虚拟空间计时(用户空间):ITIMER_VIRTUAL → 26)SIGVTALRM 只计算进程占用cpu的时间

      • ③ 运行时计时(用户+内核):ITIMER_PROF → 27)SIGPROF 计算占用cpu及执行系统调用的时间

  • new_value要设置的闹钟时间

  • old_value原闹钟时间

  • 返回值:成功返回0,失败返回-1

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

           struct timeval {
               time_t      tv_sec;         /* seconds */
               suseconds_t tv_usec;        /* microseconds */
           };

练习: 使用setitimer函数实现alarm函数,重复计算机1秒数数程序。

#include <stdio.h>
#include <unistd.h>
#include <sys/time.h>

unsigned int myalarm(unsigned int seconds)
{
    struct itimerval oldit,myit={{0,0},{0,0}};
    myit.it_value.tv_sec = seconds;
    setitimer(ITIMER_REAL,&myit,&oldit);//seconds后发送SIGALRM信号
    printf("tv_sec=%ld,tv_micsec=%ld\n",oldit.it_value.tv_sec,oldit.it_value.tv_usec);
    return oldit.it_value.tv_sec;
}

int main(){
    int ret = myalarm(5);
    printf("ret = %d\n",ret);
    sleep(3);
    ret = myalarm(3);
    printf("ret = %d\n",ret);
    while(1){
        printf("lai da wo!\n");
        sleep(1);
    }    
    return 0;
}

拓展练习,结合man page编写程序,测试it_interval、it_value这两个参数的作用。 【setitimer1.c】

提示: it_interval:用来设定两次定时任务之间间隔的时间。

​ it_value:定时的时长

两个参数都设置为0,即清0操作。

#include <stdio.h>
#include <unistd.h>
#include <sys/time.h>

int main(){
    struct itimerval myit={{0,0},{3,0}};//定义3秒以后发送SIGALRM信号
    setitimer(ITIMER_REAL,&myit,NULL);

    while(1){
        printf("who can kill ne!\n");
        sleep(1);
    }
    return 0;
}

捕获操作:

#include <stdio.h>
#include <unistd.h>
#include <sys/time.h>
#include <signal.h>

    //    typedef void (*sighandler_t)(int);
    //    sighandler_t signal(int signum, sighandler_t handler);

void catch_sig(int num){
    printf("cat %d sig\n",num);
}

int main(){

    signal(SIGALRM,catch_sig);//注册捕捉函数

    struct itimerval myit={{3,0},{5,0}};//第一次等待5秒,第二次每隔3秒
    setitimer(ITIMER_REAL,&myit,NULL);

    while(1){
        printf("who can kill ne!\n");
        sleep(1);
    }
    return 0;
}

2.5.3 信号集操作函数

​ 内核通过读取未决信号集来判断信号是否应被处理。信号屏蔽字mask可以影响未决信号集。而我们可以在应用程序中自定义set来改变mask。已达到屏蔽指定信号的目的

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l6okYsip-1659943933397)(image/image-20220626144604729.png)]

信号集设定

清空信号集:int sigemptyset(sigset_t *set);,全变成0

填充信号集:int sigfillset(sigset_t *set);,全变成1

添加某个信号到信号集:int sigaddset(sigset_t *set, int signum);

从集合中删除某个信号:int sigdelset(sigset_t *set, int signum);

是否为集合里的成员: int sigismember(const sigset_t *set, int signum);

  • 返回1代表signum在集合中,否则不在
sigprocmask

设置阻塞或者解除阻塞信号集: int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

  • how
    • SIG_BLOCK设置阻塞
    • SIG_UNBLOCK解除阻塞
    • SIG_SETMASK直接设置,设置set为新的阻塞信号集
  • set传入的信号集
  • oldset旧的信号集,传出
sigpending

获取未决信号集int sigpending(sigset_t *set);

  • set传出参数,当前的未决信号集
  • returns 0 on success and -1 on error

练习:编写程序。把所有常规信号到未决状态打印至屏幕

#include <stdio.h>
#include <unistd.h>
#include <signal.h>

int main(int argc)
{
    sigset_t pend,sigproc;
    //设置阻塞信号,等待按键产生信号
    sigemptyset(&sigproc);//清空
    sigaddset(&sigproc,SIGINT);
    sigaddset(&sigproc,SIGQUIT);
    //设置阻塞信号集
    sigprocmask(SIG_BLOCK,&sigproc,NULL);

    while(1){
    //循环未决信号集,打印
    
    sigpending(&pend);
    int i = 1;
    for( i = 1; i < 32; i++){
        if(sigismember(&pend,i) == 1){
            printf("1");
        }
        else{
            printf("0");
        }
    }
    printf("\n");
    sleep(1);
    }

    
    return 0;
}

2.5.4 信号捕捉

防止进程意外死亡

signal信号捕捉函数
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
  • signum要捕捉到信号
  • handler要执行的捕捉函数指针,函数应该声明void func(int)
sigaction注册捕捉函数
struct sigaction {
               void     (*sa_handler)(int); 	//函数指针
               void     (*sa_sigaction)(int, siginfo_t *, void *);//
               sigset_t   sa_mask;		//执行捕捉函数期间,临时屏蔽的信号集
               int        sa_flags;		//一般填0,SA_SIGINFO会使用第二个函数指针
               void     (*sa_restorer)(void);//无效
};

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

  • signum捕捉的信号
  • act传入的动作
  • oldact原动作,恢复现场

练习,简单使用一下sigaction函数

#include <signal.h>
#include <stdio.h>
#include <stdio.h>
#include <sys/time.h>

void catch_sig(int num)
{
    printf("catch %d sig\n",num);
}

int main()
{
    //注册一下捕捉函数
    struct sigaction act;
    act.sa_flags = 0;
    act.sa_handler = catch_sig;
    sigemptyset(&act.sa_mask);
    sigaction(SIGALRM,&act,NULL);

    //设置周期定时器
    struct itimerval myit = {{3,0},{5,0}};//第一次等待5秒,第二次每隔3秒
    setitimer(ITIMER_REAL,&myit,NULL);
    while(1){
        printf("who can kill me!\n");
        sleep(1);
    }

    return 0;


}
信号捕捉到特性
  1. 进程正常运行时,默认PCB中有一个信号屏蔽字,假定为☆,它决定了进程自动屏蔽哪些信号。当注册了某个信号捕捉函数,捕捉到该信号以后,要调用该函数。而该函数有可能执行很长时间,在这期间所屏蔽的信号不由☆来指定。而是用sa_mask来指定。调用完信号处理函数,再恢复为☆。

  2. XXX信号捕捉函数执行期间,XXX信号自动被屏蔽。

  3. 阻塞的常规信号不支持排队,产生多次只记录一次。(后32个实时信号支持排队)

练习1:为某个信号设置捕捉函数

练习2: 验证在信号处理函数执行期间,该信号多次递送,那么只在处理函数之行结束后,处理一次

练习3:验证sa_mask在捕捉函数执行期间的屏蔽作用。

#include <signal.h>
#include <stdio.h>
#include <stdio.h>
#include <sys/time.h>

void catch_sig(int num)
{
    printf("begin call,catch %d sig\n",num);
    sleep(5);//模拟捕获函数执行时间较长,不支持排队
    printf("end call,catch %d sig\n",num);
}

int main()
{
    //注册一下捕捉函数
    struct sigaction act;
    act.sa_flags = 0;
    act.sa_handler = catch_sig;
    sigemptyset(&act.sa_mask);//清空
    sigaddset(&act.sa_mask,SIGQUIT);//临时屏蔽ctrl+\信号

    //注册捕捉
    sigaction(SIGINT,&act,NULL);

    //设置周期定时器
    while(1){
        printf("who can kill me!\n");
        sleep(1);
    }

    return 0;


}
内核实现信号捕捉的过程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3rQEEMoa-1659943933398)(image/image-20220629230425617.png)]

2.5.5 SIGCHLD信号

SIGCHLD的产生条件
  • 子进程终止时
  • 子进程接收到SIGSTOP信号停止时
  • 子进程处在停止态,接受到SIGCONT后唤醒时
借助SIGCHLD信号回收子进程

子进程结束运行,其父进程会收到SIGCHLD信号。该信号的默认处理动作是忽略。可以捕捉该信号,在捕捉函数中完成子进程状态的回收。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>

void sys_err(char *str)
{
    perror(str);
    exit(1);
}
void do_sig_child(int signo)
{
    int status;    
    pid_t pid;
    while ((pid = waitpid(0, &status, WNOHANG)) > 0) {
        if (WIFEXITED(status))
            printf("child %d exit %d\n", pid, WEXITSTATUS(status));
        else if (WIFSIGNALED(status))
            printf("child %d cancel signal %d\n", pid, WTERMSIG(status));
    }
}
int main(void)
{
    pid_t pid;    
    int i;
    for (i = 0; i < 10; i++) {
        if ((pid = fork()) == 0)
            break;
        else if (pid < 0)
            sys_err("fork");
    }
    if (pid == 0) {    
        int n = 1;
        while (n--) {
            printf("child ID %d\n", getpid());
            sleep(1);
        }
        return i+1;
    } else if (pid > 0) {
        struct sigaction act;
        act.sa_handler = do_sig_child;
        sigemptyset(&act.sa_mask);
        act.sa_flags = 0;
        sigaction(SIGCHLD, &act, NULL);
        
        while (1) {
            printf("Parent ID %d\n", getpid());
            sleep(1);
        }
    }
    return 0;
}
SIGCHLD信号注意问题
  1. 子进程继承了父进程的信号屏蔽字和信号处理动作,但子进程没有继承未决信号集spending。

  2. 注意注册信号捕捉函数的位置。

  3. 应该在fork之前,阻塞SIGCHLD信号。注册完捕捉函数后解除阻塞。

中断系统调用(了解性内容)

系统调用可分为两类:慢速系统调用和其他系统调用。

  1. 慢速系统调用:可能会使进程永远阻塞的一类。如果在阻塞期间收到一个信号,该系统调用就被中断,不再继续执行(早期);也可以设定系统调用是否重启。如,read、write、pause、wait…

  2. 其他系统调用:getpid、getppid、fork…

结合pause,回顾慢速系统调用:

​ 慢速系统调用被中断的相关行为,实际上就是pause的行为: 如,read

​ ① 想中断pause,信号不能被屏蔽。

​ ② 信号的处理方式必须是捕捉 (默认、忽略都不可以)

​ ③ 中断后返回-1, 设置errno为EINTR(表“被信号中断”)

可修改sa_flags参数来设置被信号中断后系统调用是否重启。SA_INTERRURT不重启。 SA_RESTART重启。

扩展了解:

​ sa_flags还有很多可选参数,适用于不同情况。如:捕捉到信号后,在执行捕捉函数期间,不希望自动阻塞该信号,可将sa_flags设置为SA_NODEFER,除非sa_mask中包含该信号。

2.6 第五次作业

1、使用setitimer实现每隔一秒打印一次hello word!

#include <stdio.h>
#include <unistd.h>
#include <sys/time.h>
#include <signal.h>

void catch_sig(int num)
{
    printf("hello world!\n");
}

int main()
{
    //注册捕捉函数
    struct sigaction act;
    act.sa_flags = 0;
    act.sa_handler = catch_sig;
    sigemptyset(&act.sa_mask);
    sigaction(SIGALRM,&act,NULL);

    struct itimerval myit = {{1,0},{1,0}};
    setitimer(ITIMER_REAL,&myit,NULL);
    int i = 1;
    while(1){
        sleep(1);
        printf("%d\n",i++);
    }
    return 0;
}

2、利用SIGUSR1和SIGUSR2在父子进程之间进行消息传递,实现父子进程交替报数,间隔一秒

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/types.h>

pid_t pid;
int count = 0;

void cat_sig_father(int num)
{
    printf("%d\n",count);
    count+=2;
    kill(pid,SIGUSR2);
}

void cat_sig_child(int num)
{
    printf("%d\n",count);
    count+=2;
    kill(getppid(),SIGUSR1);
}

int main(int argc, char *argv[])
{
    pid = fork();
    if(pid == 0){
        //son
        count = 1;
        signal(SIGUSR2,cat_sig_child);//发送用户定义信号
        pid_t ppid = getppid();
        while (1)
        {
   
        }
        
    }
    else{
        usleep(10);//us
        count = 2;
        signal(SIGUSR1,cat_sig_father);//发送用户定义信号
        kill(pid,SIGUSR2);
        while (1)
        {
            
        }
    }
    return 0;
}

3、在父子进程进行管道通信时,如果管道读端都关闭,会收到SIGPIPE信号,模拟场景,对该信号进行捕捉,并且使用捕捉函数回收子进程

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
#include <fcntl.h>
#include <sys/wait.h>
void catch_sig_child(int num)
{
    printf("catch %d sig\n",num);

}

void catch_sig_father(int num)
{
    printf("catch %d sig\n",num);
    pid_t pid;
    int status;
    while((pid = waitpid(0,&status,WNOHANG)) > 0){
        if (WIFEXITED(status))
            printf("child %d exit %d\n", pid, WEXITSTATUS(status));
        else if (WIFSIGNALED(status))
            printf("child %d cancel signal %d\n", pid, WTERMSIG(status));
    }

}
int main()
{
    int fd[2];//存储文件描述符
    pipe(fd);//创建管道
    pid_t pid = fork();
    if(pid == 0){
        //son
        // 关闭读端
        //注册捕捉函数
        struct sigaction act;
        act.sa_flags = 0;
        act.sa_handler = catch_sig_child;
        sigemptyset(&act.sa_mask);
        sigaction(SIGPIPE,&act,NULL);
        close(fd[0]);
        // sleep(1);
		write(fd[1],"hello",5);
        return 111;
    }else if(pid > 0){
        //parent
        struct sigaction act;
        act.sa_flags = 0;
        act.sa_handler = catch_sig_father;
        sigemptyset(&act.sa_mask);
        sigaction(SIGCHLD,&act,NULL);
        //关闭写端
        close(fd[1]);
        //关闭读端
        close(fd[0]);
        while(1){
            sleep(1);
        }

    }
    return 0;
}

2.7 守护进程

2.7.1 进程组

概念和特性

(1)进程组,也称之为作业,BSD与1980年前后向UNIX中增加的一个新特性,代表一个或多个进程的集合。每个进程都属于一个进程组,在waitpid函数和kill函数的参数中都曾经使用到,操作系统设计的进程组的概念,是为了简化对多个进程的管理。

当父进程创建子进程的时候,默认子进程与父进程属于同一个进程组,进程组ID等于进程组第一个进程ID(组长进程)。所以,组长进程标识:其进程组ID等于其进程ID.

组长进程可以创建一个进程组,创建该进程组的进程,然后终止,只要进程组中有一个进程存在,进程组就存在,与组长进程是否终止无关。

(2)kill发送给进程组

使用 kill -n -pgid 可以将信号 n 发送到进程组 pgid 中的所有进程。例如命令 kill -9 -4115 表示杀死进程组 4115 中的所有进程。

2.7.2 会话

会话是一个或多个进程组的集合

创建会话

创建一个会话需要注意以下5点注意事项:

  1. 调用进程不能是进程组组长,该进程会变成新会话首进程(session header
  2. 该进程成为一个新进程组队组长进程
  3. 新会话会丢弃原有的控制终端,该会话没有控制终端
  4. 该调用进程是组长进程,则错误返回
  5. 建立新的会话时,先调用fork,父进程终止,子进程调用setsid
setsid函数

函数原型pid_t setsid(void);

如果这个函数的调用进程不是进程组组长,那么调用该函数会发生以下事情:
1)创建一个新会话,会话ID等于进程ID,调用进程成为会话的首进程。
2)创建一个进程组,进程组ID等于进程ID,调用进程成为进程组的组长。
3)该进程没有控制终端,如果调用setsid前,该进程有控制终端,这种联系就会断掉。
调用setsid函数的进程不能是进程组的组长,否则调用会失败,返回-1,并置errno为EPERM。

这个限制是比较合理的。如果允许进程组组长迁移到新的会话,而进程组的其他成员仍然在老的会话中,那么,就会出现同一个进程组的进程分属不同的会话之中的情况,这就破坏了进程组和会话的严格的层次关系了

getsid函数

函数原型pid_t getsid(pid_t pid);

getsid(0) returns the session ID of the calling process. getsid() returns the session ID of the process with process ID pid. If pid is 0, getsid() returns the session ID of the calling process.

2.7.3 守护进程

**守护进程:**一种长期运行的进程,这种进程在后台运行,并且不跟任何的控制终端关联。
基本特点:

  • 生存周期长[非必须],一般操作系统启动的时候就启动,关闭的时候关闭。
  • 守护进程和终端无关联,也就是他们没有控制终端,所以当控制终端退出,也不会导致守护进程退出。
  • 守护进程是在后台运行,不会占着终端,终端可以执行其他命令

linux操作系统本身是有很多的守护进程在默默执行,维持着系统的日常活动。大概30-50个。
查看后台守护进程命令:ps -efj

  • ppid = 0:内核进程,跟随系统启动而启动,生命周期贯穿整个系统。
  • cmd列名带[]这种,叫内核守护进程
  • 老祖init:也是系统守护进程,它负责启动各运行层次特定的系统服务;所以很多进程的PPID是init,也负责收养孤儿进程
  • cmd列中名字不带[]的普通守护进程(用户集守护进程)

共同点总结:

  • 大多数守护进程都是以超级用户特权运行的。
  • 守护进程没有控制终端;内核守护进程以无控制终端方式启动,普通守护进程可能是守护进程调用了setsid的结果(无控制终端)
    创建守护进程模型
守护进程的步骤
  1. 创建子进程fork,退出父进程。
  2. 子进程创建新的会话。setsid
  3. 更改当前工作目录。$HOME
  4. 修改文件权限掩码。umask
  5. 关闭打开的文件描述符号。避免浪费资源
  6. 执行核心逻辑
  7. 退出

创建一个守护进程:每分钟在$HOME/log/创建一个文件

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>
#include <sys/time.h>
#include <signal.h>

#define _FILE_NAME_FORMAT_ "%s/log/mydaemon.%ld"   

void touchfile(int num){
    char * HomeDir = getenv("HOME");
    char strFileName[256] = {0};
    sprintf(strFileName,_FILE_NAME_FORMAT_,HomeDir,time(NULL));
    int fd = open(strFileName,O_RDWR|O_CREAT,0666);
    if(fd < 0){
        perror("open err\n");
        exit(1);
    }
    close(fd);
}
int main()
{
    //1、创建子进程,父进程退出

    //2、当会长

    //3、设置掩码
    umask(0);
    //4、切换目录
    chdir(getenv("HOME"));//切换到家目录
    //5、关闭文件描述符
    // close(1),close(2),close(3);
    //6、执行核心逻辑
    struct itimerval myit = {{5,0},{5,0}};
    setitimer(ITIMER_REAL,&myit,NULL);
    struct sigaction act;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);
    act.sa_handler = touchfile;
    sigaction(SIGALRM,&act,NULL);

    while (1)
    {
        //每隔一分钟在/home/cmxiao/log下创建文件
        sleep(1);
    }
    
    //7、退出
	return 0;

}

扩展了解

  • 通过nohup指令也可以达到守护进程创建创建的效果
  • nohup cmd [> 1.log] &
    • nohup指令会让cmd收不到SIGHUB信号
    • &代表后台运行

2.8 线程

2.8.1 线程概念

什么是线程
  • LWP:light weight process 轻量级的进程,本质仍然是进程(在Linux环境下)
  • 进程:独立地址空间,拥有PCB
  • 线程:也有PCB,但没有独立的地址空间(共享)
  • 区别:在于是否共享地址空间。独居(进程);合租(线程)。一个进程内部可以有多个线程,默认情况下一个进程只有一个线程
  • Linux下:
    • 线程:最小的执行单位
    • 进程:最小的系统资源分配单位,可以看成是一个只有一个线程的进程。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dyw7k0CU-1659943933400)(image/image-20220711214313952.png)]

Linux内核线程实现原理

类Unix系统中,早期没有“线程”概念,80年代才引入,借助进程机制实现出来县城的概念,因此在这类系统中,线程和进程关系密切

  1. 轻量级进程(light weight process)也有PCB,创建线程使用的底层函数和进程一样,都是clone。
  2. 从内核看进程和线程是一样的,都有各自不同的PCB
  3. 进程至少有一个线程
  4. 线程可看做寄存器和栈的集合
  5. 在linux下,线程是最小的执行单位;进程是最小的分配资源单位

查看LWP号:ps -LF pid查看指定现程度lwp号

线程共享资源
  1. 文件描述符表
  2. 每种信号灯处理方式
  3. 当前工作目录
  4. 用户ID和组ID
  5. 内存地址空间(.text/.data/.bss/heap/共享库)
线程非共享资源
  1. 线程id
  2. 处理器线程和栈指针(内核栈)
  3. 独立的栈空间(用户空间)
  4. errno变量
  5. 信号屏蔽字
  6. 调度优先级
线程和进程的优缺点
  • 优点:
    1. 提高程序的并发性
    2. 开销小
    3. 数据通信、共享数据方便
  • 缺点:
    1. 库函数,不稳定
    2. 调试、编写困难
    3. 对信号灯支持不好
  • 优点相对突出,缺点不是硬伤。Linux下由于实现方法导致进程、线程差别不是很大

2.8.2 线程操作函数

创建一个线程
       #include <pthread.h>
       int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                          void *(*start_routine) (void *), void *arg);
       Compile and link with -pthread.
  • thread 线程的id,传出参数
  • attr 代表线程的属性
  • 第三个参数 函数指针:void* func(void*)
  • arg 线程执行函数的参数
  • 返回值:成功返回0,失败返回errno
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

void* thr(void* arg)
{
    printf("i am a thread pid = %d\n, tid=%lu\n",getpid(),pthread_self());
    return NULL;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid,NULL,thr,NULL);
    printf("i am a main thread, pid = %d,tid=%lu\n",getpid(),pthread_self());
    sleep(1);
    return 0;
}
  • pthread_self()获得当前线程id

编译的时候需要加pthread库,一个小技巧,利用alias命令重命名实现快速创建makfile文件

修改.bashrc文件

alias echomake="cat ~/LinuxTest/day7/makefile >> makefile"

设置shell里vi的快捷键set -o vi

线程退出函数
      #include <pthread.h>
       void pthread_exit(void *retval);
       Compile and link with -pthread.

线程退出注意事项:

  • 在线程中使用pthread_exit
  • 在线程中使用return(主控线程return代表退出进程)
  • exit代表退出整个进程
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <stdlib.h>
void* thr(void* arg)
{
    printf("i am a thread pid = %d, tid=%lu\n",getpid(),pthread_self());
    return N;
    // pthread_exit(NULL);
    exit(1);
}

int main()
{
    pthread_t tid;
    pthread_create(&tid,NULL,thr,NULL);
    printf("i am a main thread, pid = %d,tid=%lu\n",getpid(),pthread_self());

    sleep(10);
    printf("i will out\n");
    pthread_exit(NULL);

    return 0;
}
线程回收函数-阻塞等待
       #include <pthread.h>
       int pthread_join(pthread_t thread, void **retval);
       Compile and link with -pthread.

函数参数:

  • thread 创建的时候传出的第一个参数
  • retval 代表读传出线程的退出信息
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

void* thr(void *arg)
{
    printf("i am a thread,tid=%lu\n",pthread_self());
    sleep(5);
    return (void*)100;
    //pthread_exit((void*)100);
}
int main()
{
    pthread_t tid;
    pthread_create(&tid,NULL,thr,NULL);
    void *ret;
    pthread_join(tid,&ret);//线程回收,阻塞等待
    printf("ret exit with %d\n",(int)ret);
    pthread_exit(NULL);
}
杀死线程
   	#include <pthread.h>
    int pthread_cancel(pthread_t thread);
    Compile and link with -pthread.

函数参数:thread创建的时候传出的第一个参数

返回值:失败返回errno,成功返回0

被杀死的线程,退出的状态为-1即:#define PTHREAD_CANCEL (void *) -1

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

void* thr(void *arg)
{
    while(1)
    {
        //里面必须有取消点,也可以强行设置取消点pthread_testcancel()
        // printf("i am a thread,tid=%lu\n",pthread_self());
        // sleep(1);
    }
   
    // return (void*)100;
    pthread_exit((void*) 100);
}
int main()
{
    pthread_t tid;
    pthread_create(&tid,NULL,thr,NULL);
    sleep(5);
    pthread_cancel(tid);//杀死线程

    void *ret;
    pthread_join(tid,&ret);//线程回收,阻塞等待
    printf("ret exit with %d\n",(int)ret);
    pthread_exit(NULL);
}

注意:线程里面必须有取消点才可以被杀死,也可以强行设置取消点pthread_testcancel()

线程分离
       #include <pthread.h>
       int pthread_detach(pthread_t thread);
       Compile and link with -pthread.

此时不需要pthread_join()回收资源,系统自动回收

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>
void* thr(void *arg)
{

    printf("i am a thread,tid=%lu\n",pthread_self());
    sleep(4);
    printf("i am a thread,tid=%lu\n",pthread_self());
    pthread_exit((void*) 100);
}
int main()
{
    pthread_t tid;
    pthread_create(&tid,NULL,thr,NULL);
    pthread_detach(tid);//线程分离
    sleep(5);
    int ret = 0;
   if( (ret = pthread_join(tid,NULL)) > 0)
    {
        printf("join err:%d,%s\n",ret,strerror(ret));//查看错误信息
    }
    pthread_exit(NULL);
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sAVl7pmy-1659943933401)(image/image-20220712214621981.png)]

比较线程id

比较两个线程id是否相等,线程id在进程内部唯一,系统内不一定

       #include <pthread.h>
       int pthread_equal(pthread_t t1, pthread_t t2);
       Compile and link with -pthread.

有可能Linux在未来线程ID pthread_t类型被修改为结构体实现

控制原语对比
进程线程
forkpthread_create
exitpthread_exit
waitpthread_join
killpthread_cancel
getpidpthread_self
小练习

1、全局变量

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>

int var = 100;
void* thr(void *arg)
{

    printf("i am a thread,tid=%lu, var = %d\n",pthread_self(),var);
    sleep(2);
    var = 1001;
    printf("i am a thread,tid=%lu, var = %d\n",pthread_self(),var);
    pthread_exit((void*) 100);
}
int main()
{
    pthread_t tid;
    pthread_create(&tid,NULL,thr,NULL);
    pthread_detach(tid);//线程分离
    printf("i am a thread,tid=%lu, var = %d\n",pthread_self(),var);
    var = 1003;
    sleep(5);
    printf("i am a thread,tid=%lu, var = %d\n",pthread_self(),var);
    
    int ret = 0;
   if( (ret = pthread_join(tid,NULL)) > 0)
    {
        printf("join err:%d,%s\n",ret,strerror(ret));//查看错误信息
    }
    pthread_exit(NULL);
}

2、n个线程

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

void* thr(void* arg)
{
    int num = (int) arg;
    printf("i am %d thread,self = %lu\n",num,pthread_self());

    return (void*)(100+num);
}

int main()
{
    pthread_t tid[5];
    int i;
    for(i = 0; i < 5; i++)
    {
        pthread_create(&tid[i],NULL,thr,(void*)i);
    }
    for(i = 0; i < 5; i++)
    {   
        void* ret;
        pthread_join(tid[i],&ret);//有序回收,阻塞等待
        printf("i = %d,ret = %d\n",i,(int)ret);
    }
    return 0;
}
线程属性(扩展性了解)

本节作为指引性介绍,linux下线程的属性是可以根据实际项目需要,进行设置,之前我们讨论的线程都是采用线程的默认属性,默认属性已经可以解决绝大多数开发时遇到的问题。如我们对程序的性能提出更高的要求那么需要设置线程属性,比如可以通过设置线程栈的大小来降低内存的使用,增加最大线程个数。

typedef struct
{
    int                     etachstate;     //线程的分离状态
    int                     schedpolicy;    //线程调度策略
    struct sched_param  schedparam;     //线程的调度参数
    int                     inheritsched;   //线程的继承性
    int                     scope;      //线程的作用域
    size_t              guardsize;  //线程栈末尾的警戒缓冲区大小
    int                 stackaddr_set; //线程的栈设置
    void*               stackaddr;  //线程栈的位置
    size_t              stacksize;  //线程栈的大小
} pthread_attr_t; 
  • 主要结构体成员:
    1. 线程分离状态
    2. 线程栈大小(默认平均分配)
    3. 线程栈警戒缓冲区大小(位于栈末尾) 参 APUE.12.3 线程属性属性值不能直接设置,须使用相关函数进行操作,初始化的函数为pthread_attr_init,这个函数必须在pthread_create函数之前调用。之后须用pthread_attr_destroy函数来释放资源。线程属性主要包括如下属性:作用域(scope)、栈尺寸(stack size)、栈地址(stack address)、优先级(priority)、分离的状态(detached state)、调度策略和参数(scheduling policy and parameters)。默认的属性为非绑定、非分离、缺省的堆栈、与父进程同样级别的优先级。
线程属性初始化

注意:应先初始化线程属性,再pthread_create创建线程
初始化线程属性:int pthread_attr_init(pthread_attr_t *attr); 成功:0;失败:错误号
销毁线程属性所占用的资源:int pthread_attr_destroy(pthread_attr_t *attr); 成功:0;失败:错误号

线程的分离状态(掌握)

线程的分离状态决定一个线程以什么样的方式来终止自己。

  • 非分离状态:线程的默认属性是非分离状态,这种情况下,原有的线程等待创建的线程结束。只有当pthread_join()函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。
  • 分离状态:分离线程没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。应该根据自己的需要,选择适当的分离状态。
  • 线程分离状态的函数

设置线程属性,分离or非分离
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);

获取程属性,分离or非分离
int pthread_attr_getdetachstate(pthread_attr_t *attr, int *detachstate);

参数:

  • attr:已初始化的线程属性
  • detachstate
    • PTHREAD_CREATE_DETACHED(分离线程)
    • PTHREAD _CREATE_JOINABLE(非分离线程)

这里要注意的一点是,如果设置一个线程为分离线程,而这个线程运行又非常快,它很可能在pthread_create函数返回之前就终止了,它终止以后就可能将线程号和系统资源移交给其他的线程使用,这样调用pthread_create的线程就得到了错误的线程号。要避免这种情况可以采取一定的同步措施,最简单的方法之一是可以在被创建的线程里调用pthread_cond_timedwait函数,让这个线程等待一会儿,留出足够的时间让函数pthread_create返回。设置一段等待时间,是在多线程编程里常用的方法。但是注意不要使用诸如wait()之类的函数,它们是使整个进程睡眠,并不能解决线程同步的问题。

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>
void* thr(void *arg)
{

    printf("i am a thread,tid=%lu\n",pthread_self());
    sleep(1);
    pthread_exit((void*) 100);
}
int main()
{
    pthread_attr_t attr;
    pthread_attr_init(&attr);//初始化属性
    pthread_attr_setdetachstate(&attr,PTHREAD_CANCEL_DEFERRED);//设置属性分离

    pthread_t tid;
    pthread_create(&tid,&attr,thr,NULL);

   
    int ret = 0;
    if( (ret = pthread_join(tid,NULL)) > 0)
    {
        printf("join err:%d,%s\n",ret,strerror(ret));//查看错误信息
    }
    pthread_attr_destroy(&attr);//摧毁属性
    return 0;
}
线程的栈地址

POSIX.1定义了两个常量_POSIX_THREAD_ATTR_STACKADDR 和_POSIX_THREAD_ATTR_STACKSIZE检测系统是否支持栈属性。也可以给sysconf函数传递_SC_THREAD_ATTR_STACKADDR或 _SC_THREAD_ATTR_STACKSIZE来进行检测。

当进程栈地址空间不够用时,指定新建线程使用由malloc分配的空间作为自己的栈空间。通过pthread_attr_setstack和pthread_attr_getstack两个函数分别设置和获取线程的栈地址。

int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize); 成功:0;失败:错误号
int pthread_attr_getstack(pthread_attr_t *attr, void **stackaddr, size_t *stacksize); 成功:0;失败:错误号

  • 参数
    attr:指向一个线程属性的指针
    stackaddr:返回获取的栈地址
    stacksize:返回获取的栈大小
线程的栈大小

​ 当系统中有很多线程时,可能需要减小每个线程栈的默认大小,防止进程的地址空间不够用,当线程调用的函数会分配很大的局部变量或者函数调用层次很深时,可能需要增大线程栈的默认大小。

函数pthread_attr_getstacksize和 pthread_attr_setstacksize提供设置。
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize); 成功:0;失败:错误号
int pthread_attr_getstacksize(pthread_attr_t *attr, size_t *stacksize); 成功:0;失败:错误号

  • 参数
    attr:指向一个线程属性的指针
    stacksize:返回线程的堆栈大小
NPTL

1.查看当前pthread库版本getconf GNU_LIBPTHREAD_VERSION
2.NPTL实现机制(POSIX),Native POSIX Thread Library
3.使用线程库时gcc指定 –lpthread

线程使用注意事项
  1. 主线程退出其他线程不退出,主线程应调用pthread_exit
  2. 避免僵尸线程
    pthread_join
    pthread_detach
    pthread_create指定分离属性
    被join线程可能在join函数返回前就释放完自己的所有内存资源,所以不应当返回被回收线程栈中的值;
  3. malloc和mmap申请的内存可以被其他线程释放
  4. 应避免在多线程模型中调用fork除非,马上exec,子进程中只有调用fork的线程存在,其他线程在子进程中均pthread_exit
  5. 信号的复杂语义很难和多线程共存,应避免在多线程引入信号机制

创建多少线程:核心数*2 + 2??

2.9 第六次作业

1、实现一个守护进程,每分钟写入一次日志,要求日志文件保存在$HOME/log下

  • 命名规则:程序名.yyyymm
  • 写入内容格式:mm:dd hh:mi:ss程序名 【进程号】:消息内容
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>
#include <sys/time.h>
#include <signal.h>
#include <time.h>

#define _PROGREAM_NAME_ "touchevery"
#define _LOG_FORMAT_   "%2d-%02d %02d:%02d:%02d %s [%06d]:%s\n" //mm-dd hh:mi:ss programname [pid]:msg
#define _LOG_FILE_      "%s/log/%s%04d%02d.txt" //$HOME/log/programname.yyyymm,如果$HOME/log不存在,需要创建

void catch_alarm(int num)
{
    time_t nowtime = time(NULL);
    struct tm* nowtm = localtime(&nowtime);

    char strLogFile[100];
    memset(strLogFile,0x00,sizeof(strLogFile));//置空
    sprintf(strLogFile,_LOG_FILE_,getenv("HOME"),_PROGREAM_NAME_,nowtm->tm_year + 1900,nowtm->tm_mon+1);
    int fd = open(strLogFile,O_WRONLY|O_CREAT|O_APPEND,0666);
    if(fd < 0){
        perror("open file err");
        printf("file is %s\n",strLogFile);
        exit(1);
    }
    char buf[2014]={0};
    sprintf(buf,_LOG_FORMAT_,nowtm->tm_mon+1, nowtm->tm_mday, nowtm->tm_hour, 
            nowtm->tm_min, nowtm->tm_sec, _PROGREAM_NAME_,getpid(),"I am alive!");
    write(fd,buf,sizeof(buf));
    close(fd);
}

int main()
{
    //初始化需要对环境变量
    char* strHomeDir = getenv("HOME");
    printf("home dir is %s",strHomeDir);
    //守护进程创建
    pid_t pid = fork();
    if(pid > 0){
        exit(1);//父进程退出
    }
    setsid();//子进程当会长
    umask(0);//设置掩码
    chdir(strHomeDir);
    close(0);//关闭文件描述符

    //设置信号捕捉
    struct sigaction act;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    act.sa_handler = catch_alarm;
    sigaction(SIGALRM,&act,NULL);

    //设置时钟参数
    struct itimerval myit = {{10,0},{5,0}};//每隔60秒来一次闹钟
    setitimer(ITIMER_REAL,&myit,NULL);
    
    //循环等待
    while(1)
    {
        sleep(1);
    }
    return 0;

}

2、实现多线程拷贝

  • 类似于多进程拷贝
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <stdlib.h>


#define  __THR_CNT__ 5 //线程数
typedef struct CopyFileObj{
    int num;
    char *copySrc;
    char *copyDst;
    int size;
}CopyObj;

void * thrFun(void *arg)
{
    CopyObj *info = arg;
    int num = info->num;
    int casize = info->size/__THR_CNT__;
    int mod = info->size%casize;//整除后,余下的量
    if(num == (__THR_CNT__ -1))
        memcpy(info->copyDst+num*casize,info->copySrc+num*casize,casize+mod);
    else
        memcpy(info->copyDst+num*casize,info->copySrc+num*casize,casize);
    pthread_exit(NULL);
}

int main(int argc,char *argv[])
{
    if(argc != 3)
    {   
        printf("err: %s srcFile dstFile\n",argv[0]);
        exit(1);
    }   
    //目标扩展,从原文获得文件大小,stat
    struct stat sb;
    stat(argv[1],&sb);//为了计算大小
    int len = sb.st_size;

    int fd_src = open(argv[1],O_RDONLY);  
    int fd_dst = open(argv[2],O_RDWR|O_CREAT|O_TRUNC,0666);
    ftruncate(fd_dst,len);//扩展文件
    if( fd_src < 0 ||fd_dst < 0)
    {   
        perror("open dst or src fail \n");
        exit(2);
    }   
                     
	void* mp_src = mmap(NULL,len,PROT_READ,MAP_PRIVATE,fd_src,0);//不产生实际的共享文件
    if(mp_src == MAP_FAILED)
    {   
        perror("mp_src err");
        return -1;
    }

    void* mp_dst = mmap(NULL,len,PROT_READ|PROT_WRITE,MAP_SHARED,fd_dst,0);
    if(mp_dst == MAP_FAILED)
    {   
        perror("man err");
        return -1;
    }


    CopyObj tempObj[__THR_CNT__];

    // 创建线程 
    pthread_attr_t attr;
    pthread_attr_init(&attr);//初始化属性
    pthread_attr_setdetachstate(&attr,PTHREAD_CANCEL_DEFERRED);//设置属性分离
    pthread_t tid[5];
    int i;
    for (i = 0; i < __THR_CNT__; ++i) {
        tempObj[i].copySrc = mp_src;
        tempObj[i].copyDst = mp_dst;
        tempObj[i].num = i;
        tempObj[i].size = len;
        pthread_create(&tid[i],&attr,thrFun,&tempObj[i]);
    }
    pthread_attr_destroy(&attr);
    pthread_exit(NULL);
    munmap(mp_src,len);
    munmap(mp_dst,len);


}

2.10 线程同步

2.10.1 同步概念

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A6M9zp7O-1659943933402)(image/image-20220714202547333.png)]

  • 所谓同步,即同时起步,协调一致。不同的对象,对“同步”的理解方式略有不同。
  • 设备同步,是指在两设备之间规定一个共同的时间参考;
  • 数据库同步,是指让两个或多个数据库内容保持一致,或者按需要部分保持
    一致;
  • 文件同步,是指让两个或多个文件夹里的文件保持一致。等等
线程同步的概念
  • 线程同步简单说就是线程排队
  • 线程同步是一种制约关系,一个线程的执行依赖另一个线程消息。
    当它没有得到另一个线程的消息时应等待,得到消息被唤醒
  • 线程同步使得多个线程协调工作从而带到一致性

为什么要有线程同步?

  • 共享资源,多个线程都可对共享资源操作 [容易产生冲突]
  • 线程操作共享资源的先后顺序不确定
  • cpu处理器对存储器的操作一般不是原子操作

举个例子:
两个线程都把全局变量增加1,这个操作平台需要三条指令完成
从内存读到寄存器 →寄存器的值加1 →将寄存器的值写会内存。
如果此时线程A取值在寄存器修改还未写入内存,线程B就从内存取值就会导致两次操作实际上只修改过一次。或者说后一次线程做的事情覆盖前一次线程做的事情。实际上就执行过一次线程。

数据混乱的原因
  1. 资源共享(独享资源则不会)
  2. 调度随机(意味着数据访问会出现竞争)
  3. 线程间缺乏必要的同步机制

​ 以上3点中,前两点不能改变,欲提高效率,传递数据,资源必须共享。只要共享资源,就一定会出现竞争。只有存在竞争关系,数据就容易出现混乱。

​ 所以只能从第三点着手解决。使多个线程在访问共享资源的时候,出现互斥。

2.10.2 互斥量 mutex

基本概念
  • Linux 中提供一把互斥锁 mutex(也称之为互斥量)。
  • 每个线程在对资源操作前都尝试先加锁,成功加锁才能操作,操作结束解锁。
  • 资源还是共享的,线程间也还是竞争的,但通过“锁”就将资源的访问变成互斥操作,而后与时间有关的错误也不会再产生了。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Qypw4i2W-1659943933404)(image/image-20220714210229288.png)]

函数使用
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <stdlib.h>

pthread_mutex_t muetx = PTHREAD_MUTEX_INITIALIZER;

void* thr1(void* arg)
{
    while(1)
    {
        //先上锁
        pthread_mutex_lock(&muetx);//加锁,当线程已经被加锁的时候会阻塞
        printf("hello");
        printf("world\n");
        //释放锁
        pthread_mutex_unlock(&muetx);
        sleep(rand()%3);
    }
}

void* thr2(void* arg)
{
    while(1)
    {
        pthread_mutex_lock(&muetx);
        printf("HELLO");
        printf("WORLD\n");
        pthread_mutex_unlock(&muetx);
        sleep(rand()%3);
    }
}
int main()
{
    pthread_t tid1,tid2;

    pthread_create(&tid1,NULL,thr1,NULL);
    pthread_create(&tid2,NULL,thr2,NULL);
    
    pthread_join(tid1,NULL);//有序回收,阻塞等待
    pthread_join(tid2,NULL);//有序回收,阻塞等待  
    pthread_exit(NULL);
}
pthread_mutex_t 类型

​ 其本质是一个结构体。为简化理解,应用时可忽略其实现细节,简单当成整数看待。变量 mutex 只有两种取值 1、0。

pthread_mutex_init 函数

作用:初始化一个互斥锁—> 初值可看作 1

       #include <pthread.h>
       int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
       pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

参 1:传出参数,调用时应传 &mutex
参 2:互斥量属性。是一个传入参数,通常传 NULL,选用默认属性(线程间共享)。

静态初始化和动态初始化
  • 静态初始化:如果互斥锁 mutex 是静态分配的(定义在全局,或加了 static 关键字修饰),可以直接使用宏进行初始化。e.g. pthread_mutex_t muetx = PTHREAD_MUTEX_INITIALIZER;
  • 动态初始化:局部变量应采用动态初始化。e.g. pthread_mutex_init(&mutex, NULL)
操作锁相关函数
       #include <pthread.h>
       int pthread_mutex_lock(pthread_mutex_t *mutex);
       int pthread_mutex_trylock(pthread_mutex_t *mutex);
       int pthread_mutex_unlock(pthread_mutex_t *mutex);
pthread_mutex_lock 函数

作用:加锁。可理解为将 mutex–(或 -1),操作后 mutex 的值为 0。

int pthread_mutex_lock(pthread_mutex_t *mutex);

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>

pthread_mutex_t muetx;

void* thr(void* arg)
{
    while(1)
    {
        //先上锁
        pthread_mutex_lock(&muetx);//加锁,当线程已经被加锁的时候会阻塞
        printf("hello world\n");
        sleep(10);
        //释放锁
        pthread_mutex_unlock(&muetx);
        
    }
    return NULL;
}


int main()
{
    pthread_t tid;
    pthread_mutex_init(&muetx,NULL);
    pthread_create(&tid,NULL,thr,NULL);
    sleep(1);
    while(1){
        int ret = pthread_mutex_trylock(&muetx);
        if(ret > 0){
            printf("ret = %d,string:%s\n",ret,strerror(ret));
        }
        sleep(1);
    }
    
    pthread_join(tid,NULL);//有序回收,阻塞等待
  
    pthread_exit(NULL);
}

pthread_mutex_unlock 函数

作用:解锁。可理解为将 mutex ++(或 +1),操作后 mutex 的值为 1。

int pthread_mutex_unlock(pthread_mutex_t *mutex);

pthread_mutex_trylock 函数

作用:尝试加锁

int pthread_mutex_trylock(pthread_mutex_t *mutex);

pthread_mutex_destroy 函数

作用:销毁一个互斥锁

int pthread_mutex_destroy(pthread_mutex_t *mutex)

互斥量使用步骤
  1. 初始化
  2. 加锁
  3. 执行逻辑-操作共享数据
  4. 解锁

**注意事项:**加锁需要最小粒度,不要一直占用

2.10.3 死锁

  • 锁了又锁,自己加了一次锁成功,又加了一次。
  • 交叉锁

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ev7nMl6i-1659943933406)(image/image-20220716231330385.png)]

互斥量只是建议锁

2.10.4读写锁

​ 与互斥量类似,但读写锁运行更高的并行性。其特性为:写独占,读共享。

读写锁状态

读写锁仍然是一把锁,有不同的状态

  • 读模式加锁状态(读锁)
  • 写模式下加锁状态(写锁)
  • 不加锁状态
读写锁特性

读写锁使用场景:适合读的线程多

  • 线程A加写锁成功,线程B请求读锁(线程B阻塞)
  • 线程A持有读锁,线程B请求写锁(线程B阻塞)
  • 线程A持有读锁,线程B请求读锁(线程B加锁成功)
  • 线程A持有读锁,线程B请求写锁,线程C请求读锁(BC阻塞,A释放后B加锁,B释放后C加锁)
  • 线程A持有写锁,线程B请求读锁,线程C请求写锁(BC阻塞,A释放后C加锁,C释放后B加锁)

读写之间是互斥的—–>读的时候写阻塞,写的时候读阻塞,而且读和写在竞争锁的时候,写会优先得到锁

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UlC1LZqt-1659943933407)(image/image-20220717141350656.png)]

读写锁常用函数

初始化读写锁

int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t *restrict attr);
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;

销毁读写锁

int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

加读锁

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);

加写锁

int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);

释放锁

int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

小练习:

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

int beginnum = 1000;

pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
void * thr_write(void* arg)
{
    while (1)
    {
        pthread_rwlock_wrlock(&rwlock);
        printf("----%s--slef----%lu----%d\n",__FUNCTION__,pthread_self(),++beginnum);
        usleep(2000);//模拟占用时间
        pthread_rwlock_unlock(&rwlock);
        usleep(3000);
    }
    return NULL;
}
void * thr_read(void* arg)
{
    while (1)
    {
        pthread_rwlock_wrlock(&rwlock);
        printf("----%s--slef----%lu----%d\n",__FUNCTION__,pthread_self(),beginnum);
        usleep(2000);//模拟占用时间
        pthread_rwlock_unlock(&rwlock);
        usleep(4000);
    }
    return NULL;
}

int main()
{
    int n = 8,i = 0;
    pthread_t tid[8];
    for(i = 0; i<  5; i++){
        pthread_create(&tid[i],NULL,thr_read,NULL);
    }
    for(; i<  8; i++){
        pthread_create(&tid[i],NULL,thr_write,NULL);
    }    
    for( i = 0; i < 8; i++)
    {
        pthread_join(tid[i],NULL);
    }
}

2.10.5 条件变量

基本概念

​ 条件变量是用来等待线程而不是上锁的,条件变量通常和互斥锁一起使用。条件变量之所以要和互斥锁一起使用,主要是因为互斥锁的一个明显的特点就是它只有两种状态:锁定和非锁定,而条件变量可以通过允许线程阻塞和等待另一个线程发送信号来弥补互斥锁的不足,所以互斥锁和条件变量通常一起使用。

​ 当条件满足的时候,线程通常解锁并等待该条件发生变化,一旦另一个线程修改了环境变量,就会通知相应的环境变量唤醒一个或者多个被这个条件变量阻塞的线程。这些被唤醒的线程将重新上锁,并测试条件是否满足。一般来说条件变量被用于线程间的同步;当条件不满足的时候,允许其中的一个执行流挂起和等待。

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

条件变量的优点
  • 相较于mutex而言,条件变量可以减少竞争。如果仅仅是mutex,那么,不管共享资源里有没数据,生产者及所有消费都全一窝蜂的去抢锁,会造成资源的浪费。

  • 如直接使用mutex,除了生产者、消费者之间要竞争互斥量以外,消费者之间也需要竞争互斥量,但如果汇聚(链表)中没有数据,消费者之间竞争互斥锁是无意义的。有了条件变量机制以后,只有生产者完成生产,才会引起消费者之间的竞争。提高了程序效率。

条件变量主要应用函数

pthread_cond_init函数

函数原型:

#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

函数作用:

初始化一个条件变量

参数说明:

**cond:**条件变量,调用时应传&cond给该函数

**attr:**条件变量属性,通常传NULL,表示使用默认属性

也可以使用静态初始化的方法,初始化条件变量:pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

pthread_cond_destroy函数

函数原型:int pthread_cond_destroy(pthread_cond_t *cond);

**函数作用:**销毁一个条件变量

pthread_cond_wait函数

函数原型:int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);

函数作用:阻塞等待一个条件变量。具体而言有以下三个作用:

  1. **释放已掌握的互斥锁mutex(解锁互斥量)**相当于pthread_mutex_unlock(&mutex);
  2. 阻塞等待条件变量cond(参1)满足;
  3. 当被唤醒,pthread_cond_wait函数返回时,解除阻塞并重新申请获取互斥锁

其中1、2.两步为一个原子操作。

pthread_cond_timedwait函数

函数原型:int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);

函数作用:限时等待一个条件变量

**参数说明:**前两个比较好理解,重点说明第三个参数。

这里有个struct timespec结构体,可以在man sem_timedwait中查看。结构体原型如下:

struct timespec {
 time_t tv_sec; /* seconds */long tv_nsec; /* nanosecondes*/ 纳秒
}

struct timespec定义的形参abstime是个绝对时间。注意,是绝对时间,不是相对时间。什么是绝对时间?2018年10月1日10:10:00,这就是一个绝对时间。什么是相对时间?给洗衣机定时30分钟洗衣服,就是一个相对时间,也就是说从当时时间开始计算30分钟,诸如此类。

如:time(NULL)返回的就是绝对时间。而alarm(1)是相对时间,相对当前时间定时1秒钟。

adstime所相对的时间是相对于1970年1月1日00:00:00,也就是UNIX计时元年。

下面给出一个错误用法:

struct timespec t = {1, 0};
pthread_cond_timedwait (&cond, &mutex, &t);

这种用法只能定时到 1970年1月1日 00:00:01秒,想必这个时间大家都还没出生。

正确用法:

time_t cur = time(NULL); 获取当前时间。
struct timespec t; 定义timespec 结构体变量t
t.tv_sec = cur+1; 定时1pthread_cond_timedwait (&cond, &mutex, &t); 传参

pthread_cond_signal函数

函数原型:int pthread_cond_signal(pthread_cond_t *cond);

**函数作用:**至少唤醒一个阻塞在条件变量上的线程

pthread_cond_broadcast函数

函数原型:int pthread_cond_broadcast(pthread_cond_t *cond);

**函数作用:**唤醒全部阻塞在条件变量上的线程

生产者消费者模型

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MVGbwo9c-1659943933409)(image/image-20220717154818222.png)]

模型实现

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>
#include <stdlib.h>

pthread_mutex_t muetx = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int beginnum = 1000;

typedef struct _ProdInfo{
    int num;
    struct _ProdInfo* next;
}ProdInfo;

ProdInfo* Head = NULL;

void* thr_producter(void* arg)
{
    //负责在链表上添加数据
    while(1){
        ProdInfo * prod = malloc(sizeof(ProdInfo));
        prod->num = beginnum++;
        printf("----%s------slef=%lu-----%d\n",__FUNCTION__,pthread_self(),prod->num);
        pthread_mutex_lock(&muetx);
        //add to list
        prod->next = Head;
        Head = prod;
        pthread_mutex_unlock(&muetx);
        //发起通知
        pthread_cond_signal(&cond);
        sleep(rand()%2);
    }
    return NULL;
}

void* thr_customer(void* arg)
{
    ProdInfo * prod = NULL;
    while(1)
    {
        //取链表数据
        pthread_mutex_lock(&muetx);  
        while(Head == NULL){
            pthread_cond_wait(&cond,&muetx);//在此之前必须先加锁
        }
        prod = Head;
        Head = Head->next;
        printf("----%s------slef=%lu-----%d\n",__FUNCTION__,pthread_self(),prod->num);
        pthread_mutex_unlock(&muetx);
        sleep(rand()%4);
        free(prod);
    }
}

int main()
{
    pthread_t tid[3];
    pthread_create(&tid[0],NULL,thr_producter,NULL);
    pthread_create(&tid[1],NULL,thr_customer,NULL);
    pthread_create(&tid[2],NULL,thr_customer,NULL);
    pthread_join(tid[0],NULL);//有序回收,阻塞等待
    pthread_join(tid[1],NULL);//有序回收,阻塞等待  
    pthread_join(tid[2],NULL);//有序回收,阻塞等待  
    pthread_mutex_destroy(&muetx);
    pthread_cond_destroy(&cond);
    pthread_exit(NULL);
}

2.10.6 信号量

信号量概述

进化版的互斥锁(1 --> N)

由于互斥锁的粒度比较大,如果我们希望在多个线程间对某一对象的部分数据进行共享,使用互斥锁是没有办法实现的,只能将整个数据对象锁住。这样虽然达到了多线程操作共享数据时保证数据正确性的目的,却无形中导致线程的并发性下降。线程从并行执行,变成了串行执行。与直接使用单进程无异。

信号量,是相对折中的一种处理方式,既能保证同步,数据不混乱,又能提高线程并发。

主要应用函数

sem_init() 函数 功能:初始化一个信号量

sem_wait()函数 功能:给信号量加锁 –

sem_trywait() 函数 功能:尝试对信号量加锁 –

sem_timedwait() 函数 功能:限时尝试对信号量加锁 –

sem_post() 函数 功能:给信号量解锁 ++

sem_destroy() 函数 功能:销毁一个信号量

以上6 个函数的返回值都是:成功返回0, 失败返回-1,同时设置errno。(注意,它们没有pthread前缀)

sem_t类型,本质仍是结构体。但应用期间可简单看作为整数,忽略实现细节(类似于使用文件描述符)。

sem_t sem; 规定信号量sem不能 < 0。头文件 <semaphore.h>

注意:编译时任然要带上-l pthread

1、初始化一个信号量 :int sem_init(sem_t *sem, int pshared, unsigned int value);

  • 参1:sem信号量

  • 参2:pshared取0用于线程间;取非0(一般为1)用于进程间

  • 参3:value指定信号量初值

2、给信号量加锁int sem_wait(sem_t *sem);

3、给信号量解锁 :int sem_post(sem_t *sem);

4、尝试对信号量加锁 (与sem_wait的区别类比lock和trylock) int sem_trywait(sem_t *sem);

5、限时尝试对信号量加锁int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);

  • 参2:abs_timeout采用的是绝对时间。

定时1秒:

time_t cur = time(NULL); 获取当前时间。
struct timespec t; 定义timespec 结构体变量t
t.tv_sec = cur+1; 定时1秒
t.tv_nsec = t.tv_sec +100;
sem_timedwait(&sem, &t); 传参

6、销毁一个信号量:int sem_destroy(sem_t *sem);

生产者消费者信号量模型

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OexLkYI5-1659943933412)(image/image-20220717215150692.png)]

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>

sem_t blank,xfull;
#define _SEM_CNT_   5
int beginnum = 1000;
int queue[_SEM_CNT_];//模拟容器

void *thr_producter(void* arg)
{
    int i = 0;
    while(1){
        sem_wait(&blank);//申请资源 blank--
        printf("----%s------slef=%lu----num--%d\n",__FUNCTION__,pthread_self(),beginnum);
        queue[(i++)&_SEM_CNT_] = beginnum++;
        sem_post(&xfull);//xfull++
        sleep(rand()%3);
    }
    return NULL;
}

void *thr_customer(void* arg)
{
    int i = 0;
    int num = 0;
    while(1){
        sem_wait(&xfull);
        num = queue[(i++)&_SEM_CNT_];
        printf("----%s------slef=%lu----num--%d\n",__FUNCTION__,pthread_self(),num);
        sem_post(&blank);//xfull++
        sleep(rand()%3);
    }
    return NULL;
}

int main()
{
    sem_init(&blank,0,_SEM_CNT_);
    sem_init(&xfull,0,0);//消费者一开始默认没有产品

    pthread_t tid[2];
    pthread_create(&tid[0],NULL,thr_producter,NULL);
    pthread_create(&tid[1],NULL,thr_customer,NULL);
    

    pthread_join(tid[0],NULL);
    pthread_join(tid[1],NULL);
    sem_destroy(&blank);
    sem_destroy(&xfull);
    return 0;
}

2.10.7 进程间同步(扩展了解)

互斥量 mutex

​ 进程间也可以使用互斥锁 ,来达到同步的目的。但应在 pthread_mutex_init 初始化之前,修改其属性为进程间共享。mutex 的属性修改函数主要有以下几个。

主要应用函数

1、pthread_mutexattr_t mutexattr 类型: 用于定义互斥锁的属性。

2、pthread_mutexattr_init 函数: 初始化一个 mutex 属性对象

int pthread_mutexattr_init(pthread_mutexattr_t *attr);

3、pthread_mutexattr_destroy 函数: 销毁 mutex 属性对象(而非销毁锁)

int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);

4、pthread_mutexattr_setpshared 函数: 修改 mutex 属性

int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr,int pshared);

  • 参数 2 :pshared 取值:
    • 线程锁:PTHREAD_PROCESS_PRIVATE( mutex 的默认属性即为线程锁,进程间私有)
    • 进程锁:PTHREAD_PROCESS_SHARED

进程间互斥量操作代码示例

/*
    互斥量 实现 多进程 之间的同步 
*/

#include<unistd.h>
#include<sys/mman.h>
#include<pthread.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<fcntl.h>
#include<string.h>
#include<stdlib.h>
#include<stdio.h>

struct mt
{
    int num;
    pthread_mutex_t mutex;
    pthread_mutexattr_t mutexattr;
};


int main(void)
{
    

int i;
struct mt* mm;

pid_t pid;

/*
// 创建映射区文件
int fd = open("mt_test",O_CREAT|O_RDWR,0777);
if( fd == -1 ) 
{
    perror("open file:"); 
    exit(1); 
}
ftruncate(fd,sizeof(*mm));
mm = mmap(NULL,sizeof(*mm),PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
close(fd);
unlink("mt_test");

  */

// 建立映射区
mm = mmap(NULL,sizeof(*mm),PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANON,-1,0);

//    printf("-------before memset------\n");
    memset(mm,0x00,sizeof(*mm));
//   printf("-------after memset------\n");

pthread_mutexattr_init(&mm->mutexattr);         // 初始化 mutex 属性
pthread_mutexattr_setpshared(&mm->mutexattr, PTHREAD_PROCESS_SHARED);               // 修改属性为进程间共享

pthread_mutex_init(&mm->mutex,&mm->mutexattr);      // 初始化一把 mutex 锁

pid = fork();
if( pid == 0 )          // 子进程
{
    for( i=0; i<10;i++ )
    {
        pthread_mutex_lock(&mm->mutex);
        (mm->num)++;
        printf("-child--------------num++    %d\n",mm->num);
        pthread_mutex_unlock(&mm->mutex);
        sleep(1);
    }

}
else 
{
    for( i=0;i<10;i++)
    {
        sleep(1); 
        pthread_mutex_lock(&mm->mutex);
        mm->num += 2;
        printf("--------parent------num+=2   %d\n",mm->num);
        pthread_mutex_unlock(&mm->mutex);
    
    }
    wait(NULL);

}
pthread_mutexattr_destroy(&mm->mutexattr);  // 销毁 mutex 属性对象
pthread_mutex_destroy(&mm->mutex);          // 销毁 mutex 锁

return 0;

}
文件锁

借助 fcntl 函数来实现文件锁。操作文件的进程没有获得锁时,可以打开,但无法执行 read,write 操

fcntl 函数:获取、设置文件访问控制属性。

       #include <unistd.h>
       #include <fcntl.h>

       int fcntl(int fd, int cmd, ... /* arg */ );
  • 参数 2 :

    1. ​ F_SETLK(struct flock *); 设置文件锁 ( trylock )

    2. ​ F_SETLKW(struct flock*); 设置文件锁( lock ) W — wait

    3. ​ F_GETLK(struct flock*); 获取文件锁

  • 参数 3 :

         struct flock {
                        ...
                        short l_type;    /* Type of lock: F_RDLCK,
                                            F_WRLCK, F_UNLCK */
                        short l_whence;  /* How to interpret l_start:
                                            SEEK_SET, SEEK_CUR, SEEK_END */
                        off_t l_start;   /* Starting offset for lock */
                        off_t l_len;     /* Number of bytes to lock */
                        pid_t l_pid;     /* PID of process blocking our lock
                                            (F_GETLK only) */
                         ...
       };

进程间文件锁 代码示例:

#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdio.h>
#include<stdlib.h>

void sys_err(char*str)
{
    perror(str);
    exit(1);
}


int main(int argc,char *argv[])
{
    int fd;
    struct flock f_lock;

    if( argc< 2 )
    {
        printf("./a.out filename\n");
        exit(1);
    }
     
    if( ( fd = open(argv[1],O_RDWR)) < 0 )
        sys_err("open");

//    f_lock.l_type = F_WRLCK;          // 选用写锁
    f_lock.l_type = F_RDLCK;            // 选用读锁
    f_lock.l_whence = 0;
    f_lock.l_len = 0;                 // 0 表示整个文件加锁

    fcntl(fd,F_SETLKW,&f_lock);
    printf("get flock\n");
     
    sleep(10);
     
    f_lock.l_type = F_UNLCK;
    fcntl(fd,F_SETLKW,&f_lock);
    printf("un flock\n");
     
    close(fd);
    return 0;

}

2.10.8 哲学家用餐模型分析

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-q4HGOXeQ-1659943933414)(image/image-20220717231357592.png)]

2.11 第七次作业

1、实现哲学家就餐问题

#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
#include <time.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>

#define N 5

//信号量使用的参数
sem_t chopsticks[N];
sem_t r;
int philosophers[N] = {0, 1, 2, 3, 4};

//swap指令需要的参数
int islocked[N] = {0};

//互斥量使用的参数
pthread_mutex_t chops[N];

//延迟函数
void delay (int len) {
    int i = rand() % len;
    int x;
    while (i > 0) {
        x = rand() % len;
        while (x > 0) {
            x--;
        }
        i--;
    }
}



//这个函数使用的解决办法是最多允许四个哲学家拿起左筷子
void philosopher (void* arg) {
    int i = *(int *)arg;
    int left = i;
    int right = (i + 1) % N;
    while (1) {
        printf("哲学家%d正在思考问题\n", i);
        delay(50000);
        printf("哲学家%d饿了\n", i);
        sem_wait(&r);

        sem_wait(&chopsticks[left]);
        pthread_mutex_lock(&chops[left]);

        printf("哲学家%d拿起了%d号筷子,现在只有一支筷子,不能进餐\n", i, left);

        sem_wait(&chopsticks[right]);
        pthread_mutex_lock(&chops[right]);

        printf("哲学家%d拿起了%d号筷子, 现在有两支筷子,开始进餐\n", i, right);
        delay(50000);

        sem_post(&chopsticks[left]);
        pthread_mutex_unlock(&chops[left]);

        printf("哲学家%d放下了%d号筷子\n", i, left);

        sem_post(&chopsticks[right]);
        pthread_mutex_unlock(&chops[right]);

        printf("哲学家%d放下了%d号筷子\n", i, right);

        sem_post(&r);
    }
}

//这个函数使用的解决办法是奇数号哲学家先拿左筷子再拿右筷子,而偶数号哲学家相反。
void philosopher2 (void* arg) {
    int i = *(int *)arg;
    int left = i;
    int right = (i + 1) % N;
    while (1) {
        printf("哲学家%d正在思考问题\n", i);
        delay(50000);

        printf("哲学家%d饿了\n", i);
        if (i % 2 == 0) {//偶数哲学家,先右后左
            sem_wait(&chopsticks[right]);
            printf("哲学家%d拿起了%d号筷子,现在只有一支筷子,不能进餐\n", i, right);
            sem_wait(&chopsticks[left]);
            printf("哲学家%d拿起了%d号筷子, 现在有两支筷子,开始进餐\n", i, left);
            delay(50000);
            sem_post(&chopsticks[left]);
            printf("哲学家%d放下了%d号筷子\n", i, left);
            sem_post(&chopsticks[right]);
            printf("哲学家%d放下了%d号筷子\n", i, right);
        } else {//奇数哲学家,先左后又
            sem_wait(&chopsticks[left]);
            printf("哲学家%d拿起了%d号筷子, 现在有两支筷子,开始进餐\n", i, left);
            sem_wait(&chopsticks[right]);
            printf("哲学家%d拿起了%d号筷子,现在只有一支筷子,不能进餐\n", i, right);
            delay(50000);
            sem_post(&chopsticks[right]);
            printf("哲学家%d放下了%d号筷子\n", i, right);
            sem_post(&chopsticks[left]);
            printf("哲学家%d放下了%d号筷子\n", i, left);
        }
    }
}

int main (int argc, char **argv) {
    srand(time(NULL)); //随机数种子
    pthread_t PHD[N];


    for (int i=0; i<N; i++) {
        sem_init(&chopsticks[i], 0, 1);
    }
    sem_init(&r, 0, 4);

    for (int i=0; i<N; i++) {
        pthread_mutex_init(&chops[i], NULL);
    }

    for (int i=0; i<N; i++) {
        pthread_create(&PHD[i], NULL, (void*)philosopher, &philosophers[i]);
    }
    for (int i=0; i<N; i++) {
        pthread_join(PHD[i], NULL);
    }

    for (int i=0; i<N; i++) {
        sem_destroy(&chopsticks[i]);
    }
    sem_destroy(&r);
    for (int i=0; i<N; i++) {
        pthread_mutex_destroy(&chops[i]);
    }
    return 0;
}

2、实现常见两个现象的死锁,锁了又锁,以及交叉锁!

第三章 Linux高并发网络编程

3.1 网络编程基础

3.1.1 协议的概念

什么是协议

从应用的角度出发,协议可理解为“规则”,是数据传输和数据的解释的规则。

假设,A、B双方欲传输文件。规定:

第一次,传输文件名,接收方接收到文件名,应答OK给传输方;

第二次,发送文件的尺寸,接收方接收到该数据再次应答一个OK;

第三次,传输文件内容。同样,接收方接收数据完成后应答OK表示文件内容接收成功。

由此,无论A、B之间传递何种文件,都是通过三次数据传输来完成。A、B之间形成了一个最简单的数据传输规则。双方都按此规则发送、接收数据。A、B之间达成的这个相互遵守的规则即为协议。

这种仅在A、B之间被遵守的协议称之为原始协议。当此协议被更多的人采用,不断的增加、改进、维护、完善。最终形成一个稳定的、完整的文件传输协议,被广泛应用于各种文件传输过程中。该协议就成为一个标准协议。最早的ftp协议就是由此衍生而来。

TCP协议注重数据的传输。http协议着重于数据的解释。

典型协议
  • 传输层 常见协议有TCP/UDP协议。

  • 应用层 常见的协议有HTTP协议,FTP协议。

  • 网络层 常见协议有IP协议、ICMP协议、IGMP协议。

  • 网络接口层 常见协议有ARP协议、RARP协议。

TCP传输控制协议(Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议。

UDP用户数据报协议(User Datagram Protocol)是OSI参考模型中一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务。

HTTP超文本传输协议(Hyper Text Transfer Protocol)是互联网上应用最为广泛的一种网络协议

FTP文件传输协议(File Transfer Protocol)

IP协议是因特网互联协议(Internet Protocol)

ICMP协议是Internet控制报文协议(Internet Control Message Protocol)它是TCP/IP协议族的一个子协议,用于在IP主机路由器之间传递控制消息。

IGMP协议是 Internet 组管理协议(Internet Group Management Protocol),是因特网协议家族中的一个组播协议。该协议运行在主机和组播路由器之间。

ARP协议是正向地址解析协议(Address Resolution Protocol),通过已知的IP,寻找对应主机的MAC地址

RARP是反向地址转换协议,通过MAC地址确定IP地址。

3.1.2 网络应用程序设计模式

C/S模式

​ 传统的网络应用设计模式,客户机(client)/服务器(server)模式。需要在通讯两端各自部署客户机和服务器来完成数据通信。

B/S模式

​ 浏览器()/服务器(server)模式。只需在一端部署服务器,而另外一端使用每台PC都默认配置的浏览器即可完成数据的传输。

优缺点

​ 对于C/S模式来说,其优点明显。客户端位于目标主机上可以保证性能,将数据缓存至客户端本地,从而提高数据传输效率。且,一般来说客户端和服务器程序由一个开发团队创作,所以他们之间所采用的协议相对灵活。可以在标准协议的基础上根据需求裁剪及定制。例如,腾讯公司所采用的通信协议,即为ftp协议的修改剪裁版。

​ 因此,传统的网络应用程序及较大型的网络应用程序都首选C/S模式进行开发。如,知名的网络游戏魔兽世界。3D画面,数据量庞大,使用C/S模式可以提前在本地进行大量数据的缓存处理,从而提高观感。

​ C/S模式的缺点也较突出。由于客户端和服务器都需要有一个开发团队来完成开发。工作量将成倍提升,开发周期较长。另外,从用户角度出发,需要将客户端安插至用户主机上,对用户主机的安全性构成威胁。这也是很多用户不愿使用C/S模式应用程序的重要原因。

​ B/S模式相比C/S模式而言,由于它没有独立的客户端,使用标准浏览器作为客户端,其工作开发量较小。只需开发服务器端即可。另外由于其采用浏览器显示数据,因此移植性非常好,不受平台限制。如早期的偷菜游戏,在各个平台上都可以完美运行。

​ B/S模式的缺点也较明显。由于使用第三方浏览器,因此网络应用支持受限。另外,没有客户端放到对方主机上,缓存数据不尽如人意,从而传输数据量受到限制。应用的观感大打折扣。第三,必须与浏览器一样,采用标准http协议进行通信,协议选择不灵活

​ 因此在开发过程中,模式的选择由上述各自的特点决定。根据实际需求选择应用程序设计模式。

3.1.3 分层模型

OSI七层模型

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-He1yIhkv-1659943933415)(image/image-20220718221717597.png)]

  1. 物理层:主要定义物理设备标准,如网线的接口类型、光纤的接口类型、各种传输介质的传输速率等。它的主要作用是传输比特流(就是由1、0转化为电流强弱来进行传输,到达目的地后再转化为1、0,也就是我们常说的数模转换与模数转换)。这一层的数据叫做比特。
  2. 数据链路层:定义了如何让格式化数据以帧为单位进行传输,以及如何让控制对物理介质的访问。这一层通常还提供错误检测和纠正,以确保数据的可靠传输。如:串口通信中使用到的115200、8、N、1
  3. 网络层:在位于不同地理位置的网络中的两个主机系统之间提供连接和路径选择。Internet的发展使得从世界各站点访问信息的用户数大大增加,而网络层正是管理这种连接的层。
  4. 传输层:定义了一些传输数据的协议和端口号(WWW端口80等),如:TCP(传输控制协议,传输效率低,可靠性强,用于传输可靠性要求高,数据量大的数据),UDP(用户数据报协议,与TCP特性恰恰相反,用于传输可靠性要求不高,数据量小的数据,如QQ聊天数据就是通过这种方式传输的)。 主要是将从下层接收的数据进行分段和传输,到达目的地址后再进行重组。常常把这一层数据叫做段。
  5. 会话层:通过传输层(端口号:传输端口与接收端口)建立数据传输的通路。主要在你的系统之间发起会话或者接受会话请求(设备之间需要互相认识可以是IP也可以是MAC或者是主机名)。
  6. 表示层:可确保一个系统的应用层所发送的信息可以被另一个系统的应用层读取。例如,PC程序与另一台计算机进行通信,其中一台计算机使用扩展二一十进制交换码(EBCDIC),而另一台则使用美国信息交换标准码(ASCII)来表示相同的字符。如有必要,表示层会通过使用一种通格式来实现多种数据格式之间的转换
  7. 应用层:是最靠近用户的OSI层。这一层为用户的应用程序(例如电子邮件、文件传输和终端仿真)提供网络服务。
TCP/IP四层模型

​ TCP/IP协议族是一个四层协议系统,自底而上分别是数据链路层(以太网帧协议)、网络层、传输层和应用层。每一层完成不同的功能,且通过若干协议来实现,上层协议使用下层协议提供的服务。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1nSVwVHn-1659943933416)(image/image-20220718223027160.png)]

一般在应用开发过程中,讨论最多的是TCP/IP模型。

3.1.4 通信过程

​ 两台计算机通过TCP/IP协议通讯的过程如下所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pim5Dvsu-1659943933418)(image/image-20220719190645665.png)]

​ 上图对应两台计算机在同一网段中的情况,如果两台计算机在不同的网段中,那么数据从一台计算机到另一台计算机传输过程中要经过一个或多个路由器,如下图所示:跨路由通信

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KphbtIyq-1659943933419)(image/image-20220719190734589.png)]

​ 链路层有以太网、令牌环网等标准,链路层负责网卡设备的驱动、帧同步(即从网线上检测到什么信号算作新帧的开始)、冲突检测(如果检测到冲突就自动重发)、数据差错校验等工作。交换机是工作在链路层的网络设备,可以在不同的链路层网络之间转发数据帧(比如十兆以太网和百兆以太网之间、以太网和令牌环网之间),由于不同链路层的帧格式不同,交换机要将进来的数据包拆掉链路层首部重新封装之后再转发。

​ 网络层的IP协议是构成Internet的基础。Internet上的主机通过IP地址来标识,Internet上有大量路由器负责根据IP地址选择合适的路径转发数据包,数据包从Internet上的源主机到目的主机往往要经过十多个路由器。路由器是工作在第三层的网络设备,同时兼有交换机的功能,可以在不同的链路层接口之间转发数据包,因此路由器需要将进来的数据包拆掉网络层和链路层两层首部并重新封装。IP协议不保证传输的可靠性,数据包在传输过程中可能丢失,可靠性可以在上层协议或应用程序中提供支持。

​ 网络层负责点到点(ptop,point-to-point)的传输(这里的“点”指主机或路由器),而传输层负责端到端(etoe,end-to-end)的传输(这里的“端”指源主机和目的主机)。传输层可选择TCP或UDP协议。

​ TCP是一种面向连接的、可靠的协议,有点像打电话,双方拿起电话互通身份之后就建立了连接,然后说话就行了,这边说的话那边保证听得到,并且是按说话的顺序听到的,说完话挂机断开连接。也就是说TCP传输的双方需要首先建立连接,之后由TCP协议保证数据收发的可靠性,丢失的数据包自动重发,上层应用程序收到的总是可靠的数据流,通讯之后关闭连接。

​ UDP是无连接的传输协议,不保证可靠性,有点像寄信,信写好放到邮筒里,既不能保证信件在邮递过程中不会丢失,也不能保证信件寄送顺序。使用UDP协议的应用程序需要自己完成丢包重发、消息排序等工作。

目的主机收到数据包后,如何经过各层协议栈最后到达应用程序呢?其过程如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k2TxRJCD-1659943933421)(image/image-20220719191747796.png)]

​ 以太网驱动程序首先根据以太网首部中的“上层协议”字段确定该数据帧的有效载荷(payload,指除去协议首部之外实际传输的数据)是IP、ARP还是RARP协议的数据报,然后交给相应的协议处理。假如是IP数据报,IP协议再根据IP首部中的“上层协议”字段确定该数据报的有效载荷是TCP、UDP、ICMP还是IGMP,然后交给相应的协议处理。假如是TCP段或UDP段,TCP或UDP协议再根据TCP首部或UDP首部的“端口号”字段确定应该将应用层数据交给哪个用户进程。IP地址是标识网络中不同主机的地址,而端口号就是同一台主机上标识不同进程的地址,IP地址和端口号合起来标识网络中唯一的进程。

​ 虽然IP、ARP和RARP数据报都需要以太网驱动程序来封装成帧,但是从功能上划分,ARP和RARP属于链路层,IP属于网络层。虽然ICMP、IGMP、TCP、UDP的数据都需要IP协议来封装成数据报,但是从功能上划分,ICMP、IGMP与IP同属于网络层,TCP和UDP属于传输层。

3.1.5 协议格式

数据包封装

​ 传输层及其以下的机制由内核提供,应用层由用户进程提供(后面将介绍如何使用socket API编写应用程序),应用程序对通讯数据的含义进行解释,而传输层及其以下处理通讯的细节,将数据从一台计算机通过一定的路径发送到另一台计算机。应用层数据通过协议栈发到网络上时,每层协议都要加上一个数据首部(header),称为封装(Encapsulation),如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OUTM5jFw-1659943933422)(image/image-20220719185934992.png)]

​ 不同的协议层对数据包有不同的称谓,在传输层叫做段(segment),在网络层叫做数据报(datagram),在链路层叫做(frame)。数据封装成帧后发到传输介质上,到达目的主机后每层协议再剥掉相应的首部,最后将应用层数据交给应用程序处理。

以太网帧格式

以太网的帧格式如下所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Rv6zTh2T-1659943933423)(image/image-20220719190236105.png)]

​ 其中的源地址和目的地址是指网卡的硬件地址(也叫MAC地址),长度是48位,是在网卡出厂时固化的。可在shell中使用ifconfig命令查看,“HWaddr 00:15:F2:14:9E:3F”部分就是硬件地址。协议字段有三种值,分别对应IP、ARP、RARP。帧尾是CRC校验码。

​ 以太网帧中的数据长度规定最小46字节,最大1500字节,ARP和RARP数据包的长度不够46字节,要在后面补填充位。最大值1500称为以太网的最大传输单元(MTU),不同的网络类型有不同的MTU,如果一个数据包从以太网路由到拨号链路上,数据包长度大于拨号链路的MTU,则需要对数据包进行分片(fragmentation)。ifconfig命令输出中也有“MTU:1500”。注意,MTU这个概念指数据帧中有效载荷的最大长度,不包括帧头长度。

ARP数据报格式

​ 在网络通讯时,源主机的应用程序知道目的主机的IP地址和端口号,却不知道目的主机的硬件地址,而数据包首先是被网卡接收到再去处理上层协议的,如果接收到的数据包的硬件地址与本机不符,则直接丢弃。因此在通讯前必须获得目的主机的硬件地址。ARP协议就起到这个作用。源主机发出ARP请求,询问“IP地址是192.168.0.1的主机的硬件地址是多少”,并将这个请求广播到本地网段(以太网帧首部的硬件地址填FF:FF:FF:FF:FF:FF表示广播),目的主机接收到广播的ARP请求,发现其中的IP地址与本机相符,则发送一个ARP应答数据包给源主机,将自己的硬件地址填写在应答包中。

​ 每台主机都维护一个ARP缓存表,可以用arp -a命令查看。缓存表中的表项有过期时间(一般为20分钟),如果20分钟内没有再次使用某个表项,则该表项失效,下次还要发ARP请求来获得目的主机的硬件地址。想一想,为什么表项要有过期时间而不是一直有效?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xrVo1QsK-1659943933425)(image/image-20220719200630134.png)]

​ 源MAC地址、目的MAC地址在以太网首部和ARP请求中各出现一次,对于链路层为以太网的情况是多余的,但如果链路层是其它类型的网络则有可能是必要的。硬件类型指链路层网络类型,1为以太网,协议类型指要转换的地址类型,0x0800为IP地址,后面两个地址长度对于以太网地址和IP地址分别为6和4(字节),op字段为1表示ARP请求,op字段为2表示ARP应答。

IP段格式

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LOUBJYAC-1659943933426)(image/image-20220719201800215.png)]

​ IP数据报的首部长度和数据长度都是可变长的,但总是4字节的整数倍。对于IPv4,4位版本字段是4。4位首部长度的数值是以4字节为单位的,最小值为5,也就是说首部长度最小是4x5=20字节,也就是不带任何选项的IP首部,4位能表示的最大值是15,也就是说首部长度最大是60字节。8位TOS字段有3个位用来指定IP数据报的优先级(目前已经废弃不用),还有4个位表示可选的服务类型(最小延迟、最大?吐量、最大可靠性、最小成本),还有一个位总是0。总长度是整个数据报(包括IP首部和IP层payload)的字节数。

每传一个IP数据报,16位的标识加1,可用于分片和重新组装数据报。3位标志和13位片偏移用于分片。

TTL(Time to live)是这样用的:源主机为数据包设定一个生存时间,比如64,每过一个路由器就把该值减1,如果减到0就表示路由已经太长了仍然找不到目的主机的网络,就丢弃该包,因此这个生存时间的单位不是秒,而是跳(hop)。协议字段指示上层协议是TCP、UDP、ICMP还是IGMP。然后是校验和,只校验IP首部,数据的校验由更高层协议负责。IPv4的IP地址长度为32位。

想一想,前面讲了以太网帧中的最小数据长度为46字节,不足46字节的要用填充字节补上,那么如何界定这46字节里前多少个字节是IP、ARP或RARP数据报而后面是填充字节?

UDP数据包格式

16位源端口:

16位目的端口

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xSTuEKIz-1659943933427)(image/image-20220719211411999.png)]

TCP数据报格式

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EUhml1b8-1659943933428)(image/image-20220719211724381.png)]

​ 与UDP协议一样也有源端口号和目的端口号,通讯的双方由IP地址和端口号标识。32位序号、32位确认序号、窗口大小稍后详细解释。4位首部长度和IP协议头类似,表示TCP协议头的长度,以4字节为单位,因此TCP协议头最长可以是4x15=60字节,如果没有选项字段,TCP协议头最短20字节。URG、ACK、PSH、RST、SYN、FIN是六个控制位,本节稍后将解释SYN、ACK、FIN、RST四个位,其它位的解释从略。16位检验和将TCP协议头和数据都计算在内。紧急指针和各种选项的解释从略

3.1.6 TCP/UDP传输层协议

tcp:面向连接的安全带流式传输协议

  • 连接的时候,进行上次握手
  • 数据发送到时候,会进行数据确认
    • 数据丢失之后,会进行数据重传

udp:面向无连接的不安全的报式传输

  • 连接的时候不会握手
  • 数据发送出去之后就不管了
TCP通信时序

下图是一次TCP通讯的时序图。TCP连接建立断开。包含大家熟知的三次握手和四次握手。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8xFzHSd5-1659943933429)(image/image-20220720222014611.png)]

​ 在这个例子中,首先客户端主动发起连接、发送请求,然后服务器端响应请求,然后客户端主动关闭连接。两条竖线表示通讯的两端,从上到下表示时间的先后顺序,注意,数据从一端传到网络的另一端也需要时间,所以图中的箭头都是斜的。双方发送的段按时间顺序编号为1-10,各段中的主要信息在箭头上标出,例如段2的箭头上标着SYN, 8000(0), ACK1001, ,表示该段中的SYN位置1,32位序号是8000,该段不携带有效载荷(数据字节数为0),ACK位置1,32位确认序号是1001,带有一个mss(Maximum Segment Size,最大报文长度)选项值为1024。

==建立连接(三次握手)==的过程:

  1. 客户端发送一个带SYN标志的TCP报文到服务器。这是三次握手过程中的段1

​ 客户端发出段1,SYN位表示连接请求。序号是1000,这个序号在网络通讯中用作临时的地址,每发一个数据字节,这个序号要加1,这样在接收端可以根据序号排出数据包的正确顺序,也可以发现丢包的情况,另外,规定SYN位和FIN位也要占一个序号,这次虽然没发数据,但是由于发了SYN位,因此下次再发送应该用序号1001。mss表示最大段尺寸,如果一个段太大,封装成帧后超过了链路层的最大帧长度,就必须在IP层分片,为了避免这种情况,客户端声明自己的最大段尺寸,建议服务器端发来的段不要超过这个长度。

  1. 服务器端回应客户端,是三次握手中的第2个报文段,同时带ACK标志和SYN标志。它表示对刚才客户端SYN的回应;同时又发送SYN给客户端,询问客户端是否准备好进行数据通讯

​ 服务器发出段2,也带有SYN位,同时置ACK位表示确认,确认序号是1001,表示“我接收到序号1000及其以前所有的段,请你下次发送序号为1001的段”,也就是应答了客户端的连接请求,同时也给客户端发出一个连接请求,同时声明最大尺寸为1024。

  1. 客户必须再次回应服务器端一个ACK报文,这是报文段3。

​ 客户端发出段3,对服务器的连接请求进行应答,确认序号是8001。在这个过程中,客户端和服务器分别给对方发了连接请求,也应答了对方的连接请求,其中服务器的请求和应答在一个段中发出,因此一共有三个段用于建立连接,称为“三方握手(three-way-handshake)”。在建立连接的同时,双方协商了一些信息,例如双方发送序号的初始值、最大段尺寸等。

在TCP通讯中,如果一方收到另一方发来的段,读出其中的目的端口号,发现本机并没有任何进程使用这个端口,就会应答一个包含RST位的段给另一方。例如,服务器并没有任何进程使用8080端口,我们却用telnet客户端去连接它,服务器收到客户端发来的SYN段就会应答一个RST段,客户端的telnet程序收到RST段后报告错误Connection refused:

$ telnet 192.168.0.200 8080

Trying 192.168.0.200...

telnet: Unable to connect to remote host: Connection refused

数据传输的过程:

  1. 客户端发出段4,包含从序号1001开始的20个字节数据。

  2. 服务器发出段5,确认序号为1021,对序号为1001-1020的数据表示确认收到,同时请求发送序号1021开始的数据,服务器在应答的同时也向客户端发送从序号8001开始的10个字节数据,这称为piggyback。

  3. 客户端发出段6,对服务器发来的序号为8001-8010的数据表示确认收到,请求发送序号8011开始的数据。

​ 在数据传输过程中,ACK和确认序号是非常重要的,应用程序交给TCP协议发送的数据会暂存在TCP层的发送缓冲区中,发出数据包给对方之后,只有收到对方应答的ACK段才知道该数据包确实发到了对方,可以从发送缓冲区中释放掉了,如果因为网络故障丢失了数据包或者丢失了对方发回的ACK段,经过等待超时后TCP协议自动将发送缓冲区中的数据包重发。

==关闭连接(四次握手)==的过程:

​ 由于TCP连接是全双工的,因此每个方向都必须单独进行关闭。这原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个 FIN只意味着这一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。

  1. 客户端发出段7,FIN位表示关闭连接的请求。

  2. 服务器发出段8,应答客户端的关闭连接请求。

  3. 服务器发出段9,其中也包含FIN位,向客户端发送关闭连接请求。

  4. 客户端发出段10,应答服务器的关闭连接请求。

​ 建立连接的过程是三方握手,而关闭连接通常需要4个段,服务器的应答和关闭连接请求通常不合并在一个段中,因为有连接半关闭的情况,这种情况下客户端关闭连接之后就不能再发送数据给服务器了,但是服务器还可以发送数据给客户端,直到服务器也关闭连接为止。

滑动窗口 (TCP流量控制)

​ 介绍UDP时我们描述了这样的问题:如果发送端发送的速度较快,接收端接收到数据后处理的速度较慢,而接收缓冲区的大小是固定的,就会丢失数据。TCP协议通过“滑动窗口(Sliding Window)”机制解决这一问题。看下图的通讯过程:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b4A5ibwS-1659943933430)(image/image-20220720231058223.png)]

  1. 发送端发起连接,声明最大段尺寸是1460,初始序号是0,窗口大小是4K,表示“我的接收缓冲区还有4K字节空闲,你发的数据不要超过4K”。接收端应答连接请求,声明最大段尺寸是1024,初始序号是8000,窗口大小是6K。发送端应答,三方握手结束。
  2. 发送端发出段4-9,每个段带1K的数据,发送端根据窗口大小知道接收端的缓冲区满了,因此停止发送数据。
  3. 接收端的应用程序提走2K数据,接收缓冲区又有了2K空闲,接收端发出段10,在应答已收到6K数据的同时声明窗口大小为2K。
  4. 接收端的应用程序又提走2K数据,接收缓冲区有4K空闲,接收端发出段11,重新声明窗口大小为4K。
  5. 发送端发出段12-13,每个段带2K数据,段13同时还包含FIN位。
  6. 接收端应答接收到的2K数据(6145-8192),再加上FIN位占一个序号8193,因此应答序号是8194,连接处于半关闭状态,接收端同时声明窗口大小为2K。
  7. 接收端的应用程序提走2K数据,接收端重新声明窗口大小为4K。
  8. 接收端的应用程序提走剩下的2K数据,接收缓冲区全空,接收端重新声明窗口大小为6K。
  9. 接收端的应用程序在提走全部数据后,决定关闭连接,发出段17包含FIN位,发送端应答,连接完全关闭。

​ 上图在接收端用小方块表示1K数据,实心的小方块表示已接收到的数据,虚线框表示接收缓冲区,因此套在虚线框中的空心小方块表示窗口大小,从图中可以看出,随着应用程序提走数据,虚线框是向右滑动的,因此称为滑动窗口。

​ 从这个例子还可以看出,发送端是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据。也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),在底层通讯中这些数据可能被拆成很多数据包来发送,但是一个数据包有多少字节对应用程序是不可见的,因此==TCP协议是面向流的协议。而UDP是面向消息的协议==,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。

TCP状态转换

​ 这个图N多人都知道,它排除和定位网络或系统故障时大有帮助,但是怎样牢牢地将这张图刻在脑中呢?那么你就一定要对这张图的每一个状态,及转换的过程有深刻的认识,不能只停留在一知半解之中。下面对这张图的11种状态详细解析一下,以便加强记忆!不过在这之前,先回顾一下TCP建立连接的三次握手过程,以及 关闭连接的四次握手过程。

image-20220720234131036
  1. **CLOSED:**表示初始状态。
  2. **LISTEN:**该状态表示服务器端的某个SOCKET处于监听状态,可以接受连接。
  3. **SYN_SENT:**这个状态与SYN_RCVD遥相呼应,当客户端SOCKET执行CONNECT连接时,它首先发送SYN报文,随即进入到了SYN_SENT状态,并等待服务端的发送三次握手中的第2个报文。SYN_SENT状态表示客户端已发送SYN报文。
  4. SYN_RCVD: 该状态表示接收到SYN报文,在正常情况下,这个状态是服务器端的SOCKET在建立TCP连接时的三次握手会话过程中的一个中间状态,很短暂。此种状态时,当收到客户端的ACK报文后,会进入到ESTABLISHED状态。
  5. **ESTABLISHED:**表示连接已经建立。
  6. FIN_WAIT_1: FIN_WAIT_1和FIN_WAIT_2状态的真正含义都是表示等待对方的FIN报文。区别是:
    • FIN_WAIT_1状态是当socket在ESTABLISHED状态时,想主动关闭连接,向对方发送了FIN报文,此时该socket进入到FIN_WAIT_1状态。
    • FIN_WAIT_2状态是当对方回应ACK后,该socket进入到FIN_WAIT_2状态,正常情况下,对方应马上回应ACK报文,所以FIN_WAIT_1状态一般较难见到,而FIN_WAIT_2状态可用netstat看到。
  7. FIN_WAIT_2:主动关闭链接的一方,发出FIN收到ACK以后进入该状态。称之为半连接或半关闭状态。该状态下的socket只能接收数据,不能发。
  8. TIME_WAIT: 表示收到了对方的FIN报文,并发送出了ACK报文,等2MSL后即可回到CLOSED可用状态。如果FIN_WAIT_1状态下,收到对方同时带 FIN标志和ACK标志的报文时,可以直接进入到TIME_WAIT状态,而无须经过FIN_WAIT_2状态。
  9. CLOSING: 这种状态较特殊,属于一种较罕见的状态。正常情况下,当你发送FIN报文后,按理来说是应该先收到(或同时收到)对方的 ACK报文,再收到对方的FIN报文。但是CLOSING状态表示你发送FIN报文后,并没有收到对方的ACK报文,反而却也收到了对方的FIN报文。什么情况下会出现此种情况呢?如果双方几乎在同时close一个SOCKET的话,那么就出现了双方同时发送FIN报文的情况,也即会出现CLOSING状态,表示双方都正在关闭SOCKET连接。
  10. CLOSE_WAIT: 此种状态表示在等待关闭。当对方关闭一个SOCKET后发送FIN报文给自己,系统会回应一个ACK报文给对方,此时则进入到CLOSE_WAIT状态。接下来呢,察看是否还有数据发送给对方,如果没有可以 close这个SOCKET,发送FIN报文给对方,即关闭连接。所以在CLOSE_WAIT状态下,需要关闭连接。
  11. LAST_ACK: 该状态是被动关闭一方在发送FIN报文后,最后等待对方的ACK报文。当收到ACK报文后,即可以进入到CLOSED可用状态。
半关闭

​ 当TCP链接中A发送FIN请求关闭,B端回应ACK后(A端进入FIN_WAIT_2状态),B没有立即发送FIN给A时,A方处在半链接状态,此时A可以接收B发送的数据,但是A已不能再向B发送数据。

从程序的角度,可以使用API来控制实现半连接状态。

#include <sys/socket.h>

int shutdown(int sockfd, int how);

sockfd: 需要关闭的socket的描述符

how:	允许为shutdown操作选择以下几种方式:

SHUT_RD(0):	关闭sockfd上的读功能,此选项将不允许sockfd进行读操作。
			该套接字**不再接受数据**,任何当前在套接字接受缓冲区的数据将被无声的丢弃掉。

SHUT_WR(1):		关闭sockfd的写功能,此选项将不允许sockfd进行写操作。进程不能在对此套接字发出写操作。

SHUT_RDWR(2):	关闭sockfd的读写功能。相当于调用shutdown两次:首先是以SHUT_RD,然后以SHUT_WR。

使用close中止一个连接,但它只是减少描述符的引用计数,并不直接关闭连接,只有当描述符的引用计数为0时才关闭连接。

shutdown不考虑描述符的引用计数,直接关闭描述符。也可选择中止一个方向的连接,只中止读或只中止写。

注意:

  1. 如果有多个进程共享一个套接字,close每被调用一次,计数减1,直到计数为0时,也就是所用进程都调用了close,套接字将被释放。

  2. 在多进程中如果一个进程调用了shutdown(sfd, SHUT_RDWR)后,其它的进程将无法进行通信。但,如果一个进程close(sfd)将不会影响到其它进程。

2MSL

​ 2MSL (Maximum Segment Lifetime) TIME_WAIT状态的存在有两个理由:

(1)让4次握手关闭流程更加可靠;4次握手的最后一个ACK是是由主动关闭方发送出去的,若这个ACK丢失,被动关闭方会再次发一个FIN过来。若主动关闭方能够保持一个2MSL的TIME_WAIT状态,则有更大的机会让丢失的ACK被再次发送出去。

(2)防止lost duplicate对后续新建正常链接的传输造成破坏。lost uplicate在实际的网络中非常常见,经常是由于路由器产生故障,路径无法收敛,导致一个packet在路由器A,B,C之间做类似死循环的跳转。IP头部有个TTL,限制了一个包在网络中的最大跳数,因此这个包有两种命运,要么最后TTL变为0,在网络中消失;要么TTL在变为0之前路由器路径收敛,它凭借剩余的TTL跳数终于到达目的地。但非常可惜的是TCP通过超时重传机制在早些时候发送了一个跟它一模一样的包,并先于它达到了目的地,因此它的命运也就注定被TCP协议栈抛弃。

​ 另外一个概念叫做incarnation connection,指跟上次的socket pair一摸一样的新连接,叫做incarnation of previous connection。lost uplicate加上incarnation connection,则会对我们的传输造成致命的错误。

​ TCP是流式的,所有包到达的顺序是不一致的,依靠序列号由TCP协议栈做顺序的拼接;假设一个incarnation connection这时收到的seq=1000, 来了一个lost duplicate为seq=1000,len=1000, 则TCP认为这个lost duplicate合法,并存放入了receive buffer,导致传输出现错误。通过一个2MSL TIME_WAIT状态,确保所有的lost duplicate都会消失掉,避免对新连接造成错误。

该状态为什么设计在主动关闭这一方

(1)发最后ACK的是主动关闭一方。

(2)只要有一方保持TIME_WAIT状态,就能起到避免incarnation connection在2MSL内的重新建立,不需要两方都有。

如何正确对待2MSL TIME_WAIT?

​ RFC要求socket pair在处于TIME_WAIT时,不能再起一个incarnation connection。但绝大部分TCP实现,强加了更为严格的限制。在2MSL等待期间,socket中使用的本地端口在默认情况下不能再被使用。

​ 若A 10.234.5.5 : 1234和B 10.55.55.60 : 6666建立了连接,A主动关闭,那么在A端只要port为1234,无论对方的port和ip是什么,都不允许再起服务。这甚至比RFC限制更为严格,RFC仅仅是要求socket pair不一致,而实现当中只要这个port处于TIME_WAIT,就不允许起连接。这个限制对主动打开方来说是无所谓的,因为一般用的是临时端口;但对于被动打开方,一般是server,就悲剧了,因为server一般是熟知端口。比如http,一般端口是80,不可能允许这个服务在2MSL内不能起来。

解决方案是给服务器的socket设置SO_REUSEADDR选项,这样的话就算熟知端口处于TIME_WAIT状态,在这个端口上依旧可以将服务启动。当然,虽然有了SO_REUSEADDR选项,但sockt pair这个限制依旧存在。比如上面的例子,A通过SO_REUSEADDR选项依旧在1234端口上起了监听,但这时我们若是从B通过6666端口去连它,TCP协议会告诉我们连接失败,原因为Address already in use.

RFC 793中规定MSL为2分钟,实际应用中常用的是30秒,1分钟和2分钟等。

RFC (Request For Comments),是一系列以编号排定的文件。收集了有关因特网相关资讯,以及UNIX和因特网社群的软件文件。

程序设计中的问题

做一个测试,首先启动server,然后启动client,用Ctrl-C终止server,马上再运行server,运行结果:

itcast$ ./server
bind error: Address already in use 

这是因为,虽然server的应用程序终止了,但TCP协议层的连接并没有完全断开,因此不能再次监听同样的server端口。我们用netstat命令查看一下:

itcast$ netstat -apn |grep 6666

tcp     1    0 192.168.1.11:38103    192.168.1.11:6666    CLOSE_WAIT  3525/client   

tcp     0    0 192.168.1.11:6666    192.168.1.11:38103    FIN_WAIT2  -  

server终止时,socket描述符会自动关闭并发FIN段给client,client收到FIN后处于CLOSE_WAIT状态,但是client并没有终止,也没有关闭socket描述符,因此不会发FIN给server,因此server的TCP连接处于FIN_WAIT2状态。

现在用Ctrl-C把client也终止掉,再观察现象:

itcast$ netstat -apn |grep 6666

tcp     0    0 192.168.1.11:6666    192.168.1.11:38104    TIME_WAIT  -

itcast$ ./server

bind error: Address already in use

client终止时自动关闭socket描述符,server的TCP连接收到client发的FIN段后处于TIME_WAIT状态。TCP协议规定,主动关闭连接的一方要处于TIME_WAIT状态,等待两个MSL(maximum segment lifetime)的时间后才能回到CLOSED状态,因为我们先Ctrl-C终止了server,所以server是主动关闭连接的一方,在TIME_WAIT期间仍然不能再次监听同样的server端口。

MSL在RFC 1122中规定为两分钟,但是各操作系统的实现不同,在Linux上一般经过半分钟后就可以再次启动server了。至于为什么要规定TIME_WAIT的时间,可参考UNP 2.7节。

端口复用

​ 在server的TCP连接没有完全断开之前不允许重新监听是不合理的。因为,TCP连接没有完全断开指的是connfd(127.0.0.1:6666)没有完全断开,而我们重新监听的是lis-tenfd(0.0.0.0:6666),虽然是占用同一个端口,但IP地址不同,connfd对应的是与某个客户端通讯的一个具体的IP地址,而listenfd对应的是wildcard address。解决这个问题的方法是使用setsockopt()设置socket描述符的选项SO_REUSEADDR为1,表示允许创建端口号相同但IP地址不同的多个socket描述符。

在server代码的socket()和bind()调用之间插入如下代码:

	int opt = 1;

	setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

有关setsockopt可以设置的其它选项请参考UNP第7章。

TCP异常断开
心跳检测机制

​ 在TCP网络通信中,经常会出现客户端和服务器之间的非正常断开,需要实时检测查询链接状态。常用的解决方法就是在程序中加入心跳机制。

Heart-Beat线程

​ 这个是最常用的简单方法。在接收和发送数据时个人设计一个守护进程(线程),定时发送Heart-Beat包,客户端/服务器收到该小包后,立刻返回相应的包即可检测对方是否实时在线。

该方法的好处是通用,但缺点就是会改变现有的通讯协议!大家一般都是使用业务层心跳来处理,主要是灵活可控。

UNIX网络编程不推荐使用SO_KEEPALIVE来做心跳检测,还是在业务层以心跳包做检测比较好,也方便控制。

设置TCP属性

​ SO_KEEPALIVE 保持连接检测对方主机是否崩溃,避免(服务器)永远阻塞于TCP连接的输入。设置该选项后,如果2小时内在此套接口的任一方向都没有数据交换,TCP就自动给对方发一个保持存活探测分节(keepalive probe)。这是一个对方必须响应的TCP分节.它会导致以下三种情况:

  • 对方接收一切正常:以期望的ACK响应。2小时后,TCP将发出另一个探测分节。
  • 对方已崩溃且已重新启动:以RST响应。套接口的待处理错误被置为ECONNRESET,套接 口本身则被关闭。
  • 对方无任何响应:源自berkeley的TCP发送另外8个探测分节,相隔75秒一个,试图得到一个响应。

​ 在发出第一个探测分节11分钟 15秒后若仍无响应就放弃。套接口的待处理错误被置为ETIMEOUT,套接口本身则被关闭。如ICMP错误是“host unreachable(主机不可达)”,说明对方主机并没有崩溃,但是不可达,这种情况下待处理错误被置为EHOSTUNREACH。

根据上面的介绍我们可以知道对端以一种非优雅的方式断开连接的时候,我们可以设置SO_KEEPALIVE属性使得我们在2小时以后发现对方的TCP连接是否依然存在。

	keepAlive = 1;

	setsockopt(listenfd, SOL_SOCKET, SO_KEEPALIVE, (void*)&keepAlive, sizeof(keepAlive));

​ 如果我们不能接受如此之长的等待时间,从TCP-Keepalive-HOWTO上可以知道一共有两种方式可以设置,一种是修改内核关于网络方面的 配置参数,另外一种就是SOL_TCP字段的TCP_KEEPIDLE, TCP_KEEPINTVL, TCP_KEEPCNT三个选项。

  1. The tcp_keepidle parameter specifies the interval of inactivity that causes TCP to generate a KEEPALIVE transmission for an application that requests them. tcp_keepidle defaults to 14400 (two hours).

/*开始首次KeepAlive探测前的TCP空闭时间 */

  1. The tcp_keepintvl parameter specifies the interval between the nine retriesthat are attempted if a KEEPALIVE transmission is not acknowledged. tcp_keep ntvldefaults to 150 (75 seconds).

/* 两次KeepAlive探测间的时间间隔 */

  1. The tcp_keepcnt option specifies the maximum number of keepalive probes tobe sent. The value of TCP_KEEPCNT is an integer value between 1 and n, where n s the value of the systemwide tcp_keepcnt parameter.

/* 判定断开前的KeepAlive探测次数*/

int keepIdle = 1000;

int keepInterval = 10;

int keepCount = 10;

 
Setsockopt(listenfd, SOL_TCP, TCP_KEEPIDLE, (void *)&keepIdle, sizeof(keepIdle));

Setsockopt(listenfd, SOL_TCP,TCP_KEEPINTVL, (void *)&keepInterval, sizeof(keepInterval));

Setsockopt(listenfd,SOL_TCP, TCP_KEEPCNT, (void *)&keepCount, sizeof(keepCount));

SO_KEEPALIVE设置空闲2小时才发送一个“保持存活探测分节”,不能保证实时检测。对于判断网络断开时间太长,对于需要及时响应的程序不太适应。

当然也可以修改时间间隔参数,但是会影响到所有打开此选项的套接口!关联了完成端口的socket可能会忽略掉该套接字选项。

3.2 socket编程

什么是socket?

  • 网络通信的函数借口
  • 封装了传输层的协议
    • TCP
    • UDP

浏览器——http协议

  • 封装的是TCP

3.2.1 套接字概念

​ Socket本身有“插座”的意思,在Linux环境下,用于表示进程间网络通信的特殊文件类型。本质为内核借助缓冲区形成的伪文件。

​ 既然是文件,那么理所当然的,我们可以使用文件描述符引用套接字。与管道类似的,Linux系统将其封装成文件的目的是为了统一接口,使得读写套接字和读写文件的操作一致。区别是管道主要应用于本地进程间通信,而套接字多应用于网络进程间数据的传递

​ 套接字的内核实现较为复杂,不宜在学习初期深入学习。

​ 在TCP/IP协议中,“IP地址+TCP或UDP端口号”唯一标识网络通讯中的一个进程。“IP地址+端口号”就对应一个socket。欲建立连接的两个进程各自有一个socket来标识,那么这两个socket组成的socket pair就唯一标识一个连接。因此可以用Socket来描述网络连接的一对一关系。

​ 套接字通信原理如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-S1AD1b0j-1659943933432)(image/image-20220721225806963.png)]

​ **在网络通信中,套接字一定是成对出现的。**一端的发送缓冲区对应对端的接收缓冲区。我们使用同一个文件描述符索发送缓冲区和接收缓冲区。

​ TCP/IP协议最早在BSD UNIX上实现,为TCP/IP协议设计的应用层编程接口称为socket API。本章的主要内容是socket API,主要介绍TCP协议的函数接口,最后介绍UDP协议和UNIX Domain Socket的函数接口。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e5nX8z7M-1659943933433)(image/image-20220721225922978.png)]

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值