Linux常见目录
目录
说明
/
根目录,所有目录的起点,Linux 文件系统的顶级目录
/bin
存放常用的二进制可执行文件(如 ls、cp、mv 等),普通用户和系统都可使用
/sbin
系统管理命令,只有 root 用户可用(如 reboot、ifconfig)
/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命令
使用 -a 显示所有同名命令(包括 alias、builtin、文件)
使用 -t 显示类型(简洁)
Linux命令格式
部分
说明
命令
要执行的操作,如 ls、cp、mkdir 等
选项
用于控制命令行为的开关,通常以 - 或 -- 开头,例如 -l、--help
参数
命令作用的对象,通常是文件名、目录名、用户名等
选项类型说明
类型
示例
说明
短选项
-l
通常是一个字母,多个可组合(如 -al)
长选项
--help
更易读,通常不可组合
组合选项
-avz
相当于 -a -v -z
帮助文档查看方法 如果是内建命令(可以通过之前的type命令查看)使用 help +内建命令
如果是外部命令 对应命令名 –help
man 是 Linux 中最常用的命令之一,全称是 manual(手册) ,用于查看各种命令、函数、配置文件的使用说明。它是学习和查找 Linux 命令最权威的工具。
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
常见用法示例
命令
说明
cd /home/user
切换到绝对路径 /home/user 目录
cd ..
切换到上一级目录
cd 或 cd ~
切换到当前用户的主目录
cd -
切换到上一次所在的目录(切换目录的“切换”)
cd ./folder
切换到当前目录下的子目录 folder
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文件类型 常用的文件类型有七种:普通文件,目录文件,设备文件,管道文件,链接文件和套接字。
普通文件 是 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.txt、fileA.txt,但不匹配 file10.txt
[abc]
匹配括号内的任意一个字符
ls file[123].txt
匹配 file1.txt、file2.txt、file3.txt
[a-z]
匹配指定范围内的任意一个字符
ls file[a-c].txt
匹配 filea.txt、fileb.txt、filec.txt
[!abc]
匹配不在括号内的任意一个字符
ls file[!123].txt
匹配除 file1.txt、file2.txt、file3.txt 以外的文件
touch命令 touch 是用来 创建空文件 或 更新已有文件的时间戳 的命令。
如果 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/
递归复制整个目录 dir1 到 dir2 中
保留属性复制文件
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 文件名 tail -c 30 文件名
du和df命令 du命令 查看目录或文件占用的磁盘空间 ,会考虑磁盘块对齐、文件系统元数据、软链接等因素。
参数
含义
-h
以人类可读的方式 显示(如 KB、MB)
-s
显示指定文件/目录占用的数据块
-a
显示所有文件和目录的大小(默认只显示目录)
--max-depth=N
显示目录深度(限制递归层数)
项目
ls -l
du -sh
显示内容
文件本身大小(内容字节数)
实际磁盘占用(包含对齐和元数据)
对目录
显示目录结构本身大小
显示目录下所有内容实际占用
单位
字节(Bytes)
自动转换为 KB/MB/GB
应用场景
看文件大小/属性
查哪些文件/目录占空间最多
1 2 3 echo "hello" > file.txtls -l file.txt du -sh file.txt
df命令 查看整个磁盘的使用情况
查找相关命令 find find 是 Linux 中功能非常强大的文件搜索命令,它可以根据名称、类型、时间、大小、权限等多种条件在目录中递归查找文件,还可以执行删除、移动、打印等操作。
1 find [搜索路径] [搜索条件] [处理动作]
按文件名查询:使用参数 -name
按文件大小查询:使用参数 -size
+100k 表示大于100k的文件
-100k表示小于100k的文件
100k 表示等于100k的文件
大小方面:k小写,M大写
查询大小范围
1 find ./ -size +50k -size -100k
按文件类型查询:使用参数 -type
类型代号
含义
示例
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 中非常常用的文本搜索工具,用于在文件或标准输出中查找匹配的字符串 ,功能强大,灵活,适合日志分析、配置文件搜索、编程辅助等场景。
选项
含义说明
-n
显示匹配行的行号
-i
忽略大小写
-v
反向匹配(即显示不包含该字符串的行)
-r or -R
递归搜索目录下的所有文件
-l
只列出匹配的文件名
-c
统计匹配的行数
--color=auto
高亮显示匹配的内容
1 2 3 4 grep -i "root" /etc/passwd grep -w "hello" /etc/passwd grep -r "u_char" ./ grep -i "hello" /etc/passwd --color=auto
管道 管道(|)一个命令的输出可以通过管道作为另一个命令的输入。
压缩包管理 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单独使用,只可以对文件压缩和解压,不可以对目录。
tar和gzip结合对目录压缩
1 2 3 tar -czvf sysctl.tar.gz sysctl tar -xzvf sysctl.tar.gz tar -xzvf sysctl.tar.gz -C /temp
bzip2 和gzip一样和tar结合。
1 2 tar -cjvf test.tar.bz2 test tar -xjvf share.tar.bz2
zip和unzip 通过zip压缩文件的目标文件不需要指定扩展名,默认扩展名为zip。
1 2 zip [选项] 目标文件(没有扩展名)源文件/目录 unzip -d 解压后目录文件 压缩文件
文件权限管理 访问权限说明:
读权限(r)
对文件而言,具有读取文件内容的权限;对目录而言,具有浏览目录的权限。
写权限(w)
对文件而言,具有新增,修改文件内容的权限;对目录而言,具有删除,移动目录内文件的权限。
可执行权限(x)
对文件而言,具有执行文件的权限;对目录而言,该用户具有进入目录的权限。
通常。Unix/Linux系统只允许文件的属主(所有者)或root用户改变文件的读写权限。
chmod chmod(change mode )是 Linux/Unix 系统中用于修改文件或目录权限 的命令。它支持两种权限设置方式:数字方式 和符号方式 。
符号方式:
1 2 3 chmod u/g/o/a +/-/= rwx 文件 chomd o+w a chomd u=rw,g=r,o=r a#把a的拥有者权限为re,同属组权限为r,其他用户权限为r。
数字方式:
1 2 3 4 5 6 chomd 0777 a#把a的对应用户的权限变为rwx.
chown chown 是 Linux/Unix 中用于更改文件或目录 所属用户(owner) 和 所属用户组(group) 的命令。
1 chown [选项] [新用户][:[新用户组]] 文件/目录
新用户:新的文件拥有者
新用户组:新的用户组(可选)
需要 sudo 权限(普通用户只能修改自己拥有的文件)
1 2 3 4 sudo chown root asudo 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
离线软件包卸载:
使用 Snap 安装(跨平台容器化安装方式)
使用 Snap 卸载(跨平台容器化安装方式)
从源代码编译安装
1 2 3 ./configure make sudo make install
重定向 主要是把命令输出的内容(之前是屏幕)输入到文件里。
1 2 3 4 5 6 7 ls /etc/passwd > output.txt ls /etc/passwd >> output.txt llll 2> error.txt llll 2>> error.txt lll 2> /dev/null ls ddddd /etc/passwd &> /dev/null ls ddddd /etc/passwd &>> output.txt
其他命令 tree 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
还原(恢复)
:
进入命令模式
i、a
进入插入模式
mG/mgg
到指定行,m为目标行数
/字符串
从当前光标位置向下查找(n,N查找内容切换)
?字符串
从当前光标位置向上查找(n,N查找内容切换)
插入模式 (Insert Mode)
按 i、a、o 等从普通模式进入插入模式。
作用 :输入文字、写代码、编辑内容。
常用进入方式:
命令
含义
i
在光标前插入
a
在光标后插入
o
在当前行下方新开一行并插入
I
跳到行首插入
A
跳到行尾插入
退出插入模式 :按 Esc 返回普通模式。
命令模式 (Command-Line Mode)
按 : 从普通模式进入命令模式。
用于输入各种操作命令,如保存、退出、查找、替换等。
常用命令:
命令
功能
:w
保存
:q
退出
:wq 或 ZZ
保存并退出
: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源文件到生成一个可执行文件,中间一共经历了四个步骤:
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
静态链接和动态链接 静态链接:由链接器在链接 时把库的内容加入到可执行程序中。
优点:对运行环境的依赖较小,具有较好的兼容性。
缺点:生成的程序比较大,在装入内存消耗更多的时间。库函数有了更新,必须重新编译。
动态链接:链接器在链接时仅仅建立和所需库函数的链接关系,在程序运行时才将所需资源调入可执行程序 。
优点:在需要的时候才会调入对应的资源函数。简化程序的升级,有着较小的程序体积,实现进程间的资源共享(避免重复拷贝)
缺点:依赖动态库,不能独立运行,动态库依赖版本问题严重。
静态和动态编译对比 我们编写的应用程序大量用到了库函数,系统默认采用动态链接的方式进行编译程序,若想采用静态编译,加入-static参数。
1 2 gcc test.c -o test gcc -static test.c -o test
静态编译是要比动态编译程序大的多。
静态库和动态库 静态库可以认为是一些目标代码的集合,是在可执行程序运行前就已经加入到执行码中,成为执行程序的一部分。按照习惯,一般以”.a”作为文件后缀名。静态库的命名一般分为三个部分:前缀:lib,库名称:自己定义。后缀:.a。最终静态库的名字为libxxx.a
静态库制作:
1 2 3 4 5 gcc -c add.c -o add.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 gcc -fPIC -c add.c gcc -fPIC -c sub.c gcc -fPIC -c mul.c gcc -fPIC -c div.c gcc -shared add.o sub.o mul.o div.o -o libtest.so nm libtest.so | grep "add" 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
除了以上三种还有两种将其添加到/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 show listsize
断点操作 简单断点:
break设置断点,可以简写为b
多文件设置断点
1 2 3 4 break filename:linenum break filename:function break class::function 或者function (type ,type ) break namespace::class::function
查询所有断点
1 2 3 4 info b info break i break i b
条件断点 一般来说,为断点设置一个条件,我们使用if关键词,后面跟其断点条件。
设置一个条件断点:
1 b test.c:8 if Value == 5
维护断点 delete 范围 删除指定的断点,其简写命令为d。如果不指定断点号,则表示删除所有的断点。
比删除更好的一种方法时disable停止点,disable了的停止点,GDB不会删除,当你还需要时,enable即可。
1 2 disable 断点编号 enable 断点编号
调试代码 1 2 3 4 5 6 7 run next step finish until continue quit
数据查看
自动显示 可以设置一些自动显示的变量,当程序停住时,或在你单步跟踪时,这些变量会自动显示。相关的GDB命令是display
1 2 3 4 5 6 7 display 变量名 info display undisplay num delete display dnums disable display dnumsenable display dnumsdisable 和enable #不删除自动显示的设置,而只是让其失效和恢复。
查看修改变量的值 1 2 3 ptype width p width set var width=47
自动化编译工具Makefile make是个命令工具。
Makefile语法规则 一条规则:
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" 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
Makefile示例 测试程序:test.c add.c sub.c mul.c div.c add.h sub.h mul.h div.h
1 2 3 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文件很多,难道我们要一个一个打吗,这也未免太麻烦,还可能漏打。
自定义变量 定义变量:
引用变量:
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 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 gcc -c $< -o $@
Makefile的函数 常用的函数
1 2 3 4 wildcard src=$(wildcard *.c) patsubst obj=$(patsubst %.c,%.o,$(src) )
在makefile中所有的函数都是有返回值的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 SRC=$(wildcard ./*.c) OBJS=$(patsubst %.c,%.o,$(SRC) ) TARGET=test $(TARGET) :$(OBJS) gcc $(OBJS) -o $(TARGET) %.o:%.c gcc -c $< -o $@ 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 SRC=$(wildcard ./*.c) OBJS=$(patsubst %.c,%.o,$(SRC) ) TARGET=test $(TARGET) :$(OBJS) gcc $(OBJS) -o $(TARGET) %.o:%.c gcc -c $< -o $@ .PHONY :cleanclean: rm -rf $(OBJS) $(TARGET)
上面这个版本就是最终版。
1 2 @gcc -c $< -o $@ -gcc -c $< -o $@
系统调用 系统调用说的是操作系统提供给用户程序调用的一组”特殊”接口
系统调用和库函数的区别 Linux下对文件操作有两种方式:系统调用和库函数调用
库函数调用有两类函数组成:
不需要系统调用:不需要切换到内核空间即可完成函数全部功能,并且结果反馈给应用程序,如strcpy,bzero等字符串操作函数。
需要调用系统调用:需要切换到内核空间,这类函数通过封装系统调用去实现相应功能,如printf,fread等。
错误处理函数 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> #include <errno.h> #include <string.h> int main () { FILE *fp=fopen("xxxx" ,"r" ); if (fp==NULL ){ printf ("%d\n" ,errno); printf ("%d\n" ,strerror(errno)); perror("fopen err" ); } 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_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; ino_t st_ino; mode_t st_mode; nlink_t st_nlink; uid_t st_uid; gid_t st_gid; dev_t st_rdev; off_t st_size; blksize_t st_blksize; blkcnt_t st_blocks; struct timespec st_atim ; struct timespec st_mtim ; struct timespec st_ctim ; #define st_atime st_atim.tv_sec #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> int main () { int fd=-1 ; int newfd=-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); write(fd,"ABCDEFG" ,7 ); write(newfd,"1234567" ,7 ); 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,...) ;功能:改变已打开的文件性质,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> int main (void ) { int fd=-1 ; int newfd=-1 ; int ret=-1 ; fd=open("txt" ,O_WRONLY|O_CREAT,0644 ); if (fd==-1 ){ perror("open" ); return 1 ; } printf ("fd=%d\n" ,fd); newfd=fcntl(fd,F_DUPFD,0 ); if (-1 ==newfd){ perror("fcntl" ); return 1 ; } printf ("newfd=%d\n" ,newfd); write(fd,"123456789" ,9 ); write(newfd,"ABCDEFGH" ,8 ); 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结构体。
这个结构体的内部成员有很多,我们要知道的:
进程id。系统中每个进程有唯一的id,在C语言中用pid_t类型表示,其实就是一个非负整数。
进程的状态,有就绪,运行,挂起,停止等状态。
进程切换时需要保存和恢复的一些CPU寄存器。
描述虚拟地址空间的信息。
描述控制终端的信息。
当前工作目录。
umask掩码
文件描述符表,包含很多指向file结构体的指针。
和信号相关的信息。
用户id和组id
会话(Session)和进程组。
进程可以使用的资源上限(Resource Limit)。
进程的状态 进程状态反映进程执行过程的变化。这些状态随着进程的执行和外界条件的变化而转换。
在三态模型中,进程状态分为三个基本状态,即运行态,就绪态,阻塞态。
在五态模型中,进程分为新建态,终止态,运行态,就绪态,阻塞态。
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 通过进程名字杀死进程,进程名字是可以重复的。
进程号和相关函数 每个进程都由一个进程号来标识,其类型为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 ; pid=fork(); if (pid==0 ){ printf ("hello pid:%d ppid:%d\n" ,getpid(),getppid()); exit (0 ); }else { 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 set follow-fork-mode parent
进程退出函数 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)){ printf ("子进程被信号%d杀死了\n" ,WTERMSIG(status)); } else if (WIFSTOPPED(status)){ 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 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,...) ;int execlp (const char *file,const char *arg,...) ;int execle (const char *path,const char *arg,...) ;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" ); 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" ); execvpe("ls" ,argv,envp); printf ("hello world\n" ); return 0 ; }
进程间通信 进程是一个独立的资源分配单元,不同进程(这里所说的进程通常指的是用户进程)之间的资源是独立的,没有关联,不能在一个进程中直接访问另一个进程的资源。
但是,进程不是孤立的,不同的进程需要进行信息的交互和状态的传递等,因此需要进程间通信。(IPC)
进程间通信的目的:
数据传输:一个进程需要将它的数据发送给另一个进程。
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
资源共享:多个进程之间共享同样的资源。为了做到这一点,需要内核提供互斥和同步机制。
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
Linux操作系统支持的主要进程间通信的通信机制:
无名管道 管道也叫无名管道,它是UNIX系统IPC(进程间通信)的最古老方式,所有的UNIX系统都支持这种通信机制。
管道有如下特点:
半双工,数据在同一时刻只能在一个方向上流动。
数据只能从管道的一端写入,从另一端读出。
写入管道中的数据遵循先入先出的规则。
管道所传送的数据是无格式的,这要求管道的读出方与写入方必须事先约定好数据的格式,如多少字节算一个消息。
管道不是普通文件,不属于某个文件系统,其只存在于内存 中。
管道在内存中对应一个缓冲区。不同的系统其大小不一定相同。
从管道读数据是一次性操作 ,数据一旦被读走,他就从管道中被抛弃,释放空间以便写更多的数据。
管道没有名字,只能在具有公共祖先的进程(父进程与子进程,或者两个兄弟进程,具有亲缘关系)之间使用。
管道是一种特殊类型的文件,在应用层体现为两个打开的文件描述符。
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 ; } 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 ; ret=pipe(fd); if (-1 ==ret){ perror("pipe" ); return 1 ; } 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 int flags=fcntl(fd[0 ],F_GETFL);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不相关的进程也能交换数据。
命名管道和无名管道有一些特点是相同的,不一样的地方在于:
FIFO在文件系统中作为一个特殊的文件而存在,但FIFO的内容却放在内存中。
当使用FIFO的进程退出后,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> int main () { int ret=-1 ; 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 int fd=open("my_fifo" ,O_WRONLY);char send[100 ]="hello Mike" ;write(fd,send,strlen (send)); 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:以文件开始处的偏移量,必须是4 k的整数倍(4 k为页的大小),通常为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 ; fd=open("txt" ,O_RDWR); if (-1 ==fd){ perror("open" ); return 1 ; } addr=mmap(NULL ,1024 ,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0 ); if (addr==MAP_FAILED){ perror("mmap" ); return 1 ; } close(fd); memcpy (addr,"1234567890" ,10 ); 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 ; addr=mmap(NULL ,4096 ,PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANONYMOUS,-1 ,0 ); if (MAP_FAILED==addr){ perror("mmap" ); return 1 ; } pid=fork(); if (-1 ==pid){ perror("fork" ); munmap(addr,4096 ); return 1 ; } if (pid==0 ){ memcpy (addr,"123456789" ,10 ); } else { wait(NULL ); printf ("parent process %s\n" ,(char *)addr); } munmap(addr,4096 ); return 0 ; }
信号 信号是Linux进程间通信的最古老的方式。信号是软件中断 ,它是在软件层次上对中断机制的一次模拟,是一种异步通信的方式 。信号可以导致一个正在运行的进程被另一个进程正在运行的异步进程中断,转而处理某一个突发事件。
信号的特点:简单,不能携带大量信息,满足某个特设条件才发送。
信号可以直接进行用户空间进程和内核空间进程的交互,内核进程可以利用它来通知用户空间进程发生了那些系统事件。
一个完整的信号周期包括三个部分:信号的产生,信号在进程中的注册,信号在进程中的注销,执行信号处理函数。
这里信号的产生,注册,注销是信号的内部机制,而不是信号的函数实现。
信号的编号 Unix早期版本就提供了信号机制,但不可靠,信号可能丢失。Berkeley和AT&T都对信号做了更改,增加了可靠信号机制。但彼此不兼容。POSIX.1对可靠信号例程进行了标准化。
Linux可使用命令:
不存在编号为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> int main () { unsigned int ret=0 ; ret=alarm(5 ); printf ("上一次闹钟剩下的时间是%u\n" ,ret); sleep(2 ); ret=alarm(4 ); printf ("上一次闹钟剩下的时间是%u\n" ,ret); 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 ) ;int sigfillset (sigset_t *set ) ;int sigaddset (sigset_t *set ,int signo) ;int sigdelset (sigset_t *set ,int 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 ) sigfillset(&set ); show_set(&set ); sigdelset(&set ,SIGINT); sigdelset(&set ,SIGQUIT); show_set(&set ); 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 :要操作的信号集地址。 若set 为NULL ,则不改变信号阻塞集合,函数只把当前信号阻塞集合保存到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> void func1 (int signum) { printf ("捕捉到信号:%d\n" ,signum); } void func2 (int signum) { printf ("捕捉到信号:%d\n" ,signum); } int main () { int ret=-1 ; sigset_t set ; sigset_t oldset; signal(SIGINT,func1); printf ("按下任意键 阻塞信号2\n" ); getchar(); sigemptyset(&set ); sigemptyset(&oldset); sigaddset(&set ,SIGINT); 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
信号捕捉 信号处理方式 一个进程收到一个信号的时候,可以用如下方法进行处理:
执行系统默认动作
对大多数信号来说,系统默认动作是用来终止该进程。
忽略此信号(丢弃)
接受到此信号后没有任何动作。
执行自定义信号处理函数(捕获)
用用户自定义的信号处理函数处理该信号。
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) { } 返回值: 成功:第一次返回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> void func1 (int signum) { printf ("捕捉到信号:%d\n" ,signum); } void func2 (int signum) { printf ("捕捉到信号:%d\n" ,signum); } int main () { signal(SIGINT,func1); 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 ; 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); } 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); } 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
不可重入,可重入函数 如果有一个函数不幸被设计成为这样:那么不同任务调用这个函数时可能修改其他任务调用这个函数的数据,从而导致不可预料的后果。这样的函数是不安全的函数,也叫不可重入函数。
满足下列条件的函数多数是不可重入(不安全)的:
函数体内使用了静态的数据结构;
函数体内调用了malloc()或者free()函数(谨慎使用堆);
函数体内调用了标准I/O函数(包含缓冲区)。
相反,肯定有一个安全的函数,这个安全的函数又叫可重入函数。那么什么是可重入函数呢?可重入是指一个可以被多个任务调用的过程,任务在调用时不必担心数据是否会出错。
保证函数的可重入性的方法:
在写函数时候尽量使用局部变量(例如寄存器,栈中变量);
对于要使用的全局变量要加以保护(如采取中断,信号量等互斥方法),这样构成的函数就一定是一个可重入的函数。
信号处理函数应为可重入函数。
SIGCHLD信号 SIGCHLD信号产生的条件
子进程终止时
子进程接收到SIGSTOP信号停止时
子进程处在停止态,接受到SIGCONT后唤醒时
如何避免僵尸进程
最简单的方法,父进程通过wait()和waitpid()等函数等待子进程结束,但是,这会导致父进程挂起。
如果父进程要处理的事情很多,不能够挂起,通过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 ; } } 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(组长进程)。
会话 会话是一个或多个进程组的集合。
一个会话可以有一个控制终端。这通常是终端设备或伪终端设备;
建立与控制终端连接的会话首进程被称为控制进程;
一个会话中的几个进程组可被分为一个前台进程组以及一个或多个后台进程组;
如果一个会话有一个控制终端,则他有一个前台进程组,其他进程组为后台进程组。
如果终端接口检测到断开连接,则将挂断信号发送至控制进程(会话首进程)。
守护进程 守护进程也就是通常说的Daemon进程(精灵进程),是Linux的后台服务进程。它是一个生存期较长的进程,通常独立于控制终端并且周期性执行某种任务或等待处理某些发生的事件。一般采用以d结尾的名字。
守护进程是一个特殊的孤儿进程 ,这种进程脱离终端,为了避免进程被任何终端产生的信息所打断,其在执行过程中的信息也不在任何终端上显示。由于在Linux中,每个系统与用户进行交流的界面叫做终端,每一个从此终端开始运行的进程都会依附于这个终端,这个终端就称为这些进程的控制终端,当控制终端被关闭时,相应的进程会自动关闭。
守护进程模型
创建子进程,父进程退出(必须)
所有工作在子进程中进行形式上脱离了控制终端
在子进程中创建新会话(必须)
setsid()函数
使子进程完全独立出来,脱离控制
改变当前目录为根目录(不是必须)
chdir()函数
防止占用可卸载的文件系统
也可以换成其他路径
重设文件权限掩码(不是必须)
umask()函数(可以对文件权限进行修改,如果umask为0000,那创建普通文件文件权限为0666,目录文件文件权限为0777,但是umask默认为0002)
防止继承的文件创建屏蔽字拒绝某些权限
增加守护进程灵活性
关闭文件描述符(不是必须)
继承的打开文件不会用到,浪费系统资源,无法卸载。标准输入,标准输出,标准错误输出设备文件默认被打开
开始执行守护进程核心工作(必须)
守护进程退出处理程序模型
守护进程参考代码 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 ; pid=fork(); if (-1 ==pid){ perror("fork" ); return 1 ; } if (pid>0 ){ exit (0 ); } pid=setsid(); if (-1 ==pid){ perror("setsid" ); return 1 ; } ret=chdir("/" ); if (ret==-1 ){ perror("chdir" ); return 1 ; } umask(0 ); close(STDIN_FILENO); close(STDOUT_FILENO); close(STDERR_FILENO); 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]; pid=fork(); if (-1 ==pid){ perror("fork" ); return 1 ; } if (pid>0 ){ exit (0 ); } pid=setsid(); if (-1 ==pid){ perror("setsid" ); return 1 ; } ret=chdir("/" ); if (ret==-1 ){ perror("chdir" ); return 1 ; } umask(0 ); close(STDIN_FILENO); close(STDOUT_FILENO); close(STDERR_FILENO); while (1 ){ 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环境下线程的本质仍是进程。
为了让进程完成一定的工作,进程必须至少包含一个线程。
进程,直观点说,保存在硬盘上的程序运行以后,会在内存空间里形成一个独立的内存体,这个内存体有自己的地址空间,有自己的堆,上级挂靠单位是操作系统。操作系统以进程为单位,分配系统资源,所以我们也说,进程是CPU分配资源的最小单位。
线程存在进程当中,是操作系统调度执行的最小单位。
线程的特点
线程是轻量级进程(light-weight process),也有PCB,创建线程使用的底层函数和进程一样,都是clone。
从内核里看进程和线程是一样的,都有各自不同的PCB。
进程可以蜕变成线程。
在Linux下,线程是最小的执行单位;进程是最小的分配资源单位。
查看指定进程的LWP号:
实际上,无论是创建进程的fork,还是创建线程的pthread_create,底层实现都是调用同一个内核函数clone。
如果复制对方的地址空间(深拷贝),那么就产生一个进程
如果共享对方的地址空间(浅拷贝),就产生一个线程。
*Linux内核是不区分进程和线程的,只在用户层面上进行区分。所以,线程所有操作函数pthread_是库函数,而非系统调用。
线程共享资源
文件描述符表
每种信号的处理方式
当前工作目录
用户ID和组ID
内存地址空间(.text/.data/.bss/heap/共享库)(这里是多线程共享)
线程非共享资源
线程id
处理器现场和栈指针(内核栈)
独立的栈空间(用户空间栈)
errno变量
信号屏蔽字
调度优先级
线程常用操作 进程号在整个系统中是唯一的,但线程号不同,线程号只在它所属的进程环境中有效。
进程号用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> int main () { pthread_t tid=0 ; tid=pthread_self(); printf ("tid: %lu\n" ,tid); 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函数来结束进程,在一个线程中我们可以通过一下三种在不终止整个进程的情况下停止它的控制流。
线程从执行函数返回。
线程调用pthread_exit退出线程
线程可以被同一进程中的其他线程取消。
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 ); } 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 ; } 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秒 取消子线程。。
线程使用注意事项
主线程退出其他线程不退出,主线程应调用pthread_exit
避免僵尸线程,pthread_join/pthread_detach/pthread_create指定分离属性
malloc和mmap申请的内存可以被其他线程释放。
避免在多线程模型中调用fork,除非马上exec,子进程中只有调用fork的线程存在,其他线程在子进程中均pthread_exit
信号的复杂语义很难和多线程共存,避免在多线程引入信号机制。
线程同步 互斥锁 现代操作系统基本都是多任务操作系统,即同时有大量可调度实体在运行。在多任务操作系统中,同时运行的多个任务:
都需要访问/使用同一种资源
多个任务之间有依赖关系,某个任务的运行依赖另一个任务
同步和互斥就是用于解决这两个问题。
互斥:是指散步在不同任务之间的若干程序片段,当某个任务运行其中一个程序片段时,其他任务就不能运行他们之中的任一程序片段,只能等到该任务运行完这个程序片段后才可以运行。最基本的场景:一个公共资源同一时刻只能被一个进程或线程使用,多个进程或线程不能同时使用公共资源。
同步:是指散步在不同任务之间的若干程序片段,他们的运行必须严格按照规定的某种先后次序来运行,这种先后次序依赖于要完成的特定的任务。最基本的场景就是:两个或两个以上的进程或线程在运行过程中协同步调,按预定的先后次序运行。比如A任务的运行依赖于B任务产生的数据。
互斥锁Mutex介绍 在线程里,也有这么一把锁:互斥锁(mutex),也叫互斥量,互斥锁是一种简单的加锁的方法来控制对共享资源的访问,互斥锁只有两种状态,即加锁(lock)和解锁(unlock)。
互斥锁的操作流程如下:
在访问共享资源后临界区域前,对互斥锁进行加锁。
在访问完成后释放互斥锁
对互斥锁进行加锁后,任何试图再次对互斥锁加锁的线程将会被阻塞,直到锁被释放。
互斥锁的数据类型是: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:允许一个进程只获得初期的资源就开始运行,然后再把运行完的资源释放出来。然后再请求新的资源。
破坏不可抢占条件
当一个已经保持了某种不可抢占资源的进程,提出新资源请求不能满足时,他必须释放已经保持的所有资源,以后需要时再重新申请。
破坏循环等待条件
对系统中的所有资源类型进行线性排序,然后规定每个进程必须按序列号递增的顺序请求资源。假如进程请求到了一些序列号较高的资源,然后请求一个序列较低的资源时,必须先释放相同和更高序号的资源后才能申请低序号的资源。多个同类资源必须一起请求。
读写锁 当有一个线程已经持有互斥锁时,互斥锁将所有试图进入临界区的线程都阻塞住。但是考虑一种情形,当前持有互斥锁的线程只是要读访问共享资源,而同时有其他几个线程也想读取这个共享资源,但是由于互斥锁的排他性,所有其他线程都无法获取锁,也就无法读访问共享资源了,但是实际上多个线程同时读访问共享资源并不会导致问题。
为了满足当前能够允许多个读出,但只允许一个写入的需求,线程提供了读写锁来实现。
读写锁的特点如下:
如果有其他线程读数据,则允许其他线程执行读操作,但不允许写操作。
如果有其他线程写数据,则其他线程都不允许读,写操作。
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); 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); 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 ; } 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
条件变量 条件变量是用来等待而不是用来上锁的,条件变量本身不是锁。
条件变量用来自动阻塞一个线程,直到某种特殊情况发生为止。通常条件变量和互斥锁同时使用。
条件变量的两个动作:
条件不满足,阻塞线程
当条件满足时,通知阻塞的线程开始工作
条件变量的类型: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 )); 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 ; sem_wait(&sem); for (i='A' ;i<'Z' ;i++){ putchar (i); fflush(stdout ); usleep(10000 ); } sem_post(&sem); return NULL ; } void *fun2 (void *arg) { int i=0 ; sem_wait(&sem); for (i='a' ;i<'z' ;i++){ putchar (i); fflush(stdout ); usleep(10000 ); } 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 )); new->data = random() % 100 + 1 ; new->next = NULL ; new->next = head; head = new; printf ("生产者生产产品%d\n" , new->data); sem_post(&sem_customer); sleep(random() % 3 + 1 ); } pthread_exit(NULL ); } void *customer (void *arg) { node_t *tmp = NULL ; while (1 ) { sem_wait(&sem_customer); if (NULL == head) { printf ("产品为空" ); } tmp = head; head = head->next; printf ("消费者消费 %d\n" , tmp->data); free (tmp); sem_post(&sem_producer); 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 )); new->data = random() % 100 + 1 ; new->next = NULL ; new->next = head; head = new; printf ("生产者生产产品%d\n" , new->data); pthread_mutex_unlock(&mutex); sem_post(&sem_customer); sleep(random() % 3 + 1 ); } pthread_exit(NULL ); } void *customer (void *arg) { node_t *tmp = NULL ; while (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); sem_post(&sem_producer); 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
完结撒花