文章目录
Linux系统编程
第一章 shell
1.1 shell家族
- shell:命令解释器,根据输入的命令执行相应命令。
- 察看当前系统下有哪些shell:
cat /etc/shells - 察看当前系统正在使用的shell
echo $SHELL - 常见shell:
/bin/sh (已经被 /bin/bash 所取代)
/bin/bash (就是 Linux 默认的 shell)
/bin/ksh (Kornshell 由 AT&T Bell lab. 发展出来的,兼容于 bash)
/bin/tcsh (整合 C Shell ,提供更多的功能)
/bin/csh (已经被 /bin/tcsh 所取代)
/bin/zsh (基于 ksh 发展出来的,功能更强大的 shell)
1.2 命令和路径补齐
-
在bash下敲命令时,Tab键可以补全已经敲了一部分的文件名和目录名。
-
历史记录
history
-
主键盘快捷键
上 Ctrl-p previous 下 Ctrl-n next 左 Ctrl-b backward 右 Ctrl-f forward Del Ctrl-d Home Ctrl-a the first letter End Ctrl-e end Backspace Backspace
第二章 目录和文件
Linux系统: “所见皆文件”
2.1 Linux系统目录
- bin:存放二进制可执行文件
- boot:存放开机启动程序
- dev:存放设备文件: 字符设备、块设备
- home:存放普通用户
- etc:用户信息和系统配置文件 passwd、group
- lib:库文件:libc.so.6
- root:管理员宿主目录(家目录)
- usr:用户资源管理目录
2.2 系统文件类型
- 普通文件:-
- 目录文件:d
- 字符设备文件:c
- 块设备文件:b
- 软连接:l
- 管道文件:p
- 套接字:s
- 未知文件
2.3 软硬连接
-
软连接:快捷方式
为保证软连接可以任意搬移,创建时务必对源文件使用绝对路径。
-
硬链接:
ln file file.hard
- 操作系统给每一个文件赋予唯一的 inode,当有相同inode的文件存在时,彼此同步。
- 删除时,只将硬链接计数减一。减为0时,inode 被释放。
第三章 文件属性和用户用户组
3.1 用户
-
创建用户:
sudo adduser 新用户名
-
修改文件所属用户:
sudo chown 新用户名 待修改文件。
sudo chown wangwu a.c
-
删除用户:
sudo deluser 用户名
3.2 用户组
-
创建用户组:
sudo addgroup 新组名
-
修改文件所属用户组:
sudo chgrp 新用户组名 待修改文件。
sudo chgrp g88 a.c
-
删除组:
sudo delgroup 用户组名
-
使用chown 一次修改所有者和所属组:
sudo chown 所有者:所属组 待操作文件。
第四章 查找与检索
4.1 find
find命令:找文件
-
-type 按文件类型搜索 d/p/s/c/b/l/ f:文件(普通文件是f)
-
-name 按文件名搜索
find ./ -name "*file*.jpg"(*是通配符,*代表0-n;?代表一个)
-
-maxdepth 指定搜索深度。应作为第一个参数出现。
find ./ -maxdepth 1 -name "*file*.jpg"
-
-size 按文件大小搜索. 单位:k、M、G
find /home/itcast -size +20M -size -50M//大于20,小于50
-
-atime、mtime、ctime 天 amin、mmin、cmin 分钟。
(a是access访问时间,m是modify属性修改时间,c是change内容修改时间)
-
-exec:将find搜索的结果集执行某一指定命令。
find /usr/ -name '*tmp*' -exec ls -ld {} \;// \表示转义字符,这里代表转本义,让它作为结束标记,;代表语句结束
-
-ok: 以交互式的方式 将find搜索的结果集执行某一指定命令
-
-xargs:将find搜索的结果集执行某一指定命令。 当结果集数量过大时,可以分片映射。相比于exec效率更高。
find /usr/ -name '*tmp*' | xargs ls -ld
-
-print0:确保每个输出文件名后面跟着一个空字符(null character),这对于处理包含空格或特殊字符的文件名非常有用
find /usr/ -name '*tmp*' -print0 | xargs -0 ls -ld
4.2 grep
grep命令:找文件当中的内容
-
-i
:忽略大小写,不区分大小写地进行匹配。 -
-v
:反向匹配,显示不匹配模式的行。 -
-n
:显示匹配行的行号。 -
-l
:只显示包含匹配模式的文件名,而不显示匹配的行。 -
-r
:递归搜索,搜索指定目录下的所有文件和子目录。 -
-w
:只匹配整个单词,而不是部分匹配。 -
-c
:只显示匹配的行数,而不显示匹配的行内容。 -
-A num
:显示匹配行和后面的 num 行。 -
-B num
:显示匹配行和前面的 num 行。 -
-C num
:显示匹配行和前后的 num 行。 -
grep -r 'copy' ./ -n -n参数::显示行号
-
ps aux | grep 'cupsd' -- 检索进程结果集。
第五章 安装卸载软件
5.1 软件安装
- 联网
- 更新软件资源列表到本地。 sudo apt-get update
- 安装 sudo apt-get install 软件名
- 卸载 sudo apt-get remove 软件名
- 使用软件包(.deb) 安装: sudo dpkg -i 安装包名
5.2 解压缩
-
tar压缩:
tar -zcvf 要生成的压缩包名 压缩材料。 tar zcvf test.tar.gz file1 dir2 使用 gzip方式压缩。 tar jcvf test.tar.gz file1 dir2 使用 bzip2方式压缩。
-
tar解压:
//将 压缩命令中的 c --> x tar zxvf test.tar.gz 使用 gzip方式解压缩。 tar jxvf test.tar.gz 使用 bzip2方式解压缩。
-
rar压缩:
rar a -r 压缩包名(带.rar后缀) 压缩材料。 rar a -r testrar.rar stdio.h test2.mp3
-
rar解压:
unrar x 压缩包名(带.rar后缀)
-
zip压缩:
zip -r 压缩包名(带.zip后缀) 压缩材料。 zip -r testzip.zip dir stdio.h test2.mp3
-
zip解压:
unzip 压缩包名(带.zip后缀) unzip testzip.zip
第六章 进程管理
6.1 who
- 查看当前在线上的用户情况。所有的选项都是可选的,不使用任何选项时,who命令将显示以下三项内容:
- login name:登录用户名
- terminal line:使用终端设备
- login time:登录到系统的时间
6.2 ps
- ps命令用于监控后台进程的工作情况,因为后台进程是不和屏幕键盘这些标准输入/输出设备进行通信的,所以如果需要检测其情况,便可以使用ps命令了。
- -e 显示所有进程。
- -f 全格式。
- -h 不显示标题。
- -l 长格式。
- -w 宽输出。
- -r 只显示正在运行的进程。
- -a:即all,查看当前系统所有用户的所有进程
- -u:查看进程所有者及其他一些详细信息
- -x:显示没有控制终端的进程
- 这个命令参数有很多,但一般的用户只需掌握一些最常用的命令参数就可以了。 最常用的三个参数是u、a、x
6.3 其他常用指令
-
jobs:用来显示当前shell 下正在运行哪些作业(即后台作业)
-
fg [job…]:指定的后台作业或挂起作业移到前台运行。 参数job是一个或多个进程的PID,或者是命令名称,或者是作业号(作业号前面要带一个%号)
-
bg [job…]:把被挂起的进程提到后台执行。 其中,job是一个或多个进程的PID、命令名称或者作业号,在参数前要带%号
-
kill:向指定进程发送信号
-
env:查看当前进程环境变量
- vim ∼/.bashrc:配置当前用户环境变量
- vim /etc/profile:配置系统环境变量,配置时需要有root权限
- export PATH=$PATH:新路径
-
top:动态监视系统的进程和系统资源的使用情况
1
:显示每个CPU核心的详细信息。m
:按内存使用情况排序。P
:按CPU使用率排序。T
:按运行时间排序。
第七章 用户管理
7.1 创建用户
-
sudo useradd -s /bin/bash -g itcast -d /home/itcast -m itcast
sudo useradd -s /bin/sh -g group -G adm,root xwp
此命令新建了一个用户xwp,该用户的登录Shell是/bin/sh,他属于group用户组,同时
又属于adm和root用户组,其中group用户组是其主组
- -s 指定新用户登陆时shell类型
- -g 指定所属组,该组必须已经存在
- -G 指定附属组,该组必须已经存在
- -d 用户家目录
- -m 用户家目录不存在时,自动创建该目录
7.2 创建用户组
- sudo groupadd itcast
7.3 设置密码
- sudo passwd itcast
7.4 切换用户
- su 用户名
7.5 root用户
- 变成root用户:sudo su
- 设置root密码:passwd
7.6 删除用户
-
userdel 选项 用户名
常用的选项是-r,他的作用是把用户的主目录一起删除
第八章 网络管理
8.1 ifconfig
-
查看网卡信息
ifconfig
-
关闭网卡
sudo ifconfig eth0 down
-
开启网卡eth0
sudo ifconfig eth0 up
-
给eth0配置临时IP
sudo ifconfig eth0 IP
8.2 ping
-
ping [选项] 主机名/IP地址:
查看网络上的主机是否在工作。它向该主机发送ICMP ECHO_REQUEST包。有时我们想从网络上的某台主机上下载文件,可是又不知道那台主机是否开着,就需要使用ping命令查看
-c 数目 在发送指定数目的包后停止。
-d 设定SO_DEBUG的选项。
-f 大量且快速地送网络封包给一台机器,看它的回应。
-I 秒数 设定间隔几秒送一个网络封包给一台机器,预设值是一秒送一次。
-l 次数 在指定次数内,以最快的方式送封包数据到指定机器(只有超级用户可以使用此选项)。
-q 不显示任何传送封包的信息,只显示最后的结果。
-r 不经由网关而直接送封包到一台机器,通常是查看本机的网络接口是否有问题。
-s 字节数 指定发送的数据字节数,预设值是56,加上8字节的ICMP头,一共是64ICMP数据字节。
8.3 netstat
-
netstat [选项]
显示网络连接、路由表和网络接口信息,可以让用户得知目前都有哪些网络连接正在运作。命令中各选项的含义如下:
- -a 显示所有socket,包括正在监听的。
- -c 每隔1秒就重新显示一遍,直到用户中断它。
- -i 显示所有网络接口的信息,格式同“ifconfig -e”。
- -n 以网络IP地址代替名称,显示出网络连接情形。
- -r 显示核心路由表,格式同“route -e”。
- -t 显示TCP协议的连接情况。
- -u 显示UDP协议的连接情况。
- -v 显示正在进行的工作。
第九章 其他命令
9.1 man
- 看手册(叫做manual或man page)。每一个命令和系统函数都有自己的man page。
- man man
- man read 查看read命令的man page
- man 2 read 查看read系统函数的man page(在第二个section中,表示为read(2))
- man -k read 以read为关键字查找相关的man page
9.2 创建终端
- 创建终端标签 Ctrl + Shift + t
- 切换标签 Alt+n(n=1)
- 新开终端 Ctrl + Shift + n
9.3 关机重启
关机重启这些操作都需要有root权限
- poweroff
- shutdown -t 秒数 [-rkhncfF] 时间 [警告讯息]
- -t 秒数 : 设定在切换至不同的runlevel之前, 警告和删除二讯号之间的延迟时间(秒)
- -k : 仅送出警告讯息文字, 但不是真的要 shutdown
- -r : shutdown 之後重新开机
- -h : shutdown 之後关机
- -n : 不经过 init , 由 shutdown 指令本身来做关机动作.(不建议你用)
- -f : 重新开机时, 跳过 fsck 指令, 不检查档案系统
- -F : 重新开机时, 强迫做 fsck 检查
- -c : 将已经正在 shutdown 的动作取消
9.4 查看空闲内存
- free -m
9.5 需要安装的组件
- sudo apt-get install openssh-server
- sudo apt-get install nfs-kernel-server
- sudo apt-get install vsftpd
第十章 vim
10.1 vi简介
-
Vi有三种基本工作模式:
-
命令模式:
任何时候,不管用户处于何种模式,只要按一下ESC键
-
文本输入模式:
在命令模式下输入插入命令i、附加命令a 、打开命令o、修改命令c、取代命令r或替换命令s都可以进入文本输入模式
-
末行模式:
在命令模式下,用户按“:”键即可进入末行模式下,此时Vi会在显示窗口的最后一行(通常也是屏幕的最后一行)显示一个“:”作为末行模式的提示符,等待用户输入命令
-
10.2 vim基础操作
-
跳转到指定行:
88G (命令模式)
:88 (末行模式)
-
跳转文件首:
gg (命令模式)
-
跳转文件尾:
G(命令模式)
-
自动格式化程序:
gg=G(命令模式)
-
大括号对应:
% (命令模式)
-
光标移至行首:
0 (命令模式)执行结束,工作模式不变
-
光标移至行尾:
$ (命令模式)执行结束,工作模式不变
-
删除单个字符:
x (命令模式)执行结束,工作模式不变。
-
替换单个字符:
将待替换的字符用光标选中, r (命令模式),再按欲替换的字符
-
替换多个字符:
R(命令模式)
-
删除一个单词:
dw(命令模式)光标置于单词的首字母进行操作。
-
删除光标至行尾:
D 或者 d$(命令模式)
-
删除光标至行首:
d0 (命令模式)
-
删除指定区域:(实际放到了寄存器中,可以用y进行拷贝)
v: 按块进行选择
V: 按行进行选择
-
删除指定1行:
在光标所在行,按 dd (命令模式)
-
删除指定N行:
在光标所待删除首行,按 Ndd (命令模式)
-
复制一行:
yy
-
粘贴:
p:向后、P:向前。
-
查找
- 找设想内容:
- 命令模式下, 按 “/” 输入欲搜索关键字,回车。使用 n 检索下一个。
- 找看到的内容:
- 命令模式下,将光标置于单词任意一个字符上,按 “*”(向后)/ “#”(向前)
- 找设想内容:
-
单行替换:
将光标置于待替换行上, 进入末行模式,输入 😒 /原数据/新数据
-
通篇替换:
末行模式, :%s /原数据/新数据/g g:不加,只替换每行首个
-
指定行的替换:
末行模式, :起始行号,终止行号s /原数据/新数据/g g:不加,只替换每行首个
:29,35s /printf/println/g
-
撤销、反撤销:
u、ctrl+r(命令模式)
-
跳转至 man 手册:
将光标置于待查看函数单词上,使用 K(命令模式)跳转。 指定卷, nK
注:man手册共9卷,1可执行程序或 shell 命令,2系统调用(内核提供的函数),3库调用(程序库中的函数)(默认跳1)
-
查看宏定义:
将光标置于待查看宏定义单词上,使用 [d 查看定义语句
-
在末行模式执行shell命令:
:!命令 :! ls -l
10.3 vim分屏操作
- sp:横屏分。 Ctrl+ww 切换。
- vsp:竖屏分。Ctrl+ww 切换。
第十一章 gcc
11.1 gcc编译过程
-
预处理:-E .i(文件后缀)
展开头文件、展开条件编译、替换宏定义、去掉注释、删除空行、空字符等
-
编译:-S .s(文件后缀)
检查语法错误(消耗最多的)
-
汇编:-c .o(文件后缀)
翻译,把汇编语言翻译成二进制的机器语言
-
连接:无参
生成a.out、数据段合并、地址回填
11.2 常用选项
- -v / –v / –version 查看gcc版本号
- -I:目录 指定头文件目录,注意-I和目录之间没有空格
- -c :只编译,生成.o文件,不进行链接
- -g :包含调试信息,主要支持 gdb 调试。得到 二进制 文件!!!
- -On: n=0∼3 编译优化,n越大优化得越多
- -Wall: 提示更多警告信息
- -D:向程序中“动态”注册宏定义。 #define NAME VALUE
- -E: 生成预处理文件
- -M:生成.c文件与头文件依赖关系以用于Makefile,包括系统库的头文件
- -MM: 生成.c文件与头文件依赖关系以用于Makefile,不包括系统库的头文件
11.3 静态库制作及使用步骤
-
将 .c 生成 .o 文件
gcc -c add.c -o add.o
-
使用 ar 工具制作静态库
ar rcs lib库名.a add.o sub.o div.o
-
编译静态库到可执行文件中
gcc test.c lib库名.a -o a.out
11.4 动态库制作及使用
-
将 .c 生成 .o 文件,(生成与位置无关的代码 -fPIC)
gcc -c add.c -o add.o -fPIC
-
使用 gcc -shared 制作动态库
gcc -shared -o lib库名.so add.o sub.o div.o
-
编译可执行程序时,指定所使用的动态库。 -l:指定库名(去掉lib前缀和.so后缀) -L:指定库路径。
gcc test.c -o a.out -lmymath -L./lib
-
运行可以执行程序 ./a.out 出错!!!! — ldd a.out --> “not found”
error while loading shared libraries: libxxx.so: cannot open shared object file: No such file or directory
原因:
链接器:工作于链接阶段,工作时需要 -l 和 -L
动态链接器:工作于程序运行阶段,工作时需要提供动态库所在目录位置。
-
解决方式
- 通过环境变量: export LD_LIBRARY_PATH=动态库路径((临时生效, 终端重启环境变量失效))
- 永久生效: 写入 终端配置文件。 .bashrc 建议使用绝对路径。
- vi ~/.bashrc
- 写入 export LD_LIBRARY_PATH=动态库路径 保存
- . .bashrc/ source .bashrc / 重启 终端 —> 让修改后的.bashrc生效
- 拷贝自定义动态库 到 /lib (标准C库所在目录位置)
- 配置文件法
- sudo vi /etc/ld.so.conf
- 写入 动态库绝对路径 保存
- sudo ldconfig -v 使配置文件生效。
第十二章 gdb调试工具
12.1 gdb基础指令
-g:在编译时加上该参数,才能得到调试表
基础指令:
- list: list 1 列出源码。根据源码指定 行号设置断点。
- b: b 20 在20行位置设置断点。
- r/run: 运行程序
- n/next: 下一条指令(会越过函数)
- s/step: 下一条指令(会进入函数)
- p/print:p i 查看变量的值。
- continue:继续执行断点后续指令。
- finish:结束当前函数调用。
- quit:退出gdb当前调试。
其他指令:
- run:使用run查找段错误出现位置。
- set args: 设置main函数命令行参数 (在 start、run 之前)
- run 字串1 字串2 …: 设置main函数命令行参数
- info b: 查看断点信息表
- b 20 if i = 5: 设置条件断点。
- ptype:查看变量类型。
- bt:列出当前程序正存活着的栈帧。
- frame: 根据栈帧编号,切换栈帧。
- display:设置跟踪变量
- undisplay:取消设置跟踪变量。 使用跟踪变量的编号。
栈帧:随着函数调用而在stack上开辟的一片内存空间。用于存放函数调用时产生的局部变量(还有形参,两者地位等同)和临时值。
12.2 父子进程间的调试
- 设置父进程调试路径:set follow-fork-mode parent (默认)
- 设置子进程调试路径:set follow-fork-mode child
第十三章 Makefile项目管理
13.1 规则
-
基本语法
目标:依赖 (tab)命令 如:add.o:add.c gcc –Wall –g –c add.c –o add.o 目标:要生成的目标文件 依赖:目标文件由哪些文件生成 命令:通过执行该命令由依赖文件生成目标
- 目标的时间必须晚于依赖条件的时间,否则,更新目标
- 依赖条件如果不存在,找寻新的规则去产生依赖条件。
- ALL:指定 makefile 的终极目标。
13.2 工作原理
基本原则:
-
若想生成目标,检查规则中的依赖条件是否存在,如不存在,则寻找是否有规则用来生成该依赖文件
-
检查规则中的目标是否需要更新,必须先检查它的所有依赖,依赖中有任一个被更新,则目标必须更新
- 分析各个目标和依赖之间的关系
- 根据依赖关系自底向上执行命令
- 根据修改时间比目标新,确定更新
- 如果目标不依赖任何条件,则执行对应命令,以示更新
13.3 Makefile 变量
2个函数:($是取出变量值)(%也是通配符,但是具有锁定的意味)
-
src = $(wildcard ./*.c):
匹配当前工作目录下的所有.c 文件。将文件名组成列表,赋值给变量 src。 src = add.c sub.c div1.c
-
obj = $(patsubst %.c, %.o, $(src)):
将参数3中,包含参数1的部分,替换为参数2。 obj = add.o sub.o div1.o
3个自动变量:
- $@: 在规则的命令中,表示规则中的目标。
- $^: 在规则的命令中,表示所有依赖条件。
- $<: 在规则的命令中,表示第一个依赖条件。如果将该变量应用在模式规则中,它可将依赖条件列表中的依赖依次取出,套用模式规则。
模式规则:
%.o:%.c
gcc -c $< -o %@
静态模式规则:
$(obj):%.o:%.c
gcc -c $< -o %@
clean: (没有依赖)
- -rm -rf $(obj) a.out “-”:作用是,删除不存在文件时,不报错。顺序执行结束。
伪目标:
- .PHONY: clean ALL
参数:
- -n:模拟执行make、make clean 命令。
- -f:指定文件执行 make 命令。 ex:xxxx.mk
第十四章 文件IO
14.1 系统调用
由操作系统实现并提供给外部应用程序的编程接口。(Application Programming Interface,API)。是应用程序同系统之间数据交互的桥梁
14.2 C标准库文件 IO 函数
open函数:
原型1:int open(char *pathname, int flags) #include <unistd.h>
-
参数:
-
pathname: 欲打开的文件路径名
-
flags:文件打开方式,需包含头文件 #include <fcntl.h>
O_RDONLY|O_WRONLY|O_RDWR | O_CREAT|O_APPEND|O_TRUNC|O_EXCL|O_NONBLOCK …
-
-
返回值:
- 成功: 打开文件所得到对应的 文件描述符(整数)
- 失败: -1, 设置errno
原型2:int open(char *pathname, int flags, mode_t mode)
-
参数:
-
pathname: 欲打开的文件路径名
-
flags:文件打开方式:
O_RDONLY|O_WRONLY|O_RDWR | O_CREAT|O_APPEND|O_TRUNC|O_EXCL|O_NONBLOCK …
-
mode: 参数3使用的前提, 参2指定了 O_CREAT。取值8进制数,用来描述文件的 访问权限。 rwx 0664
创建文件最终权限 = mode & ~umask
-
返回值:
- 成功: 打开文件所得到对应的 文件描述符(整数)
- 失败: -1, 设置errno
-
close函数:
函数原型:int close(int fd);
-
参数:
- fd:已打开的文件句柄
-
返回值:
-
成功: 返回0
-
失败: -1, 设置errno
//错误处理函数,如何使用 errno printf("xxx error: %d\n", errno); char *strerror(int errnum); printf("xxx error: %s\n", strerror(errno)); void perror(const char *s); perror("open error");
-
read函数:
函数原型:ssize_t read(int fd, void *buf, size_t count);
会把参数fd所指的文件传送count个字节到buf指针所指的内存中。
- 参数:
- fd:文件描述符
- buf:存数据的缓冲区
- count:缓冲区大小,如果为0,则read()不会有作用,并返回0
- 返回值:
- 0:读到文件末尾
- 成功: > 0 读到的字节数。
- 失败: -1, 设置 errno
- -1: 并且 errno = EAGIN 或 EWOULDBLOCK, 说明不是read失败,而是read在以非阻塞方式读一个设备文件(网络文件),并且文件无数据。
write函数:
函数原型:ssize_t write(int fd, const void *buf, size_t count);
- 参数:
- fd:文件描述符
- buf:待写出数据的缓冲区
- count:数据大小
- 返回值:
- 成功: 写入的字节数。
- 失败: -1, 设置 errno
fcntl的用法:
int (int fd, int cmd, ...);
int flgs = fcntl(fd, F_GETFL);
flgs |= O_NONBLOCK;
fcntl(fd, F_SETFL, flgs);
//获取文件状态: F_GETFL
//设置文件状态: F_SETFL
lseek函数:
函数原型:off_t lseek(int fd, off_t offset, int whence);
-
参数:
- fd:文件描述符
- offset: 偏移量
- whence:起始偏移位置: SEEK_SET/SEEK_CUR/SEEK_END
-
返回值:
- 成功:较起始位置偏移量
- 失败:-1 errno
-
应用场景:
-
文件的“读”、“写”使用同一偏移位置。
-
使用lseek获取文件大小
-
使用lseek拓展文件大小:要想使文件大小真正拓展,必须引起IO操作。
也可以使用 truncate 函数,直接拓展文件。 int ret = truncate(“dict.cp”, 250);
-
stat/lstat 函数:
函数原型:int stat(const char *path, struct stat *buf);
- 参数
- path: 文件路径
- buf:(传出参数) 存放文件属性。
- 获取文件大小: buf.st_size
- 获取文件类型: buf.st_mode
- 获取文件权限: buf.st_mode
- 返回值:
- 成功: 0
- 失败: -1 errno
- 符号穿透(指向原本文件):stat会,lstat不会。
目录操作函数:
-
DIR * opendir(char *name);
-
int closedir(DIR *dp);
-
struct dirent *readdir(DIR * dp);
struct dirent { inode char dname[256]; }
14.3 文件描述符
-
文件描述符是一个非负整数,用于表示一个打开的文件。它是一个抽象的引用,由操作系统内核分配给每个打开的文件。文件描述符是进程与文件系统交互的接口,用于执行文件操作(如读、写、关闭等)。
-
文件描述符是一个进程级的概念,每个进程都有自己的文件描述符表。
- PCB进程控制块:本质 结构体,结构体中的成员是文件描述符表
-
文件描述符的取值:0/1/2/3/4。。。。/1023 表中可用的最小的。
- 0 - STDIN_FILENO
- 1 - STDOUT_FILENO
- 2 - STDERR_FILENO
14.4 阻塞
阻塞、非阻塞: 是设备文件、网络文件的属性。
- 产生阻塞的场景
- 读设备文件
- 读网络文件(读常规文件无阻塞概念)
- /dev/tty – 终端文件
- open(“/dev/tty”, O_RDWR|O_NONBLOCK) — 设置 /dev/tty 非阻塞状态。(默认为阻塞状态)
- 记个小知识点:flags |= O_NONBLOCK(位或,相当于a加上了后面的b,有一加一,有零加零,或者说获得后面的状态)
14.5 传入传出参数
传入参数:
- 指针作为函数参数
- 同常有const关键字修饰
- 指针指向有效区域, 在函数内部做读操作
传出参数:
- 指针作为函数参数
- 在函数调用之前,指针指向的空间可以无意义,但必须有效
- 在函数内部,做写操作
- 函数调用结束后,充当函数返回值
传入传出参数:
- 指针作为函数参数
- 在函数调用之前,指针指向的空间有实际意义
- 在函数内部,先做读操作,后做写操作
- 函数调用结束后,充当函数返回值
第十五章 文件系统
15.1 文件存储
inode
- 其本质为结构体,存储文件的属性信息。如:权限、类型、大小、时间、用户、盘块位置……也叫作文件属性管理结构,大多数的 inode 都存储在磁盘上。
- 每一个文件和目录在文件系统中都有一个唯一的inode编号。通过该编号,操作系统可以快速访问和定位文件的元数据和内容。因此,可以说inode是文件在文件系统中的标识符。
- 当我们创建文件或目录时,系统会为其分配一个空闲的inode,并为其分配一个独特的inode号码。文件系统维护一个inode表,记录了所有的inode和相关的信息。通过inode表,操作系统可以快速找到某个特定inode对应的文件或目录。
- inode不同于文件名,它是文件在文件系统中的内部标识。文件名和inode之间的映射关系由目录结构进行维护。目录包含了文件名和对应文件的inode号码,通过目录结构,我们可以根据文件名找到相应的inode,进而访问文件的内容和元数据。
- 少量常用、近期使用的 inode 会被缓存到内存中。
- inode和文件描述符的关系
- 当一个文件被打开时,操作系统会为该文件分配一个文件描述符,并在文件描述符表中记录相关信息。
- 文件描述符表中会包含一个指向该文件的inode的引用。通过inode,操作系统可以访问文件的元数据和实际数据。
- 文件描述符是进程与文件系统交互的接口,而inode是文件系统内部用于管理文件的结构。
dentry
- 目录项(目录结构),其本质依然是结构体,重要成员变量有两个 {文件名,inode,…},而文件内容(data)保存在磁盘盘块中。
文件系统
- 文件系统是一组规则,规定对文件的存储及读取的一般方法。文件系统在磁盘格式化过程中指定。常见的文件系统有fat32、ntfs、exfat、ext2 、ext3 、ext4
隐式回收
- 当进程结束运行时,所有该进程打开的文件会被关闭,申请的内存空间会被释放。系统的这一特性称之为隐式回收系统资源。
15.2 文件操作
递归遍历目录:ls-R.c
1. 判断命令行参数,获取用户要查询的目录名。 int argc, char *argv[1]
argc == 1 --> ./
2. 判断用户指定的是否是目录。 stat S_ISDIR(); --> 封装函数 isFile() { }
3. 读目录: read_dir() {
opendir(dir)
while (readdir()){
普通文件,直接打印
目录:
拼接目录访问绝对路径。sprintf(path, "%s/%s", dir, d_name)
递归调用自己。--》 opendir(path) readdir closedir
}
closedir()
}
read_dir() --> isFile() ---> read_dir()
dup 和 dup2:
函数原型:int dup(int oldfd); 文件描述符复制。
int dup2(int oldfd, int newfd); 文件描述符复制,把旧的拷贝给新的。重定向,让2也指向1。
-
参数
- oldfd: 已有文件描述符,要被复制的文件描述符
- newfd: 新文件描述符,目标文件描述符,重新定向到oldfd,两个描述符都将指向前者
-
返回值
- 成功:新文件描述符
- 失败:-1
-
示例
#include <unistd.h> #include <fcntl.h> #include <stdio.h> int main() { int fd = open("input.txt", O_RDONLY); // 打开一个文件作为源文件描述符 if (fd == -1) { perror("open"); return 1; } // 复制文件描述符并重定向标准输入,将标准输入重定向到 "input.txt" 。这意味着,之后对标准输入的操作(如 fgets)将会从 "input.txt" 文件中读取数据,而不是从键盘或其他原本的标准输入源 if (dup2(fd, STDIN_FILENO) == -1) { perror("dup2"); return 1; } // 从标准输入读取数据 char buffer[256]; while (fgets(buffer, sizeof(buffer), stdin) != NULL) { printf("Read from redirected stdin: %s", buffer); } close(fd); // 关闭文件描述符 return 0; }
fcntl 函数实现 dup:
函数原型:int fcntl(int fd, int cmd, …)
- 参数:
- cmd: F_DUPFD
- 返回值:
- 若被占用,返回最小可用的
- 未被占用,返回=该值的文件描述符
第十六章 进程
16.1 进程相关概念
- 程序:
- 死的。只占用磁盘空间。 ——剧本。
- 进程
- 活的。运行起来的程序。占用内存、cpu等系统资源。 ——戏。
- PCB进程控制块:
- 进程id
- 文件描述符表
- 进程状态: 初始态、就绪态、运行态、挂起态、终止态
- 进程工作目录位置
- *umask掩码
- 信号相关信息资源
- 用户id和组id
16.2 fork函数
函数原型:
pid_t fork(void)
作用:创建子进程。父子进程各自返回。父进程返回子进程pid。 子进程返回 0
-
getpid(); 获取自己的ID
-
getppid(); 获取父进程的ID
-
父子进程相同:
- 刚fork后。 data段、text段、堆、栈、环境变量、全局变量、宿主目录位置、进程工作目录位置、信号处理方式
-
父子进程不同:
- 进程id、返回值、各自的父进程、进程创建时间、闹钟、未决信号集
-
父子进程共享:
- 文件描述符
- mmap映射区
- 注意:读时共享写时复制只发生在特定的资源上(如内存页),而不是所有的资源。而对于文件描述符及其对应的文件也是采用了类似的机制,在进行写操作时,操作系统会为该进程分配一个新的文件描述符来确保文件的独立修改。
-
循环创建N个子进程
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <pthread.h> int main(int argc, char *argv[]) { int i; pid_t pid; for (i = 0; i < 5; i++) { if (fork() == 0) // 循环期间, 子进程不 fork break; } if (5 == i) { // 父进程, 从 表达式 2 跳出 sleep(5); printf("I'm parent \n"); } else { // 子进程, 从 break 跳出 sleep(i); printf("I'm %dth child\n", i+1); } return 0; }
16.3 exec函数族
使进程执行某一程序。成功无返回值,失败返回 -1
借助 PATH 环境变量找寻待执行程序函数原型:
int execlp(const char *file, const char *arg, …);
- 参数
- 程序名
- argv0
- argv1
自己指定待执行程序路径函数原型:
int execl(const char *path, const char *arg, …);
int execvp();
16.4 进程的状态
孤儿进程:
- 父进程先于子进程终止,子进程沦为“孤儿进程”,会被 init 进程领养。
僵尸进程:
- 子进程终止,父进程尚未对子进程进行回收,在此期间,子进程为“僵尸进程”。 kill 对其无效。
16.5 进程回收
wait函数:
回收子进程退出资源, 阻塞回收任意一个。
函数原型:pid_t wait(int *status);
- 参数
- (传出参数) 回收进程的状态。
- 返回值
- 成功:回收进程的pid
- 失败: -1, errno
- 函数的作用:
- 阻塞等待子进程退出
- 清理子进程残留在内核的 pcb 资源
- 通过传出参数,得到子进程结束状态
获取子进程正常终止值:
WIFEXITED(status) --》 为真 --》调用 WEXITSTATUS(status) --》 得到 子进程 退出值。
获取导致子进程异常终止信号:
WIFSIGNALED(status) --》 为真 --》调用 WTERMSIG(status) --》 得到 导致子进程异常终止的信号编号。
**waitpid函数:**指定某一个进程进行回收。可以设置非阻塞。 waitpid(-1, &status, 0) == wait(&status);
函数原型:pid_t waitpid(pid_t pid, int *status, int options);
- 参数:
- pid:指定回收某一个子进程pid
- 大于0: 待回收的子进程pid
- -1:任意子进程
- 0:同组的子进程。
- status:(传出) 回收进程的状态。
- options:WNOHANG 指定回收方式为,非阻塞。
- pid:指定回收某一个子进程pid
- 返回值:
- 大于0:表成功回收的子进程 pid
- 0 : 函数调用时, 参3 指定了WNOHANG, 并且,没有子进程结束。
- -1: 失败。errno
总结:
- wait、waitpid 一次调用,回收一个子进程。
- 想回收多个。while
第十七章 进程间通信方式
- 管道:简单
- 信号:开销小
- mmap映射:非血缘关系进程间
- socket(本地套接字):稳定
17.1 管道
实现原理:内核借助环形队列机制,使用内核缓冲区实现。
特质:
- 伪文件
- 管道中的数据只能依次读取。
- 数据在管道中,只能单向流动。
局限性:
- 自己写,不能自己读。
- 数据不可以反复读。
- 半双工通信。
- 血缘关系进程间可用。
**pipe函数:**创建,并打开管道。
函数原型:int pipe(int fd[2]);
- 参数:
- fd[0]: 读端
- fd[1]: 写端
- 返回值:
- 成功: 0
- 失败: -1 errno
管道的读写行为:
- 读管道
- 管道有数据,read返回实际读到的字节数。
- 管道无数据:
- 无写端,read返回0 (类似读到文件尾)
- 有写端,read阻塞等待。
- 写管道:
- 无读端, 异常终止。 (SIGPIPE导致的)
- 有读端:
- 管道已满, 阻塞等待
- 管道未满, 返回写出的字节个数。
pipe管道: 用于有血缘关系的进程间通信。 ps aux | grep ls | wc -l
-
父子进程间通信
-
兄弟进程间通信
-
代码示例
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/wait.h> #include <unistd.h> int main(void) { int fd[2]; pid_t pid; int i; int ret = pipe(fd); if (ret == -1) { perror("pipe error:"); exit(1); } for (i = 0; i < 2; i++){ pid = fork(); if (pid == -1) { perror("pipe error:"); //ls | wc -l exit(1); } if (pid == 0) break; } if (i == 0) { //兄 ls close(fd[0]); dup2(fd[1], STDOUT_FILENO); execlp("ls", "ls", NULL); } else if (i == 1) { // 弟 wc -l close(fd[1]); dup2(fd[0], STDIN_FILENO); execlp("wc", "wc", "-l", NULL); } else if (i == 2) { //父 close(fd[0]); close(fd[1]); for(i = 0; i < 2; i++) wait(NULL); } return 0; }
fifo管道:可以用于无血缘关系的进程间通信
-
命名管道: mkfifo
-
无血缘关系进程间通信
- 读端,open fifo O_RDONLY
- 写端,open fifo O_WRONLY
-
代码示例
-
mkfifo
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/stat.h> #include <errno.h> #include <pthread.h> void sys_err(const char *str) { perror(str); exit(1); } int main(int argc, char *argv[]) { int ret = mkfifo("mytestfifo", 0664); if (ret == -1) sys_err("mkfifo error"); return 0; }
-
fifo_w
#include <stdio.h> #include <unistd.h> #include <sys/stat.h> #include <sys/types.h> #include <fcntl.h> #include <stdlib.h> #include <string.h> void sys_err(char *str) { perror(str); exit(-1); } int main(int argc, char *argv[]) { int fd, i; char buf[4096]; if (argc < 2) { printf("Enter like this: ./a.out fifoname\n"); return -1; } fd = open(argv[1], O_WRONLY); //打开管道文件 if (fd < 0) sys_err("open"); i = 0; while (1) { sprintf(buf, "hello itcast %d\n", i++); write(fd, buf, strlen(buf)); // 向管道写数据 sleep(1); } close(fd); return 0; }
-
fifo_r
#include <stdio.h> #include <unistd.h> #include <sys/stat.h> #include <sys/types.h> #include <fcntl.h> #include <stdlib.h> #include <string.h> void sys_err(char *str) { perror(str); exit(1); } int main(int argc, char *argv[]) { int fd, len; char buf[4096]; if (argc < 2) { printf("./a.out fifoname\n"); return -1; } //int fd = mkfifo("testfifo", 644); //open(fd, ...); fd = open(argv[1], O_RDONLY); // 打开管道文件 if (fd < 0) sys_err("open"); while (1) { len = read(fd, buf, sizeof(buf)); // 从管道的读端获取数据 write(STDOUT_FILENO, buf, len); sleep(3); //多個读端时应增加睡眠秒数,放大效果. } close(fd); return 0; }
-
文件实现进程间通信:
- 打开的文件是内核中的一块缓冲区。多个无血缘关系的进程,可以同时访问该文件。
17.2 共享内存映射
创建共享内存映射函数原型:
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
- 参数
- addr:指定映射区的首地址。通常传NULL,表示让系统自动分配
- length:共享内存映射区的大小。(<= 文件的实际大小)
- prot:共享内存映射区的读写属性。PROT_READ、PROT_WRITE、PROT_READ|PROT_WRITE
- flags:标注共享内存的共享属性。MAP_SHARED、MAP_PRIVATE
- fd: 用于创建共享内存映射区的那个文件的文件描述符。
- offset:默认0,表示映射文件全部。偏移位置。需是 4k 的整数倍。
- 返回值
- 成功:映射区的首地址。
- 失败:MAP_FAILED (void*(-1)), errno
释放映射区函数原型:
int munmap(void *addr, size_t length);
- 参数
- addr:mmap 的返回值
- length:大小
使用注意事项:
- 用于创建映射区的文件大小为 0,实际指定非0大小创建映射区,出 “总线错误”
- 用于创建映射区的文件大小为 0,实际指定0大小创建映射区, 出 “无效参数”
- 用于创建映射区的文件读写属性为,只读。映射区属性为 读、写。 出 “无效参数”
- 创建映射区,需要read权限。当访问权限指定为 “共享”MAP_SHARED时, mmap的读写权限,应该 <=文件的open权限,只写不行。
- 文件描述符fd,在mmap创建映射区完成即可关闭。后续访问文件,用 地址访问
- offset 必须是 4096的整数倍。(MMU 映射的最小单位 4k )
- 对申请的映射区内存,不能越界访问
- munmap用于释放的地址,必须是mmap申请返回的地址
- 映射区访问权限为 “私有”MAP_PRIVATE, 对内存所做的所有修改,只在内存有效,不会反应到物理磁盘上
- 映射区访问权限为 “私有”MAP_PRIVATE, 只需要open文件时,有读权限,用于创建映射区即可
mmap函数的保险调用方式:
- fd = open(“文件名”, O_RDWR);
- mmap(NULL, 有效文件大小, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
父子进程使用 mmap 进程间通信:
-
父进程 先 创建映射区。 open( O_RDWR) mmap( MAP_SHARED );
-
指定 MAP_SHARED 权限
-
fork() 创建子进程
-
一个进程读, 另外一个进程写
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <sys/mman.h> #include <sys/wait.h> int var = 100; int main(void) { int *p; pid_t pid; int fd; fd = open("temp", O_RDWR|O_CREAT|O_TRUNC, 0644); if(fd < 0){ perror("open error"); exit(1); } ftruncate(fd, 4); //p = (int *)mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);//共享 p = (int *)mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd, 0);//不共享,写时复制 if(p == MAP_FAILED){ //注意:不是p == NULL perror("mmap error"); exit(1); } close(fd); //映射区建立完毕,即可关闭文件 pid = fork(); //创建子进程 if(pid == 0){ *p = 7000; // 写共享内存 var = 1000; printf("child, *p = %d, var = %d\n", *p, var); } else { sleep(1); printf("parent, *p = %d, var = %d\n", *p, var); // 读共享内存 wait(NULL); int ret = munmap(p, 4); //释放映射区 if (ret == -1) { perror("munmap error"); exit(1); } } return 0; } //子进程各自拥有独立的地址空间,全局变量是独立的副本,因此修改全局变量并不会影响另一方
无血缘关系进程间 mmap 通信:
-
两个进程 打开同一个文件,创建映射区
-
指定flags 为 MAP_SHARED(同一个文件在mmap创建的映射区是同一份,前提flags是MAP_SHARED)
-
一个进程写入,另外一个进程读出
-
mmap_w
#include <stdio.h> #include <sys/stat.h> #include <sys/types.h> #include <fcntl.h> #include <unistd.h> #include <stdlib.h> #include <sys/mman.h> #include <string.h> struct STU { int id; char name[20]; char sex; }; void sys_err(char *str) { perror(str); exit(1); } int main(int argc, char *argv[]) { int fd; struct STU student = {10, "xiaoming", 'm'}; char *mm; if (argc < 2) { printf("./a.out file_shared\n"); exit(-1); } fd = open(argv[1], O_RDWR | O_CREAT, 0664); ftruncate(fd, sizeof(student)); mm = mmap(NULL, sizeof(student), PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0); if (mm == MAP_FAILED) sys_err("mmap"); close(fd); while (1) { memcpy(mm, &student, sizeof(student)); student.id++; sleep(1); } munmap(mm, sizeof(student)); return 0; }
-
mmap_r
#include <stdio.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <stdlib.h> #include <sys/mman.h> #include <string.h> struct STU { int id; char name[20]; char sex; }; void sys_err(char *str) { perror(str); exit(-1); } int main(int argc, char *argv[]) { int fd; struct STU student; struct STU *mm; if (argc < 2) { printf("./a.out file_shared\n"); exit(-1); } fd = open(argv[1], O_RDONLY); if (fd == -1) sys_err("open error"); mm = mmap(NULL, sizeof(student), PROT_READ, MAP_SHARED, fd, 0); if (mm == MAP_FAILED) sys_err("mmap error"); close(fd); while (1) { printf("id=%d\tname=%s\t%c\n", mm->id, mm->name, mm->sex); sleep(2); } munmap(mm, sizeof(student)); return 0; }
-
注意:无血缘关系进程间通信。
-
mmap:数据可以重复读取
-
fifo:数据只能一次读取
-
匿名映射:只能用于血缘关系进程间通信
p = (int *)mmap(NULL, 40, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
17.3 信号
信号共性:
- 简单、不能携带大量信息、满足条件才发送
信号的特质:
- 信号是软件层面上的“中断”。一旦信号产生,无论程序执行到什么位置,必须立即停止运行,处理信号,处理结束,再继续执行后续指令
- 所有信号的产生及处理全部都是由【内核】完成的
信号相关的概念:
- 产生信号:
- 按键产生
- 系统调用产生
- 软件条件产生
- 硬件异常产生
- 命令产生
概念:
- 未决:产生与递达之间状态
- 递达:产生并且送达到进程。直接被内核处理掉
- 信号处理方式: 执行默认处理动作、忽略、捕捉(自定义)
- 阻塞信号集(信号屏蔽字): 本质:位图。用来记录信号的屏蔽状态。一旦被屏蔽的信号,在解除屏蔽前,一直处于未决态。可以直接操作,进而影响未决信号集。
- 未决信号集:本质:位图。用来记录信号的处理状态。该信号集中的信号,表示,已经产生,但尚未被处理,无法直接操作。
信号4要素:
信号使用之前,应先确定其4要素,而后再用!!!
- 编号、名称、对应事件、默认处理动作
kill命令 和 kill函数:
函数原型:int kill(pid_t pid, int signum);
-
参数
- pid:
- 大于0:发送信号给指定进程
- = 0:发送信号给跟调用kill函数的那个进程处于同一进程组的进程
- < -1: 取绝对值,发送信号给该绝对值所对应的进程组的所有组员
- = -1:发送信号给,有权限发送的所有进程
- signum:待发送的信号
- pid:
-
返回值:
- 成功: 0
- 失败: -1 errno
-
代码示例
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <signal.h> #define N 5 int main(void) { int i; pid_t pid, q; for (i = 0; i < N; i++) { pid = fork(); if (pid == 0) break; if (i == 2) q = pid; } if (i < 5) { //子进程 while(1) { printf("I'm child %d, getpid = %u\n", i, getpid()); sleep(1); } } else { //父进程 sleep(3); kill(q, SIGKILL); printf("------------kill %d child %u finish\n", 2, q); while (1); } return 0; } #if 0 int main(void) { int i; //默认创建5个子进程 for(i = 0; i < N; i++) //出口1,父进程专用出口 if(fork() == 0) break; //出口2,子进程出口,i不自增 if (i == 3) { sleep(1); printf("-----------child ---pid = %d, ppid = %d\n", getpid(), getppid()); kill(getppid(), SIGKILL); } else if (i == N) { printf("I am parent, pid = %d\n", getpid()); while(1); } return 0; } #endif
**alarm 函数:**使用自然计时法,定时发送SIGALRM给当前进程
函数原型:unsigned int alarm(unsigned int seconds);
-
参数:
- seconds:定时秒数
-
返回值:上次定时剩余时间,无错误现象
-
alarm(0);取消闹钟
-
time 命令 : 查看程序执行时间。 实际时间 = 用户空间时间 + 内核空间时间 + 等待时间(耗时最长)
-
代码示例
#include <stdio.h> #include <stdlib.h> #include <sys/time.h> /* struct itimerval { struct timeval{ it_value.tv_sec; it_value.tv_usec; } it_interval; struct timeval { it_value.tv_sec; it_value.tv_usec; } it_value; } it, oldit; */ unsigned int my_alarm(unsigned int sec) { struct itimerval it, oldit; int ret; it.it_value.tv_sec = sec; it.it_value.tv_usec = 0; it.it_interval.tv_sec = 0; it.it_interval.tv_usec = 0; ret = setitimer(ITIMER_REAL, &it, &oldit); if (ret == -1) { perror("setitimer"); exit(1); } return oldit.it_value.tv_sec; } int main(void) { int i; my_alarm(1); //alarm(sec); for(i = 0; ; i++) printf("%d\n", i); return 0; }
setitimer函数:
函数原型:int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
-
参数:
-
which:
- ITIMER_REAL: 采用自然计时。 ——> SIGALRM
- ITIMER_VIRTUAL: 采用用户空间计时 —> SIGVTALRM
- ITIMER_PROF: 采用内核+用户空间计时 —> SIGPROF
-
new_value:定时秒数
-
结构体类型
struct itimerval { struct timeval { time_t tv_sec; /* seconds */ suseconds_t tv_usec; /* microseconds */ }it_interval;---> 周期定时秒数 struct timeval { time_t tv_sec; suseconds_t tv_usec; }it_value; ---> 第一次定时秒数 };
-
-
old_value:传出参数,上次定时剩余时间
e.g struct itimerval new_t; struct itimerval old_t; new_t.it_interval.tv_sec = 0; new_t.it_interval.tv_usec = 0; new_t.it_value.tv_sec = 1; new_t.it_value.tv_usec = 0; int ret = setitimer(&new_t, &old_t); //定时1秒
-
-
返回值:
- 成功: 0
- 失败: -1 errno
其他几个发信号函数:
- int raise(int sig);
- void abort(void);
信号集操作函数:
- sigset_t set; 自定义信号集。
- sigemptyset(sigset_t *set); 清空信号集
- sigfillset(sigset_t *set); 全部置1
- sigaddset(sigset_t *set, int signum); 将一个信号添加到集合中
- sigdelset(sigset_t *set, int signum); 将一个信号从集合中移除
- sigismember(const sigset_t *set,int signum); 判断一个信号是否在集合中。 在–》1, 不在–》0
设置信号屏蔽字和解除屏蔽:
函数原型:int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
- 参数:
- how:
- SIG_BLOCK: 设置阻塞
- SIG_UNBLOCK: 取消阻塞
- SIG_SETMASK: 用自定义set替换mask
- set: 自定义set
- oldset:旧有的 mask
- how:
查看未决信号集:
函数原型:int sigpending(sigset_t *set);
- 参数
- set: 传出的 未决信号集
信号捕捉:
函数原型:signal();
-
代码示例
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <errno.h> #include <signal.h> void do_sig(int a) { printf("Hi, SIGINT, how do you do !\n"); } int main(void) { if (signal(SIGINT, do_sig) == SIG_ERR) { perror("signal"); exit(1); } while (1) { printf("---------------------\n"); sleep(1); } return 0; }
sigaction(); 重点!!!
-
代码示例
#include <stdio.h> #include <signal.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <pthread.h> void sys_err(const char *str) { perror(str); exit(1); } void sig_catch(int signo) // 回调函数 { if (signo == SIGINT) { printf("catch you!! %d\n", signo); sleep(10); } /* else if (signo == SIGQUIT) printf("-----------catch you!! %d\n", signo); */ return ; } int main(int argc, char *argv[]) { struct sigaction act, oldact; act.sa_handler = sig_catch; // set callback function name 设置回调函数 sigemptyset(&(act.sa_mask)); // set mask when sig_catch working. 清空sa_mask屏蔽字, 只在sig_catch工作时有效 sigaddset(&act.sa_mask, SIGQUIT); act.sa_flags = 0; // usually use. 默认值 int ret = sigaction(SIGINT, &act, &oldact); //注册信号捕捉函数 if (ret == -1) sys_err("sigaction error"); // ret = sigaction(SIGQUIT, &act, &oldact); //注册信号捕捉函数 while (1); return 0; }
-
信号捕捉特性:
- 捕捉函数执行期间,信号屏蔽字 由 mask --> sa_mask , 捕捉函数执行结束。 恢复回mask
- 捕捉函数执行期间,本信号自动被屏蔽(sa_flgs = 0)
- 捕捉函数执行期间,被屏蔽信号多次发送,解除屏蔽后只处理一次!
-
借助信号完成 子进程回收
17.4 守护进程
守护进程:
daemon进程。通常运行于操作系统后台,脱离控制终端。一般不与用户直接交互。周期性的等待某个事件发生或周期性执行某一动作。
不受用户登录注销影响。通常采用以d结尾的命名方式。
守护进程创建步骤:
- fork子进程,让父进程终止。
- 子进程调用 setsid() 创建新会话
- 通常根据需要,改变工作目录位置 chdir(), 防止目录被卸载
- 通常根据需要,重设umask文件权限掩码,影响新文件的创建权限(不影响目录)
- 通常根据需要,关闭/重定向 文件描述符
- 守护进程 业务逻辑。while()
第十八章 线程
18.1 线程概念
- 进程:有独立的 进程地址空间。有独立的pcb。 是分配资源的最小单位。
- 线程:有独立的pcb。没有独立的进程地址空间。 是执行的最小单位。
- ps -Lf 进程id ---------------->可以查看线程号
线程共享:
独享 栈空间(内核栈、用户栈)
共享 ./text./data ./rodataa ./bsss heap —> 共享【全局变量】(除了errno)
18.2 线程控制原语
查看线程id函数原型:
pthread_t pthread_self(void); 获取线程id。 线程id是在进程地址空间内部,用来标识线程身份的id号。
- 返回值:本线程id
- 检查出错返回:在线程中输出打印 fprintf(stderr, “xxx error: %s\n”, strerror(ret));
创建线程函数原型:
int pthread_create(pthread_t *tid, const pthread_attr_t *attr, void *(*start_rountn)(void *), void *arg);
-
参数
- 参1:传出参数,表新创建的子线程 id
- 参2:线程属性。传NULL表使用默认属性
- 参3:子线程回调函数。创建成功,ptherad_create函数返回时,该函数会被自动调用
- 参4:参3的参数。没有的话,传NULL
-
返回值
- 成功:0
- 失败:errno
-
循环创建N个子线程:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <pthread.h> int var = 100; void *tfn(void *arg) { int i; i = (int)arg; sleep(i); if (i == 1) { var = 333; printf("var = %d\n", var); return (void *)var; } else if (i == 3) { var = 777; printf("I'm %dth pthread, pthread_id = %lu\n var = %d\n", i+1, pthread_self(), var); pthread_exit((void *)var); } else { printf("I'm %dth pthread, pthread_id = %lu\n var = %d\n", i+1, pthread_self(), var); pthread_exit((void *)var); } return NULL; } int main(void) { pthread_t tid[5]; int i; int *ret[5]; for (i = 0; i < 5; i++) pthread_create(&tid[i], NULL, tfn, (void *)i);//将 int 类型 i, 强转成 void *, 传参 for (i = 0; i < 5; i++) { pthread_join(tid[i], (void **)&ret[i]); printf("-------%d 's ret = %d\n", i, (int)ret[i]); } printf("I'm main pthread tid = %lu\t var = %d\n", pthread_self(), var); sleep(i); return 0; }
退出当前线程函数原型:
void pthread_exit(void *retval);
- 参数:
- retval:退出值。 无退出值时,NULL
- 比较
- exit(); 退出当前进程
- return: 返回到调用者那里去
- pthread_exit(): 退出当前线程
阻塞 回收线程函数原型:
int pthread_join(pthread_t thread, void **retval); //会严格等待子线程完全终止后,才进行回收
- 参数:
- thread: 待回收的线程id
- retval:传出参数。 回收的那个线程的退出值,不需要传NULL;线程异常借助,值为 -1
- 返回值:
- 成功:0
- 失败:errno
设置线程为分离状态,线程退出后自动释放资源:
int pthread_detach(pthread_t thread);
-
参数:
- thread: 待分离的线程id
-
返回值:
- 成功:0
- 失败:errno
-
代码示例
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <pthread.h> void *tfn(void *arg) { printf("thread: pid = %d, tid = %lu\n", getpid(), pthread_self()); return NULL; } int main(int argc, char *argv[]) { int ret = 0; pthread_t tid; ret = pthread_create(&tid, NULL, tfn, NULL); if (ret != 0) { perror("pthread_create error"); } ret = pthread_detach(tid); if (ret != 0) { fprintf(stderr, "pthread_detach error:%s\n", strerror(ret)); exit(1); } sleep(1); ret = pthread_join(tid, NULL); if (ret != 0) { fprintf(stderr, "pthread_join error:%s\n", strerror(ret)); exit(1); } printf("main: pid = %d, tid = %lu\n", getpid(), pthread_self()); pthread_exit((void *)0); }
杀死一个线程函数原型:
int pthread_cancel(pthread_t thread); 需要到达取消点(到达内核的一个契机,系统调用,保存点)
-
参数:
- 待杀死的线程id
-
返回值:
- 成功:0
- 失败:errno
-
如果,子线程没有到达取消点, 那么 pthread_cancel 无效;我们可以在程序中,手动添加一个取消点。使用 pthread_testcancel();
-
成功被 pthread_cancel() 杀死的线程,返回 0.使用pthead_join 回收
-
代码示例
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <pthread.h> void *tfn(void *arg) { while (1) { printf("thread: pid = %d, tid = %lu\n", getpid(), pthread_self()); sleep(1); } return NULL; } int main(int argc, char *argv[]) { pthread_t tid; int ret = pthread_create(&tid, NULL, tfn, NULL); if (ret != 0) { fprintf(stderr, "pthread_create error:%s\n", strerror(ret)); exit(1); } printf("main: pid = %d, tid = %lu\n", getpid(), pthread_self()); sleep(5); ret = pthread_cancel(tid); // 终止线程 if (ret != 0) { fprintf(stderr, "pthread_cancel error:%s\n", strerror(ret)); exit(1); } while (1); pthread_exit((void *)0); }
线程与进程的比较:
线程控制原语 | 进程控制原语 |
---|---|
pthread_create(); | fork(); |
pthread_self(); | getpid(); |
pthread_exit(); | exit(); |
pthread_join(); | wait()/waitpid(); |
pthread_cancel(); | kill(); |
pthread_detach(); |
18.3 互斥锁
线程同步:
协同步调,对公共区域数据按序访问。防止数据混乱,产生与时间有关的错误。
锁的使用:
建议锁!对公共数据进行保护。所有线程【应该】在访问公共数据前先拿锁再访问。但,锁本身不具备强制性。
使用mutex(互斥量、互斥锁)一般步骤:
-
pthread_mutex_t lock; 创建锁
-
pthread_mutex_init; 初始化 1
-
pthread_mutex_lock; 加锁 1-- --> 0
-
访问共享数据(stdout)
-
pthrad_mutext_unlock(); 解锁 0++ --> 1
-
pthead_mutex_destroy; 销毁锁
#include <stdio.h> #include <string.h> #include <pthread.h> #include <stdlib.h> #include <unistd.h> pthread_mutex_t mutex; void err_thread(int ret, char *str) { if (ret != 0) { fprintf(stderr, "%s:%s\n", str, strerror(ret)); pthread_exit(NULL); } } void *tfn(void *arg) { srand(time(NULL)); while (1) { pthread_mutex_lock(&mutex); printf("hello "); sleep(rand() % 3); /*模拟长时间操作共享资源,导致cpu易主,产生与时间有关的错误*/ printf("world\n"); pthread_mutex_unlock(&mutex); sleep(rand() % 3); } return NULL; } int main(void) { int flag = 5; pthread_t tid; srand(time(NULL)); pthread_mutex_init(&mutex, NULL); pthread_create(&tid, NULL, tfn, NULL); while (flag--) { pthread_mutex_lock(&mutex); //锁被占用时会阻塞,直到子线程释放锁 printf("HELLO "); sleep(rand() % 3); printf("WORLD\n"); pthread_mutex_unlock(&mutex); sleep(rand() % 3); } pthread_cancel(tid); // 将子线程杀死,子线程中自带取消点 pthread_join(tid, NULL); pthread_mutex_destroy(&mutex); return 0; //main中的return可以将整个进程退出 } /*线程之间共享资源stdout*/
初始化互斥量:
pthread_mutex_t mutex;
- pthread_mutex_init(&mutex, NULL); 动态初始化。
- pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; 静态初始化。
注意事项:
- 尽量保证锁的粒度, 越小越好。(访问共享数据前,加锁。访问结束【立即】解锁。)
- 互斥锁,本质是结构体。 我们可以看成整数。 初值为 1。(pthread_mutex_init() 函数调用成功。)
- 加锁: --操作,阻塞线程。
- 解锁: ++操作,唤醒阻塞在锁上的线程。
- try锁:尝试加锁,成功–。失败,返回,同时设置错误号 EBUSY
restrict关键字:
用来限定指针变量。被该关键字限定的指针变量所指向的内存操作,必须由本指针完成。
【死锁】:
是使用锁不恰当导致的现象:
- 对一个锁反复lock。
- 两个线程,各自持有一把锁,请求另一把。
18.4 读写锁
- 锁只有一把。以读方式给数据加锁——读锁。以写方式给数据加锁——写锁。
- 读共享,写独占。
- 写锁优先级高。
- 相较于互斥量而言,当读线程多的时候,提高访问效率
函数原型:
- pthread_rwlock_t rwlock;
- pthread_rwlock_init(&rwlock, NULL);
- pthread_rwlock_rdlock(&rwlock); try
- pthread_rwlock_wrlock(&rwlock); try
- pthread_rwlock_unlock(&rwlock);
- pthread_rwlock_destroy(&rwlock);
18.5 条件变量
本身不是锁,但是通常结合锁来使用。
申请条件变量:
pthread_cond_t cond;
初始化:
- pthread_cond_init(&cond, NULL); 动态初始化。
- pthread_cond_t cond = PTHREAD_COND_INITIALIZER; 静态初始化。
阻塞等待条件:
pthread_cond_wait(&cond, &mutex);
- 作用
- 阻塞等待条件变量满足
- 等待过程中解锁已经加锁成功的信号量 (相当于 pthread_mutex_unlock(&mutex))
- 当条件满足,函数返回时,重新加锁信号量 (相当于, pthread_mutex_lock(&mutex))
唤醒阻塞在条件变量上的 (至少)一个线程:
pthread_cond_signal();
唤醒阻塞在条件变量上的 所有线程:
pthread_cond_broadcast();
-
代码示例
/*借助条件变量模拟 生产者-消费者 问题*/ #include <stdlib.h> #include <unistd.h> #include <pthread.h> #include <stdio.h> #include <stdatomic.h> /*链表作为公享数据,需被互斥量保护*/ struct msg { struct msg *next; int num; }; struct msg *head; /* 静态初始化 一个条件变量 和 一个互斥量*/ pthread_cond_t has_product = PTHREAD_COND_INITIALIZER; pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; atomic_int shutdown = 0; // 使用原子操作避免锁 void *consumer(void *p) { struct msg *mp; while (!atomic_load(&shutdown) { // 检查终止标志 pthread_mutex_lock(&lock); while (head == NULL && !shutdown) { // 防止虚假唤醒 pthread_cond_wait(&has_product, &lock); } if (head == NULL) { // shutdown触发且链表空 pthread_mutex_unlock(&lock); break; } mp = head; head = head->next; pthread_mutex_unlock(&lock); printf("-Consume %lu---%d\n", pthread_self(), mp->num); free(mp); } // 消费剩余数据 while (head != NULL) { pthread_mutex_lock(&lock); mp = head; head = head->next; pthread_mutex_unlock(&lock); free(mp); } return NULL; } void *producer(void *p) { struct msg *mp; while (!atomic_load(&shutdown)) { // 检查终止标志 struct msg *mp = malloc(sizeof(struct msg)); mp->num = rand() % 1000; printf("-Produce ---------------------%d\n", mp->num); pthread_mutex_lock(&lock); mp->next = head; head = mp; pthread_mutex_unlock(&lock); pthread_cond_signal(&has_product); sleep(rand() % 2); } return NULL; } int main(int argc, char *argv[]) { pthread_t pid, cid; srand(time(NULL)); pthread_create(&pid, NULL, producer, NULL); pthread_create(&cid, NULL, consumer, NULL); sleep(5); // 运行5秒后终止 atomic_store(&shutdown, 1); // 触发终止 pthread_cond_broadcast(&has_product); // 唤醒消费者 pthread_join(pid, NULL); pthread_join(cid, NULL); return 0; }
18.6 信号量
应用于线程、进程间同步
相当于 初始化值为 N 的互斥量。 N值,表示可以同时访问共享数据区的线程数
**函数原型:**int sem_init(sem_t *sem, int pshared, unsigned int value);
- 参数
- sem: 信号量
- pshared:
- 0: 用于线程间同步
- 1: 用于进程间同步
- value:N值。(指定同时访问的线程数)
其他函数:
sem_destroy();
sem_wait(); 一次调用,做一次-- 操作, 当信号量的值为 0 时,再次 – 就会阻塞。 (对比 pthread_mutex_lock)
sem_post(); 一次调用,做一次++ 操作. 当信号量的值为 N 时, 再次 ++ 就会阻塞。(对比 pthread_mutex_unlock)
-
代码示例
/*信号量实现 生产者 消费者问题*/ #include <stdlib.h> #include <unistd.h> #include <pthread.h> #include <stdio.h> #include <semaphore.h> #define NUM 5 int queue[NUM]; //全局数组实现环形队列 sem_t blank_number, product_number; //空格子信号量, 产品信号量 void *producer(void *arg) { int i = 0; while (1) { sem_wait(&blank_number); //生产者将空格子数--,为0则阻塞等待 queue[i] = rand() % 1000 + 1; //生产一个产品 printf("----Produce---%d\n", queue[i]); sem_post(&product_number); //将产品数++ i = (i+1) % NUM; //借助下标实现环形 sleep(rand()%1); } } void *consumer(void *arg) { int i = 0; while (1) { sem_wait(&product_number); //消费者将产品数--,为0则阻塞等待 printf("-Consume---%d\n", queue[i]); queue[i] = 0; //消费一个产品 sem_post(&blank_number); //消费掉以后,将空格子数++ i = (i+1) % NUM; sleep(rand()%3); } } int main(int argc, char *argv[]) { pthread_t pid, cid; sem_init(&blank_number, 0, NUM); //初始化空格子信号量为5, 线程间共享 -- 0 sem_init(&product_number, 0, 0); //产品数为0 pthread_create(&pid, NULL, producer, NULL); pthread_create(&cid, NULL, consumer, NULL); pthread_join(pid, NULL); //死循环,回收不了 pthread_join(cid, NULL); sem_destroy(&blank_number); sem_destroy(&product_number); return 0; }