Linux常见目录

目录 说明
/ 根目录,所有目录的起点,Linux 文件系统的顶级目录
/bin 存放常用的二进制可执行文件(如 lscpmv 等),普通用户和系统都可使用
/sbin 系统管理命令,只有 root 用户可用(如 rebootifconfig
/etc 配置文件目录(如 /etc/passwd/etc/fstab/etc/ssh/sshd_config
/home 普通用户的家目录(如 /home/user1
/root 超级用户(root)的家目录
/lib 核心共享库和驱动模块,供 /bin/sbin 下的程序使用
/usr 存放用户应用程序和文件,子目录中包括 /usr/bin/usr/lib
/usr/bin 普通用户使用的应用程序(非基本命令)
/usr/sbin 非系统引导时使用的系统管理员命令
/var 可变数据,如日志文件 /var/log、邮件、缓存、锁等
/tmp 临时文件,系统重启后可能会被清空
/opt 第三方软件安装目录(如 Chrome、VMware 等)
/dev 设备文件(如硬盘 /dev/sda,终端 /dev/tty
/proc 虚拟文件系统,内核和进程信息(如 /proc/cpuinfo/proc/meminfo
/sys 另一种虚拟文件系统,提供与内核、设备驱动的交互接口
/boot 存放启动相关文件,如内核、grub 等(如 /boot/vmlinuz-*
/media 可移动媒体挂载点(如 U 盘、光盘)
/mnt 临时挂载点,一般管理员手动挂载文件系统用
/run 系统运行时临时文件(如 PID、Socket)
/srv 提供服务的数据目录(如 Web 服务、FTP 服务的数据)

Bash解析器常用快捷键

1.tap键

补齐命令,补齐路径,显示当前目录下的所有目录

2.清屏 clear

3.中断进程 ctrl+c

4 遍历输入的历史命令箭头上(ctrl+p)箭头下(ctrl+n)

5 光标相关操作

光标左移: ctrl+b(箭头左)

光标右移: ctrl+f(箭头右)

移动到头部: ctrl+a(home键)

移动到尾部:ctrl+e(end键)

6字符删除

删除光标前面的字符:ctrl+h(Backspace)

删除光标后面的字符:ctrl+d

光标后面的字符即光标覆盖的字符

删除光标前的所有内容:ctrl+u

删除光标后的所有内容:ctrl+k

终端相关快捷键

(终端一定要选中)

ctrl+shift+N 新建一个终端

ctrl+shift+T 在终端里新建一个标签

ctrl+D 关闭当前一个终端

内建命令和外部命令对比

内建命令 vs 外部命令 对比总结

比较项 内建命令(Builtin Command) 外部命令(External Command)
定义 由 Shell 内部直接实现的命令 是文件系统中的可执行程序(如 /bin/ls
执行速度 快(不需新建进程) 较慢(需 fork 子进程执行)
资源消耗 少,执行在当前 Shell 进程中 多,执行时创建子进程
路径查找 不需要依赖 PATH 环境变量 需要从 PATH 中查找可执行文件
是否可以替换 一般不推荐重定义 可以覆盖、替换或删改(如 alias 覆盖)
是否常驻内存 是,Shell 启动时加载 否,执行时加载,执行完释放
例子 cd, echo, exit, pwd, type ls, cp, gcc, vim, python
查看方法 type cd → builtin type ls → file

命令类型查看方法

使用type命令

1
type [选项] 命令名

使用 -a 显示所有同名命令(包括 alias、builtin、文件)

使用 -t 显示类型(简洁)

Linux命令格式

1
命令 [选项] [参数]
部分 说明
命令 要执行的操作,如 lscpmkdir
选项 用于控制命令行为的开关,通常以 --- 开头,例如 -l--help
参数 命令作用的对象,通常是文件名、目录名、用户名等

选项类型说明

类型 示例 说明
短选项 -l 通常是一个字母,多个可组合(如 -al
长选项 --help 更易读,通常不可组合
组合选项 -avz 相当于 -a -v -z

帮助文档查看方法

如果是内建命令(可以通过之前的type命令查看)使用 help +内建命令

1
help pwd

如果是外部命令 对应命令名 –help

1
ls --help

man 是 Linux 中最常用的命令之一,全称是 manual(手册),用于查看各种命令、函数、配置文件的使用说明。它是学习和查找 Linux 命令最权威的工具。

man 命令基本语法:

1
man [选项] [命令名或函数名]

man 手册的 9 个部分(章节)

章节号 内容 示例
1 用户命令(常见终端命令) man ls
2 系统调用(内核提供的函数) man 2 open
3 C 库函数 man 3 printf
4 设备文件和特殊文件 man 4 tty
5 配置文件格式 man 5 crontab
6 游戏与趣味(极少)
7 杂项(宏定义、协议、约定等) man 7 signal
8 系统管理员命令(只能 root 执行) man 8 ifconfig
9 内核开发接口(不常见)

常用选项

选项 作用
-k 关键词 搜索相关命令(相当于 apropos
-f 命令名 显示命令属于哪个章节(相当于 whatis
-a 显示所有章节中匹配的 man 页
-M 指定手册路径
--help 查看 man 自身帮助信息

目录相关命令

pwd

用于显示当前终端所在的工作目录(即当前绝对路径)。

cd

1
cd [目录路径]

常见用法示例

命令 说明
cd /home/user 切换到绝对路径 /home/user 目录
cd .. 切换到上一级目录
cdcd ~ 切换到当前用户的主目录
cd - 切换到上一次所在的目录(切换目录的“切换”)
cd ./folder 切换到当前目录下的子目录 folder

mkdir

用于创建新目录的命令,创建不了已存在目录。

1
mkdir [选项] 目录名

mkdir test 在当前目录下创建test文件夹

mkdir /tmp/test

mkdir file{1..100}在当前目录创建100个文件夹,file1,file2,file3…file100

mkdir “file{1..100}”在当前目录创建file{1..100}文件夹,只会创建一个。

mkdir “a b” 在当前目录创建a b一个文件夹。

mkdir a b 在当前目录创建a b两个文件夹。

选项 说明
-p 递归创建目录(父目录不存在时自动创建)
-m MODE 设置新建目录的权限,如 -m 755
-v 显示详细创建过程(verbose)

rmdir

用法和mkdir相同

用于删除空目录的命令

命令 说明
rmdir testdir 删除当前目录下的 testdir(需为空)
rmdir -p a/b/c 递归删除空目录链:先删 c,再删 b,再删 a
rmdir ./mydir/ 删除当前目录中的 mydir(需为空)
选项 说明
-p 递归删除路径中的所有空目录(从子到父)
--ignore-fail-on-non-empty 删除目录时忽略非空目录导致的错误

Linux文件类型

常用的文件类型有七种:普通文件,目录文件,设备文件,管道文件,链接文件和套接字。

1

普通文件 是 Linux 中最常见的一类文件,主要用于存储用户数据。包括:

  • 文本文件(如 .txt, .c, .py
  • 二进制文件(如 可执行程序、图片、音频等)
  • 脚本文件(如 .sh, .py,可以被解释执行)

在 Linux 中,目录文件(Directory File) 是一种特殊的文件类型,用来组织和存放文件和其他目录(子目录)。
它本质上是一个保存了文件名和 inode 编号之间映射关系的文件。

设备文件(Device File)是 Linux 中用于访问硬件设备的接口,本质上就是一种特殊的文件,程序通过它来与硬件设备通信。

设备文件通常位于 /dev 目录中。

常见设备文件举例

路径 类型 功能描述
/dev/sda 块设备 第一块硬盘
/dev/tty 字符设备 当前终端
/dev/null 字符设备 写入数据会被丢弃
/dev/zero 字符设备 会源源不断输出 0
/dev/random 字符设备 伪随机数生成器

管道文件(或称命名管道,FIFO = First In First Out)是一种特殊文件,用于在不同进程之间传输数据
写入管道的数据会按顺序被读取,类似“排队喝水”的水管,先进先出。

管道文件与匿名管道不同之处在于它有名字,存在于文件系统中(通常创建在某个路径下),因此不同进程不必有父子关系也能通信。

链接文件 是指向另一个文件的引用,常用于:

  • 创建多个路径指向同一个文件(节省空间)
  • 为长路径或常用文件创建别名(提高效率)
  • 实现共享与替代功能
类型 描述 ls -l 标识
软链接(符号链接) 类似 Windows 快捷方式,是一个指向目标路径的独立文件 l
硬链接 直接指向目标文件的 inode,本质上是同一个文件的另一个名字 -

在 Linux 中,你可以使用 ls -l 命令来区分各种文件类型。ls -l 输出的每一行开头的第一个字符表示文件类型。下面是 七种常见文件类型及其 ls -l 显示符号

七种常见文件类型及其标识

文件类型 ls -l 类型字符 示例路径 含义说明
普通文件 - -rw-r--r-- file.txt 常见的文本、二进制、可执行文件等
目录文件 d drwxr-xr-x dir/ 存储文件的容器
字符设备文件 c crw------- /dev/tty 逐字符访问设备,如终端、串口等
块设备文件 b brw-rw---- /dev/sda 按块访问设备,如硬盘、U 盘等
管道文件 p prw-r--r-- mypipe 用于进程间通信的 FIFO 管道
链接文件 l lrwxrwxrwx link -> target 指向其他文件的软链接
套接字文件 s srwxrwxrwx socket 进程间网络通信接口,如 /tmp/.X11-unix/X0

文件相关命令

ls命令

命令 含义
ls 简单列出当前目录内容
ls -a 显示所有文件,包括隐藏文件(以.开头)
ls -l 以长格式列出,显示权限、类型、时间等
ls -lh 长格式 + 人类可读大小(如 KB, MB)
ls -lt 按修改时间排序,最新的在前
ls -r 反向排序
ls -R 递归列出子目录
ls -d */ 只列出目录 ls -d只显示一个.

ls -l命令会输出长格式

1
2
权限       硬链接数 拥有者 所属组  大小    修改日期      文件名
drwxr-xr-x 2 user user 4096 Jun 21 13:00 mydir

对权限部分说明一下权限部分总共10个字符

位置 含义
1 文件类型标识
2-4 拥有者(user)权限
5-7 同组用户(group)权限
8-10 其他用户(others)权限

文件类型标识(第1个字符)

字符 类型
- 普通文件
d 目录
l 软链接
c 字符设备文件
b 块设备文件
p 管道(FIFO)
s 套接字

权限字符说明(2-10位置)

字符 含义
r 读权限 (read)
w 写权限 (write)
x 执行权限 (execute)
- 无该权限
s setuid/setgid 位(特殊执行权限)
t 粘滞位(sticky bit)

三组权限详解

组别 位置 含义
拥有者 2~4 字符 拥有该文件/目录的用户权限
组用户 5~7 字符 属于该文件组的用户权限
其他用户 8~10 字符 系统中除拥有者和组以外的所有用户权限

什么是通配符?

通配符是一种简化文件名匹配的符号,用于在命令中匹配多个文件或目录。它可以让你不用输入完整文件名,就能选中符合规则的文件。

常用的通配符类型

通配符 作用 例子 匹配结果示例
* 匹配任意数量的任意字符(包括0个) ls *.txt 匹配所有以 .txt 结尾的文件
? 匹配任意一个单字符 ls file?.txt 匹配 file1.txtfileA.txt,但不匹配 file10.txt
[abc] 匹配括号内的任意一个字符 ls file[123].txt 匹配 file1.txtfile2.txtfile3.txt
[a-z] 匹配指定范围内的任意一个字符 ls file[a-c].txt 匹配 filea.txtfileb.txtfilec.txt
[!abc] 匹配不在括号内的任意一个字符 ls file[!123].txt 匹配除 file1.txtfile2.txtfile3.txt 以外的文件

touch命令

touch 是用来 创建空文件更新已有文件的时间戳 的命令。

1
touch file.txt

如果 file.txt 不存在,会被创建为空文件;如果存在,文件时间被更新。

1
touch file1.txt file2.txt file3.txt

一次创建或更新多个文件。

1
2
touch file{2,3,4}#同时创建file2,file3,file4三个空文件和mkdir file{1..100}用法是一样的。
touch "file{2,3,4}"#创建file{2,3,4}这一个文件。

cp命令

cp 是 Linux 中用于 复制文件或目录 的命令。

任务 命令示例 说明
复制文件 cp file1.txt file2.txt file1.txt 内容复制为 file2.txt
复制文件到目录 cp file1.txt /home/user/docs/ file1.txt 复制进目录
复制目录(加 -r cp -r dir1/ dir2/ 递归复制整个目录 dir1dir2
保留属性复制文件 cp -p file1.txt file2.txt 保留原文件的时间戳、权限等信息
强制覆盖目标文件 cp -f file1.txt file2.txt 如果 file2.txt 存在,强制覆盖
复制并提示 cp -i file1.txt file2.txt 有冲突时会提示确认
显示复制过程 cp -v file1.txt file2.txt 复制时显示详细过程(verbose 模式)
选项 含义
-r 递归复制目录(必须用于复制目录)
-i 覆盖文件前提示确认
-f 强制覆盖目标文件而不提示
-p 保留原文件的属性(权限、时间等)
-u 只在源文件较新时才复制
-v 显示复制过程(verbose)
-a 归档模式,等价于 -dpR,用于备份
--parents 保留源路径结构复制文件(适用于目录结构迁移)

rm命令

是用于在 Linux 中 删除文件和目录 的命令。注意:rm 删除后不会进入回收站,无法轻易恢复,请务必小心使用。

功能 命令 说明
删除单个文件 rm file.txt 删除文件 file.txt
删除多个文件 rm file1.txt file2.txt 一次删除多个文件
递归删除目录及内容 rm -r mydir/ 删除目录 mydir 及其所有子目录和文件
强制删除文件/目录 rm -f file.txt / rm -rf mydir/ 忽略不存在的文件,且不提示确认
删除前确认 rm -i file.txt 删除前逐一询问确认
显示正在删除的文件 rm -v file.txt 显示被删除的文件名
选项 含义
-r--recursive 递归删除目录及其内容(删除整个目录树)
-f--force 强制删除,不提示,即使目标不存在也不报错
-i 删除前询问确认,适合新手使用以防误删
-I 删除多个文件或目录时才询问一次,比 -i 安全且不烦人
-v--verbose 显示正在删除的每一个文件或目录
--preserve-root 默认保护根目录 / 不被删除(系统安全机制,防止 rm -rf / 误操作)

mv命令

移动文件或目录 到新位置,重命名 文件或目录。

功能 命令示例 说明
移动文件 mv a.txt /home/user/docs/ a.txt 移动到 /home/user/docs/ 目录
重命名文件 mv old.txt new.txt old.txt 重命名为 new.txt
移动并重命名 mv a.txt /home/user/docs/b.txt 移动 a.txt 到新目录并改名为 b.txt
移动目录 mv dir1/ /home/user/backup/ 移动整个目录到新的路径
覆盖已有文件 mv -f a.txt b.txt 如果 b.txt 存在,则强制覆盖
覆盖前确认 mv -i a.txt b.txt 如果 b.txt 存在,移动前会询问是否覆盖
显示移动过程 mv -v a.txt b.txt 显示正在移动的内容
选项 含义
-f 强制覆盖已有目标文件,不提示
-i 如果目标文件存在,提示是否覆盖(interactive)
-n 不覆盖已有的目标文件(no-clobber)
-v 显示移动过程(verbose)
-u 仅在源文件较新或目标文件不存在时才移动

文件内容查看相关命令

cat命令

用于 查看、创建、合并文件 内容,常用于快速查看文本文件内容。

选项 含义
-n 给所有行编号
-b 只对非空行编号
-s 压缩连续空白行为一行
-T 显示 Tab 为 ^I
-E 显示每行结尾的 $(换行符可见)
-A 相当于 -vET,显示所有不可见字符

less命令

用于分页显示文件内容的命令,支持 向前/向后翻页浏览,适合查看大型文本文件。它比 cat 更强大,且不会一次性加载全部内容到内存中。

使用时常用快捷键(进入 less 后)

快捷键 功能说明
空格 向下翻一页
b 向上翻一页
Enter 向下滚动一行
k 向上一行(vi 风格)
j 向下一行
G 跳到文件末尾
g 跳到文件开头
/关键词 向下搜索(如 /error
?关键词 向上搜索
n 重复上一次搜索
N 反向重复搜索
q 退出 less

head命令

head 用于345

-n N 显示前 N 行(如 head -n 15 file.txt
-c N 显示前 N 个字节(如 head -c 100 file.txt
-q 多文件时不显示文件名头部(quiet)
-v 总是显示文件名头部(verbose)

tail命令

用于显示文件的最后几行内容,默认是最后 10 行。常用于:

  • 查看日志尾部;
  • 实时监控文件内容变化(配合 -f 选项);
  • 截取文件结尾部分数据。
1
2
3
tail /etc/passwd #默认显示后十行
tail -n 30 文件名 #显示后30行内容
tail -c 30 文件名 #显示后30个字符

du和df命令

du命令

查看目录或文件占用的磁盘空间,会考虑磁盘块对齐、文件系统元数据、软链接等因素。

参数 含义
-h 人类可读的方式显示(如 KB、MB)
-s 显示指定文件/目录占用的数据块
-a 显示所有文件和目录的大小(默认只显示目录)
--max-depth=N 显示目录深度(限制递归层数)
1
du -sh 文件名/目录
项目 ls -l du -sh
显示内容 文件本身大小(内容字节数) 实际磁盘占用(包含对齐和元数据)
对目录 显示目录结构本身大小 显示目录下所有内容实际占用
单位 字节(Bytes) 自动转换为 KB/MB/GB
应用场景 看文件大小/属性 查哪些文件/目录占空间最多
1
2
3
echo "hello" > file.txt
ls -l file.txt # 显示 6 bytes
du -sh file.txt # 显示 4.0K

df命令

查看整个磁盘的使用情况

1
2
df -h # 显示所有文件系统的使用情况(人类可读格式)
df -h /home # 查看 /home 所在分区的磁盘使用情况

查找相关命令

find

find 是 Linux 中功能非常强大的文件搜索命令,它可以根据名称、类型、时间、大小、权限等多种条件在目录中递归查找文件,还可以执行删除、移动、打印等操作。

1
find [搜索路径] [搜索条件] [处理动作]

按文件名查询:使用参数 -name

1
find ./ -name "*.txt" #查找当前路径下符合后缀是。txt的文件

按文件大小查询:使用参数 -size

1
find ./ -size +100k

+100k 表示大于100k的文件

-100k表示小于100k的文件

100k 表示等于100k的文件

大小方面:k小写,M大写

查询大小范围

1
find ./ -size +50k -size -100k

按文件类型查询:使用参数 -type

1
find ./ -type f #查询当前的普通文件
类型代号 含义 示例
f 普通文件 find . -type f 查找所有普通文件(这里不是-,要和ls -l的文件类型区分)
d 目录 find . -type d 查找所有目录
l 符号链接(软链接) find . -type l 查找所有软链接
c 字符设备文件 /dev/null
b 块设备文件 硬盘等块设备
s 套接字文件 Socket 类型文件
p 命名管道(FIFO) 通信用的特殊文件

grep

grep 是 Linux 中非常常用的文本搜索工具,用于在文件或标准输出中查找匹配的字符串,功能强大,灵活,适合日志分析、配置文件搜索、编程辅助等场景。

1
grep [选项] "模式" [文件]
选项 含义说明
-n 显示匹配行的行号
-i 忽略大小写
-v 反向匹配(即显示不包含该字符串的行)
-r or -R 递归搜索目录下的所有文件
-l 只列出匹配的文件名
-c 统计匹配的行数
--color=auto 高亮显示匹配的内容
1
2
3
4
grep -i "root" /etc/passwd #不分大小写,在passwd查找root
grep -w "hello" /etc/passwd #在passwd查找完全匹配hello单词的行
grep -r "u_char" ./ #递归搜索当前目录下的符合u_char的行
grep -i "hello" /etc/passwd --color=auto #在/etc/passwd文件中找hello并且忽略大小写,然后高亮显示匹配的关键字

管道

管道(|)一个命令的输出可以通过管道作为另一个命令的输入。

1
ifconfig | grep "ens33" #在ifconfig输出的文字查找存在ens33的行

压缩包管理

tar

把一系列文件归档到一个文件,也可以把档案文件解开以恢复数据。

1
tar [选项] -f [文件名.tar] [要打包或解压的文件/目录]#f必须放到选项的最后
1
2
3
tar -cvf sysctl.tar sysctl #打包文件,但是不压缩
tar -xvf sysctl.tar #解包文件
tar -tvf sysctl.tar #查看压缩文件内容

gzip

对单个文件进行压缩或解压,压缩率高、速度快,默认生成 .gz 文件。

tar和gzip命令结合使用实现文件打包,压缩。

tar只负责打包文件,但不压缩,用gzip压缩tar打包后的文件,其扩展名一般为xxx.tar.gz。

gzip单独使用,只可以对文件压缩和解压,不可以对目录。

1
gzip test1 test2 #不保留源文件压缩。 

tar和gzip结合对目录压缩

1
2
3
tar -czvf sysctl.tar.gz sysctl #打包和压缩文件
tar -xzvf sysctl.tar.gz #解包和解压文件
tar -xzvf sysctl.tar.gz -C /temp #解包和解压文件到temp目录

bzip2

和gzip一样和tar结合。

1
2
tar -cjvf test.tar.bz2 test #生成一个bz2压缩包
tar -xjvf share.tar.bz2 #解压

zip和unzip

通过zip压缩文件的目标文件不需要指定扩展名,默认扩展名为zip。

1
2
zip [选项] 目标文件(没有扩展名)源文件/目录
unzip -d 解压后目录文件 压缩文件 #-d解压到指定目录

文件权限管理

访问权限说明:

读权限(r)

对文件而言,具有读取文件内容的权限;对目录而言,具有浏览目录的权限。

写权限(w)

对文件而言,具有新增,修改文件内容的权限;对目录而言,具有删除,移动目录内文件的权限。

可执行权限(x)

对文件而言,具有执行文件的权限;对目录而言,该用户具有进入目录的权限。

通常。Unix/Linux系统只允许文件的属主(所有者)或root用户改变文件的读写权限。

chmod

chmodchange mode)是 Linux/Unix 系统中用于修改文件或目录权限的命令。它支持两种权限设置方式:数字方式符号方式

1
chmod [选项] 模式 文件名

符号方式:

1
2
3
chmod u/g/o/a +/-/= rwx 文件 #+添加权限 -撤销权限 =设定权限 u/g/o对应的是拥有者,同属组,其他。
chomd o+w a #向a的其他用户添加写权限
chomd u=rw,g=r,o=r a#把a的拥有者权限为re,同属组权限为r,其他用户权限为r。

数字方式:

1
2
3
4
5
6
#由于rwx通过二进制来区分 rwx就是111,十进制为7
#rwx 7
#rw- 6
#r-- 4
#r-x 5
chomd 0777 a#把a的对应用户的权限变为rwx.

chown

chown 是 Linux/Unix 中用于更改文件或目录 所属用户(owner)所属用户组(group) 的命令。

1
chown [选项] [新用户][:[新用户组]] 文件/目录

新用户:新的文件拥有者

新用户组:新的用户组(可选)

需要 sudo 权限(普通用户只能修改自己拥有的文件)

1
2
3
4
#把文件所有者修改为root用户
sudo chown root a
#把文件所属者改为yustone,所属组改为root
sudo chown yustone:root a

软件安装和卸载

使用包管理器安装和卸载

Ubuntu / Debian 系列

安装

1
2
sudo apt update          # 更新软件源
sudo apt install 软件名 # 安装软件

卸载

1
2
sudo apt remove 软件名           # 删除程序但保留配置文件
sudo apt purge 软件名 # 连配置文件一并删除

离线软件包安装:

1
sudo dpkg -i package.deb

离线软件包卸载:

1
sudo dpkg -r 软件名

使用 Snap 安装(跨平台容器化安装方式)

1
sudo snap install 软件名

使用 Snap 卸载(跨平台容器化安装方式)

1
sudo snap remove 软件名

从源代码编译安装

1
2
3
./configure
make
sudo make install

重定向

主要是把命令输出的内容(之前是屏幕)输入到文件里。

1
2
3
4
5
6
7
ls /etc/passwd > output.txt #标准正确输出重定向到output.txt(这里是覆盖原文件)
ls /etc/passwd >> output.txt #标准正确输出追加重定向到output.txt(这里是追加)
llll 2> error.txt #标准错误输出重定向到error.txt(这里是覆盖原文件)
llll 2>> error.txt #标准错误输出追加重定向到error.txt
lll 2> /dev/null #标准错误输出重定向到黑洞
ls ddddd /etc/passwd &> /dev/null #标准正确输出和标准错误输出全部重定向到黑洞。
ls ddddd /etc/passwd &>> output.txt #标准正确输出和标准错误输出以追加的方式全部重定向到output.txt.

其他命令

tree

tree以树状形式查看指定目录内容。

1
tree 目录

ln

ln命令主要用于创建链接文件。

链接文件分为软链接和硬链接:

硬链接只能链接普通文件,不能链接目录。软链接不占用磁盘空间,源文件删除则链接失效。

1
2
ln 源文件 链接文件 #硬链接
ln -s 源文件 链接文件 #软链接

如果没有-s选项代表建立一个硬链接文件,两个文件占用同一块的硬盘空间,即使删除了源文件,链接文件还是存在,所以-s比较常用。如果软链接文件和源文件不在同一目录,源文件最好使用绝对路径,不要使用相对路径。软链接文件存储的是目标文件的路径。

vim

vim 是一款强大的 文本编辑器,常用于 Linux / Unix 系统编程、脚本编辑、配置文件修改等场景。它是 vi 的增强版本,具有更强的功能,比如语法高亮、代码折叠、多窗口、多标签支持等。

vim的三种模式

普通模式(Normal Mode)

默认启动模式,你一打开 Vim 就是在这个模式。

作用:浏览、复制、剪切、粘贴、删除、移动光标、跳转、执行命令等。

常用命令:

命令 说明
h/l 左/右移动光标
j/k 下/上移动光标
[n]dd 删除当前行开始的n行(准确说是剪切)
[n]x 删除光标后n个字符
[n]X 删除光标前n个字符
[n]yy 复制从当前行开始的n行
p 粘贴
u 撤销前一个命令
Ctrl + r 还原(恢复)
: 进入命令模式
ia 进入插入模式
mG/mgg 到指定行,m为目标行数
/字符串 从当前光标位置向下查找(n,N查找内容切换)
?字符串 从当前光标位置向上查找(n,N查找内容切换)

插入模式(Insert Mode)

iao 等从普通模式进入插入模式。

作用:输入文字、写代码、编辑内容。

常用进入方式:

命令 含义
i 在光标前插入
a 在光标后插入
o 在当前行下方新开一行并插入
I 跳到行首插入
A 跳到行尾插入

退出插入模式:按 Esc 返回普通模式。

命令模式(Command-Line Mode)

: 从普通模式进入命令模式。

用于输入各种操作命令,如保存、退出、查找、替换等。

常用命令:

命令 功能
:w 保存
:q 退出
:wqZZ 保存并退出
:q! 强制退出(不保存)
:x 等同于 :wq
:/关键字 向下查找关键字
:s/旧/新/g 当前行替换所有匹配项
:1,10s/abc/123/g 把第一行到第十行之间的abc全部替换为123
:%s/旧/新/g 全文替换所有匹配项
:w filename 保存到指定文件(绝对路径)
:sp 文件名 当前文件和另一个文件水平分屏
:vsp 文件名 当前文件和另一个文件垂直分屏
ctrl+w+w 在多个窗口切换光标

gcc编译器

GCC(GNU Compiler Collection)是由 GNU 项目开发的一组编译器,最初是为了 C 语言开发的,现在支持多种编程语言

gcc编译器从拿到一个c源文件到生成一个可执行文件,中间一共经历了四个步骤:

2

1
2
3
4
5
6
7
8
gcc -E hello.c -o hello.i
gcc -S hello.i -o hello.s
gcc -c hello.s -o hello.o
gcc hello.o -o hello
./hello #执行
gcc 源文件 -o 可执行文件 #一步到位
gcc hello.c -o hello
./hello
选项 含义
-o 指定输出文件名
-Wall 打开所有警告信息
-g 生成调试信息,用于 GDB 调试
-O0/-O1/-O2/-O3 优化等级(0 表示无优化)
-c 只编译不链接,生成 .o 目标文件
-I 添加头文件搜索路径
-L 添加库文件搜索路径
-l 链接指定的库(例如 -lm 表示链接 math 库)
-v/–version 查看gcc版本号
-D 编译时定义宏
1
2
3
gcc -Wall test.c #显示所有的警告信息
gcc -Wall -Werror test.c #把警告信息当作错误处理
gcc tmp.c -DDEBUG #可以用来区分测试版和发布版,DEBUG是定义的宏

静态链接和动态链接

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

优点:对运行环境的依赖较小,具有较好的兼容性。

缺点:生成的程序比较大,在装入内存消耗更多的时间。库函数有了更新,必须重新编译。

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

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

缺点:依赖动态库,不能独立运行,动态库依赖版本问题严重。

静态和动态编译对比

我们编写的应用程序大量用到了库函数,系统默认采用动态链接的方式进行编译程序,若想采用静态编译,加入-static参数。

1
2
gcc test.c -o test
gcc -static test.c -o test

静态编译是要比动态编译程序大的多。

静态库和动态库

静态库可以认为是一些目标代码的集合,是在可执行程序运行前就已经加入到执行码中,成为执行程序的一部分。按照习惯,一般以”.a”作为文件后缀名。静态库的命名一般分为三个部分:前缀:lib,库名称:自己定义。后缀:.a。最终静态库的名字为libxxx.a

静态库制作:

3

1
2
3
4
5
gcc -c add.c -o add.o #-c是只编译不链接输出.o文件
gcc -c sub.c -o sub.o
gcc -c mul.c -o mul.o
gcc -c div.c -o dic.o
ar -rcs libtest.a add.o sub.o mul.o div.o#使用打包工具ar将准备好的.o文件打包为.a文件libtest.a

在使用ar工具需要添加参数:rcs

r更新,c创建,s建立索引

静态库使用:

静态库制作完成之后,需要将.a文件和头文件一起发布给用户。假设测试文件是main.c,静态库文件为libtest.a,头文件为head.h

编译命令:

1
gcc main.c -L./ -I./ -ltest -o main#注意这里链接库的名字

-L:表示要连接的库所在目录。

-ltest:指定链接时需要的库,去掉前缀和后缀

-I(这里是大写的i):表示要连接的头文件目录

动态库制作

共享库在程序编译时并不会被链接到目标代码中,而是在程序运行时才被载入。不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例,规避了空间浪费问题。

动态库在程序运行时才被载入,也解决了静态库对程序的更新,部署和发布的再次编译的问题,用户只需要更新动态库即可,增量更新。

一般以”.so”作为文件后缀名。共享库的命名一般分为三个部分:前缀lib,库名称:自己定义,后缀:.so。所以最终的动态库的名字应该为:libxxx.so。

1
2
3
4
5
6
7
8
9
10
11
#生成目标文件,此时要加编译选项:-fPIC(fpic)参数-fPIC创建与地址无关的编译程序(pic,position independent code),是为了能在多个应用程序间共享。
gcc -fPIC -c add.c
gcc -fPIC -c sub.c
gcc -fPIC -c mul.c
gcc -fPIC -c div.c
#生成共享库,此时要加链接器选项:-shared(指定生成动态链接库)
gcc -shared add.o sub.o mul.o div.o -o libtest.so
#通过nm命令查看对应的函数
nm libtest.so | grep "add"
#通过ldd命令查看可执行文件依赖的动态库
ldd test

动态库使用

引用动态库编译成可执行文件(和静态库一样)

1
gcc main.c -L./ -I./ -ltest -o main#注意这里链接库的名字

这一步是可以过的,但是到了,执行main时发现找不到对应文件。第一种就是把libtest.so复制到/lib里(需要sudo,这个方法不推荐,最好不要动Linux原本文件,覆盖了就不好玩了。)

1
gcc main.c -I./ -ltest -o main#注意这里链接库的名字,执行可执行文件就可以执行了,这种方法不推荐

动态库加载失败问题解决

当系统加载可执行代码,能够知道其所依赖的库的名字,但还需要知道其绝对路径。此时就需要系统动态载入器(dynamic linker/loader)。对于elf格式的可执行程序,是由ld-linux.so*来完成,他先后搜索elf文件的DT_RPATH段—环境变量LD_LIBRARY_PATH—/etc/ld.so.cache文件列表—/lib/,/usr/lib目录找到库文件后将其载入内存。

拷贝自己制作的共享库到/lib或者/usr/lib(不能是/lib64目录)

临时设置LD_LIBRARY_PATH(只在当前终端生效):

1
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:库路径#在原环境变量追加新的变量,库路径为绝对路径。

永久设置:把export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:库路径这段话,设置到~/.bashrc

1
source ~/.bashrc #让配置文件生效

除了以上三种还有两种将其添加到/etc/ld.so.conf,这里只需要添加绝对路径,然后sudo ldconfig -v使路径生效。

还有使用符号链接,但一定要使用绝对路径。

1
sudo ln -s 库文件的绝对路径 /lib/库文件

GDB调试器

GDB主要完成下面四个功能:

1.启动程序,可以按照你的自定义的要求随心所欲的运行程序。

2.可让被调试的程序在你指定的断点停住。

3.当程序被停住时,可以检查此时你的程序中所发生的事。

4.动态的改变你程序的执行环境。

生成调试信息

一般来说GDB主要调试的是C/C++的程序,要调试C/C++的程序,首先在编译时,我们必须把调试信息加到可执行文件中。使用编译器(cc/gcc/g++)的-g参数可以做到这一点。

1
2
gcc -g hello.c -o hello
g++ -g hello.c -o hello

启动GDB

启动gdb:gdb program

program也就是你的执行文件,一般在当前目录下。

设置启动参数:启动后设置

1
2
3
set args #可指定运行参数
set args 10 20 30 40 "hello world"
show args #命令可以查看设置好的运行参数

启动程序:

run:程序开始执行,如果有断点,停在第一个断点处。

start:程序向下执行一行。

n:执行下一步。

显示源代码

用list(也可直接打l)命令来打印程序的源代码。默认打印10行。

1
2
3
list function#显示函数名为function的函数的源码
set listsize count #设置一次显示源码的行数默认是10行
show listsize #查看当前listsize的设置

断点操作

简单断点:

break设置断点,可以简写为b

1
2
b 10 #设置断点,在源程序第十行
b func #设置断点,在func函数入口处

多文件设置断点

1
2
3
4
break filename:linenum #在源文件filename的linenum行处停住
break filename:function #在源文件filename的function函数的入口处停住
break class::function或者function(type,type) #在类class的function函数入口处停住
break namespace::class::function #在名称空间为namespace的类class的function函数的入口处停住。

查询所有断点

1
2
3
4
info b
info break
i break
i b

条件断点

一般来说,为断点设置一个条件,我们使用if关键词,后面跟其断点条件。

设置一个条件断点:

1
b test.c:8 if Value == 5 #对test.c文件的当变量Value满足为5时,在test.c文件的第8行产生断点

维护断点

delete 范围 删除指定的断点,其简写命令为d。如果不指定断点号,则表示删除所有的断点。

1
d 10-12 #删除编号为10-12的断点。编号可以使用i b命令查看

比删除更好的一种方法时disable停止点,disable了的停止点,GDB不会删除,当你还需要时,enable即可。

1
2
disable 断点编号 #使指定断点无效,简写命令是dis。如果什么都不指定,表示disable所有的停止点。
enable 断点编号 #使无效断点生效,简写命令是ena。如果什么都不指定,表示enable所有的停止点

调试代码

1
2
3
4
5
6
7
run #运行程序,可简写为r。程序开始执行,如果有断点,停在第一个断点处。
next #单步跟踪,函数调用当作一条简单语句执行,可简写为n。
step #单步跟踪,函数调用进入被调用函数体内,可简写为s。
finish #退出进入的函数
until #在一个循环体单步跟踪时,这个命令可以运行程序直到退出循环体,可简写为u。
continue #继续运行程序,停在下一个断点的位置,可简写为c。
quit #退出gdb,可简写为q。

数据查看

1
2
3
#查看运行时的数据
#print打印变量,字符串,表达式等的值,可简写为p。
p count #打印count的值

自动显示

可以设置一些自动显示的变量,当程序停住时,或在你单步跟踪时,这些变量会自动显示。相关的GDB命令是display

1
2
3
4
5
6
7
display 变量名 #在run启动程序后,使用该命令。
info display #查看display设置的自动显示的信息(可以看到对应变量名的编号)
undisplay num #info display是显示的编号 使对应编号的自动显示功能失效。
delete display dnums #删除自动显示,dnums意为所设置好了的自动显示的编号。如果要同时删除几个,编号可以用空格分隔,如果要删除一个范围的编号,可以用减号表示。
disable display dnums
enable display dnums
disableenable#不删除自动显示的设置,而只是让其失效和恢复。

查看修改变量的值

1
2
3
ptype width #查看变量width的类型
p width #打印变量width的值 p是print命令的缩写。
set var width=47 #将变量var值设置为47。在改变程序变量取值时,最好都使用set var格式的GDB命令。

自动化编译工具Makefile

make是个命令工具。

1
sudo apt install make #下载make命令

Makefile语法规则

一条规则:

1
2
目标:依赖文件列表
<Tab>命令列表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
all:test1 test2
echo "hello all"

test1:
echo "hello test1"

test2:
echo "hello test2"
#总共三条规则
#执行结果:make -f 1.mk
echo "hello test1"
hello test1
echo "hello test2"
hello test2
echo "hello all"
hello all

Makefile基本规则三要素:

目标:

​ 通常是要产生的文件名称,目标可以是可执行文件或其他obj文件,也可以是一个动作的名称。

依赖文件:

​ 用来输入从而产生目标的文件。

​ 一个目标通常有几个依赖文件(可以没有)

命令:

​ make执行的动作,一个规则可以含几个命令(可以没有)

​ 有多个命令,每个命令占一行。

make命令格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
make [-f file][options][targets]
[-f file]:
make默认在工作目录中寻找为GNUmakefile,makefile,Makefile的文件作为makefile输入文件。
-f可以指定以上名字以外的文件作为makefile输入文件。
[options]:
-v 显示make工具的版本
-w 在处理makefile之前和之后显示工作路径
-C dir 读取makefile之前改变工作路径至dir目录
-n 只打印要执行的命令但不执行
-s 执行但不显示执行的命令
[targets]:
若使用make命令时没有指定目标,则make工具默认会实现makefile文件内的第一个规则
指定了make工具要实现的目标,目标可以是一个或多个(多个目标用空格隔开)
make test1 -f 1.mk #就会执行目标为test1对应的语句。

Makefile示例

测试程序:test.c add.c sub.c mul.c div.c add.h sub.h mul.h div.h

1
2
3
#最简单的Makefile(首先vim Makefile)
test:test.c add.c sub.c mul.c div.c
gcc test.c add.c sub.c mul.c div.c -o test

缺点:效率低,修改一个文件,所有文件都要重新编译。

1
2
3
4
5
6
7
8
9
10
11
12
test:test.o add.o sub.o mul.o div.o 
gcc test.o add.o sub.o mul.o div.o -o test
add.o:add.c
gcc -c add.c -o add.o
sub.o:sub.c
gcc -c sub.c -o sub.o
mul.o:mul.c
gcc -c mul.c -o mul.o
div.o:div.c
gcc -c div.c -o div.o
test.o:test.c
gcc -c test.c -o test.o

这样,下次编译,他只会编译你修改的文件,最后再链接,这样是比较高效的。

Makefile中的变量

在Makefile中使用变量有点类似c语言的宏定义,使用该变量相当于内容替换,使用变量可以使Makefile易于维护。如果.o文件很多,难道我们要一个一个打吗,这也未免太麻烦,还可能漏打。

自定义变量

定义变量:

1
变量名=变量值

引用变量:

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

makefile的变量名:

makefile变量名可以以数字开头。

变量是大小写敏感的。

变量一般在makefile的头部定义

变量几乎可在makefile的任何地方使用

1
2
3
4
5
6
7
8
9
10
11
12
13
OBJS = test.o add.o sub.o mul.o div.o
test:$(OBJS)
gcc $(OBJS) -o test
add.o:add.c
gcc -c add.c -o add.o
sub.o:sub.c
gcc -c sub.c -o sub.o
mul.o:mul.c
gcc -c mul.c -o mul.o
div.o:div.c
gcc -c div.c -o div.o
test.o:test.c
gcc -c test.c -o test.o

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

1
2
3
4
CC=gcc
CPPFLAGS:
CFLAGS:
LDFLAGS:

自动变量

1
2
3
4
#这些变量不能单独使用 必须在命令中使用
# $@ 表示目标
# $^ 表示所有的依赖
# $< 表示第一个依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
OBJS = test.o add.o sub.o mul.o div.o
TARGET=test
$(TARGET):$(OBJS)
gcc $^ -o $@
add.o:add.c
gcc -c $< -o $@
sub.o:sub.c
gcc -c $< -o $@
mul.o:mul.c
gcc -c $< -o $@
div.o:div.c
gcc -c $< -o $@
test.o:test.c
gcc -c $< -o $@

模式规则

1
2
3
#模式规则匹配示例
%.o:%.c
$(cc) -c $(CFLAGS) $(CPPFLAGS) $< -o $@
1
2
3
4
5
6
7
8
OBJS=test.o add.o sub.o mul.o div.o
TARGET=test
$(TARGET):$(OBJS)
gcc $(OBJS) -o $(TARGET)
#模式匹配 所有的.o都依赖对应的.c
#将所有的.c生成对应的.o
%.o:%.c
gcc -c $< -o $@

Makefile的函数

常用的函数

1
2
3
4
wildcard #查找指定目录下的指定类型的文件
src=$(wildcard *.c) #找到当前目录下所有后缀为.c的文件,赋值给src
patsubst #匹配替换
obj=$(patsubst %.c,%.o,$(src)) #把src变量里所有后缀为.c的文件替换成.o

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#获取当前目录下所有的.c文件
SRC=$(wildcard ./*.c)
#将SRC中所有出现的.c的替换成.o
OBJS=$(patsubst %.c,%.o,$(SRC))
TARGET=test
$(TARGET):$(OBJS)
gcc $(OBJS) -o $(TARGET)
#模式匹配 所有的.o都依赖对应的.c
#将所有的.c生成对应的.o
%.o:%.c
gcc -c $< -o $@
#clean目标清除编译生成的中间文件
#执行命令 make clean
clean:
rm -rf $(OBJS) $(TARGET)

Makefile中的伪目标

clean用途:清除编译生成的中间.o文件和最终目标文件

make clean 如果当前目录下有同名clean文件,则不执行clean对应的命令,解决方案:

伪目标声明:.PHONY:clean 声明目标为伪目标之后,makefile将不会判断目标是否存在或者该目标是否需要更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#获取当前目录下所有的.c文件
SRC=$(wildcard ./*.c)
#将SRC中所有出现的.c的替换成.o
OBJS=$(patsubst %.c,%.o,$(SRC))
TARGET=test
$(TARGET):$(OBJS)
gcc $(OBJS) -o $(TARGET)
#模式匹配 所有的.o都依赖对应的.c
#将所有的.c生成对应的.o
%.o:%.c
gcc -c $< -o $@
#clean目标清除编译生成的中间文件
#执行命令 make clean
#声明clean为伪目标
.PHONY:clean
clean:
rm -rf $(OBJS) $(TARGET)

上面这个版本就是最终版。

1
2
@gcc -c $< -o $@ #在命令前加上@符号,表示不显示命令本身(默认显示),只显示结果
-gcc -c $< -o $@ #加上-符号,此条命令出错,make也会继续执行后续的命令。

系统调用

系统调用说的是操作系统提供给用户程序调用的一组”特殊”接口

系统调用和库函数的区别

Linux下对文件操作有两种方式:系统调用和库函数调用

库函数调用有两类函数组成:

不需要系统调用:不需要切换到内核空间即可完成函数全部功能,并且结果反馈给应用程序,如strcpy,bzero等字符串操作函数。

需要调用系统调用:需要切换到内核空间,这类函数通过封装系统调用去实现相应功能,如printf,fread等。

4

错误处理函数

errno是记录系统的最后一次错误代码,代码是一个int型的值,在errno.h中定义。查看错误代码errno是调试程序的一个重要方法。

当Linux C api函数出现异常时,一般会将errno全局变量赋一个整数值。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h> //fopen
#include <errno.h> //errno
#include <string.h> //strerror(errno)
int main(){
FILE *fp=fopen("xxxx","r");
if(fp==NULL){
printf("%d\n",errno);//打印错误码
printf("%d\n",strerror(errno));//把errno的数字转换为相应的文字
perror("fopen err"); //打印错误原因的字符串
//perror和printf("%d\n",strerror(errno));实现效果相同。
}
return 0;
}

虚拟地址空间

每个进程都会分配虚拟地址空间,在32位机器上,该地址空间为4G。Linux每个运行的程序(进程),操作系统就会为其分配一个04G的地址空间(虚拟地址空间)。03G是用户区,3G~4G是内核区。在进程里平时所说的指针变量,保存的就是虚拟地址,当应用程序使用虚拟地址访问内存时,处理器会将其转换为物理地址(MMU)MMU将虚拟地址转换为物理地址。这样做的好处在于:进程隔离,更好的保护系统安全运行,屏蔽物理差异带来的麻烦,方便操作系统和编译器安排进程地址。

文件描述符

打开现存文件或新建文件时,系统内核会返回一个文件描述符,文件描述符用来指定已打开的的文件。这个文件描述符相当于已打开文件的标号,文件描述符是非负整数,是文件的标识,操作这个文件描述符相当于操作这个描述符所指定的文件。

程序运行起来后(每个进程)都有一张文件描述符的表,标准输入,标准输出,标准错误输出设备文件被打开,对应的文件描述符0,1,2记录在表中。程序运行起来后这三个文件描述符是默认打开的。

在程序运行起来后,打开其他文件时,系统会返回文件描述符表中最小可用的文件描述符,并将此文件描述符记录在表中。

最大打开的文件个数:

​ Linux中一个进程最多只能打开NR_OPEN_DEFAULT(即1024)个文件(当然这个数量的设定是可以修改的),故当文件不再使用时应该及时调用close()函数关闭文件。

常用文件IO函数

open函数

1
2
3
4
5
#include<sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname,int flags);
int open(const char *pathname,int flags,mode_t mode);

功能:

​ 打开文件,如果文件不存在则可以选择创建。

close函数

1
2
3
#include <unistd.h>

int close(int fd)

功能:

​ 关闭已打开的文件。

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include<stdio.h>
#include<string.h>
#include<sys/type.h>
#include<sys/stat.h>
#include<fcntl.h>
//打开和关闭文件
int main(){
int fd=-1;
//以只读的方式打开一个文件,如果文件不存在就报错。
//fd=open("txt",O_RDONLY);
//以只写的方式打开一个文件 如果文件存在就直接打开 如果文件不存在就新建一个文件。
//fd=open("txt",O_WRONLY|O_CREAT,644);
//以只写的方式打开一个文件,如果文件存在就报错,如果文件不存在就新建一个。
//fd=open("txt",O_WRONLY|O_CREAT|O_EXCL,644);
//以读写方式打开一个文件,如果文件存在就打开,如果文件不存在就新建一个文件。
//fd=open("txt",O_RDWR|O_CREAT,644);
//O_TRUNC 清空文件内容,如果文件存在,打开并清空,不存在就新建一个文件。
//fd=open("txt",O_WRONLY|O_TRUNC|O_CREAT,644);
//O_APPEND 追加的方式
//以只写的方式和追加的方式打开一个文件 如果文件不存在会报错。
fd=open("txt",O_WRONLY|O_APPEND)
if(fd==-1){
perror("open");
return 1;
}
//关闭文件
close(fd);
return 0;
}

write函数

1
2
3
4
5
6
7
8
9
10
11
#include<unistd.h>
ssize_t write(int fd,const void* buf,size_t count);
功能:
把指定数目的数据写到文件(fd)
参数:
fd: 文件描述符
buf:数据首地址
count:写入数据的长度(字节)
返回值:
成功:实际写入数据的字节个数
失败:-1

read函数

1
2
3
4
5
6
7
8
9
10
11
#include<unistd.h>
ssize_t read(int fd,void *buf,size_t count);
功能:
把指定数目的数据读到内存(缓冲区)
参数:
fd:文件描述符
buf:内存首地址
count:读取的字节个数
返回值:
成功:实际读取的字节个数
失败:-1

阻塞和非阻塞的概念

读常规文件是不会阻塞,不管读多少字节,read一定会在有限的时间内返回。

从终端设备或网络则不一定,如果从终端输入的数据没有换行符,调用read读终端设备就会堵塞,如果网络上没有接受到数据包,调用read从网络读就会,至于会阻塞多久也是不确定的,如果没有一直收到数据到达就一直阻塞在那里。

同样,写常规文件是不会阻塞的,而向终端设备或网络写则不一定。

阻塞和非阻塞是对于文件而言,而不是指read,write等的属性。

lseek函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<sys/types.h>
#include<unistd.h>
off_t lseek(int fd,off_t offset,int whence);
功能:
改变文件的偏移量
参数:
fd:文件描述符
offset:根据whence来移动的位移数(偏移量),可以是正数,也可以是负数,如果正数,则相对于whence往右移动,如果是负数则相对于whence向左移动。如果向前移动的字节数超过了文件开头则出错返回,如果向后移动的字节数超过了文件末尾,再次写入时将增大文件尺寸。
whence:其取值如下:
SEEK_SET:从文件开头移动offset个字节
SEEK_CUR:从当前位置移动offset个字节
SEEK_END:从文件末尾移动offset个字节
返回值:
若lseek成功执行,则返回新的偏移量
如果失败,返回-1

所有打开的文件都有一个当前文件偏移量(current file offset)以下简称为cfo。cfo通常是一个非负整数,用于表明文件开始处到文件当前位置的字节数。

读写操作通常开始cfo,并且使cfo变大,增量为读写的字节数。文件被打开时,cfo会被初始化为0,除非使用了O_APPEND。

如果把文件偏移量移到最后,使用read函数,将不会读出数据。要将文件描述符提前移到开头或者其他可以读到的位置。所以要注意文件描述符的位置。

文件操作相关函数

stat函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<sys/types.h>
#include<sys/stat.h>
#include<unistd.h>

int stat(const char* path,struct stat *buf);
int lstat(const char* pathname,struct stat *buf);
功能:
获取文件状态信息
stat和lstat的区别:
当文件是一个符号链接时,lstat返回的是该符号链接本身的信息;
而stat返回的是该链接指向的文件的信息。
参数:
path:文件名
buf:保存文件信息的结构体
返回值:
成功:0
失败:-1 文件不存在

stat函数和命令stat 文件名的功能类似。

struct stat *buf结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 512 B blocks allocated */

/* Since POSIX.1-2008, this structure supports nanosecond
precision for the following timestamp fields.
For the details before POSIX.1-2008, see VERSIONS. */
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
};

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<unistd.h>

int main(void){
int ret=-1;
struct stat s;
//获取指定文件信息
ret=stat("txt",&s);
if(ret==-1){
perror("stat");
return 1;
}
//文件属性信息
printf("st_dev:%lu\n",s.st_dev);
printf("st_ino:%ld\n",s.st_ino);
return 0;
}

st_mode;可以获取文件类型和文件三种用户的权限。

access函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<unistd.h>

int access(const char* pathname,int mode);
功能:测试指定文件是否具有某种属性
参数:
pathname:文件名
mode:文件权限,4种权限(判断文件所属者)
R_OK:是否有读权限
W_OK:是否有写权限
X_OK:是否有执行权限
F_OK:测试文件是否存在
返回值:
0:有某种权限,或者文件存在
-1:没有,或者文件不存在

chmod函数

1
2
3
4
5
6
7
8
9
10
#include <sys/stat.h>

int chmod(const char*pathname,mode_t mode);
功能:修改文件权限
参数:
pathname:文件名
mode:权限(8进制数)
返回值:
成功:0
失败:-1

chown函数

1
2
3
4
5
6
7
8
9
10
11
#include<unistd.h>

int chown(const char *pathname,uid_t owner,gid_t group);
功能:修改文件所有者和所属组
参数:
pathname:文件或目录名
owner:文件所有者id,通过查看/etc/passwd得到所有者id
group;文件所属组id,通过查看/etc/group得到用户组id
返回值:
成功:0
失败:-1

truncate函数

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<unistd.h>
#include<sys/types.h>

int truncate(const char *path,off_t length);
功能:修改文件大小
参数:
path:文件名字
length:指定的文件大小
比原来小,删掉后边的部分
比原来大,向后拓展
返回值:
成功:0
失败:-1

link函数

1
2
3
4
5
6
7
8
9
#include<unistd.h>
int link(const char* oldpath,const char *newpath);
功能:创建一个硬链接
参数:
oldpath:源文件名字
newpath:硬链接名字
返回值:
成功:0
失败:-1

symlink函数

1
2
3
4
5
6
7
8
9
10
#include<unistd.h>

int symlink(const char* target,const char* linkpath);
功能:创建一个软链接
参数:
target:源文件名字
linKpath:软链接名字
返回值:
成功:0
失败:-1

剩下的还有readlink函数unlink函数rename函数

文件描述符复制

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

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

dup函数

1
2
3
4
5
6
7
8
9
10
#include<unistd.h>

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

函数示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
//dup dup2
int main(){
int fd=-1;
int newfd=-1;
//1.打开文件
fd=open("txt",O_RDWR|O_CREAT,0644);
if(fd==-1){
perror("open");
return 1;
}
newfd=dup(fd);
if(-1==newfd){
perror("dup");
return 1;
}
printf("newfd=%d\n",newfd);
//2.操作
write(fd,"ABCDEFG",7);
//因为这两个文件描述符共享一张文件表,所以当前文件的偏移量是共享的,所以下次不会覆盖而是追加
//使用另外一个文件描述符
write(newfd,"1234567",7);
//3.关闭文件描述符
close(fd);
close(newfd);
return 0;

}

dup2函数

1
2
3
4
5
6
7
8
9
10
#include<unistd.h>
int dup2(int oldfd,int newfd);
功能:
通过oldfd复制出一个新的文件描述符newfd,如果成功,newfd和函数返回值是同一个返回值,最终oldfd和新的文件描述符newfd都指向同一个文件。
参数:
oldfd:需要复制的文件描述符
newfd:新的文件描述符,这个描述符可以人为指定一个合法数字(0-1023),如果指定的数字已经被占用,此函数会自动关闭close()断开这个数字和某个文件的关联,再来使用这个合法数字。
返回值:
成功:返回newfd
失败;返回-1

注意:这里有个场景,我有两个程序都是要打开同一个文件并且向里面写东西,当然这就有两个open函数,那我先后打开该文件,前者写的会被后者写的覆盖掉吗,这是会的。因为每次open函数都会把文件偏移量的位置放回0。但是dup函数和dup2函数不会,因为复制的文件描述符共享一个文件描述符表项的。

fcntl函数

1
2
3
4
5
6
7
8
9
10
11
12
#include<unistd.h>
#include<fcntl.h>

int fcntl(int fd,int cmd,.../*arg */);
功能:改变已打开的文件性质,fcntl针对描述符提供控制。
参数:
fd:操作的文件描述符
cmd:操作方式
arg:针对cmd的值,fcntl能够接受第三个参数int arg。
返回值:
成功:返回某个其他值
失败:-1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<unistd.h>
#include<fcntl.h>
//fcntl复制文件描述符功能
int main(void){
int fd=-1;
int newfd=-1;
int ret=-1;

//1.打开文件
fd=open("txt",O_WRONLY|O_CREAT,0644);
if(fd==-1){
perror("open");
return 1;
}
printf("fd=%d\n",fd);
//2.文件描述符复制
//功能等价于dup函数
//第三个参数0 表示返回一个最小的可用的文件描述符,并且大于或者等于0
newfd=fcntl(fd,F_DUPFD,0);
if(-1==newfd){
perror("fcntl");
return 1;
}
printf("newfd=%d\n",newfd);
//3.写操作
write(fd,"123456789",9);
write(newfd,"ABCDEFGH",8);
//4.关闭文件
close(fd);
close(newfd)
return 0;
}

fcntl函数还可以改变文件状态标记。

目录相关操作

getcwd函数

1
2
3
4
5
6
7
8
9
10
#include<unistd.h>

char *getcwd(char *buf,size_t size);
功能:获取当前进程的工作目录
参数:
buf:缓冲区,存储当前的工作目录
size:缓冲区大小
返回值:
成功:buf中保存当前进程工作目录位置
失败:NULL

chdir函数

1
2
3
4
5
6
7
8
9
#include<unistd.h>

int chdir(const char *path);
功能:修改当前进程(应用程序)的路径
参数:
path:切换的路径
返回值:
成功:0
失败:-1

opendir函数

1
2
3
4
5
6
7
8
9
#include<sys/types.h>
#include<dirent.h>
DIR *opendir(const char*name);
功能:打开一个目录
参数:
name:目录名
返回值:
成功:返回指向该目录结构体指针
失败:NULL

closedir函数

1
2
3
4
5
6
7
8
9
10
#include<sys/types.h>
#include<dirent.h>

int closedir(DIR *dirp);
功能:关闭目录
参数:
dirp:opendir返回指针
返回值:
成功:0
失败:-1

readdir函数

1
2
3
4
5
6
7
8
#include<dirent.h>
struct dirent* readdir(DIR *dirp);
功能:读取目录
参数:
dirp:opendir的返回值
返回值:
成功:目录结构体指针
失败:NULL

readdir函数要读取目录所有内容是要循环读取。

进程控制

进程和程序

我们平时写的C语言代码,通过编译器编译,最终它会成为一个可执行程序,当这个可执行程序运行起来后(没有结束前),他就成为了一个进程。程序是存放在存储介质上的一个可执行文件,而进程是程序执行的过程。进程的状态是变化的,其包括进程的创建,调度和消亡。程序是静态的,进程是动态的。

在Linux系统中,操作系统是通过进程去完成一个一个的任务,进程是管理事务的基本单元

进程拥有自己独立的处理环境(当前需要用到那些环境变量,程序运行的目录在哪里,当前是哪个用户在运行此程序)和系统资源(处理器cpu占用率,存储器,I/O设备,数据,程序)。

并行和并发

并行:指在同一时刻,有多条指令在多个处理器上同时执行。

并发:指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。

MMU

MMU是Memory Management Unit的缩写,中文名是内存管理单元,它是中央处理器(CPU)中用来管理虚拟存储器,物理存储器的控制线路,同时也负责虚拟地址映射为物理地址,以及提供硬件机制的内存访问授权,多用户多进程操作系统。

进程控制块PCB

进程运行时,内核为进程每个进程分配一个PCB(进程控制块),维护进程相关的信息,Linux内核的进程控制块是task_struct结构体。

这个结构体的内部成员有很多,我们要知道的:

  1. 进程id。系统中每个进程有唯一的id,在C语言中用pid_t类型表示,其实就是一个非负整数。
  2. 进程的状态,有就绪,运行,挂起,停止等状态。
  3. 进程切换时需要保存和恢复的一些CPU寄存器。
  4. 描述虚拟地址空间的信息。
  5. 描述控制终端的信息。
  6. 当前工作目录。
  7. umask掩码
  8. 文件描述符表,包含很多指向file结构体的指针。
  9. 和信号相关的信息。
  10. 用户id和组id
  11. 会话(Session)和进程组。
  12. 进程可以使用的资源上限(Resource Limit)。

进程的状态

进程状态反映进程执行过程的变化。这些状态随着进程的执行和外界条件的变化而转换。

在三态模型中,进程状态分为三个基本状态,即运行态,就绪态,阻塞态。

在五态模型中,进程分为新建态,终止态,运行态,就绪态,阻塞态。

5

ps

进程是一个具有一定独立功能的程序,它是操作系统动态执行的基本单元。

ps命令可以查看进程的详细情况,常用选项(选项可以不加”-“)。

参数 含义
-a 显示终端上的所有进程,包括其他用户的进程
-u 显示进程的详细状态`
-x 显示无控制终端的进程
-r 只显示正在运行的进程

ps aux

ps ef和aux等价

ps -a

top

用来动态显示运行中的进程。

kill

kill命令指定进程号的进程,需要配合ps使用。

使用格式:

kill [-signal] pid

信号值从0-15,其中9为绝对终止,可以处理一般信号无法终止的进程。

有些进程不能直接杀死,这时候我们需要加一个参数”-9”,”-9”代表强制结束。

killall

通过进程名字杀死进程,进程名字是可以重复的。

1
killall -9 sleep

进程号和相关函数

每个进程都由一个进程号来标识,其类型为pid_t(整型),进程号的范围:0~32767(2的15次方-1)。进程号总是唯一的,但进程号可以重用。当一个进程终止后,其进程号就可以再次使用。

三个不同的进程号

进程号(PID):

标识进程的一个非负整型数。

父进程号(PPID):
任何进程(除init进程)都是由另一个进程创建,该进程称为被创建进程的父进程,对应的进程号成为父进程号(PPID)。如A进程创建了B进程,A的进程号就是B进程的父进程号。

进程组号(PGID):

进程组是一个或多个进程的集合,他们之间相互关联,进程组可以接收同一终端的各种信号,关联的进程有一个进程组号(PGID)。默认情况下,当前的进程号会当作当前的进程组号。

getpid函数

1
2
3
4
5
6
7
8
#include<sys/types.h>
#include<unistd.h>

pid_t getpid();
功能:
获取本进程号(PID)
参数:无
返回值:本进程号

getppid函数

1
2
3
4
5
6
7
8
#include<sys/types.h>
#include<unistd.h>

pid_t getppid();
功能:获取调用此函数的进程的父进程号
参数:无
返回值:调用此函数的进程的父进程号(PPID)

getpgid函数

1
2
3
4
5
6
7
8
9
#include<sys/types.h>
#include<unistd.h>

pid_t getpgid(pid_t pid);
功能:获取进程组号(PGID)
参数:
pid:进程号
返回值:
参数为0时返回当前进程组号,否则返回参数指定的进程的进程组号。

进程的创建

系统允许一个进程创建新进程,新进程即为子进程,子进程还可以创建新的子进程,形成进程树结构模型。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<sys/types.h>
#include<unistd.h>

pid_t fork();
功能:
用于从一个已存在的进程中创建一个新进程,新进程称为子进程,原进程称为父进程。
参数:无
返回值:
成功:子进程返回0,父进程中返回子进程ID,pid_t为整型。
失败:返回-1
失败的两个主要原因:
1.当前的进程数已经达到系统规定的上限,这时errno的值被设置为EAGAIN。
2.系统内存不足,这时errno的值被设置为ENOMEM。

使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>

//创建一个子进程
int main()
{
//创建子进程
fork();

printf("hello world\n");
return 0;
}

这里的结果会输出两次hello world。这是为什么呢?

首先,fork函数创建子进程,子进程会将父进程的代码复制下来来执行了,当然这就有人说了,那么子进程执行fork函数,那就不是一直创建子进程了吗。这里涉及到了一个问题。当父进程调用fork函数,父进程会有一个pc指针指向fork函数调用后的代码,当然,子进程也把这个pc指针的值也继承了下来,所以子进程也是一样执行。

父子进程关系

使用fork函数得到的子进程是父进程的一个复制品,它从父进程处继承了整个进程的地址空间:包括进程上下文(进程执行活动全过程的的静态描述),进程堆栈,打开的文件描述符,信号控制设定,进程优先级,进程组号等。

子进程所独有的只有它的进程号,计时器等(只有小量信息)。因此,使用fork()函数的代价是很大的。

简单来说,一个进程调用fork函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,只有少数值和原来的进程的值不同。相当于克隆了一个自己。

实际上,Linux的fork函数使用是通过写时拷贝(copy-on-write)实现。写时拷贝是一种可以推迟甚至避免拷贝数据的技术。内核此时并不复制整个进程的地址空间,而是让父子进程共享同一个地址空间。只用在需要写入的时候才会复制地址空间,从而使各个进程拥有各自的地址空间。也就是说,资源的复制是在需要写入的时候才会进行,在此之前,只有以只读方式共享。

fork之后父子进程共享文件,fork产生的子进程和父进程相同的文件描述符指向相同的文件表,引用计数增加,共享文件偏移指针。

区分父子进程

fork()函数被调用一次,但返回两次。两次返回的区别是:子进程的返回值是0,而父进程的返回值则是新子进程的进程ID。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
//区分父子进程
int main(){
pid_t pid=-1;
//创建一个子进程
//fork函数在在子进程中返回0 在父进程中返回子进程的pid
pid=fork();
if(pid==0){
//子进程
printf("hello pid:%d ppid:%d\n",getpid(),getppid());
exit(0);
}else{
//父进程
//这里pid返回的就是新子进程的进程id
printf("hello world pid:%d cpid:%d\n",getpid(),pid);

}
return 0;
}

父子进程堆空间

当在创建子进程前在堆声明时,一定要对该指针指向区域释放两次。不然会出现内存泄露。

如何检测内存是否泄露

1
2
gcc zfork.c
valgrind ./a.out

GDB调试多进程

使用GDB调试的时候,GDB只能跟踪一个进程。可以在fork函数调用之前,通过指令设置GDB调试工具跟踪父进程或者跟踪子进程。默认跟踪父进程。

1
2
3
set follow-fork-mode child #设置gdb在fork之后跟踪子进程
set follow-fork-mode parent #设置跟踪父进程(默认)
#注意,一定要在fork函数调用之前设置才有效。

进程退出函数

1
2
3
4
5
6
7
8
9
10
11
#include<stdlib.h>
void exit(int status);

#include<unistd.h>
void _exit(int status);
功能:
结束调用此函数的进程
参数:
status:返回给父进程的参数(低8位有效),至于这个参数是多少根据需要来填写。
返回值:

exit()和_exit()函数功能和用法是一样的,无非是包含的头文件不一样,还有的区别就是:exit()属于标准库函数,_exit()属于系统调用函数。

_exit()函数不会关闭文件描述符和I刷新I/O缓冲区。exit()函数会做这些。

等待子进程退出函数

在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。但是仍然为其保留一定的信息,这些信息主要指进程控制块PCB的信息(包括进程号,退出状态,运行时间)。

父进程可以调用wait或waitpid得到他的退出状态同时彻底清除掉这个进程。

wait()和waitpid函数的功能一样,区别在于wait()函数会堵塞,waitpid()可以设置不堵塞,waitpid()还可以指定等待那个子进程结束。

一次wait或waitpid调用只能清理一个子进程,清理多个子进程应使用循环。

wait函数

1
2
3
4
5
6
7
8
9
10
11
#include<sys/types.h>
#include<sys/wait.h>

pid_t wait(int *status);
功能:
等待任意一个子进程结束,如果任意一个子进程结束了,此函数会回收该子进程的资源。
参数:
status:进程退出时的状态信息。
返回值:
成功:已经结束子进程的进程号
失败:-1

调用wait函数的进程会挂起(阻塞),直到它的一个子进程退出或收到一个不能被忽视的信号时才被唤醒(相当于继续往下执行)。

若调用进程没有子进程,该函数立即返回;若它的子进程已经结束,该函数同样会立即返回,并且会回收那个早已结束进程的资源。

所以wait函数的主要功能为回收已经结束子进程的资源。如果参数status的值不是NULL,wait函数就会把子进程退出时的状态取出并且存入其中,这是一个整数值(int),指出了子进程是正常退出还是被非正常结束的。这个退出信息在一个int中包含了多个字段,直接使用这个值是没有意义的,我们需要宏定义取出其中的每个字段。

使用对应的宏函数如WIFEXITED(status)为非0表明进程正常结束。WEXITSTATUS(status)若WIFEXITED(status)值为真,获取进程退出状态(exit的参数)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//属于正常退出
if(WIFEXITED(status)){
printf("子进程退出状态码:%d\n",WEXITSTATUS(status));
}
else if(WIFSIGNALED(status)){
//在bash输入kill 子进程号
printf("子进程被信号%d杀死了\n",WTERMSIG(status));
}
else if(WIFSTOPPED(status)){
//向指定进程发送暂停信号
//kill -19 子进程号
//唤醒指定暂停的进程
//kill -18 子进程号
printf("子进程被信号%d暂停\n",WSTOPSIG(status));
}

waitpid函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include<sys/types.h>
#include<sys/wait.h>
pid_t waitpid(pid_t pid,int *status,int options);
功能:
等待子进程终止,如果子进程终止了,此函数会回收子进程的资源。
参数:
pid:参数pid的值有以下几种类型:
pid>0 等待进程ID等于pid的子进程。
pid=0 等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid不会等待他。
pid=-1 等待任一子进程,此时waitpid和wait作用一样。
pid<-1 等待指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值。
status:进程退出时的状态信息。和wait()用法一样。
options:options提供了一些额外的选项来控制waitpid().
0:同wait(),阻塞父进程,等待子进程退出。
WNOHANG:没有任何已经结束的子进程,则立即返回
WUNTRACED:如果子进程暂停了则此函数马上返回,并且不予理会子进程的结束状态。
返回值:
当正常返回时,返回收集到的已经回收子进程的进程号
如果设置了选项WNOHANG,而调用中waitpid()发现没有已经退出的子进程可等待,则返回0;
如果调用中出错,则返回-1,这是errno会被设置成相应的值以指示错误所在。
1
2
3
4
//等价于wait()
waitpid(-1,&status,0);
//第三个参数表示不阻塞
waitpid(-1,&status,WNOHANG)

孤儿进程

父进程运行结束,但子进程还在运行(未运行结束)的子进程就称为孤儿进程。

每当出现一个孤儿进程的时候,内核就把孤儿进程的父进程设置为init,而init进程会循环wait()它的已经退出的子进程。所以init进程会处理孤儿进程的善后工作。

孤儿进程并没有什么危害。

僵尸进程

进程终止,父进程尚未回收,子进程残留资源(PCB)存放于内核中,变成僵尸进程。这样就导致了一个问题,如果进程不调用wait()或者waitpid()的话,那么保留的那段信息就不会释放,其进程号就会一直被占用。如果产生大量的僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程。

进程替换

在Windows平台下,我们可以通过双击运行可执行程序,让这个可执行程序成为一个进程;而在Linux平台,我们可以通过./运行,让一个可执行程序成为一个进程。

但是,如果我们本来就运行着一个程序,我们如何在这个进程内部启动一个外部程序,由内核将这个外部程序读入内存,使其执行起来成为一个进程呢,我们通过exec函数族实现。

exec函数族是一组函数,在Linux并不存在exec函数。这组函数一共有6个。

1
2
3
4
5
6
7
8
9
10
11
#include<unistd.h>
extern char **environ;


int execl(const char *path,const char *arg,.../*(char *) NULL*/);
int execlp(const char *file,const char *arg,.../*(char *) NULL*/);
int execle(const char *path,const char *arg,.../*,(char *) NULL,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[]);
int execve(const char *filename,char *const argv[],char *const envp[]);

其中只有execve()是真正意义的系统调用,其他都是在此基础上经过包装的库函数。

exec函数族的作用是根据指定的文件名或目录名找到可执行文件,并用它来取代调用进程的内容。

进程在调用一种exec函数时,该进程完全由新程序替换,而新程序则从其main函数开始执行。因为调用exec并不创建新进程,所以前后进程ID(当然还有父进程号,进程组号,当前工作目录)并未改变。exec只是用另一个新程序替换了当前进程的正文,数据,堆和栈段(进程替换)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main(){
printf("hello \n");
//arg0 arg1 arg2 ... argn
//arg0一般是可执行文件名 argn必须是NULL
//execlp("ls","ls","-l","/home",NULL);
//第一个参数是可执行文件的绝对路径或者相对路径
//第二个参数一般是可执行文件的名字
//中间的参数就是可执行文件的参数
//最后一个参数是NULL
execl("/bin/ls","ls","-l","/home",NULL);
printf("hello world\n");
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#define _GNU_SOURCE
int main(){
char *argv[]={"ls","-l","/home",NULL};
char *envp[]={"ADDR=BEIJING",NULL};
printf("hello\n");
//第一个参数是可执行文件
//第二个参数是指针数组 最后一定以NULL结束
//execvp("ls",argv);
//第一个参数是可执行文件
//第二个参数是指针数组 最后一定以NULL结束
//execv("/bin/ls",argv);
//最后一个参数是环境变量指针数组
//execle("/bin/ls","ls","-l","/home",NULL,envp);
//第一个参数是可执行文件
//第二个参数是参数列表 指针数组
//第三个参数是环境变量列表 指针数组
execvpe("ls",argv,envp);
printf("hello world\n");
return 0;
}

进程间通信

进程是一个独立的资源分配单元,不同进程(这里所说的进程通常指的是用户进程)之间的资源是独立的,没有关联,不能在一个进程中直接访问另一个进程的资源。

但是,进程不是孤立的,不同的进程需要进行信息的交互和状态的传递等,因此需要进程间通信。(IPC)

进程间通信的目的:

数据传输:一个进程需要将它的数据发送给另一个进程。

通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。

资源共享:多个进程之间共享同样的资源。为了做到这一点,需要内核提供互斥和同步机制。

进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

Linux操作系统支持的主要进程间通信的通信机制:

6

无名管道

管道也叫无名管道,它是UNIX系统IPC(进程间通信)的最古老方式,所有的UNIX系统都支持这种通信机制。

管道有如下特点:

  1. 半双工,数据在同一时刻只能在一个方向上流动。
  2. 数据只能从管道的一端写入,从另一端读出。
  3. 写入管道中的数据遵循先入先出的规则。
  4. 管道所传送的数据是无格式的,这要求管道的读出方与写入方必须事先约定好数据的格式,如多少字节算一个消息。
  5. 管道不是普通文件,不属于某个文件系统,其只存在于内存中。
  6. 管道在内存中对应一个缓冲区。不同的系统其大小不一定相同。
  7. 从管道读数据是一次性操作,数据一旦被读走,他就从管道中被抛弃,释放空间以便写更多的数据。
  8. 管道没有名字,只能在具有公共祖先的进程(父进程与子进程,或者两个兄弟进程,具有亲缘关系)之间使用。

管道是一种特殊类型的文件,在应用层体现为两个打开的文件描述符。

7

pipe函数

1
2
3
4
5
6
7
8
9
#include<unistd.h>
int pipe(int pipefd[2]);
功能:创建无名管道
参数:
pipefd:为int型数组的首地址,其存放了管道的文件描述符pipefd[0],pipefd[1]。
当一个管道建立时,他会创建两个文件描述符fd[0]和fd[1]。其中fd[0]固定用于读管道,而fd[1]固定用于写管道。一般文件I/O的函数都可以用来操作管道(lseek()除外)。
返回值:
成功:0
失败:-1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
//用于创建无名管道
int main(){
int fd[2];
int ret=-1;
//创建一个无名管道
ret=pipe(fd);
if(ret==-1){
perror("pipe");
return 1;
}
//fd[0]用于读 fd[1]用于写
printf("fd[0]:%d fd[1]:%d\n",fd[0],fd[1]);
//关闭文件描述符
close(fd[0]);
close(fd[1]);
return 0;
}

父子进程通过无名管道通信

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#define SIZE 64
//父子进程使用无名管道进行通信
//父进程写管道 子进程读管道
int main(){
int ret=-1;
int fd[2];
char buffer[SIZE];
pid_t pid=-1;
//1.创建无名管道
ret=pipe(fd);
if(-1==ret){
perror("pipe");
return 1;
}
//2.创建子进程
pid=fork();
if(-1==pid){
perror("fork");
return 1;
}
//子进程 读管道
if(0==pid){
//关闭写端
close(fd[1]);
memset(buf,0,SIZE);
ret=read(fd[0],buf,SIZE);
if(ret<0){
perror("read");
exit(-1);
}
printf("child process buf :%s\n",buf);
//关闭读端
close(fd[0]);
exit(0)
}
//父进程 写管道
//关闭读端
close(fd[0]);
ret=write(fd[1],"ABCDE",5);
if(ret<0){
perror("write");
return 1;
}
printf("parent process write: len:%d\n",ret);

//关闭写端
close(fd[1]);

return 0;
}

管道读写特点

四种情况:

​ 第一种:

​ 如果写端没有关闭,管道中没有数据,这个时候读管道进程去读管道会阻塞。

​ 如果写端没有关闭,管道中有数据,这个时候读管道进程会将数据读出,下一次读没数据就会阻塞。

​ 第二种:

​ 管道所有的写端关闭,读进程去读管道的内容,读取全部内容,最后返回0。

​ 第三种:

​ 所有读端没有关闭,如果管道被写满了,写管道进程写管道会被阻塞。

​ 第四种:

​ 所有的读端被关闭,写管道进程写管道会收到一个信号,然后退出。

设置为非阻塞的方法

1
2
3
4
5
//获取原来的flags
int flags=fcntl(fd[0],F_GETFL);
//设置新的flags
flag |= O_NONBLOCK;
fcntl(fd[0],F_SETFL,flags);

如果写端没有关闭,读端设置为非阻塞,如果没有数据,直接返回-1。

查看管道缓冲区的大小

1
ulimit -a#可以查看管道大小(大小为4k)

函数

1
2
3
4
5
6
7
8
9
10
11
#include<unistd.h>
long fpathconf(int fd,int name);
功能:该函数可以通过name参数查看不同的属性值
参数:
fd:文件描述符
name:
_PC_PIPE_BUF,查看管道缓冲区大小
_PC_NAME_MAX,文件名字字节数的上限
返回值:
成功:根据name返回的值的意义也不同。
失败:-1

有名管道

管道,由于没有名字,只能用于亲缘关系的进程间通信。为了克服这一缺点,提出了命名管道(FIFO),也叫有名管道,FIFO文件。

命名管道不同于无名管道之处在于它提供了一个路径名与之关联,以FIFO的文件形式存在于文件系统中,这样,即使与FIFO的创建进程不存在亲缘关系的进程,只要可以访问该路径马,就能够彼此通过FIFO互相通信,因此,通过FIFO不相关的进程也能交换数据。

  1. 命名管道和无名管道有一些特点是相同的,不一样的地方在于:
  2. FIFO在文件系统中作为一个特殊的文件而存在,但FIFO的内容却放在内存中。
  3. 当使用FIFO的进程退出后,FIFO文件将继续保存在文件系统中以便以后使用。
  4. FIFO有名字,不相关的进程可以通过打开命名管道进行通信。

通过命令创建有名管道

1
mkfifo fifo #创建有名管道 fifo是管道的名字

通过函数创建有名管道

1
2
3
4
5
6
7
8
9
10
11
#include<sys/types.h>
#include<sys/stat.h>
int mkfifo(const char *pathname,mode_t mode);
功能:
命名管道的创建。
参数:
pathname:普通的路径名,也就是创建后FIFO的名字。
mode:文件的权限,与打开普通文件的open()函数中的mode参数相同。(0644
返回值:
成功:0 状态码
失败:如果文件已经存在,则会出错且返回-1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>


//通过mkfifo函数创建一个管道文件
int main(){
int ret=-1;
//创建一个有名管道 管道的名字fifo
ret=mkfifo("fifo",0644);
if(ret==-1){
perror("mkfifo");
return 1;
}
printf("创建一个有名管道ok\n");
return 0;
}

有名管道读写操作

一旦使用mkfifo创建了FIFO,就可以使用open打开它,常见的文件I/O函数都可用于fifo。如:close,read,write,unlink等。

FIFO严格遵循先进先出,对管道及FIFO的读总是从开始处返回数据,对他们的写则把数据添加到末尾。他们不支持诸如lseek()等文件定位操作。

1
2
3
4
5
6
7
8
9
10
11
//进程1,写操作
int fd=open("my_fifo",O_WRONLY);
char send[100]="hello Mike";
write(fd,send,strlen(send));

//进程2,读操作
int fd=open("my_fifo",O_RDONLY);//等着只写
char recv[100]={0};
//读数据,命名管道没数据时会阻塞,有数据时就取出来
read(fd,recv,sizeof(revc));
printf("read from my_fifo buf=[%s]\n",recv);

一个为只读而打开一个管道的进程会阻塞直到另外一个进程为只写打开该管道。

一个为只写而打开一个管道的进程会阻塞直到另外一个进程为只读打开该管道。

读管道:

​ 管道中有数据,read返回实际读到的字节数。

​ 管道无数据:

​ 管道写端被全部关闭,read返回0(相当于读文件末尾)。

​ 写端没有完全被关闭,read阻塞等待。

写管道:

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

​ 管道读端没有全部关闭:

​ 管道已满,write阻塞。

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

共享存储映射

存储映射I/O使一个磁盘文件与存储空间中的一个缓冲区相映射。

于是当从缓冲区中取数据,就相当于读文件中的相应字节。将数据存入缓冲区,则相应的字节就自动写入文件。这样,就可在不适用read和write函数的情况下,使用地址(指针)完成I/O操作。

共享内存可以说是最有用的进程间通信方式,也是最快的IPC形式,因为进程可以直接读内存,而不需要任何数据的拷贝。

存储映射函数

mmap函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include<sys/mman.h>

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:对映射区的写入操作会产生一个映射区的复制(copy-on-write),对此区域所作的修改不会写回原文件。
fd:由open返回的文件描述符,代表要映射的文件。
offset:以文件开始处的偏移量,必须是4k的整数倍(4k为页的大小),通常为0,表示从文件头开始映射
返回值:
成功:返回创建的映射区首地址
失败:MAP_FAILED宏
munmap函数
1
2
3
4
5
6
7
8
9
10
11
#include<sys/mman.h>

int munmap(void *addr,size_t length);
功能:
释放内存映射区
参数:
addr:使用mmap函数创建的映射区的首地址
length:映射区的大小
返回值:
成功:0
失败:-1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include<stdio.h>
#include<stdlib.h>
#include<string.h>

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

//存储映射
int main(){
int fd=-1;
int ret=-1;
void *addr=NULL;
//1.以读写形式打开一个文件。
fd=open("txt",O_RDWR);
if(-1==fd){
perror("open");
return 1;
}
//2.将文件映射到内存
addr=mmap(NULL,1024,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
if(addr==MAP_FAILED){
perror("mmap");
return 1;
}
//3.关闭文件
close(fd);
//4.写文件
memcpy(addr,"1234567890",10);
//5.断开存储映射
munmap(addr,1024);
return 0;
}

匿名映射实现父子进程通信

通过使用我们发现,使用映射区来完成文件读写操作十分方便,父子进程间通信也较容易。但缺陷是,每次创建映射区一定要依赖一个文件才能实现。

通常为了建立映射区要open一个temp文件,创建好了再unlink,close掉,比较麻烦。同样需要借助标志位flags来指定。使用MAP_ANONYMOUS(或者MAP_ANON)。

1
int *p=mmap(NULL,4,PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANONYMOUS,-1,0);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<error.h>
#include<fcntl.h>
#include<unistd.h>
#include<sys/mman.h>
#include<sys/wait.h>
//父子进程使用匿名映射进行进程间通信
int main(){
int ret=-1;
pid_t pid=-1;
void *addr=NULL;
//1.创建匿名映射
addr=mmap(NULL,4096,PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANONYMOUS,-1,0);
if(MAP_FAILED==addr){
perror("mmap");
return 1;
}
//2.创建子进程
pid=fork();
if(-1==pid){
perror("fork");
munmap(addr,4096);
return 1;
}
//3.父子进程通信
if(pid==0){
//子进程写
memcpy(addr,"123456789",10);
}
else{
//父进程读
wait(NULL);
printf("parent process %s\n",(char *)addr);
}
//4.断开映射
munmap(addr,4096);
return 0;
}

信号

信号是Linux进程间通信的最古老的方式。信号是软件中断,它是在软件层次上对中断机制的一次模拟,是一种异步通信的方式。信号可以导致一个正在运行的进程被另一个进程正在运行的异步进程中断,转而处理某一个突发事件。

信号的特点:简单,不能携带大量信息,满足某个特设条件才发送。

信号可以直接进行用户空间进程和内核空间进程的交互,内核进程可以利用它来通知用户空间进程发生了那些系统事件。

一个完整的信号周期包括三个部分:信号的产生,信号在进程中的注册,信号在进程中的注销,执行信号处理函数。

8

这里信号的产生,注册,注销是信号的内部机制,而不是信号的函数实现。

信号的编号

Unix早期版本就提供了信号机制,但不可靠,信号可能丢失。Berkeley和AT&T都对信号做了更改,增加了可靠信号机制。但彼此不兼容。POSIX.1对可靠信号例程进行了标准化。

Linux可使用命令:

1
kill -l(字母l)#查看相应的信号。

不存在编号为0的信号。其中1-31号信号称之为常规信号(也叫普通信号或标准信号),34-64称之为实时信号,驱动编程与硬件相关。

Linux 常用信号(signals)一览表

编号 信号名 默认行为 说明(用途)
1 SIGHUP 终止进程 终端挂起或控制进程终止(如终端关闭)
2 SIGINT 终止进程 键盘中断(Ctrl+C)
3 SIGQUIT 创建核心转储 键盘退出(Ctrl+\),带 core dump
4 SIGILL 创建核心转储 非法指令
6 SIGABRT 创建核心转储 调用 abort() 函数终止程序
8 SIGFPE 创建核心转储 浮点异常(如除以0)
9 SIGKILL 强制终止 无法被捕获或忽略,立即终止
11 SIGSEGV 创建核心转储 无效内存引用(段错误)
13 SIGPIPE 终止进程 管道破裂(写到没有读者的管道)
14 SIGALRM 终止进程 计时器超时(alarm()
15 SIGTERM 终止进程 终止信号(kill 默认发送)
17 SIGCHLD 忽略/捕获 子进程结束时通知父进程
18 SIGCONT 继续执行 继续一个停止的进程
19 SIGSTOP 停止进程 无法被捕获或忽略,强制停止
20 SIGTSTP 停止进程 用户请求停止(Ctrl+Z)
21 SIGTTIN 停止进程 后台进程读终端输入
22 SIGTTOU 停止进程 后台进程写终端输出
23 SIGURG 忽略/捕获 套接字有紧急数据
24 SIGXCPU 创建核心转储 超出 CPU 时间限制
25 SIGXFSZ 创建核心转储 超出文件大小限制
26 SIGVTALRM 终止进程 虚拟定时器到期
27 SIGPROF 终止进程 分析定时器到期
28 SIGWINCH 忽略/捕获 终端窗口大小发生变化
29 SIGIO 忽略/捕获 I/O 事件通知
30 SIGPWR 忽略/终止 电源故障
31 SIGSYS 创建核心转储 非法的系统调用

信号四要素

每个信号必备四要素,分别是:

编号,名称,事件,默认处理动作

1
man 7 signal#查看帮助文档获取信号的信息

默认动作:

​ Term:终止进程

​ Ign:忽略信号(默认即时对该种信号忽略操作)

​ Core:终止进程,生成Core文件。(查验死亡原因,用于gdb调试)

​ Stop:停止(暂停)进程

​ Cont:继续运行进程

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

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

阻塞信号集和未决信号集

信号的实现手段导致信号有很强的延时性,但对于用户来说,时间非常短,不易察觉。

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

阻塞信号集

将某些信号加入集合,对他们设置屏蔽,当屏蔽X信号后,再收到该信号,该信号的处理将推后(处理发生在解除屏蔽后)。

未决信号集

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

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

信号产生函数

kill函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<sys/types.h>
#include<signal.h>

int kill(pid_t pid,int sig);
功能:给指定进程发送指定信号(不一定杀死)
参数:
pid:取值有4种情况:
pid>0:将信号传送给进程ID为pid的进程。
pid=0:将信号传送给当前进程所在进程组中的所有进程。
pid=-1:将信号传送给系统内所有的进程
pid<-1:将信号传送给指定进程组的所有进程。这个进程组号等于pid的绝对值。
sig:信号的编号,这里可以填数字编号,也可以填信号的宏定义,可以通过命令kill -l进行查看,不推荐直接使用数字,应使用宏名,因为不同操作系统信号编号可能不同,但名称一致。
返回值:
成功:0
失败:-1

超级用户(root)可以发送信号给任意用户,普通用户是不能向系统用户发送信号。也不能向其他普通用户发送信号,终止其进程。只能向自己创建的进程发送信号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<signal.h>
#include<unistd.h>
//父进程杀死子进程
int main(){
pid_t pid=-1;
//创建一个子进程
pid=fork();
if(-1==pid){
perror("fork");
return 1;
}
//子进程
if(0==pid){
while(1){
printf("child process do work....\n");
sleep(1);
}
//进程退出
exit(0);
}else{
//父进程
sleep(3);
printf("子进程退出\n");
kill(pid,SIGTERM);
printf("父进程该结束了,已经完成了他的使命\n");

}

return 0;
}

raise函数

1
2
3
4
5
6
7
8
#include<signal.h>
int raise(int sig);
功能:给当前进程发送指定信号(自己给自己发),等价于kill(getpid(),sig);
参数:
sig:信号编号
返回值:
成功:0
失败:非0

abort函数

1
2
3
4
5
6
#include<stdlib.h>

void abort(void);
功能:给自己发送异常终止信号编号为6 SIGABRT,并产生core文件,等价于kill(getpid(),SIGABRT);
参数:无
返回值:无

alarm函数(闹钟)

1
2
3
4
5
6
7
8
9
10
#include<unistd.h>

unsigned int alarm(unsigned int seconds);
功能:
设置定时器(闹钟)。在指定seconds后,内核会给当前进程发送编号14 SIGALRM信号。进程收到该信号,默认动作终止。每个进程都有且只有一个唯一的定时器。
取消定时器alarm(0),返回旧闹钟余下秒数。
参数:
seconds:指定的时间,以秒为单位
返回值:
返回0或剩余的秒数

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>

//测试alarm函数
int main(){
unsigned int ret=0;
//第一次设置闹钟 5秒钟之后就超时 发送对应的信号
ret=alarm(5);
printf("上一次闹钟剩下的时间是%u\n",ret);//这时ret为0
sleep(2);
//之前没有超时的闹钟被新的设置给覆盖
ret=alarm(4);
printf("上一次闹钟剩下的时间是%u\n",ret);//这时ret为3
printf("按下任意键继续。。。。\n");
getchar();
return 0;
}

setitimer函数(定时器)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include<sys/time.h>

int setitimer(int which,const struct itimerval *new_value,struct itimerval *old_value);
功能:
设置定时器(闹钟)。可替代alarm函数。精度微妙us,可以实现周期定时。
参数:
which:指定定时方式
自然定时:ITIMER_REAL 编号14的信号SIGALRM计算自然时间
虚拟空间计时(用户空间):ITIMER_VIRTUAL 编号26的信号SIGVTALRM 只计算进程占用cpu的时间
运行时计时(用户+内核):ITIMER_PROF 编号27的信号SIGPROF计算占用cpu及执行系统调用的时间
new_value:struct itimerval,负责设定timeout时间
struct itimerval{
struct timerval it_interval;//闹钟触发周期
struct timerval it_value;//闹钟触发时间
};
struct timerval{
long tv_sec;//秒
long tv_usec;//微秒
}
itimerval.it_value:设定第一次执行function所延迟的秒数
itimerval.it_interval:设定以后每几秒执行function
old_value:存放旧的timeout值,一般指定为NULL
返回值:
成功:0
失败:-1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/time.h>

int main(){
int ret=-1;

struct itimerval tmo;
//第一次触发时间
tmo.it_value.tv_sec=3;
tmo.it_value.tv_usec=0;
//触发周期
tmo.it_interval.tv_sec=2;
tmo.it_interval.tv_usec=0;
//设置定时器
ret=setitimer(ITIMER_REAL,&tmo,NULL)
if(-1==ret){
perror("setitimer");
return 1;
}
//进程收到闹钟超时信号之后就会终止该进程,要想周期起来,要把信号捕捉
printf("按下任意键继续。。。\n");
getchar();
return 0;
}

信号集

在PCB中有两个非常重要的信号集。一个称之为”阻塞信号集”,另一个称之为”未决信号集”。

这两个信号集都是内核使用位图机制来实现的。但是操作系统不允许我们直接对其进行位操作。而需自定义另外一个集合,借助信号集操作函数来对PCB中的这两个信号集进行修改。阻塞信号集可以读写,未决信号集不可以写,只可以读。

自定义信号集函数

信号集是一个能表示多个信号的数据类型,sigset_t set,set即一个数据集。既然是一个集合,就需要对集合进行添加/删除等操作。

相关函数:

1
2
3
4
5
6
7
#include<signal.h>

int sigemptyset(sigset_t *set);//将set集合置空
int sigfillset(sigset_t *set);//将所有的信号加入set集合
int sigaddset(sigset_t *set,int signo);//将signo信号加入到set集合
int sigdelset(sigset_t *set,int signo);//将set集合中移除signo信号
int sigismember(const sigset_t *set,int signo);//判断信号是否存在

示例程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<signal.h>
//显示信号集
void show_set(sigset_t *s){
int i=0;
for(i=1;i<32;i++){
//判断指定的信号是否在集合中
if(sigismember(s,i)){
printf("1");
}else{
printf("0");
}
}
putchar('\n');
}
//信号集处理函数
int main(){
int i=0;
//信号集集合
sigset_t set;
//清空集合
sigemptyset(&set);
show_set(&set)
//将所有的信号加入到set集合中
sigfillset(&set);
show_set(&set);
//将信号2和信号3从信号集中移除
sigdelset(&set,SIGINT);
sigdelset(&set,SIGQUIT);
show_set(&set);
//将信号2添加到集合中
sigaddset(&set,SIGINT);
show_set(&set);
return 0;
}

sigprocmask函数

信号阻塞集也称信号屏蔽集。每个进程都有一个阻塞集,创建子进程时子进程将继承父进程的阻塞集。信号阻塞集用来描述那些信号递送到该进程的时候被阻塞(在信号发生时记住它,直到进程准备好时再将信号通知进程)。

所谓阻塞并不是禁止传送信号,而是暂缓信号的传送。若将被阻塞的信号从信号阻塞中删除,且对应的信号在被阻塞时发生了,进程会收到相应的信号。

我们可以通过sigprocmask()修改当前的信号掩码来改变信号的阻塞情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<signal.h>

int sigprocmask(int how,const sigset_t *set,sigset_t *oldset);
功能:
检查或修改信号阻塞集,根据how指定的方法对进程的阻塞集合进行修改,新的信号阻塞集由set指定,而原先的信号阻塞集合由oldset保存。
参数:
how:信号阻塞集合的修改方法,有三种情况:
SIG_BLOCK:向信号阻塞集合中添加set信号集,新的信号掩码是set和旧信号掩码的并集。相当于mask=mask|set.
SIG_UNBLOCK:从信号阻塞集合中删除set信号集,从当前信号掩码中去除set中的信号,相当于mask=mask&~set.
SIG_SETMASK:将信号阻塞集合设为set信号集,相当于原来信号阻塞集内容清空,然后按照set中的信号重新设置信号阻塞集。相当于mask=set.
set:要操作的信号集地址。
setNULL,则不改变信号阻塞集合,函数只把当前信号阻塞集合保存到oldset中。
oldset:保存原先信号阻塞集地址
返回值:
成功:0
失败:-1,失败时错误代码只可能是EINVAL,表示参数how不合法

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<signal.h>
#include<unistd.h>
//信号处理函数1
void func1(int signum){
printf("捕捉到信号:%d\n",signum);
}
//信号处理函数2
void func2(int signum){
printf("捕捉到信号:%d\n",signum);
}
//信号注册函数
int main(){
int ret=-1;
//信号集
sigset_t set;
sigset_t oldset;
//信号注册
//Ctrl+C
signal(SIGINT,func1);
//Ctrl+\
signal(SIGQUIT,func2);
printf("按下任意键 阻塞信号2\n");
getchar();
sigemptyset(&set);
sigemptyset(&oldset);
sigaddset(&set,SIGINT);
//设置屏蔽编号为2的信号
ret=sigprocmask(SIG_BLOCK,&set,&oldset);
if(ret==-1){
perror("sigprocmask");
return 1;
}
printf("设置屏蔽编号为2的信号成功了。。。。\n");
printf("按下任意键 解除阻塞信号2\n");
getchar();
//将信号屏蔽集设置为原来的集合,解除屏蔽后,不管之前发了多少次相同的信号,最后只会捕捉一次
ret=sigprocmask(SIG_SETMASK,&oldset,NULL);
if(ret==-1){
perror("sigprocmask");
return 1;
}
printf("按下任意键退出。。。。\n");
getchar();

return 0;
}

sigpending函数

1
2
3
4
5
6
7
8
#include<signal.h>
int sigpending(sigset_t *set);
功能:读取当前进程的未决信号集
参数:
set:未决信号集
返回值:
成功:0
失败:-1

信号捕捉

信号处理方式

一个进程收到一个信号的时候,可以用如下方法进行处理:

  1. 执行系统默认动作

    对大多数信号来说,系统默认动作是用来终止该进程。

  2. 忽略此信号(丢弃)

    接受到此信号后没有任何动作。

  3. 执行自定义信号处理函数(捕获)

    用用户自定义的信号处理函数处理该信号。

SIGKILL和SIGSTOP不能更改信号的处理方式,因为他们向用户提供了一种使进程终止的可靠方法。

signal函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include<signal.h>
typedef void(*sighandler_t)(int);
sighandler_t signal(int signum,sighandler_t handler);
功能:
注册信号处理函数(不可用于SIGKILL,SIGSTOP信号),即可确定收到信号后处理函数的入口地址。此函数不会阻塞。
参数:
signum:信号的编号,这里可以填数字编号,也可以填信号的宏定义,可以通过命令kill -l进行相应查看。
handler:取值有三种情况
SIG_IGN:忽略该信号。
SIG_DFL:执行系统默认动作
信号处理函数名:自定义信号处理函数。如:func
回调函数的定义如下:
void func(int signo){
//signo 为触发的信号,为signal()第一个参数
}
返回值:
成功:第一次返回NULL,下一次返回此信号上一次注册的信号处理函数的地址。如果需要使用此返回值,必须在前面声明此函数指针的类型。
失败:返回SIG_ERR

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<signal.h>
#include<unistd.h>
//信号处理函数1
void func1(int signum){
printf("捕捉到信号:%d\n",signum);
}
//信号处理函数2
void func2(int signum){
printf("捕捉到信号:%d\n",signum);
}
//信号注册函数
int main(){
//信号注册
//Ctrl+C
signal(SIGINT,func1);
//Ctrl+\
signal(SIGQUIT,func2);
while(1){
getchar();
}
return 0;
}

在学会捕捉后,对之前的setitimer函数实现的例子更新一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/time.h>
#include<signal.h>
//信号处理函数
void func(int signo){
printf("捕捉到信号:%d\n",signo);
}
int main(){
int ret=-1;

struct itimerval tmo;
//第一次触发时间
tmo.it_value.tv_sec=3;
tmo.it_value.tv_usec=0;
//触发周期
tmo.it_interval.tv_sec=2;
tmo.it_interval.tv_usec=0;
//捕捉信号 SIGALRM
signal(SIGALRM,func);
//设置定时器
ret=setitimer(ITIMER_REAL,&tmo,NULL)
if(-1==ret){
perror("setitimer");
return 1;
}
//进程收到闹钟超时信号之后就会终止该进程,要想周期起来,要把信号捕捉
printf("按下任意键继续。。。\n");
getchar();
return 0;
}

sigaction函数

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<signal.h>

int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact);
功能:
检查或修改指定信号的设置(或同时执行这两种操作)。
操作:
signum:要操作的信号。
act: 要设置的对信号的新处理方式(传入参数)。
oldact:原来对信号的处理方式(传出参数)。
如果act指针非空,则要改变指定信号的处理方式(设置),如果oldact指针非空,则系统将此前指定信号的处理方式存入oldact
返回值:
成功:0
失败:-1

struct sigaction 结构体

1
2
3
4
5
6
7
struct sigaction{
void(*sa_handler)(int);//旧的信号处理函数指针
void(*sa_sigaction)(int,siginfo_t*,void *);//新的信号处理函数指针
sigset_t sa_mask; //信号阻塞集 在信号处理函数执行过程中,临时屏蔽指定的信号
int sa_flags; //信号处理方式
void(*sa_restorer)(void); //已弃用
};

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<signal.h>
//信号处理函数
void fun(int signo){
printf("捕捉到信号 %d\n",signo);
}
//演示sigaction函数使用
int main(){
int ret=-1;
struct sigaction act;
//使用旧的信号处理函数指针
act.sa_handler=fun;
//标志为默认 默认使用旧的信号处理函数指针
act.sa_flags=0;
ret=sigaction(SIGINT,&act,NULL);
if(-1==ret){
perror("sigaction");
return 1;
}
printf("按下任意键退出。。。。\n");
while(1){
getchar();
}
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<signal.h>
//新的信号处理函数
void fun1(int signo,siginfo_t *info,void *context){
printf("捕捉到信号 %d\n",signo);
}
//演示sigaction函数使用
int main(){
int ret=-1;
struct sigaction act;
//使用新的信号处理函数指针
act.sa_sigaction=fun1;
//标志指定使用新的信号处理函数指针
act.sa_flags=SA_SIGINFO;

if(-1==ret){
perror("sigaction");
return 1;
}
printf("按下任意键退出。。。。\n");
while(1){
getchar();
}
return 0;
}

sigqueue函数(一般不用,用kill函数)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<signal.h>

int sigqueue(pid_t pid,int sig,const union sigval value);
功能:
给指定进程发送信号
参数:
pid:进程号
sig:信号的编号
value:通过信号传递的参数。
union sigval类型如下:
union sigval{
int sival_int;
void *sival_ptr;
}
返回值:
成功:0
失败:-1

不可重入,可重入函数

如果有一个函数不幸被设计成为这样:那么不同任务调用这个函数时可能修改其他任务调用这个函数的数据,从而导致不可预料的后果。这样的函数是不安全的函数,也叫不可重入函数。

满足下列条件的函数多数是不可重入(不安全)的:

  1. 函数体内使用了静态的数据结构;
  2. 函数体内调用了malloc()或者free()函数(谨慎使用堆);
  3. 函数体内调用了标准I/O函数(包含缓冲区)。

相反,肯定有一个安全的函数,这个安全的函数又叫可重入函数。那么什么是可重入函数呢?可重入是指一个可以被多个任务调用的过程,任务在调用时不必担心数据是否会出错。

保证函数的可重入性的方法:

  1. 在写函数时候尽量使用局部变量(例如寄存器,栈中变量);
  2. 对于要使用的全局变量要加以保护(如采取中断,信号量等互斥方法),这样构成的函数就一定是一个可重入的函数。

信号处理函数应为可重入函数。

SIGCHLD信号

SIGCHLD信号产生的条件

  1. 子进程终止时
  2. 子进程接收到SIGSTOP信号停止时
  3. 子进程处在停止态,接受到SIGCONT后唤醒时

如何避免僵尸进程

  1. 最简单的方法,父进程通过wait()和waitpid()等函数等待子进程结束,但是,这会导致父进程挂起。
  2. 如果父进程要处理的事情很多,不能够挂起,通过signal()函数人为处理信号SIGCHLD,只要有子进程退出自动调用指定好的回调函数,因为子进程结束后,父进程会收到该信号SIGCHLD(17号),可以在其回调函数里调用wait()或waitpid()回收。

示例程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<signal.h>
#include<sys/wait.h>
//信号处理函数
void fun(int signo){
printf("捕捉到信号 %d\n",signo);
printf("有子进程退出。。。。\n");
//以非阻塞方式
while((waitpid(-1,NULL,WNOHANG))<0){
continue;
}
}
//演示sigaction函数使用
int main(){
pid_t pid=-1;
struct sigaction act;
//使用旧的信号处理函数指针
act.sa_handler=fun;
//标志为默认 默认使用旧的信号处理函数指针
act.sa_flags=0;
sigaction(SIGCHLD,&act,NULL);
pid=fork();
if(pid<0){
perror("fork error");
return 1;
}
if(pid==0){
printf("子进程累");
sleep(2);
printf("子进程退出");
exit(0)
}else{
//父进程
while(1){
printf("父进程do working");
sleep(1);
}
}

return 0;
}

守护进程和线程

进程组

进程组,也称之为作业。代表一个或多个进程的集合。

每个进程都属于一个进程组。在waitpid函数和kill函数的参数中都曾使用过。操作系统设计的进程组的概念,是为了简化堆多个进程的管理。

当父进程,创建子进程的时候,默认子进程与父进程属于同一进程组。进程组ID为第一个进程ID(组长进程)。

会话

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

  1. 一个会话可以有一个控制终端。这通常是终端设备或伪终端设备;
  2. 建立与控制终端连接的会话首进程被称为控制进程;
  3. 一个会话中的几个进程组可被分为一个前台进程组以及一个或多个后台进程组;
  4. 如果一个会话有一个控制终端,则他有一个前台进程组,其他进程组为后台进程组。
  5. 如果终端接口检测到断开连接,则将挂断信号发送至控制进程(会话首进程)。

守护进程

守护进程也就是通常说的Daemon进程(精灵进程),是Linux的后台服务进程。它是一个生存期较长的进程,通常独立于控制终端并且周期性执行某种任务或等待处理某些发生的事件。一般采用以d结尾的名字。

守护进程是一个特殊的孤儿进程,这种进程脱离终端,为了避免进程被任何终端产生的信息所打断,其在执行过程中的信息也不在任何终端上显示。由于在Linux中,每个系统与用户进行交流的界面叫做终端,每一个从此终端开始运行的进程都会依附于这个终端,这个终端就称为这些进程的控制终端,当控制终端被关闭时,相应的进程会自动关闭。

守护进程模型

  1. 创建子进程,父进程退出(必须)

    所有工作在子进程中进行形式上脱离了控制终端

  2. 在子进程中创建新会话(必须)

    setsid()函数

    使子进程完全独立出来,脱离控制

  3. 改变当前目录为根目录(不是必须)

    chdir()函数

    防止占用可卸载的文件系统

    也可以换成其他路径

  4. 重设文件权限掩码(不是必须)

    umask()函数(可以对文件权限进行修改,如果umask为0000,那创建普通文件文件权限为0666,目录文件文件权限为0777,但是umask默认为0002)

    防止继承的文件创建屏蔽字拒绝某些权限

    增加守护进程灵活性

  5. 关闭文件描述符(不是必须)

    继承的打开文件不会用到,浪费系统资源,无法卸载。标准输入,标准输出,标准错误输出设备文件默认被打开

  6. 开始执行守护进程核心工作(必须)

    守护进程退出处理程序模型

守护进程参考代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
//创建守护进程
int main(){
pid_t pid=-1;
int ret=-1;
//1.创建子进程 父进程退出
pid=fork();
if(-1==pid){
perror("fork");
return 1;
}
if(pid>0){
//父进程退出
exit(0);
}
//2.创建新的会话 完全脱离控制终端
pid=setsid();
if(-1==pid){
perror("setsid");
return 1;
}
//3.改变当前工作目录
ret=chdir("/");
if(ret==-1){
perror("chdir");
return 1;
}
//4.设置权限掩码
umask(0);
//5.关闭文件描述符
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
//6.执行核心的任务
//每隔一秒钟输出当前的时间到/tmp/txt.log文件中
while(1){
system("date >> /tmp/txt.log");
sleep(1);
}
return 0;
}

获取系统时间,日志文件需要创建当天的文件,文件名为当天的日期。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<time.h>
#define SIZE 64
//创建守护进程
int main(){
time_t t=-1;
pid_t pid=-1;
int ret=-1;
struct tm *pT=NULL;
char file_name[SIZE];
//1.创建子进程 父进程退出
pid=fork();
if(-1==pid){
perror("fork");
return 1;
}
if(pid>0){
//父进程退出
exit(0);
}
//2.创建新的会话 完全脱离控制终端
pid=setsid();
if(-1==pid){
perror("setsid");
return 1;
}
//3.改变当前工作目录
ret=chdir("/");
if(ret==-1){
perror("chdir");
return 1;
}
//4.设置权限掩码
umask(0);
//5.关闭文件描述符
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
//6.执行核心的任务
//每隔一秒钟输出当前的时间到/tmp/txt.log文件中
while(1){
//获取当前时间 以秒为单位 从1970-01-01 00:00:00 开始到现在秒数。
t=time(NULL);
//转化为时间
pT=localtime(&t);
if(pT==NULL){
return 1;
}
//转化为文件名
memset(file_name,0,SIZE);
sprintf(file_name,"%s%d%d%d%d%d%d.log","touch /home/deng/log/",pT->tm_year+1990,pT->tm_mon+1,pT->tm_mday,pT->tm_hour,pT->tm_min,pT->tm_sec);
system(file_name);
sleep(1);
}
return 0;
}

线程

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

所以线程是轻量级的线程,在Linux环境下线程的本质仍是进程。

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

9

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

线程存在进程当中,是操作系统调度执行的最小单位。

线程的特点

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

查看指定进程的LWP号:

1
ps -Lf pid

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

  1. 如果复制对方的地址空间(深拷贝),那么就产生一个进程
  2. 如果共享对方的地址空间(浅拷贝),就产生一个线程。

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

线程共享资源

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

线程非共享资源

  1. 线程id
  2. 处理器现场和栈指针(内核栈)
  3. 独立的栈空间(用户空间栈)
  4. errno变量
  5. 信号屏蔽字
  6. 调度优先级

线程常用操作

进程号在整个系统中是唯一的,但线程号不同,线程号只在它所属的进程环境中有效。

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

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

pthread_self函数

1
2
3
4
5
6
7
8
#include<pthread.h>
pthread_t pthread_self(void);
功能:
获取线程号
参数:

返回值:
调用线程的线程id。

pthread_equal函数:

1
2
3
4
5
6
7
8
int pthread_equal(pthread_t t1,pthread_t t2);
功能:
判断线程号t1和t2是否相等。为了方便移植,尽量使用函数来比较线程ID.
参数:
t1,t2:待判断的线程号
返回值:
相等:非0
不相等:0

参考程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<pthread.h>

//指定链接线程库
//gcc pthread_self.c -pthread
//线程常用函数
int main(){
pthread_t tid=0;
//获取当前线程的线程号
tid=pthread_self();
printf("tid: %lu\n",tid);
//比较两个线程ID是否相同
if(pthread_equal(tid,pthread_self())){
printf("两个线程ID相同\n");
}else{
printf("两个线程ID不相同\n");
}
return 0;
}
线程的创建

pthread_create函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<pthread.h>
int pthread_create(pthread_t *thread,const pthread_attr_t *attr,void *(*start_routine)(void *),void *arg);

功能:
创建一个线程。
参数:
thread:线程标识符地址
attr:线程属性结构体地址,通常设置为NULL
start_routine:线程函数的入口地址
arg:传入线程函数的参数
返回值:
成功:0
失败:非0

在一个线程中调用pthread_create()函数创建新的进程后,当前线程从pthread_create()返回继续往下执行,而新的线程所执行的代码由我们传给pthread_create的函数指针start_routine决定。

由于pthread_create的错误码不保存在errno中,因此不能直接用perror()打印错误信息,可以用strerror()把错误码转换成错误信息再打印。

参考程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<pthread.h>
//线程调度后执行的任务
void *fun(void *arg){
printf("新的线程执行任务 tid:%lu\n",pthread_self());
return NULL;
}
void *fun1(void *arg){
int var=(int)(long)arg;
printf("线程2 var=%d\n",var);
return NULL;
}
int main(){
int ret=-1;
pthread_t tid=-1;
pthread_t tid2=-1;
//创建一个线程
ret=pthread_create(&tid,NULL,fun,NULL);
if(ret!=0){
printf("pthread_Create failed\n");
return 1;
}
//创建一个线程
ret=pthread_create(&tid2,NULL,fun1,(void *)0x3);
if(ret!=0){
printf("pthread_Create failed\n");
return 1;
}
printf("main thread....tid:%lu\n",pthread_self());
printf("按下任意键主线程退出");
getchar();
return 0;
}
线程资源回收

pthread_join函数:

1
2
3
4
5
6
7
8
9
10
#include<pthread.h>
int pthread_join(pthread_t thread,void **retval);
功能:
等待线程结束(此函数会阻塞),并回收线程资源,类似进程的wait函数。如果线程已经结束,那么该函数会立即返回。
参数:
thread:被等待的线程号。
retval:用来存储线程退出状态的指针的地址
返回值:
成功:0
失败:非0

参考程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<pthread.h>
#include<unistd.h>
//线程处理函数
void *fun(void *arg){
int i=0;
for(i=0;i<5;i++){
printf("fun thread do working %d\n",i);
sleep(1);
}
return (void *)0x3;
}
//回收线程的资源
int main(){
int ret=-1;
pthread_t tid=-1;
void *retp=NULL;
//创建一个线程
ret=pthread_create(&tid,NULL,fun,NULL);
if(ret!=0){
printf("pthread_create failed...\n");
return 1;
}
printf("main thread running...\n");
//等待线程结束 会阻塞
ret=pthread_join(tid,&retp);
if(ret!=0){
printf("pthread_join failed...\n");
return 1;
}
printf("retp: %p\n",retp);
printf("main thread exit...\n");
return 0;
}
1
2
3
4
5
6
7
8
main thread running...
fun thread do working 0
fun thread do working 1
fun thread do working 2
fun thread do working 3
fun thread do working 4
retp: 0x3
main thread exit...
线程分离

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

不能对一个已经处于detach状态的线程调用pthread_join,这样的调用将返回EINVAL错误。

pthread_detach函数:

1
2
3
4
5
6
7
8
9
10
#include<pthread.h>

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

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
// 线程处理函数
void *fun(void *arg)
{
int i = 0;
for (i = 0; i < 5; i++)
{
printf("fun thread do working %d\n", i);
sleep(1);
}
return NULL;
}
// 回收线程的资源
int main()
{
int ret = -1;
pthread_t tid = -1;
// 创建一个线程
ret = pthread_create(&tid, NULL, fun, NULL);
if (ret != 0)
{
printf("pthread_create failed...\n");
return 1;
}
printf("main thread running...\n");
//设置线程分离
ret=pthread_detach(tid);
if(ret!=0){
printf("pthread_detach failed...\n");
return 1;
}
printf("按下任意键主线程退出。。。\n");
getchar();
return 0;
}
1
2
3
4
5
6
7
main thread running...
按下任意键主线程退出。。。
fun thread do working 0
fun thread do working 1
fun thread do working 2
fun thread do working 3
fun thread do working 4
线程退出

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

  1. 线程从执行函数返回。
  2. 线程调用pthread_exit退出线程
  3. 线程可以被同一进程中的其他线程取消。

pthread_exit函数:

1
2
3
4
5
6
7
8
#include<pthread.h>

void pthread_exit(void *retval);
功能:
退出调用线程。一个进程中的多个线程是共享该进程的数据段,因此,通常线程退出后所占用的资源并不会释放。
参数:
retval:存储线程退出状态的指针。
返回值:无

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
// 线程处理函数
void *fun(void *arg)
{
int i = 0;
for (i = 0; i < 5; i++)
{
printf("fun thread do working %d\n", i);
sleep(1);
}
//return NULL;
//相当于return NULL
pthread_exit(NULL);
//终止整个进程
//exit(0);
}
// 回收线程的资源
int main()
{
int ret = -1;
pthread_t tid = -1;
// 创建一个线程
ret = pthread_create(&tid, NULL, fun, NULL);
if (ret != 0)
{
printf("pthread_create failed...\n");
return 1;
}
printf("main thread running...\n");
//设置线程分离
ret=pthread_detach(tid);
if(ret!=0){
printf("pthread_detach failed...\n");
return 1;
}
printf("按下任意键主线程退出。。。\n");
getchar();
return 0;
}
线程取消
1
2
3
4
5
6
7
8
9
#include<pthread.h>
int pthread_cancel(pthread_t thread);
功能:
杀死(取消)线程
参数:
thread:目标线程ID
返回值:
成功:0
失败:出错编号

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

取消点:是线程检查是否被取消,并按请求进行动作的一个位置。通常是一些系统调用creat,open,pause,close,read,write…可粗略认为一个系统调用(进入内核)为一个取消点。

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
// 线程处理函数
void *fun(void *arg)
{
int i = 0;
for (i = 0; i < 5; i++)
{
printf("fun thread do working %d\n", i);
sleep(1);
}
pthread_exit(NULL);
}
// 回收线程的资源
int main()
{
int ret = -1;
pthread_t tid = -1;
// 创建一个线程
ret = pthread_create(&tid, NULL, fun, NULL);
if (ret != 0)
{
printf("pthread_create failed...\n");
return 1;
}
printf("main thread running...\n");
//设置线程分离
ret=pthread_detach(tid);
if(ret!=0){
printf("pthread_detach failed...\n");
return 1;
}
sleep(3);
pthread_cancel(tid);
printf("主线程睡眠了3秒 取消子线程。。\n");
return 0;
}
1
2
3
4
5
main thread running...
fun thread do working 0
fun thread do working 1
fun thread do working 2
主线程睡眠了3秒 取消子线程。。
线程使用注意事项
  1. 主线程退出其他线程不退出,主线程应调用pthread_exit
  2. 避免僵尸线程,pthread_join/pthread_detach/pthread_create指定分离属性
  3. malloc和mmap申请的内存可以被其他线程释放。
  4. 避免在多线程模型中调用fork,除非马上exec,子进程中只有调用fork的线程存在,其他线程在子进程中均pthread_exit
  5. 信号的复杂语义很难和多线程共存,避免在多线程引入信号机制。

线程同步

互斥锁

现代操作系统基本都是多任务操作系统,即同时有大量可调度实体在运行。在多任务操作系统中,同时运行的多个任务:

  1. 都需要访问/使用同一种资源
  2. 多个任务之间有依赖关系,某个任务的运行依赖另一个任务

同步和互斥就是用于解决这两个问题。

互斥:是指散步在不同任务之间的若干程序片段,当某个任务运行其中一个程序片段时,其他任务就不能运行他们之中的任一程序片段,只能等到该任务运行完这个程序片段后才可以运行。最基本的场景:一个公共资源同一时刻只能被一个进程或线程使用,多个进程或线程不能同时使用公共资源。

同步:是指散步在不同任务之间的若干程序片段,他们的运行必须严格按照规定的某种先后次序来运行,这种先后次序依赖于要完成的特定的任务。最基本的场景就是:两个或两个以上的进程或线程在运行过程中协同步调,按预定的先后次序运行。比如A任务的运行依赖于B任务产生的数据。

互斥锁Mutex介绍

在线程里,也有这么一把锁:互斥锁(mutex),也叫互斥量,互斥锁是一种简单的加锁的方法来控制对共享资源的访问,互斥锁只有两种状态,即加锁(lock)和解锁(unlock)。

互斥锁的操作流程如下:

  1. 在访问共享资源后临界区域前,对互斥锁进行加锁。
  2. 在访问完成后释放互斥锁
  3. 对互斥锁进行加锁后,任何试图再次对互斥锁加锁的线程将会被阻塞,直到锁被释放。

互斥锁的数据类型是:pthread_mutex_t

pthread_mutex_init函数

初始化互斥锁:

1
2
3
4
5
6
7
8
9
10
11
12
#include <pthread.h>

int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
功能:
初始化一个互斥锁
参数:
mutex:互斥锁地址。类型是pthread_mutex_t.
attr:设置互斥量的属性,通常采用默认属性,即可将attr设为NULL.

返回值:
成功:0,成功申请的锁是默认打开的。
失败:非0错误码

pthread_mutex_destory函数

1
2
3
4
5
6
7
8
9
#include<pthread.h>
int pthread_mutex_destory(pthread_mutex_t *mutex);
功能:
销毁指定的一个互斥锁。互斥锁在使用完毕,必须要对互斥锁进行销毁,以释放资源。
参数:
mutex:互斥锁地址。
返回值:
成功:0
失败:非0错误码

pthread_mutex_lock函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<pthread.h>

int pthread_mutex_lock(pthread_mutex_t *mutex);
功能:
对互斥锁上锁,若互斥锁已经上锁,则调用者阻塞,直到互斥锁解锁后上锁。
参数:
mutex:互斥锁地址
返回值:
成功:0
失败:非0错误码

int pthread_mutex_trylock(pthread_mutex_t *mutex);
调用该函数时,若互斥锁未加锁,则上锁,返回0
若互斥锁已加锁,则函数直接返回失败,即EBUSY.

pthread_mutex_unlock函数

1
2
3
4
5
6
7
8
9
10
#include<pthread.h>

int pthread_mutex_unlock(pthread_mutex_t *mutex);
功能:
对指定的互斥锁解锁
参数:
mutex:互斥锁地址
返回值:
成功:0
失败:非0错误码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<pthread.h>
#include<unistd.h>
//互斥锁变量
pthread_mutex_t mutex;


//输出大写字母
void *fun1(void *arg){
int i=0;
//加锁
pthread_mutex_lock(&mutex);
for(i='A';i<'Z';i++){
putchar(i);
fflush(stdout);
usleep(10000);
}
//解锁
pthread_mutex_unlock(&mutex);
return NULL;
}
//输出小写字母
void *fun2(void *arg){
int i=0;
//加锁
pthread_mutex_lock(&mutex);
for(i='a';i<'z';i++){
putchar(i);
fflush(stdout);
usleep(10000);
}
//解锁
pthread_mutex_unlock(&mutex);
return NULL;
}
int main(){
int ret=-1;
pthread_t tid1,tid2;
//初始化一个互斥量 互斥锁
ret=pthread_mutex_init(&mutex,NULL);
if(0!=ret){
printf("pthread_mutex_init failed...\n");
return 1;
}
printf("初始化一个互斥量成功。。。。\n");

//创建两个线程
pthread_create(&tid1,NULL,fun1,NULL);
pthread_create(&tid2,NULL,fun2,NULL);
//等待两个线程结束
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);


printf("\n");
printf("main thread exit...\n");
//销毁互斥量 互斥锁
pthread_mutex_destroy(&mutex);
}

1
2
3
初始化一个互斥量成功。。。。
ABCDEFGHIJKLMNOPQRSTUVWXYabcdefghijklmnopqrstuvwxy
main thread exit...

死锁(DeadLock)

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象。若无外力作用,他们将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

预防死锁的方法

破坏请求和保持条件

协议1:所有进程开始前,必须一次性地申请所需的所有资源,这样运行期间就不会提出资源需求,破坏了请求条件,即使有一种资源不能满足需求,也不会给他分配正在空闲的进程,这样他就没有资源,这样他就没有资源,就破环了保持条件,从而预防。

协议2:允许一个进程只获得初期的资源就开始运行,然后再把运行完的资源释放出来。然后再请求新的资源。

破坏不可抢占条件

当一个已经保持了某种不可抢占资源的进程,提出新资源请求不能满足时,他必须释放已经保持的所有资源,以后需要时再重新申请。

破坏循环等待条件

对系统中的所有资源类型进行线性排序,然后规定每个进程必须按序列号递增的顺序请求资源。假如进程请求到了一些序列号较高的资源,然后请求一个序列较低的资源时,必须先释放相同和更高序号的资源后才能申请低序号的资源。多个同类资源必须一起请求。

读写锁

当有一个线程已经持有互斥锁时,互斥锁将所有试图进入临界区的线程都阻塞住。但是考虑一种情形,当前持有互斥锁的线程只是要读访问共享资源,而同时有其他几个线程也想读取这个共享资源,但是由于互斥锁的排他性,所有其他线程都无法获取锁,也就无法读访问共享资源了,但是实际上多个线程同时读访问共享资源并不会导致问题。

为了满足当前能够允许多个读出,但只允许一个写入的需求,线程提供了读写锁来实现。

读写锁的特点如下:

  1. 如果有其他线程读数据,则允许其他线程执行读操作,但不允许写操作。
  2. 如果有其他线程写数据,则其他线程都不允许读,写操作。

POSIX定义的读写锁的数据类型是:pthread_rwlock_t。

pthread_rwlock_init函数

1
2
3
4
5
6
7
8
9
10
11
#include<pthread.h>

int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t *restrict attr);
功能:
用来初始化rwlock所指向的读写锁
参数:
rwlock:指向要初始化的读写指针。
attr:读写锁的属性指针。如果attr为NULL则会使用默认的属性初始化读写锁,否则使用指定的attr初始化读写锁。
返回值:
成功:0,读写锁的状态将成为已初始化和已解锁
失败:非0错误码

pthread_rwlock_destroy函数

1
2
3
4
5
6
7
8
9
10
#include<pthread.h>

int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
功能:
用于销毁一个读写锁,并释放所有相关联的资源(所谓的所有指的是由 pthread_rwlock_init()自动申请的资源)
参数:
rwlock:读写锁指针
返回值:
成功:0
失败:非0错误码

pthread_rwlock_rdlock函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<pthread.h>

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
功能:
以阻塞方式在读写锁上获取读锁(读锁定)
如果没有写者持有该锁,并且没有写者阻塞在该锁上,则调用线程会获取读锁。
如果调用线程未获取读锁,则它将阻塞直到他获取了该锁。一个线程可以在一个读写锁上多次执行读锁定。线程可以成功调用pthread_rwlock_rdlock函数多次,但是之后该线程必须调用pthread_rwlock_unlock函数n次才能解除锁定。
参数:
rwlock;读写锁指针
返回值:
成功:0
失败:非0错误码
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
用于尝试以非阻塞的方式来在读写锁上获取读锁
如果有任何的写者持有该锁或有写者阻塞在该读写锁上,则立即失败返回。

pthread_rwlock_wrlock函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<pthread.h>

int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
功能:
在读写锁上获取写锁(写锁定)
如果没有写者持有该锁,并且没有读者持有该锁,则调用线程获取写锁
如果调用线程未获取写锁,则它将阻塞直到它获取了该锁。
参数:
rwlock:读写锁指针
返回值:
成功:0
失败:非0错误码
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
用于尝试以非阻塞的方式来在读写锁上获取写锁
如果有任何的读者和写者持有该锁,则立即失败返回。

pthread_rwlock_unlock函数

1
2
3
4
5
6
7
8
9
10
#include<pthread.h>

int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
功能:
无论是写锁还是读锁,都可以通过此函数解锁
参数:
rwlock:读写锁指针
返回值:
成功:0
失败:非0错误码

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
#include <stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>


//读写锁变量
pthread_rwlock_t rwlock;
//全局变量
int num=0;
//读线程
void *fun_read(void *arg){
//获取线程的编号
int index=(int)(long)arg;
while(1){
//加读写锁读锁
pthread_rwlock_rdlock(&rwlock);
printf("线程%d 读取num的值 %d\n",index,num);
//解锁
pthread_rwlock_unlock(&rwlock);
//随机睡眠1到3秒
sleep(random()%3+1);

}
return NULL;
}

//写线程
void *fun_write(void *arg){

//获取线程的编号
int index=(int)(long)arg;
while(1){
//加读写锁的写锁
pthread_rwlock_wrlock(&rwlock);
num++;
printf("线程%d 修改num的值 %d\n",index,num);
//解锁
pthread_rwlock_unlock(&rwlock);
//随机睡眠1到3秒
sleep(random()%3+1);

}
return NULL;
}
int main(){
int i=0;
int ret=-1;
pthread_t tid[8];
//设置随机种子
srandom(getpid());
//初始化读写锁
ret=pthread_rwlock_init(&rwlock,NULL);
if(ret!=0){
printf("pthread_rwlock_init failed..\n");
return 1;
}
//创建8个线程
for(i=0;i<8;i++){
//创建读线程
if(i<5){

pthread_create(&tid[i],NULL,fun_read,(void *)(long)i);
}
else{
//创建写线程
pthread_create(&tid[i],NULL,fun_write,(void *)(long)i);
}
}

//回收八个线程的资源
for(i=0;i<8;i++){
pthread_join(tid[i],NULL);
}
//销毁读写锁
pthread_rwlock_destroy(&rwlock);
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
线程0 读取num的值 0
线程1 读取num的值 0
线程2 读取num的值 0
线程3 读取num的值 0
线程4 读取num的值 0
线程5 修改num的值 1
线程7 修改num的值 2
线程6 修改num的值 3
线程3 读取num的值 3
线程4 读取num的值 3
线程6 修改num的值 4
线程0 读取num的值 4
线程2 读取num的值 4
线程5 修改num的值 5
线程6 修改num的值 6
线程1 读取num的值 6
线程0 读取num的值 6
线程2 读取num的值 6
线程4 读取num的值 6
线程7 修改num的值 7
线程5 修改num的值 8
线程6 修改num的值 9
线程3 读取num的值 9
线程7 修改num的值 10

条件变量

条件变量是用来等待而不是用来上锁的,条件变量本身不是锁。

条件变量用来自动阻塞一个线程,直到某种特殊情况发生为止。通常条件变量和互斥锁同时使用。

条件变量的两个动作:

  1. 条件不满足,阻塞线程
  2. 当条件满足时,通知阻塞的线程开始工作

条件变量的类型:pthread_cond_t

pthread_cond_init函数

1
2
3
4
5
6
7
8
9
10
11
12
#include<pthread.h>

int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);

功能:
初始化一个条件变量
参数:
cond:指向要初始化的条件变量指针。
attr:条件变量属性,通常为默认值,传NULL即可
返回值:
成功:0
失败:非0错误码

pthread_cond_destroy函数

1
2
3
4
5
6
7
8
9
10
#include<pthread.h>

int pthread_cond_destroy(pthread_cond_t *cond);
功能:
销毁一个条件变量
参数:
cond:指向要初始化的条件变量指针
返回值:
成功:0
失败:非0错误码

pthread_cond_wait函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<pthread.h>

int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
功能:
阻塞等待一个条件变量
1.阻塞等待条件变量cond(参数1)满足
2.释放已掌握的互斥锁(解锁互斥量)相当于pthread_mutex_unlock(&mutex);
以上的两步为一个原子操作(不可中断,取消)
3.当被唤醒时,pthread_cond_wait函数返回时,解除阻塞并重新申请互斥锁pthread_mutex_lock(&mutex);
参数:
cond:指向要初始化的条件变量指针
mutex:互斥锁
返回值:
成功:0
失败:非0错误码

pthread_cond_signal函数

唤醒阻塞在条件变量上的线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include<pthread.h>

int pthread_cond_signal(pthread_cond_t *cond);
功能:
唤醒至少一个阻塞在条件变量上的线程
参数:
cond:指向要初始化的条件变量指针
返回值:
成功:0
失败:非0错误码

int pthread_cond_broadcast(pthread_cond_t *cond);
功能:
唤醒全部阻塞在条件变量上的线程
参数:
cond:指向要初始化的条件变量指针
返回值:
成功:0
失败:非0错误码

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>


int flag=0;
//互斥量
pthread_mutex_t mutex;

//条件变量
pthread_cond_t cond;

//改变条件的线程
void *fun1(void *arg){
while(1){
//加锁
pthread_mutex_lock(&mutex);
flag=1;
//解锁
pthread_mutex_unlock(&mutex);

//唤醒因为条件而阻塞线程
pthread_cond_signal(&cond);
sleep(2);
}
return NULL;
}

//等待条件的线程
void *fun2(void *arg){
while(1){
//加锁
pthread_mutex_lock(&mutex);
//表示条件不满足
if(flag==0){
//等待条件满足 会阻塞
pthread_cond_wait(&cond,&mutex);
}
printf("线程2因为条件满足 开始运行。。。\n");
flag=0;
//解锁
pthread_mutex_unlock(&mutex);
}
return NULL;
}
//条件变量的应用
int main(){
int ret=-1;
pthread_t tid1,tid2;
//初始化条件变量
ret=pthread_cond_init(&cond,NULL);
if(ret!=0){
printf("pthread_cond_init failed...\n");
return 1;
}
//初始化互斥量
ret=pthread_mutex_init(&mutex,NULL);
if(ret!=0){
printf("pthread_mutex_init failed...\n");
return 1;
}
//创建两个线程
pthread_create(&tid1,NULL,fun1,NULL);
pthread_create(&tid2,NULL,fun2,NULL);

//回收线程
ret=pthread_join(tid1,NULL);
if(ret!=0){
printf("pthread_join failed..\n");
return 1;
}
ret=pthread_join(tid2,NULL);
if(ret!=0){
printf("pthread_join failed..\n");
return 1;
}

//销毁互斥量
pthread_mutex_destroy(&mutex);
//销毁条件变量
pthread_cond_destroy(&cond);
return 0;
}
1
2
3
4
5
6
7
8
9
10
线程2因为条件满足 开始运行。。。
线程2因为条件满足 开始运行。。。
线程2因为条件满足 开始运行。。。
线程2因为条件满足 开始运行。。。
线程2因为条件满足 开始运行。。。
线程2因为条件满足 开始运行。。。
线程2因为条件满足 开始运行。。。
线程2因为条件满足 开始运行。。。
线程2因为条件满足 开始运行。。。
线程2因为条件满足 开始运行。。。

生产者和消费者模型(由条件变量实现)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

typedef struct _node_t
{
int data;
struct _node_t *next;
} node_t;
//条件变量
pthread_cond_t cond;
//互斥量
pthread_mutex_t mutex;
node_t *head = NULL;
// 生产者线程
void *producer(void *arg)
{
// 循环生产产品
while (1)
{
//加锁
pthread_mutex_lock(&mutex);
node_t *new = malloc(sizeof(node_t));
if (new == NULL)
{
printf("malloc failed....\n");
break;
}
memset(new, 0, sizeof(node_t));
// 1-100
new->data = random() % 100 + 1;
new->next = NULL;

// 头插法
new->next = head;
head = new;
printf("生产者生产产品%d\n",new->data);
//解锁
pthread_mutex_unlock(&mutex);
//唤醒因为条件变量阻塞的线程
pthread_cond_signal(&cond);
// 随机睡眠
sleep(random() % 3 + 1);
}

pthread_exit(NULL);
}

// 消费者线程
void *customer(void *arg)
{
node_t *tmp = NULL;
// 循环消费
while (1)
{
//加锁
pthread_mutex_lock(&mutex);
if (NULL == head)
{
//如果链表为空 就阻塞
pthread_cond_wait(&cond,&mutex);
}
else
{

tmp = head;
head = head->next;
printf("消费者消费 %d\n",tmp->data);
free(tmp);
}
//解锁
pthread_mutex_unlock(&mutex);
}

pthread_exit(NULL);
}

// 生产者和消费者模型 条件变量的版本
int main()
{
int ret=-1;
pthread_t tid1 = -1, tid2 = -1;
//设置随机种子
srandom(getpid());
//初始化条件变量
ret=pthread_cond_init(&cond,NULL);
if(ret!=0){
printf("pthread_cond_init failed...\n");
return 1;
}
//初始化互斥量
ret=pthread_mutex_init(&mutex,NULL);
if(ret!=0){
printf("pthread_mutex_init failed...\n");
return 1;
}

// 创建两个线程 生产者线程 消费者线程
pthread_create(&tid1, NULL, producer, NULL);
pthread_create(&tid2, NULL, customer, NULL);

// 等待两个线程结束
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);

//销毁
pthread_cond_destroy(&cond);
pthread_mutex_destroy(&mutex);
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
产者生产产品71
消费者消费 71
生产者生产产品69
消费者消费 69
生产者生产产品69
消费者消费 69
生产者生产产品54
消费者消费 54
生产者生产产品20
消费者消费 20
生产者生产产品4
消费者消费 4
生产者生产产品33
消费者消费 33
生产者生产产品23
消费者消费 23

条件变量的优缺点

相对于mutex而言,条件变量可以减少竞争。

如直接使用mutex,除了生产者,消费者之间要竞争互斥量以外,消费者之间也需要竞争互斥量,但如果链表中没有数据,消费者之间竞争互斥锁是无意义的。

有了条件变量机制后,只有生产者完成生产,才会引起消费者之间的竞争。提高了程序效率。

信号量

信号量广泛用于进程或进程间的同步和互斥,信号量本质上是一个非负的整数计数器,它用来控制对公共资源的访问。

编程时可根据操作信号量值的结果判断是否对公共资源具有访问的权限,当信号值大于0时,则可以访问,否则将阻塞。

PV原语对信号量的操作,一次P操作使信号量减1,一次V操作使信号量加1.

信号量主要用于进程或线程间的同步和互斥这两种典型情况。

信号量数据类型:sem_t

sem_init函数

初始化信号量:

1
2
3
4
5
6
7
8
9
10
11
12
#include<semaphore.h>

int sem_init(sem_t *sem,int pshared,unsigned int value);
功能:
创建一个信号量并初始化它的值。一个无名信号量在被使用前必须先初始化。
参数:
sem:信号量地址
pshared:等于0,信号量在线程间共享(常用);不等于0,信号量在进程间共享
value:信号量的初始值
返回值:
成功:0
失败:-1

sem_destroy函数

销毁信号量:

1
2
3
4
5
6
7
8
9
10
#include<semaphore.h>

int sem_destroy(sem_t *sem);
功能:
删除sem标识的信号量
参数:
sem:信号量地址
返回值:
成功:0
失败:-1

信号量P操作(减1)

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<semaphore.h>

int sem_wait(sem_t *sem);
功能:
将信号量的值减1,操作前,先检查信号量(sem)的值是否为0,若信号量为0,此函数会阻塞,直到信号量大于0时才进行减1操作。
参数:
sem:信号量的地址
返回值:
成功:0
失败:-1
int sem_trywait(sem_t *sem);
以阻塞的方式来对信号量进行减1操作。
若操作前,信号量的值等于0,则对信号量的操作失败,函数立即返回。

信号量V操作(加1)

1
2
3
4
5
6
7
8
9
10
#include<semaphore.h>

int sem_post(sem_t *sem);
功能:
将信号量的值加1并发出信号唤醒等待线程( sem_wait() );
参数:
sem:信号量的地址
返回值:
成功:0
失败:-1

获取信号量的值

1
2
3
4
5
6
7
8
9
10
11
12
#include<semaphore.h>

int sem_getvalue(sem_t *sem,int *sval);

功能:
获取sem标识的信号量的值,保存在sval中
参数:
sem:信号量地址
sval:保存信号量值的地址
返回值:
成功:0
失败:-1

信号量用于互斥

一次性打印完大写或者小写。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<pthread.h>
#include<unistd.h>
#include<semaphore.h>

//信号量变量
sem_t sem;


//输出大写字母
void *fun1(void *arg){
int i=0;
//申请资源 将可用资源减1
sem_wait(&sem);
for(i='A';i<'Z';i++){
putchar(i);
fflush(stdout);
usleep(10000);
}
//释放资源 将可用资源加1
sem_post(&sem);
return NULL;
}
//输出小写字母
void *fun2(void *arg){
int i=0;
//申请资源 将可用资源减1
sem_wait(&sem);
for(i='a';i<'z';i++){
putchar(i);
fflush(stdout);
usleep(10000);
}
//释放资源 将可用资源加1
sem_post(&sem);
return NULL;
}
int main(){
int ret=-1;
pthread_t tid1,tid2;
//初始化一个信号量
ret=sem_init(&sem,0,1);
if(0!=ret){
printf("sem_init failed...\n");
return 1;
}
printf("初始化一个信号量成功。。。。\n");

//创建两个线程
pthread_create(&tid1,NULL,fun1,NULL);
pthread_create(&tid2,NULL,fun2,NULL);
//等待两个线程结束
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);


printf("\n");
printf("main thread exit...\n");
//销毁信号量
sem_destroy(&sem);

return 0;
}

1
2
3
初始化一个信号量成功。。。。
ABCDEFGHIJKLMNOPQRSTUVWXYabcdefghijklmnopqrstuvwxy
main thread exit...

信号量用于同步

生产者和消费者模型(信号量)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
typedef struct _node_t
{
int data;
struct _node_t *next;
} node_t;
node_t *head = NULL;
// 信号量变量
// 容器的个数
sem_t sem_producer;
// 可以卖产品的个数
sem_t sem_customer;
// 生产者线程
void *producer(void *arg)
{
// 循环生产产品
while (1)
{
// 申请一个资源 容器
sem_wait(&sem_producer);
node_t *new = malloc(sizeof(node_t));
if (new == NULL)
{
printf("malloc failed....\n");
break;
}
memset(new, 0, sizeof(node_t));
// 1-100
new->data = random() % 100 + 1;
new->next = NULL;

// 头插法
new->next = head;
head = new;
printf("生产者生产产品%d\n", new->data);

// 通知消费者消费 将可以卖的商品个数加1
sem_post(&sem_customer);
// 随机睡眠
sleep(random() % 3 + 1);
}

pthread_exit(NULL);
}

// 消费者线程
void *customer(void *arg)
{
node_t *tmp = NULL;
// 循环消费
while (1)
{
// 申请资源 可以卖的商品个数减1
sem_wait(&sem_customer);
if (NULL == head)
{
// 如果链表为空 就阻塞
printf("产品为空");
}
tmp = head;
head = head->next;
printf("消费者消费 %d\n", tmp->data);
free(tmp);
// 释放资源 将容器个数加1
sem_post(&sem_producer);
// 睡眠1-3秒
sleep(random() % 3 + 1);
}

pthread_exit(NULL);
}

// 生产者和消费者模型 条件变量的版本
int main()
{
int ret = -1;
pthread_t tid1 = -1, tid2 = -1;
// 设置随机种子
srandom(getpid());
// 初始化
ret = sem_init(&sem_producer, 0, 4);
if (ret != 0)
{
printf("sem_producer_init failed...\n");
return 1;
}
ret = sem_init(&sem_customer, 0, 0);
if (ret != 0)
{
printf("sem_customer_init failed...\n");
return 1;
}
// 创建两个线程 生产者线程 消费者线程
pthread_create(&tid1, NULL, producer, NULL);
pthread_create(&tid2, NULL, customer, NULL);

// 等待两个线程结束
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);

// 销毁
sem_destroy(&sem_producer);
sem_destroy(&sem_customer);
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
生产者生产产品7
消费者消费 7
生产者生产产品10
消费者消费 10
生产者生产产品65
消费者消费 65
生产者生产产品84
消费者消费 84
生产者生产产品13
消费者消费 13
生产者生产产品10
消费者消费 10
生产者生产产品51

多生产者和多消费者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
typedef struct _node_t
{
int data;
struct _node_t *next;
} node_t;
node_t *head = NULL;
//互斥锁
pthread_mutex_t mutex;
// 信号量变量
// 容器的个数
sem_t sem_producer;
// 可以卖产品的个数
sem_t sem_customer;
// 生产者线程
void *producer(void *arg)
{
// 循环生产产品
while (1)
{
// 申请一个资源 容器
sem_wait(&sem_producer);
pthread_mutex_lock(&mutex);
node_t *new = malloc(sizeof(node_t));
if (new == NULL)
{
printf("malloc failed....\n");
break;
}
memset(new, 0, sizeof(node_t));
// 1-100
new->data = random() % 100 + 1;
new->next = NULL;

// 头插法
new->next = head;
head = new;
printf("生产者生产产品%d\n", new->data);
pthread_mutex_unlock(&mutex);
// 通知消费者消费 将可以卖的商品个数加1
sem_post(&sem_customer);
// 随机睡眠
sleep(random() % 3 + 1);
}

pthread_exit(NULL);
}

// 消费者线程
void *customer(void *arg)
{
node_t *tmp = NULL;
// 循环消费
while (1)
{
// 申请资源 可以卖的商品个数减1
sem_wait(&sem_customer);
pthread_mutex_lock(&mutex);
if (NULL == head)
{
// 如果链表为空 就阻塞
printf("产品为空");
}
tmp = head;
head = head->next;
printf("消费者消费 %d\n", tmp->data);
free(tmp);
pthread_mutex_unlock(&mutex);
// 释放资源 将容器个数加1
sem_post(&sem_producer);
// 睡眠1-3秒
sleep(random() % 3 + 1);
}

pthread_exit(NULL);
}

// 生产者和消费者模型 条件变量的版本
int main()
{
int ret = -1;
pthread_t tid[6];
// 设置随机种子
srandom(getpid());
//互斥锁初始化
pthread_mutex_init(&mutex,NULL);
// 初始化
ret = sem_init(&sem_producer, 0, 8);
if (ret != 0)
{
printf("sem_producer_init failed...\n");
return 1;
}
ret = sem_init(&sem_customer, 0, 0);
if (ret != 0)
{
printf("sem_customer_init failed...\n");
return 1;
}

for(int i=0;i<6;i++){
//生产者
if(i<2){
pthread_create(&tid[i], NULL, producer, NULL);
}else{
pthread_create(&tid[i], NULL, customer, NULL);
}
}
// 等待线程结束
for(int i=0;i<6;i++){
pthread_join(tid[i], NULL);
}

// 销毁
sem_destroy(&sem_producer);
sem_destroy(&sem_customer);
//互斥锁销毁
pthread_mutex_destroy(&mutex);
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
生产者生产产品59
消费者消费 59
生产者生产产品68
消费者消费 68
生产者生产产品12
消费者消费 12
生产者生产产品28
消费者消费 28
生产者生产产品97
消费者消费 97
生产者生产产品12
消费者消费 12
生产者生产产品51
消费者消费 51
生产者生产产品99
消费者消费 99
生产者生产产品96

完结撒花