文章目录
- 一. shell基础
- 二. Linux文件IO操作
- 三. 进程
- 四. 信号
- 五. 文件描述符复制
- 六. 无名管道
- 七. 有名管道(命名管道)
- 八. 消息队列
- 九. mmap
- 十. 共享内存
- 十一. 线程
- 十二. 线程的同步与互斥
- 十三. 条件变量
- 十四. 生产者和消费者
- 十五. 信号量
- 千锋教育__Linux高级程序设计
一. shell基础
1 shell概述
shell本质是脚本文件:完成批处理。
shell是软件也是语言。
软件:shell命令解析器:(sh、ash、bash),将脚本文件逐行解析执行。
2 系统默认调用的两个脚本文件
/etc/profile
: 对系统的所有用户都有效,用户登录系统的时候执行。
~/.bashrc
:对登录的用户有效,用户登录,打开终端。
3 shell语法
- 定义开头:
#!/bin/bash
指明脚本解析器用bash- 写脚本
- 给脚本增加可执行权限
chmod +x 脚本文件
- 执行脚本文件
./00_shell.sh
先检测#! 使用#!指定的shell,如果没有使用默认的shell
. 00_shell.sh
使用当前shell解析00_shell.sh
bash 00_shell.sh
直接指定试用bash解析00_shell.sh
注:
用./
和ash
去执行会在后台启动一个新的shell去执行脚本
用.
去执行脚本不会启动新的shell,直接由当前的shell去解析执行脚本。
3.1 自定义变量
变量定义中不能带空格
#!/bin/bash
# 定义变量
num=10
# 对变量读操作
echo $num
# 对变量写操作
num=100
echo $num
# 清除变量
unset num
echo "---$num"
注:
脚本逐行解析,前面有错误,影响不大的话不影响后面代码的执行。
3.2 获取键盘输入
#!/bin/bash
num=0
# 从键盘获取变量的值 -p先打印再从键盘获取变量
read -p "请输入num的值:" num
echo "num = $num"
3.3 只读变量
# 只读变量
readonly num=10
echo "num=$num"
num=1000 #报错
echo "num=$num" #打印结果仍为10
3.4 将脚本的变量导出为环境变量
直接使用系统的环境变量
显示环境变量 env
清除环境变量 unset
#!/bin/bash
# 直接使用系统的环境变量
echo "PWD=$PWD"
#将shell的变量导出为环境变量, source次脚本后,其他脚本也能使用这个环境变量,重启终端后此环境变量消失
export num=1000
echo "num=$num"
注:也可在终端上直接输入export num=1000
3.5 变量的注意事项
1. 命名规则
变量名由字母、数字、下划线组成,不能以数字开头,不能是关键字。
2. 变量使用时注意点
# 等号两边不能直接接空格符
num = 100 # 错误
num=100 # ok
# 若变量中本身就包含了空格,则整个字符串都要用双引号、或单引号括起来
num=10 20 30 # 错误
num="10 20 30" # ok
num='10 20 30' # ok
3. 双引号和单引号的区别
双引号内的特殊字符可以保有变量特性,但是单引号内的特殊字符则仅为一般字符
3.6 修改环境变量的值
增加环境变量(终端下)
export NUM=10
在原本的环境变量下追加值(终端下)
export NUM=$NUM:20 # $SUM为原来的值
3.7 预设变量
$#
:传给shell脚本参数的数量$*
:传给shell脚本参数的内容$?
:命令执行后返回的状态$0
:当前执行的进程名$$
:当前进程的进程号$1、$2、... 、$9
:运行脚本时传递给其他的参数,用空格隔开"$$"
:变量最常见的用途是用作临时文件的名字以保证临时文件不会重复"$?"
:用于检查上一个命令执行是否正确(在Linux中,命令退出状态为0表示该命令正在执行。)
3.8 脚本的特殊用法
""
(双引号):包含的变量会被解释
''
(单引号):包含的变量会当做字符串解释
``
(数字键1左面的反引号):反引号中的内容作为系统命令,并执行其内容,可以替换输出为一个变量
\
转义字符:同C语言\n \t \r \a等echo命令需加-e转义
(
命令序号)
:由子shell来完成,不影响当前shell中的变量
{
命令序列}
:在当前shell中执行,会影响当前变量
3.9 条件测试语句
语法1:使用关键字test
#!/bin/bash
test condition
语法2:使用[ ]
#!/bin/bash
[ condition ] # condition左右两侧有空格
3.9.1 文件测试
测试文件状态的条件表达式
-e
:文件是否存在
-d
:是否是文件夹\目录
-f
:是否是文件
-r
:是否可读
-w
:是否可写
-x
:是否可执行
-L
:是否符号连接
-c
:是否是字符设备
-b
:是否是块设备
-s
:文件是否为空
#!/bin/sh
test -e test.txt
echo "$?" # 1 文件不存在
[ -e test.txt ]
echo "$?" # 1 文件不存在
[ -d a ]
echo "$?" # 0 是文件夹
test -f a
echo "$?" # 1 不是普通文件
3.9.2 字符串测试
字符串测试格式如下:
test str_operator "str"
test "str1" str_operator "str2"
str_operator "str"
[ "str1" str_operator "str2" ]
其中str_operator
可以是:
=
:两个字符串相等
!=
:两个字符串不相等
-z
:空串
-n
:非空串
#!/bin/bash
str1=""
test -z "$str1"
echo "$?" # 0 表示空串
str2="hello string"
echo "$?" # 1 表示非空串
str3="hehe"
str4="haha"
test "$str3" = "$str4"
echo "$?" # 1 不相等
test "$str3" != "$str4"
echo "$?" # 0 不相等
3.9.3 字符串的操作扩展
#!/bin/bash
str="hehe:haha:xixi:lala"
# 测量字符串的长度 ${#str}
echo "str的长度为:${#str}" # 19
# 从下标3的位置提取 ${str:3}
echo ${str:3} # "e:haha:xixi:lala"
# 从下标为3的位置提取长度为6的字节
echo ${str:3:6} # "e:haha"
# ${str/old/new} 用new替换str中出现的第一个old
echo ${str/:/#} # "hehe#haha:xixi:lala"
# ${str//old/new} 用new替换str中所有的old
echo ${str//:/#} # "hehe#haha#xixi#lala"
3.9.4 数值测试
测试数字格式如下:
test num1 num_operator num2
num1 num_operator num2
-eq
:数值相等
-ne
:数值不相等
-gt
:数1大于数2
-ge
:数1大于等于数2
-le
:数1小于等于数2
-lt
:数1小于数2
#!/bin/bash
read -p "请输入两个数值数据" data1 data2
# 判断是否相等
test $data1 -eq $data2
# 判断是否大于
[ $data1 -gt $data2 ]
echo "$?"
3.9.5 数值的扩展
#!/bin/bash
# ${num:-val} 如果num存在,整个表达式的值为num,否则为val
echo ${num:-100} # 100
num=200
echo ${num:-100} # 200
# ${num:=val} 如果num存在,整个表达式的值为num,否则为val,并创建num,将val的值赋给num
echo ${num:=100} # 100
echo "num=$num" # 100
3.9.6 复合测试
命令执行控制:
command1 && command2
command1 || command2
&&
:&&左边命令(command1)执行成功(即返回0)shell才执行&&右边的命令(command2)
||
:||左边的命令(command1)未执行成功(即返回非0)shell才执行||右边的命令(command2)
#!/bin/bash
test -e /home && test -d /home && echo "true"
test 2 -lt 3 && test 5 -gt 3 &&echo "equal"
test "aaa" = "aaa" || echo "not equal" && echo "equal"
多重条件判定
command | description |
---|---|
-a | (and)两状况同时成立test -r file -a -x file file同时具有r与x权限时,才为true |
-o | (or)两状况任何一个成立test -r file -o -x file file具有r或x权限时,就传回true |
! | 相反状态test ! -x file 当file不具有x时,回传true |
#!/bin/bash
test -f test.c && test -r test.c && test -w test.c
# 等价
test -f test.c -a -r test.c -a -w test.c
3.10 if控制语句
格式一:
if [ 条件1 ];then # if后空格,条件1左右两侧空格
执行第一段程序 # 执行第一段程序前需要用tab键
else
执行第二段程序 # 执行第二段程序前需要用tab键
fi
例:查看当前某个文件是否存在,如果存在查看文件内容,不存在创建改文件,赋值内容输出内容
#!/bin/bash
read -p "请输入一个文件名" fileName
if [ -e $fileName ];then
if [ -f $fileName -a -s $fileName ];then
cat $fileName
else
echo "存在,但不是普通文件或者为空文件"
fi
else
# 文件不存在,创建文件
touch $fileName
echo "hello file" >> $fileName
cat $fileName
fi
格式二:
if [ 条件1 ];then # if后空格,条件1左右两侧空格
执行第一段程序 # 执行第一段程序前需要用tab键
elif [ 条件2 ];then # elif后空格,条件2左右两侧空格
执行第二段程序 # 执行第二段程序前需要用tab键
else
执行第三段程序 # 执行第三段程序前需要用tab键
fi
#!/bin/bash
read -p "请输入你的选择(yes/no):" yes
if [ $yes = "yes" ];then
echo "选择了yes"
elif [ $yes = "no" ];then
echo "选择了no"
else
echo "选择了其他"
fi
3.11 case控制语句
语法:
case $变量名称 in
"第一个变量内容")
程序段一
;;
"第二个变量内容")
程序段二
;;
*)
其他程序段
exit 1
;;
esac
例:
#!/bin/bash
read -p "请输入你的选择(yes/no):" yes
case $yes in
y* | Y*)
echo "选择了yes"
;;
n* | N*)
echo "选择了no"
;;
*)
echo "选择了其他"
exit 1
;;
esac
3.12 for循环语句
格式一:
for (( 初始值; 限制值; 执行步阶 ))
do
程序段
done
# 初始值:变量在循环中的起始值
# 限制值:当变量值在这个限制范围内时,就继续进行循环
# 执行步阶:每作一次循环时,变量的变化值
例:
#!/bin/bash
# declare是bash的一个内建命令,可以用来声明shell变量、设置变量的属性。declare也可以写作为typeset。
# declare -i sum 代表强制把sum变量当做int型参数运算
declare -i i=0
declare -i sum=0
for (( i=0; i<=100; i++ ))
do
sum=$sum+$i
done
echo "sum=$sum"
格式二:
for var in con1 con2 con3 ...
do
程序段
done
# 第一次循环时,$var的内容为con1
# 第二次循环时,$var的内容为con2
# 第三次循环时,$var的内容为con3
# ...
例:
#!/bin/bash
for i in 10 20 30 40 50
do
echo "i=$i"
done
例:扫描当前目录的文件
#!/bin/bash
for fileName in `ls` # ls把当前目录全部列出到in后面
do
if [ -d $fileName ];then # 如果fileName是文件夹
echo "$fileName为文件夹"
elif [ -f $fileName ];then # 如果fileName是普通文件
case $fileName in
*.sh) # 如果fileName是脚本文件
echo "$fileName为脚本文件"
esac
fi
done
注:break跳出循环,continue直接进入下一次循环
3.13 while循环语句
格式
while [ condition ] # 当condition成立的时候进入while循环,直到condition不成立时才退出循环
do
程序段
done
例
#!/bin/bash
declare -i i=0
declare -i sum=0
while [ $i -le 100 ] # i<100
do
sum+=$i
i=$i+1
done
echo "sum=$sum"
3.14 until循环语句
格式
until [ condition ]
do
程序段
done
注:这种方式与while恰恰相反,当condition成立时的时候退出循环,否则继续循环。
例
#!/bin/bash
declare -i i=0
declare -i sum=0
while [ $i -gt 100 ] # i>100
do
sum+=$i
i=$i+1
done
echo "sum=$sum"
3.15 shell函数
有些脚本段间相互重复,如果能只写一次代码块而在任何地方都能引用那就提高了代码的可重用性。
shell允许将一组命令集或语句形成一个可用块,这些块称为shell函数。
定义函数的两种格式
格式一:
函数名()
{
命令 ...
}
格式二:
function 函数名()
{
命令 ...
}
例1:封装一个函数计算两个数据的和
#!/bin/bash
declare -i a
# 函数定义
function my_add()
{
a=$1+$2
return $a
}
read -p "请输入两个数值:" data1 data2
# 函数调用
my_add $data1 $data2 # $data1传给$1,$data2传给$2
# $?代表函数的返回值
echo "函数的返回值为$?"
例2:入其他文件的脚本
#!/bin/bash
# 导入其他文件的脚本 如_shell.sh
source _shell.sh
declare -i a
read -p "请输入两个数值:" data1 data2
# 函数调用
my_add $data1 $data2 # my_add从其他文件_shell.sh中调用,本文件中没有
# $?代表函数的返回值
echo "函数的返回值为$?"
二. Linux文件IO操作
1 系统调用
系统调用:就是操作系统(内核)提供给用户程序调用的一组“特殊”接口(函数接口)。
用户程序可以通过这组“特殊”接口来获得操作系统内核提供的服务,比如用户可以通过文件系统相关的调用请求系统打开文件、关闭文件或读写文件,可以通过时钟相关的系统调用获得系统时间或设置定时器等。
从逻辑上来说,系统调用可被看成是一个内核与用户空间程序交互的接口——它好比一个中间人,把用户进程的请求传达给内核,待内核把请求处理完毕后再将处理结果送回给用户空间。
进程的空间分为:内核空间和用户空间
系统服务之所以需要通过系统调用来提供给用户空间的根本原因是为了对系统进行"保护",因为我们知道Linux的运行空间分为内核空间与用户空间,它们各自运行在不同的级别中,逻辑上相互隔离。
所以用户进程在通常情况下不允许访问内核数据,也无法使用内核函数,它们只能在用户空间操作用户数据,调用用户空间函数。
比如我们熟悉的"hello world"程序(执行时)就是标准的用户空间进程,它使用的打印函数 printf就属于用户空间函数,打印的字符"hello word"字符串也属干用户空间数据。
但是很多情况下,用户进程需要获得系统服务(调用系统程序),这时就必须利用系统提供给用户的“特殊接口”――系统调用了,它的特殊性主要在于规定了用户进程进入内核的具体位置。
换句话说,用户访问内核的路径是事先规定好的,只能从规定位置进入内核,而不准许肆意跳入内核。有了这样的陷入内核的统一访问路径限制才能保证内核安全无误。我们可以形象地描述这种机制:作为一个游客,你可以买票要求进入野生动物园,但你必须老老实实地坐在观光车上,按照规定的路线观光游览。当然,不准下车,因为那样太危险,不是让你丢掉小命,就是让你吓坏了野生动物。
系统调用是属于操作系统内核的一部分的,必须以某种方式提供给进程让它们去调用。CPU可以在不同的特权级别下运行,而相应的操作系统也有不同的运行级别,用户态和内核态。运行在内核态的进程可以毫无限制的访问各种资源,而在用户态下的用户进程的各种操作都有着限制,比如不能随意的访问内存、不能开闭中断以及切换运行的特权级别。显然,属于内核的系统调用一定是运行在内核态下,但是如何切换到内核态呢?
答案是软件中断。软件中断和我们常说的中断(硬件中断)不同之处在于,它是通过软件指令触发而并非外设引发的中断,也就是说,又是编程人员开发出的一种异常(该异常为正常的异常)。操作系统一般是通过软件中断从用户态切换到内核态。
2 系统调用和库函数的区别
Linux下对文件操作有两种方式:系统调用(system call)和库函数调用(Library functions)。
系统调用:是内核提供的一组函数接口(内核提供)。
库函数:是第三方的函数接口(用户提供)。
- 不需要调用系统调用
不需要切换到内核空间即可完成函数全部功能,并且将结果反馈给应用程序,如strcpy、bzexo 等字符串操作函数。 - 需要调用系统调用
需要切换到内核空间,这类函数通过封装系统调用去实现相应功能,如 printf、fread等。
系统调用是需要时间的,程序中频繁的使用系统调用会降低程序的运行效率。当运行内核代码时,CPU工作在内核态,在系统调用发生前需要保存用户态的栈和内存环境,然后转入内核态工作。系统调用结束后,又要切换回用户态。这种环境的切换会消耗掉许多时间。
3 C库中IO函数工作流程
库函数访问文件的时候根据需要,设置不同类型的缓冲区,从而减少了直接调用IO系统调用的次数,提高了访问效率。
这个过程类似于快递员给某个区域(内核空间)送快递一样,快递员有两种方式送:
- 来一件快递就马上送到目的地,来一件送一件,这样导致来回走比较频繁(系统调用)
- 等快递攒着差不多后(缓冲区),才一次性送到目的地(库函数调用)
4 文件描述符
Linux将系统调用打开或新建的文件用非负整数来表示。而这个非负整数就是文件描述符。
系统会为每一个进程分配文件描述符表,管理该进程的所有文件描述符。
在Linux的世界里,一切设备皆文件。我们可以系统调用中I/O的函数(I:input,输入; O:output,输出),对文件进行想应的操作( open()、close()、write() 、read() 等)。
打开现存文件或新建文件时,系统(内核)会返回一个文件描述符,文件描述符用来指定已打开的文件。这个文件描述符相当于这个已打开文件的标号,文件描述符是非负整数,是文件的标识,操作这个文件描述符相当于操作这个描述符所指定的文件。
程序运行起来后(每个进程)都有一张文件描述符的表,标准输入、标准输出、标准错误输出设备文件被打开,对应的文件描述符0、1、2记录在表中。程序运行起来后这三个文件描述符是默认打开的。
0:标注输入设备(键盘)scanf
1:标准输出设备(终端)printf
2:标准错误输出(终端)perror
4.1 文件描述符表是如何管理文件描述符的呢?
文件描述符表是通过“位图”来管理文件描述符。使用1024位二进制位管理,位数代表的就是文件描述符,位上的值1表示打开,0表示关闭。
4.2 查看当前系统文件描述最大数量
查看当前系统文件描述最大数量:
ulimit -a
修改当前系统文件描述最大数量:
ulimit -n 2048
5 文件IO的操作
文件常用操作IO:
open
、close
、read
、write
5.1 open 打开文件
打开文件,如果文件不存在则可以选择创建
#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);
/*
参数
pathname:文件的路劲及文件名
flags:打开文件的权限,系选项O_RDONLY,O_WRONLY,O_RDWR
mode:这个参数只有在文件不存在时有效,指新建文件时指定文件的权限
返回值:
成功:成功返回打开的文件描述符
失败:-1
*/
5.1.1 flags文件的操作权限(read write)
必选项
取值 | 含义 |
---|---|
O_RDONLY | 以只读的方式打开 |
O_WRONLY | 以只写的方式打开 |
O_RDWR | 以可读、可写的方式打开 |
可选项
取值 | 含义 |
---|---|
O_CREAT | 文件不存在则创建文件,使用此选项时需使用mode说明文件的权限 |
O_EXCL | 如果同时指定了O_CREAT,且文件已经存在,则出错 |
O_TRUNC | 如果文件存在,则清空文本内容 |
O_APPEND | 写文件时,数据添加到文件末尾 |
O_NONBLOCK | 对于设备文件,以O_NONBLOCK方式打开可以做非阻塞I/O |
5.1.2 mode文件在磁盘的用户权限
磁盘文件的用户权限分类:所有拥有者权限(u)、同组用户权限(g)、其他用户权限 (o)
任何选项都分为:读(4)
、写(2)
、执行(1)
这3个数值可以组合,如7---->0111(可读可写可执行)
mode的权限表示0xxx每一个x都是(4,2,1)的组合
0777 所有者、同组用户、其他用户都是可读可写可执行
0666所有者、同组用户、其他用户都是可读可写
0651所有者可读可写、同组用户可读可执行、其他用户可执行
5.1.3 mode的系统掩码
查看掩码:
umask
文件的最终权限 = 给定的权限 & ~umask
查看各组用户的默认操作权限:
umask -S
5.2 close 关闭文件文件描述符
关闭已打开的文件
#include <unistd.h>
int close(int fd);
/*
参数:
fd:文件描述符,open()的返回值
返回值:
成功:0
失败:-1,并设置errno
*/
注:close工作步骤,先将文件描述符的数量-1
5.3 write 向文件写数据
把指定数目的数据写到文件(fd)
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
/*
参数:
fd:文件描述符
buf:数据首地址
count:写入数据的长度(字节)
返回值:
成功:实际写入数据的字节个数
失败:-1
*/
5.4 read 读取文件数据
把指定数据的数据读到内存(缓冲区)
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
/*
参数:
fd:文件描述符
buf:内存首地址
count:读取的字节个数
返回值:
成功:实际读到的字节个数
读完数据返回0
失败:-1
*/
5.5 案例:实现cp命令
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(int argc, char const *arfv[])
{
//判断参数是否正确(./a.out b.txt test)
if(argc != 3)
{
printf("./a.out b.txt test);
return 0;
}
//以只读的方式 打开b.txt文件
int fd_r = open(argv[1], O_RDONLY);
if(fd_r < 0)
{
perror("open");
return 0;
}
//以写的方式,在test目录中打开b.txt
char file_name[32]="";
sprintf(file_name, "%s/%s",argv[2], argv[1]); //文件路劲
int fd_w = open(file_name, O_WRONLY | O_CREAT, 0666);
if(fd_w < 0)
{
perror("open");
return 0;
}
//不停的从fd_r中读取文件数据写入到fd_w文件中
while(1)
{
unsigned char buf[128] = "";
int len = read(fd_r, buf, sizeof(buf));
write(fd_w, buf, len);
printf("len=%d\n",len);
if(len <= 0)
break();
}
//关闭文件
close(fd_r);
close(fd_w);
return 0;
}
6 文件的阻塞特性
阻塞和非阻塞针对的是文件描述符而不是read write函数
文件描述符默认为阻塞的
6.1 通过open函数在打开文件的时候设置文件描述符为非阻塞
文件描述符事先不存在才用 open
案列1:open打开文件 默认为阻塞特性
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
void test01()
{
//打开终端
int fd = open("/dev/tty", O_RDONLY);
if(fd < 0)
{
perror("open");
return;
}
printf("准备读取数据......\n");
unsigned char buf[128]="";
read(fd, buf, sizeof(buf)); //默认为阻塞(直到有数据才解阻塞)
printf("读到终端数据:%s\n",buf);
close(fd);
}
int main(int argc, char const *argv[])
{
test01();
return 0;
}
案列2:open打开文件 设置为非阻塞特性
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
void test01()
{
//打开终端
int fd = open("/dev/tty", O_RDONLY | O_NONBLOCK); //默认为阻塞,加上O_NONBLOCK后为不阻塞
if(fd < 0)
{
perror("open");
return;
}
printf("准备读取数据......\n");
unsigned char buf[128]="";
read(fd, buf, sizeof(buf)); //不阻塞
printf("读到终端数据:%s\n",buf);
close(fd);
}
int main(int argc, char const *argv[])
{
test01();
return 0;
}
6.2 通过fcntl设置文件的阻塞特性
文件描述符事先存在用 fcntl
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /*arg*/);
/*
功能:改变已打开的文件性质,fcntl针对描述符提供控制
参数:
fd: 操作的文件描述符
cmd: 操作方式
arg: 针对cmd的值,fcntl能够接受第三个参数int arg
返回值:
成功:返回某个其他值
失败:-1
*/
fcntl
函数有5种功能:
- 复制一个现有的描述符(cmd = F_DUPFD)
- 获得/设置文件描述符标记(cmd = F_GETFD或F_SETFD)
- 获得/设置文件状态标记(cmd = F_GETFL或F_SETFL)
- 获得/设置异步I/O所有权(cmd = F_GETOWN或F_SETOWN)
- 获得/设置记录锁(cmd = GETLK,F_SETLK或F_SETKW)
设置一个存在的文件描述符的阻塞特性的步骤
- fcntl 获取文件描述符的状态标记
- 修改获取到的文件描述符的状态标记
- 将修改后的状态标记使用 fcntl 设置到文件描述符中
案列1:设置一个存在的文件描述符的阻塞特性
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
void test01()
{
// 获取文件状态标记
int flag = fcntl(0, F_GETFL);
// 修改文件的状态标记(具备非阻塞)
flag = flag | O_NONBLOCK;
// 设置 让新的文件状态标记生效
fcntl(0, F_SETFL, &flag);
printf("准备读取数据......\n");
unsigned char buf[128]="";
read(0, buf, sizeof(buf)); //不阻塞
printf("读到终端数据:%s\n",buf);
}
int main(int argc, char const *argv[])
{
test01();
return 0;
}
7 获取文件的状态信息
获取文件状态信息
stat
和lstat
的区别:
当文件是一个符号链接时,lstat返回的是该符号链接本身的信息;
而stat返回的是该链接指向的文件信息。
#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);
/*
参数:
path:文件名
buf:保存文件信息的结构体
返回值:
成功:0
失败:-1
*/
strcut stat
结构体说明
struct stat{
dev_t st_dev; //文件的设备编号
ino_t st_ino; //节点
mode_t st_mode; //文件的类型和存取的权限
nlink_t st_nlink; //连到该文件的硬连接数目,刚建立的文件值为1
uid_t st_uid; //用户ID
git_t st_gid; //组ID
dev_t st_rdev; //(设备类型)若此文件为设备文件,则为其设备编...
off_t st_size; //文件字节数(文件大小)
blksize_t st_blksize; //块大小(文件系统的I/O缓冲区大小)
blkcnt_t st_blocks; //块数
time_t st_atime; //最后一次访问时间
time_t st_mtime; //最后一次修改时间
time_t st_ctime; //最后一次改变时间
st_mode
(16位整数)参数说明:
案列1:获取文件的属性、大小
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
void test01()
{
//获取文件的状态信息
struct stat s;
stat("b.txt", &s);
//分析文件的状态类型(重要)
if(S_ISREG(s.st_mode))
{
printf("为普通文件\n");
}
else if(S_ISDIR(s.st_mode))
{
printf("为目录文件\n");
}
//获取文件的权限
if((s.st_mode & S_IRUSR) == S_IRUSR)
{
printf("所拥有者具备读权限\n");
}
if((s.st_mode & S_IWUSR) == S_IWUSR)
{
printf("所拥有者具备写权限\n");
}
if((s.st_mode & S_IXUSR) == S_IXUSR)
{
printf("所拥有者具备执行权限\n");
}
}
int main(int argc, char const *argv[])
{
test01();
return 0;
}
8 文件目录操作
8.1 得到文件目录的句柄
#include <sys/types.h>
#include <dirent.h>
DIR *opendir(const char *name);
功能:打开一个目录
参数:
name:目录名
返回值:
成功:返回指向该目录结构体指针
失败:NULL
例
#include <stdio.h>
#include <sys/types.h>
#include <dirent.h>
int main(int argc, char const *argv[])
{
//1. 获取文件目录句柄
DIR *dir = opendir("./");
if(dir == NULL)
{
perror("opendir");
return 0;
}
}
8.2 读取目录
#include <dirent.h>
struct dirent *readdir(DIR *direp);
功能:读取目录(调用一次只能读取一个文件)
参数:
dirp:opendir的返回值
返回值:
成功:目录结构体指针
失败:NULL
相关结构体说明:
struct dirent
{
ino_t d_ino; //此目录进入点的inode
off_t d_off; //目录文件开头至此目录进入点得位移
signed short int d_reclen; //d_name的内容长度,不包含NULL字符
unsigned char d_type; //d_type所指的文件类型
char d_name[256]; //文件名
}
例
#include <stdio.h>
#include <sys/types.h>
#include <dirent.h>
int main(int argc, char const *argv[])
{
//1. 获取文件目录句柄
DIR *dir = opendir("./");
if(dir == NULL)
{
perror("opendir");
return 0;
}
//2. 读取文件
struct dirent *ret;
while(ret = readdir(dir))
{
if((ret->d_type & DT_REG) == DT_REG)
{
printf("%s是普通文件\n", ret->d_name);
}
else if(ret->d_type &DT_DIR) == DT_DIR)
{
printf("%s是目录文件\n", ret->d_name);
}
}
}
8.3 关闭目录
#include <sys/types.h>
#include <dirent.h>
int closedir(DIR *dirp);
功能:
关闭目录
参数:
dirp:opendir返回的指针
返回值:
成功:0
失败:-1
例
#include <stdio.h>
#include <sys/types.h>
#include <dirent.h>
int main(int argc, char const *argv[])
{
//1. 获取文件目录句柄
DIR *dir = opendir("./");
if(dir == NULL)
{
perror("opendir");
return 0;
}
//2. 读取文件
struct dirent *ret;
while(ret = readdir(dir))
{
if((ret->d_type & DT_REG) == DT_REG)
{
printf("%s是普通文件\n", ret->d_name);
}
else if(ret->d_type &DT_DIR) == DT_DIR)
{
printf("%s是目录文件\n", ret->d_name);
}
}
//3. 关闭目录句柄
closedir(dir);
return 0;
}
三. 进程
1 进程的概述
我们平时写的C语言代码,通过编译器编译,最终它会成为一个可执行程序,当这个可执行程序运行起来后(没有结束之前),它就成为了一个进程。程序是存放在存储介质上的一个可执行文件,而进程是程序执行的过程。进程的状态是变化的,其包括进程的创建、调度和消亡。程序是静态的,进程是动态的。
程序 静态的 占磁盘空间。
进程 动态的(调度、执行、消亡),占内存空间。
(进程是程序执行到结束间的这个过程)
2 单道、多道程序设计
单道程序设计所有进程一个一个排队执行。若A阻塞,B只能等待,即使CPU处于空闲状态。而在人机交互时阻塞的出现是必然的。所有这种模型在系统资源利用上及其不合理,在计算机发展历史上存在不久,大部分便被淘汰了。
多道程序设计在计算机内存中同时存放几道相互独立的程序,它们在管理程序控制之下,相互穿插的运行。多道程序设计必须有硬件基础作为保证。在计算机中时钟中断即为多道程序设计模型的理论基础。并发时,任意进程在执行期间都不希望放弃cpu。因此系统需要一种强制让进程让出 cpu资源的手段。时钟中断有硬件基础作为保障,对进程而言不可抗拒。操作系统中的中断处理函数,来负责调度程序执行。在多道程序设计模型中,多个进程轮流使用CPU(分时复用CPU资源)。而当下常见CPU为纳秒级,1秒可以执行大约10亿条指令。由于人眼的反应速度是毫秒级,所以看似同时在运行。1s = 1000ms 1ms = 1000us 1us = 1000ns 1s = 1000000000nse
3 并行和并发的区别
并行和并发都是指多个任务同时执行。
并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行。(多核)
并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。(单核)
4 进程控制块(PCB)
进程运行时,内核为进程每个进程分配一个 PCB(进程控制块),维护进程相关的信息,Linux内核的进程控制块是task_struct 结构体。
其内部成员有很多,我们掌握以下部分即可:进程id。系统中每个进程有唯一的id,在C语言中用pid_t类型表示,其实就是一个非负整数。进程的状态,有就绪、运行、挂起、停止等状态。进程切换时需要保存和恢复的一些CPU寄存器。描述虚拟地址空间的信息。描述控制终端的信息。当前工作目录(Current Working Directory)。umask掩码。文件描述符表,包含很多指向file结构体的指针。和信号相关的信息。用户id和组id。会话(Session)和进程组。进程可以使用的资源上限(Resource Limit)。
PCB存在于进程的内核空间里
系统会为每一个进程分配一个进程ID,其类型为pid_t(非负整数)
进程是系统分配资源的基本单位,本质就是一个结构体,进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。Linux操作系统下的PCB是:
task_struct
进程 = 程序 + 操作系统维护进程的相关数据结构
task_struct{
1. 标识符:描述本进程的唯一标识符,用来区别其他进程
2. 状态:任务状态,退出代码 ,退出信号等
3. 优先级:相对于其他进程的优先级
4. 程序计数器:程序中即将被执行的下一条指令的地址
5. 内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
6. 上下文数据:进程执行时处理的寄存器中的数据,上下文数据,就是进程1在被CPU处理的时候有可能这个进程没被处理完就被进程2抢占了CPU,这时候寄存器中的数据就会被覆盖掉,所以,在CPU处理进程2之前,就会把寄存器中进程1的数据拷贝到上下文数据中,等到下一次再执行进程1的时候,把数据再读到寄存器中。
7. I/O状态信息:包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
8. 记账信息:可能包括处理器时间总和,使用的时钟树总和,时间限制,记帐号等
9. 其他信息
}
5 进程的状态
进程状态反映进程执行过程的变化。这些状态随着进程的执行和外界条件的变化而转换。在三态模型中,进程状态分为三个基本状态,即运行态,就绪态,阻塞态。在五态模型中,进程分为新建态、终止态,运行态,就绪态,阻塞
- 就绪态:执行条件全部满足,等待CPU的执行调度
- 执行态:正在被CPU调度执行
- 等待态:不具备CPU调度执行的执行条件,等待条件满足。
● 运行状态(TASK_RUNNING):进程当前正在运行,或者正在运行队列中等待调度。
● 可中断的阻塞状态(TASK_INTERRUPTIBLE):进程处于阻塞(睡眠)状态,正在等待某些事件发生或能够占用某些资源。处在这种状态下的进程 可以被信号中断。接收到信号或被显式的唤醒呼叫(如调用 wake_up 系列宏:wake_up、wake_up_interruptible等)唤醒之后,进程将转变为 TASK_RUNNING 状态。
● 不可中断的阻塞状态(TASK_UNINTERRUPTIBLE):此进程状态类似于可中断的阻塞状态(TASK_INTERRUPTIBLE),只是它 不会处理信号,把信号传递到这种状态下的进程不能改变它的状态。在一些特定的情况下(进程必须等待,直到某些不能被中断的事件发生),这种状态是很有用 的。只有在它所等待的事件发生时,进程才被显示的唤醒呼叫唤醒。
● 可终止的阻塞状态(TASK_KILLABLE):该状态的运行机制类似于TASK_UNINTERRUPTIBLE,只不过处在该状态下的进程可以响应 致命信号。它可以替代有效但可能无法终止的不可中断的阻塞状态(TASK_UNINTERRUPTIBLE),以及易于唤醒但安全性欠佳的可中断的阻塞状 态TASK_INTERRUPTIBLE)。
● 暂停状态(TASK_STOPPED):进程的执行被暂停,当进程收到 SIGSTOP、SIGSTP、SIGTTIN、SIGTTOU等信号时,就会进入暂停状态。
● 跟踪状态(TASK_TRACED):进程的执行被调试器暂停。当一个进程被另一个监控时(如调试器使用ptrace()系统调用监控测试程序),任何信号都可以把这个进程置于跟踪状态。
● 僵尸状态(EXIT_ZOMBIE):进程运行结束,父进程尚未使用 wait 函数族(如调用 waitpid()函数)等系统调用来“收尸”,即等待父进程销毁它。处在该状态下的进程“尸体”已经放弃了几乎所有的内存空间,没有任何可执行代码,也 不能被调度,仅仅在进程列表中保留一个位置,记载该进程的推出状态等信息供其他进程收集。
● 僵尸撤销状态(EXIT_DEAD):这是最终状态,父进程调用 wait 函数族“收尸”后,进程彻底由系统删除。
5.1 如何查看进程状态:ps auxe
stat中的参数意义如下
参数 | 含义 |
---|---|
D | 不可中断Uninterruptible(usually IO) |
R | 正在运行,或在队列中的进程 |
S | 处于休眠状态 |
T | 停止或被追踪 |
Z | 僵尸进程 |
W | 进入内存交换(从内核2.6开始无效) |
X | 死掉的进程 |
< | 高优先级 |
N | 低优先级 |
s | 包含子进程 |
+ | 位于前台的进程组 |
5.2 ps命令可以查看进程信息:
进程是一个具有一定独立功能的程序,它是操作系统执行的基本单元。
ps命令可以查看进程的详细状态,常用选项(选项可以不加“-”)如下:
选项 | 含义 |
---|---|
-a | 显示终端上的所有进程,包括其他用户的进程 |
-u | 显示进程的详细状态 |
-x | 显示没有控制终端的进程 |
-w | 显示加宽,以便显示更多的信息 |
-r | 只显示正在运行的进程 |
以树状显示:pstree
6 进程号 PID
每个进程都由一个进程号来标识,其类型为pid_t(整型),进程号的范围:0~32767。进程号总是唯一的,但进程号可以重用。当一个进程终止后,其进程号就可以再次使用。
三种不同的进程号:
1. 进程号(PID):标识进程的一个非负整型数。
2. 父进程(PPID):任何进程(除init进程)都是由另一个进程创建,该进程称为被创建进程的父进程,对应的进程号称为父进程号(PPID)。
如,A进程创建了B进程,A的进程号就是B进程的父进程号。
3. 进程组号(PGID):进程组是一个或多个进程的集合。
他们之间相互关联,进程组可以接收同一终端的各种信号,关联的进程有一个进程组号(PGID)。这个过程有点类似于QQ群,组相当于QQ群,各个进程相当于各个好友,把各个好友都拉入这个QQ群里,主要是方便管理,特别是通知某些事时,只要在群里吼一声,所有人都收到,简单粗暴。但是,这个进程组号和QQ群号是有点区别的,默认的情况下,当前的进程号会当做当前的进程组号。
6.1 获取进程号
获取本进程号(PID)
pid_t getpid(void)
#include <sys/type.h>
#include <unistd.h>
pid_t getpid(void);
参数:
无
返回值:
本进程号
6.2 获取父进程号
获取调用此函数进程的父进程号(PPID)
pid_t getppid(void)
#include <sys/type.h>
#include <unistd.h>
pid_t getppid(void);
参数:
无
返回值:
调用此函数进程的父进程号(PPID)
6.3 获取进程组的ID
获取进程组(PGID)
pid_t getpgid(pid_t pid)
#include <sys/type.h>
#include <unistd.h>
pid_t getpgid(pid_t pid);
参数:
pid:进程号
返回值:
参数为 0 时返回当前进程组号,否则返回参数指定进程的进程组号
7 创建进程 fork
系统允许一个进程创建新进程,新进程即为子进程,子进程还可以创建新的子进程,形成进程树结构模型。
7.1 fork
用于从一个已存在的进程中创建,一个新进程新进程称为子进程,原进程称为父进程。
pid_t fork(void)
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
参数:
无
返回值:
成功:父进程(此进程)返回PID(pid_t为整型),fork的子进程返回0
失败:返回-1
失败的两个主要原因是:
1)当前的进程数已经到达了系统规定的上限,这时errno的值被设置为EAGAIN
2)系统内存不足,这时errno的值被设置为ENOMEM
7.2 fork出来的子进程和父进程之间的关系
使用fork函数得到的子进程是父进程的一个复制品,它从父进程处继承了整个进程的地址空间。地址空间:包括进程上下文、进程堆栈、打开的文件描述符、信号控制设定、进程优先级、进程组号等。子进程所独有的只有它的进程号,计时器等。困此,使用fork函数的代价是很太的。
注:1. 父子进程从fork后开始继续执行。
2. 父子进程宏观上同时运行,微观上不确定,由系统决定,可能父进程先执行,因为创建子进程后,子进程需要创建时间。
3. fork后的子进程,相当于完全复制了一份父进程,如父进程有4G的内存空间,内存中地址0x2000为int a = 10,那么子进程也有4G的内存空间,内存中地址0x2000也为int a = 10,但是这两份空间是相互独立的,不是同一份,这时修改子进程中a的值为1000,并不会影响父进程的a,父进程的a任然为10。
例
#include <stdio.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
//创建子进程
pid_t pid = fork();
if(pid < 0)
{
perror("创建失败\n");
return 0;
}
else if(pid == 0) //子进程
{
printf("子进程ID:%d\n",getpid());
}
else if(pid > 0) //父进程
{
printf("父进程ID:%d\n",getppid());
}
getchar();
return 0;
}
7.3 子进程复制父进程的资源(各自独立)
#include <stdio.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
int num = 10;
//创建子进程
pid_t pid = fork();
if(pid < 0)
{
perror("创建失败\n");
return 0;
}
else if(pid == 0) //子进程
{
// 在子进程中修改num的值
num = 1000;
printf("子进程ID:%d 中num=%d\n",getpid(),num);
}
else if(pid > 0) //父进程
{
printf("父进程ID:%d 中num=%d\n",getppid(),num);
}
getchar();
return 0;
7.5 父子进程同时运行
#include <stdio.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
int num = 10;
//创建子进程
pid_t pid = fork();
if(pid < 0)
{
perror("创建失败\n");
return 0;
}
else if(pid == 0) //子进程
{
while(1)
{
printf("子进程ID:%d 中num=%d\n",getpid(),num);
}
}
else if(pid > 0) //父进程
{
while(1)
{
printf("父进程ID:%d 中num=%d\n",getppid(),num);
}
}
getchar();
return 0;
7.6 特殊进程
特殊进程分类
- 孤儿进程
- 僵尸进程
- 守护进程
7.6.1 孤儿进程
父进程先结束,子进程就是孤儿进程,孤儿进程会被1号进程接管(1号进程负责给子进程回收资源)
#include <stdio.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
//创建子进程
pid_t pid = fork();
if(pid < 0)
{
perror("创建失败\n");
return 0;
}
else if(pid == 0) //子进程
{
while(1)
{
printf("子进程ID:%d 父进程ID:%d\n",getpid(),getppid());
sleep(1);
}
}
else if(pid > 0) //父进程
{
printf("父进程ID:%d 3秒后结束\n",getppid());
sleep(3);
}
return 0;
注:1. 孤儿进程无危害。
2. 父进程由终端控制,父进程结束后,孤儿进程会脱离终端控制。
7.6.2 僵尸进程
子进程结束,父进程没有回收子进程资源(PCB),子进程就是僵尸进程。
注:
1. 系统创建一个进程会开辟一个内存空间,如4G内存,其中用户空间占3G,内核空间占1G,内核空间会自动生成一个PCB。
2. 进程结束,用户空间和内核空间都会被回收,但PCB不会被回收,PCB需要由父进程进行主动回收。
3. 子进程结束,父进程需要给子进程回收PCB,否则子进程变为僵尸进程(为僵尸进程后,等父进程结束,僵尸进程的父进程为系统1号进程,1号进程会自动回收僵尸进程 )。
4. 系统生成的进程,此进程的父进程都为bash,此进程结束后,bash会自动回收此进程的PCB。
7.6.3 守护进程
守护进程是脱离终端的孤儿进程,在后端运行。为特殊服务存在的(一般用于服务器)。
7.7 回收子进程资源
在每个进程退出的时候,内核释放该进程所有的资源、包括打开的文件、占用的内存等。但是仍然为其保留一定的信息,这些信息主要主要指进程控制块PCB的信息(包括进程号、退出状态、运行时间等)。父进程可以通过调用 wait 或 waitpid 得到它的退出状态同时彻底清除掉这个进程。wait()和waitpid()函数的功能一样,区别在于,wait()函数会阻塞,waitpid0可以设置不阻塞,waitpid()还可以指定等待哪个子进程结束。注意:一次wait或waitpid 调用只能清理一个子进程,清理多个子进程应使用循环。wait、waitpid基本都是在父进程调用
7.7.1 wait函数
等待任意一个进程结束,如果任意一个进程结束了,此函数会回收该进程的资源。
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
参数:
status:进程退出时的状态信息。
返回值:
成功:已经结束子进程的进程号
失败:-1
注:wait带阻塞。
调用wait()函数的进程会挂起(阻塞),直到它的一个子进程退出或收到一个不能被忽视的信号时才被唤醒(相当于继续往下执行)。若调用进程没有子进程,该函数立即返回;若它的子进程已经结束,该函数同样会立即返回,并且会回收那个早已结束进程的资源。所以,wait()函数的主要功能为回收已经结束子进程的资源。如果参数status的值不是NULL,wait()就会把子进程退出时的状态取出并存入其中,这是一个整数值(int),指出了子进程是正常退出还是被非正常结束的。这个退出信息在一个int中包含了多个字段,直接使用这个值是没有意义的,我们需要用宏定义取出其中的每个字段。
WIFEXITED(status)
:取出子进程的退出信息如果子进程是正常终止的,取出的字段值非零。
WEXITSTATUS(status)
:返回子进程的退出状态,退出状态保存在status 变量的8~16位。在用此宏前应先用宏WIFEXITED判断子进程是否正常退出,正常退出才可以使用此宏。
注意:此status是个wait的参数指向的整型变量。
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char const *argv[])
{
//创建子进程
pid_t pid = fork();
if(pid < 0)
{
perror("创建失败\n");
return 0;
}
else if(pid == 0) //子进程
{
int i = 5;
for( i = 5; i > 0; i--)
{
printf("子进程ID:%d 剩余生命值%ds\n",getpid(),i);
sleep(1);
}
printf("子进程ID:%d 退出了\n",getpid());
//显示结束
——exit(10);
}
else if(pid > 0) //父进程
{
printf("父进程ID:%d 等待子进程结束\n",getppid());
int status = 0;
pid_t pid = wait(&status):
if(WIFXITED)(status))
{
//输出状态值
printf("子进程退出的状态值: %d\n", WEXITSTATUS(status));
}
printf("父进程ID:%d 等到子进程%d结束\n",getppid(),pid);
}
return 0;
7.7.2 waitpid函数
等待子进程终止,如果子进程终止了,此函数会回收子进程的资源。
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int option);
参数:
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:如果子进程暂停了则此函数马上返回,并且不予以理会进程的结束状态。(由于涉及到一些跟踪调试方面的知识,加之极少用到)
返回值:
waitpid()的返回值比wait()稍微复杂一些,一共有三种情况:
1)当正常返回的时候,waitpid()返回收集到的已经回收子进程的进程号;
2)如果设置了选项WNOHANG,而调用中waitpid()还有子进程在运行,且没有子进程退出,返回0;父进程的所有子进程都已经退出了返回-1;返回>0表示等到一个子进程退出
3)如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在,如:当pid所对应的子进程不存在,或此进程存在,但不是调用进程的子进程,waitpid()就会出错返回,这时errno被设置为ECHILD
例:等价于wait的案例
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char const *argv[])
{
//创建子进程
pid_t pid = fork();
if(pid < 0)
{
perror("创建失败\n");
return 0;
}
else if(pid == 0) //子进程
{
int i = 5;
for( i = 5; i > 0; i--)
{
printf("子进程ID:%d 剩余生命值%ds\n",getpid(),i);
sleep(1);
}
printf("子进程ID:%d 退出了\n",getpid());
//显示结束
——exit(10);
}
else if(pid > 0) //父进程
{
printf("父进程ID:%d 等待子进程结束\n",getppid());
int status = 0;
pid_t pid = waitpid(-1, &status, 0):
if(WIFXITED)(status))
{
//输出状态值
printf("子进程退出的状态值: %d\n", WEXITSTATUS(status));
}
printf("父进程ID:%d 等到子进程%d结束\n",getppid(),pid);
}
return 0;
7.8 创建多个子进程
7.8.1 创建2个子进程
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char const *argv[])
{
int i = 0;
for(i = 0; i < 2; i++)
{
pid_t pid = fork();
}
while(1)
{
}
return 0;
}
注:for循环两次,应该fork两个子进程,结果有三个!!!
因为for循环时,创建子进程,但for循环还没结束,条件任然成立,所以子进程会进入for循环创建孙进程。
解决方案
防止子进程创建孙进程
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char const *argv[])
{
int i = 0;
for(i = 0; i < 2; i++)
{
pid_t pid = fork();
if(pid == 0)
{
break;
}
}
while(1)
{
}
return 0;
}
7.8.2 创建多进程
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#define N 3
int main(int argc, char const *argv[])
{
int i = 0;
for(i = 0; i < N; i++)
{
pid_t pid = fork();
if(pid == 0)
{
break;
}
}
//判断具体的子进程
if(i == 0) //子进程1
{
//完成任务A
int j = 5;
for(j = 0; j > 0; j--)
{
printf("子进程%d 剩余时间%ds\n",getpid(),j);
sleep(1);
}
_exit(-1);
}
else if(i == 1)//子进程2
{
//完成任务B
int j = 3;
for(j = 0; j > 0; j--)
{
printf("子进程%d 剩余时间%ds\n",getpid(),j);
sleep(1);
}
_exit(-1);
}
else if(i == 2)//子进程3
{
//完成任务C
int j = 8;
for(j = 0; j > 0; j--)
{
printf("子进程%d 剩余时间%ds\n",getpid(),j);
sleep(1);
}
_exit(-1);
}
else if(i == N)//父进程
{
while(1)
{
//回收所有子进程的资源
pid_t pid = waitpid(-1, NULL, WNOHANG);//不阻塞
if(pid > 0) //某个子进程退出了
{
printf("子进程%d退出了\n",pid);
}
else if(pid == 0) //还有子进程在运行
{
continue;
}
else if(pid == -1) //所有子进程都退出了
{
break;
}
}
}
return 0;
}
7.9 终端
在UNIX系统中,用户通过终端登录系统后得到一个Shell进程,这全终端成为Shell进程的控制终端(Controlling Terminal),进程中,控制终端是保存在PCB中的信息,而fork会复制PCB中的信息,因此由Shell进程启动的其它进程的控制终端也是这个终端。默认情况下(没有重定向),每个进程的标准输入、标准输出和标准错误输出都指向控制终端,进程从标准输入读也就是读用户的键盘输入进程往标准输出或标准错误输出写也就是输出到显示器上。信号中还讲过,在控制终端输入一些特殊的控制键可以给前台进程发信号,例如
Ctrl+C
表示SIGINT
,Ctrl+\
表示SIGQUIT
。
7.9.1 ttyname 函数
由文件描述符查出对应的文件名
#include <unistd.h>
char *ttyname(int fd);
参数:
fd:文件描述符
返回值:
成功:终端名
失败:NULL
例:借助ttyname函数,通过实验看一下各种不同的终端对应的设备文件名
#include <unistd.h>
#include <stdio.h>
int main()
{
printf("fd 0: %s\n", ttyname(0));
printf("fd 1: %s\n", ttyname(1));
printf("fd 2: %s\n", ttyname(2));
return 0;
}
7.10 进程组
进程组,也称之为作业。BSD于1980年前后向Unix中增加的一个新特性代表一个或多个进程的集合。每个进程都属于一个进程组。在
waitpid
函数和kill
函数的参数中都曾使用到。操作系统设计的进程组的概念,是为了简化对多个进程的管理。当父进程,创建子进程的时候,默认子进程与父进程属于同一进程组。进程组ID为第一个进程ID(组长进程)。所以,组长进程标识 :其进程组ID为其进程ID。
可以使用kill -SIGKILL -进程组ID(负的)
来将整个进程组内的进程全部杀死:
组长进程可以创建一个进程组,创建该进程组中的进程,然后终止。只要进程组中有一个进程存在,进程组就存在,与组长进程是否终止无关。进程组生存期:进程组创建到最后一个进程离开(终止或转移到另一个进程组)。一个进程可以为自己或子进程设置进程组ID。I
7.10.1 getpgrp 函数
获取当前进程的进程组ID
#include <unistd.h>
pid_t getpgrp(void);
参数:
无
返回值:
总是返回调用者的进程组ID
7.10.2 getpgid 函数
获取指定进程的进程组ID
#include <unistd.h>
pid_t getpgrp(void);
参数:
pid:进程号,如果pid = 0, 那么该函数作用和getpgrp一样
返回值:
成功:进程组ID
失败:-1
7.10.3 setpgid 函数
改变进程默认所属的进程租。通常可用来加入一个现有的进程组或创建一个新进程组。
#include <unistd.h>
int setpgid(pid_t pid, pid_t pgid);
参数:
将参1对应的进程,加入参2对应的进程组中
返回值:
成功:0
失败:-1
7.11 会话
会话是一个或多个进程组的集合。一个会话可以有一个控制终端。这通常是终端设备或伪终端设备;建立与控制终端连接的会话首进程被称为控制进程;一个会话中的几个进程组可被分为一个前台进程组以及一个或多个后台进程组;如果一个会话有一个控制终端,则它有一个前台进程组,其它进程组为后台进程组;如果终端接口检测到断开连接,则将挂断信号发送至控制进程(会话首进程)。
如果进程ID== 进程组ID==会话ID那么该进程为会话首进程
7.11.1 创建会话的步骤
1)调用进程不能是进程组组长,该进程变成新会话首进程(session header)
2)该调用进程是组长进程,则出错返回。
3)该进程成为一个新进程组的组长进程4)需有root权限(ubuntu不需要)
5)新会话丢弃原有的控制终端,该会话没有控制终端
6)建立新会话时,先调用fork,父进程终止,子进程调用setsid
<br /
7.11.2 getsid 函数
获取进程所属的会话ID
#include <unistd.h>
pid_t getsid(pid_t pid);
参数:
pid:进程号,pid为0表示查看当前进程session ID
返回值:
成功:返回调用进程的会话ID
失败;-1
注:组长进程不能成为新会话首进程,新会话首进程必定会成为组长进程
7.11.3 setsid 函数
创建一个会话,并以自己的ID设置进程组ID,同时也是新会话的ID。调用了setsid函数的进程,即是新的会长,也是新的组长。
#include <unistd.h>
pid_t setid(void);
参数:
无
返回值:
成功:返回调用进程的会话ID
失败;-1
案例1:创建一个会话
#include <stdio.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
pid_t pid = fork();
//父进程结束 子进程设置会话
if(pid>0)
{
exit(-1);
}
else if(pid == 0)
{
setsid();
}
pprintf("进程ID:%d\n",getpid());
while(1);
return 0;
}
注:虽然子进程脱离了会话终端,但是PCB还在,所以还能在终端上打印。
7.12 创建守护进程模型
- 创建子进程,父进程退出(必须)所有工作在子进程中进行形式上脱离了控制终端
- 在子进程中创建新会话(必须)
setsid()
函数使子进程完全独立出来,脱离控制- 改变当前目录为根目录(不是必须)
chdir()
函数防止占用可卸载的文件系统也可以换成其它路径- 重设文件权限掩码(不是必须)
umask()
函数防止继承的文件创建屏蔽字拒绝某些权限增加守护进程灵活性- 关闭文件描述符(不是必须)继承的打开文件不会用到,浪费系统资源,无法卸载
- 开始执行守护进程核心工作(必须)守护进程退出处理程序模型
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
int main(int argc, char const *argv[])
{
pid_t pid = fork();
//父进程结束
if(pid > 0)
{
_exit(-1);
}
//子进程设置会话
setsid();
//改变工作目录(非必须)
chdir("/");
//设置权限掩码
umask(0002);
//关闭文件描述符0 1 2
close(0);
close(1);
close(2);
//守住进程的核心任务
while(1)
{
//核心任务
}
return 0;
}
7.13 vfork 创建进程
创建一个新进程
#include <sys/types.h>
#include <unistd.h>
pid_t vfork(void)
参数:
无
返回值:
成功:
1. 在子进程中返回0
2. 在父进程中返回子进程ID
失败:
返回-1
注:vfork 函数和fork 函数一样都是在已有的进程中创建一个新的进程,但它们创建的子进程是有区别的。
fork和 vfork函数的区别:vfork保证子进程先运行,在它调用exec或exit之后,父进程才可能被调度运行。vfork和 fork一样都创建一个子进程,但它并不将父进程的地址空间完全复制到子进程中,因为子进程会立即调用exec(或exit),于是也就不访问该地址空间。相反,在子进程中调用exec或exit之前,它在父进程的地址空间中运行,在exec之后子进程会有自己的进程空间。
结论:vfork创建的子进程会保证子进程先运行,只有当子进程退出(或调用exec)的时候,父进程才运行。
vfork创建的子进程和父进程公用一个空间。
子进程先运行完后父进程再运行
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main(int argc, char const *argv[])
{
//vfork创建子进程
pid_d pid =vfork();
if(pid == 0) //子进程
{
int i = 0;
for(i; i < 5; i++)
{
printf("子进程%d中的i=%d\n",getpid(),i);
sleep(1);
}
//显示退出
_exit(-1);
}
else if(pid > 0) //父进程
{
int i = 0;
for(i; i < 5; i++)
{
printf("父进程%d中的i=%d\n",getpid(),i);
sleep(1);
}
}
return 0;
}
验证vfork子进程和父进程用同一地址空间
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main(int argc, char const *argv[])
{
int num = 10;
//vfork创建子进程
pid_d pid =vfork();
if(pid == 0) //子进程
{
num = 1000;
//显示退出
_exit(-1);
}
else if(pid > 0) //父进程
{
printf("父进程中的num=%d\n",num);
}
return 0;
}
7.14 exec函数族
exec函数族:在进程中启动另一个进程
在 Windows平台下,我们可以通过双击
运行可执行程序,让这个可执行程序成为一个进程;而在 Linux平台,我们可以通过./
运行,让一个可执行程序成为一个进程。但是,如果我们本来就运行着一个程序(进程),我们如何在这个进程内部启动一个外部程序,由内核将这个外部程序读入内存,使其执行起来成为一个进程呢?这里我们通过exec
函数族实现。exec函数族,顾名思义,就是一簇函数,在Linux中,并不存在 exec()函数,exec指的是一组函数,一共有有6个:
#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 *pathc, 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[]);
六个exec函数中只有execve是真正意义的系统调用(内核提供的接口),其它函数都是在此基础上经过封装的库函数。
l(list)
:参数地址列表,以空指针结尾。参数地址列表char *arg0, char *arg1, … , char *argn, NULL v(vector):在有条参数地址的指钛数组的地址。使用时先构造一个指针数组,指针数组存各参数的地址,然后将该指针数组地址作为函数的参数。
p(path)
按PATH环境变量指定的且录搜索可执行文件。以p结尾的exec函数取文件名做为参数。当指定filename作为参数时,若filename中包含/,则将其视为路径名,并直接到指定的路径中执行程序。
e(environment)
:在有环境变量字符串地址的指针数组的地址。execle和 execve改变的是exec启动的程序的环境变量(新的环境变量完全由environment 指定),其他四个函数启动的程序则使用默认系统环境变量。
案列1:在代码中使用execl
执行ls
命令
execl(可执行文件位置,可执行文件名,可执行文件的选项,以NULL结尾);
#include <stdio.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
printf("执行ls命令前\n");
execl("/bin/ls","ls","-a","-l","h",NULL);
printf("执行ls命令后\n");
return 0;
}
注:如果execl成功执行,execl后的代码将不会执行
exec函数族与一般的函数不同,exec函数族中的函数执行成功后不会返回。只有调用失败了,它们才会返回一1。失败后从原程序的调用点接着往下执行。在平时的编程中,如果用到了exec函数族,一定要记得加错误判断语句。
exec函数族取代调用进程的数据段、代码段和堆栈段。
注:调用exec函数族后,如原进程4G内存空间,新进程4G内存空间,新进程会完全覆盖原进程的内存空间,除了原进程ID,还保留了下列特征不变:父进程号、进程组号、控制终端根目录、当前工作目录、进程信号屏蔽集、未处理信号…
案列2:在代码中使用execlp
执行ls
命令
#include <stdio.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
printf("执行ls命令前\n");
execlp("ls","ls","-a","-l","h",NULL);
printf("执行ls命令后\n");
return 0;
}
案列3:在代码中使用execvp
执行ls
命令
#include <stdio.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
printf("执行ls命令前\n");
char *avg[] = {"ls","-a","-l","h",NULL};
execvp("ls", avg);
printf("执行ls命令后\n");
return 0;
}
案例4:vfrok和exec配合使用
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main(int argc, char const *argv[])
{
int num = 10;
//vfork创建子进程
pid_d pid =vfork();
if(pid == 0) //子进程
{
//子进程负责启动其他程序
execlp("ls","ls","-a","-l","-h",NULL);
//显示退出
_exit(-1);
}
else if(pid > 0) //父进程
{
//父进程运行自己的程序
int i = 0;
for(; i < 5; i++)
{
printf("父进程%d中的i=%d\n",getpid(),i);
sleep(1);
}
}
return 0;
}
7.15 fork、vfork、exec总结
fork:开辟一片和父进程一样大的内存空间,完全复制,父子进程同时运行。
vfork:生成一个子进程,但子进程仍在父进程内存空间中,并且先运行子进程,待子进程结束或子进程中执行exec后父进程才可执行。
exec:在进程中运行另外一个进程。如用vfork没调用exec那讲毫无意义,vfork调用exec,原本vfork的子进程在父进程的内存空间中,exec后会将子进程迁出父进程空间并开辟一片空间给自己存放需要运行一个进程的代码。
四. 信号
1 信号的概述
信号的概念信号是Linux进程间通信的最古老的方式。信号是软件中断,它是在软件层次上对中断机制的一种模拟,是一种异步通信的方式。信号可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件。“中断"在我们生活中经常遇到,譬如,我正在房间里打游戏,突然送快递的来了,把正在玩游戏的我给“中断"了,我去签收快递(处理中断),处理完成后,再继续玩我的游戏。这里我们学习的“信号"就是属于这么一种“中断”。我们在终端上敲“Ctrl+c",就产生一个“中断",相当于产生一个信号,接着就会处理这么一个“中断任务”(默认的处理方式为中断当前进程)。
信号的特点:简单,不能携带大量信息,满足某个特设条件才发送
信号可以直接进行用户空间进程和内核空间进程的交互,内核进程可以利用它来通知用户空间进程发生了哪些系统事件。一个完整的信号周期包括三个部分:信号的产生,信号在进程中的注册,信号在进程中的注销,执行信号处理函数。如下图所示:
注:这里信号的产生,注册,注销时信号的内部机制,而不是信号的函数实现。
2 信号的编号
查看相应信号编号命令
kill -l
不存在编号为0的信号。其中1-31号信号称之为常规信号(也叫普通信号或标准信号),34-64称之为实时信号,驱动编程与硬件相关。名字上区别不大。而前32个名字各不相同。
2.1 信号四要素
- 编号
- 名称
- 事件
- 默认处理动作,可通过man 7 signal查看帮助文档获取
2.2 发起信号的方式:
- 当用户按某些终端键时,将产生信号
- 硬件异常将产生信号。除数为0,无效的内存访问等
- 软件异常将产生信号(定时器)
- 调用系统函数(如:keill、raise、abort)将发送信号
3 未决信号集合、信号阻塞集
未决信号集:信号发生但未被处理的信号集合(在PCB中)
信号阻塞集:加入信号阻塞集的信号不被处理(在PCB中)
信号的实现手段导致信号有很强的延时性,但对于用户来说,时间非常短,不易察觉。Linux内核的进程控制块PCB是一个结构体,task_struct,除了包含进程id,状态,工作目录,用户 id,组 id,文件描述符表,还包含了信号相关的信息,主要指阻塞信号集和未决信号集。1阻塞信号集(信号屏蔽字〕将某些信号加入集合,对他们设置屏蔽,当屏蔽x信号后,再收到该信号,该信号的处理将推后(处理发生在解除屏蔽后)。未决信号集信号产生,未决信号集中描述该信号的位立刻翻转为1,表示信号处于未决状态。当信号被处理对应位翻转回为0。这一时刻往往非常短暂。信号产生后由于某些原因(主要是阻塞)不能抵达。这类信号的集合称之为未决信号集。在屏蔽解除前,信号一直处于未决状态。
4 信号的API
4.1 kill 函数
给指定进程发送指定信号(不一定杀死)
#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
例
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
#include <sys/wait.h>
int main(int argc, char const *argv[])
{
pid_t pid = fork();
if(pid == 0)
{
while(1)
{
printf("进程%d -------任务-------\n",getpid());
}
_exit(-1);
}
else if(pid > 0)
{
printf("给你5秒时间去做任务\n");
sleep(5);
keill(pid, SIGKILL);
wait(NULL); //等待子进程结束
}
return 0;
}
4.2 raise 函数
给当前进程发送指定信号(自己给自己发),等价于kill (getpid() ,sig)
#include <signal.h>
int raise(int sig);
参数:
sig:信号编号
返回值:
成功:0
失败:非0值
例
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
#include <sys/wait.h>
int main(int argc, char const *argv[])
{
int i = 0;
printf("5秒后结束进程\n");
sleep(5);
raise(SIGINT);
printf("执行不到这里!");
return 0;
}
4.3 abort函数
给自己发送异常终止信号6 (SIGABRT),并产生core文件,等价于kill (getpid(), SIGABRT)
#include <stdlib.h>
void abort(void);
参数:
无
返回值:
无
例
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
#include <sys/wait.h>
#include <stdlib.h>
int main(int argc, char const *argv[])
{
int i = 0;
printf("5秒后结束进程\n");
sleep(5);
abort();
printf("执行不到这里!");
return 0;
}
4.4 alarm函数(闹钟)
设置定时器(闹钟)。在指定seconds后,内核会给当前进程发送14 (SIGALRM)信号。进程收到该信号,默认动作终止。每个进程都有且只有唯一的一个定时器。
取消定时器alarm(0),返回旧闹钟余下秒数。
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
参数:
seconds:指定的时间,以秒为单位
返回值:
返回0或剩余的秒数
定时,与进程状态无关(自然定时法)!就绪、运行、挂起(阻塞、暂停)、终止、僵尸…无论进程处于何种状态,alarm都计时。
例
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
#include <sys/wait.h>
#include <stdlib.h>
int main(int argc, char const *argv[])
{
int i = 0;
printf("7秒后结束进程\n");
int s = alarm(5);
printf("s = %d\n", s);
sleep(2);
printf("刚过2秒\n");
s = alarm(5);
printf("s = %d\n", s);
getchar();
return 0;
}
4.5 setitimer 函数(定时器)
设置定时器(闹钟)。可代替alarm函数。精度微妙us,可以实现周期定时。
#include <sys/time.h>
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
参数:
which:指定定时方式
1)自然定时:ITIMER_REAL -> 14(SIGALRM)计算自然时间
2)虚拟空间计时(用户空间):ITIMER_VIRTUAL -> 26(SIGVTALRM)只计算进程占用CPU的时间
3)运行时计时(用户 + 内核):ITIMER_PROF -> 27(SIGPROF)计算占用CPU及执行系统调用的时间
new_value:struct itimerval,负责设定timeout时间
struct itimerval{
struct timerval it_interval; //闹钟出发周期
struct timerval it_value; //闹钟触发时间
};
struct timeval{
long tv_sec; //秒
long tv_usec; //微妙
};
itimerval.it_value:设定第一次执行function所延迟的秒数
itimerval.it_interval:设定以后每几秒执行function
old_value:存放旧的timeout值,一般指定为NULL
返回值:
成功:0
失败:-1
例:5秒过后,每2秒打印一次
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
#include <sys/time.h>
void my_fun(int sig)
{
printf("触发的信号sig = %d\n", sig);
}
int main(int argc, char const *argv[])
{
struct itimerval tv;
//设置一次运行所需时间
tv.it_value.tv_sec = 5;
tv.it_value.tv_usec = 0;
//设置周期执行时间
tv.it_interval.tv_sec = 2;
tv.it_interval.tv_usec = 0;
//注册信号的自定义函数
signal(SIGALRM, my_fun);
setitimer(ITIMER_REAL, &tv, NULL);
getchar();
return 0;
}
5 给信号注册自定义函数
信号处理方式:一个进程收到一个信号的时候,可以用如下方法进行处理:
- 执行系统默认动作对大多数信号来说,系统默认动作是用来终止该进程。
- 忽略此信号(丢弃)接收到此信号后没有任何动作。
- 执行自定义信号处理函数(捕捉)用用户定义的信号处理函数处理该信号。
注:SIGKILL和SIGSTOP不能更改信号的处理方式,因为它们向用户提供了一种进程终止的可靠方法。
内核实现捕捉过程:
注:捕捉信号并且信号信号的t理方式有两众函数,signal和sigactione
5.1 signal 函数
注册信号处理函数(不可用于SIGKELL、SIGSTOP信号),即确定收到信号后处理函数的入口地址。此函数不会阻塞。
#include <signal.h>
typedef void(*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
参数:
signum:信号的编号,这里可以填数字编号,也可以填信号的宏定义,可以通过命令kill -l 进行相应查看。
handler:取值有3种情况:
1)SIG_IGN:忽略该信号
2)SIG_DFL:执行系统默认动作
3)信号处理函数名:自定义信号处理函数,如:func
回调函数的定义如下:
void func(int signo)
{
//signo为触发的信号,为signal()第一个参数的值
}
返回值:
成功:第一次返回NULL,下一次返回此信号上一次注册的信号处理函数的地址。如果需要使用此返回值,必须在前面先声明此函数指针的类型。
失败:返回SIG_ERR
注:该函数由ANSI定义,由于历史原因在不同版本的Unix和不同版本的Linux中可能有不同的行为。因此应该尽量避免使用它,取而代之使用sigaction
函数。
5.2 sigaction 函数
检查或修改指定信号的设置(或同时执行这两种操作)
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
参数:
signum:要操作的信号
act:要设置的对信号的新处理方式(传入参数)
oldact:原来对信号的处理方式(传出参数)
(如果act指针非空,则要改变指定信号的处理方式(设置),如果aldact指针非空,则系统将此前指定信号的处理方式存入oldact
返回值:
成功:0
失败:-1
结构体:
struct sigaction
{
void(*sa_handler)(int); //旧的信号处理函数指针
void(*sa_sigaction)(int, siginfo_t *,void *); //新的信号处理函数指针
sigset_t sa_mask; //信号阻塞集
int sa_flags; //信号处理的方式
void(*sa_restoret)(void); //已弃用
};
1)sahandler、sasigaction:信号处理函数指针,和signal()里的函数指针用法一样,应根据情况给sasigaction、sahandler两者之一赋值,其取值如下:
a)SIGIGN:忽略信号
b)SIGDFL:执行系统默认动作
c)处理函数名:自定义处理函数
2)samask:信号阻塞集,在信号处理函数执行过程中,临时屏蔽指定的信号
3)saflags:用于指定信号处理的行为,通常设置为0,表使用默认属性。它可以是以下值的“按位或”组合:
a)SA_RESTART:使被信号打断的系统调用自动重新发起(已弃用)
b)SA_NOCLDSTOP:使父进程在它的子进程暂停或继续运行时不会收到SIGCHLD信号
c)SA_NOCLDWAIT:使父进程在它的子进程退出时不会收到SIGCHLD信号,这时子进程如果退出也不会成为僵尸进程
d)SA_NODEFER:使对信号的屏蔽无效,即在信号处理函数执行期间仍能发出这个信号
e)SA_RESETHAND:信号处理之后重新设置为默认的处理方式。
f)SA_SIGINFO:使用sasigaction成员而不是sahandler作为信号处理函数
信号处理函数:
void (*sa_sigaction)(int signum, siginfo_t *info, void *context);
参数说明:
signum:信号的编号
info:记录信号发送进程信息的结构体
context:可以赋给指定ucontext_t类型的一个对象的指针,已引用在传递信号时被中断的接受进程或线程的上下文
例:按下crtl + c 打印消息
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/time.h>
void my_func(int sig)
{
printf("crtl + c 被按下了\n");
}
int main()
{
struct sigaction act;
//act存放回调函数
act.sa_handler = my_func;
//act添加阻塞集 act.sa_mask
sigemptyset(&act.sa_mask); //清空阻塞集
//SA_RESETHAND:信号处理之后重新设置为默认的处理方式
act.sa_flags = 0; //默认方式
//act.sa_flags |= SA_RESETHAND; //打开第一次按下crtl + c打印消息,之后则取消进程
sigaction(SIGINT, &act, NULL);
while(1);
return 0;
6 信号集
6.1 信号集概述
在PCB中有两个非常重要的信号集。一个称之为“阻塞信号集",另一个称之为“未决信号集”。这两个信号集都是内核使用位图机制来实现的。但操作系统不允许我们直接对其进行位操作。而需自定义另外一个集合,借助信号集操作函数来对PCB中的这两个信号集进行修改。
6.2 自定义信号集函数
为了方便对多个信号进行处理,一个用户进程常常需要对多个信号做出处理,在Linux系统中引入了信号集(信号的集合)。这个信号集有点类似于我们的QQ群,一个个的信号相当于QQ群里的一个个好友。信号集是一个能表示多个信号的数据类型,
sigset_t set
,set
即一个信号集。既然是一个集合,就需要对集合进行添加/删除等操作。相关函数说明如下:
#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); //判断信号是否存在
除sigismember外,其余操作函数中的set均为传出参数。sigset_t类型的本质是位图。但不应该直接使用位操作,而应该使用上述函数,保证跨系统操作有效。
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/time.h>
int main(int argc, char const *argv[])
{
//定义一个信号集合
sigset_t set;
//清空信号集合
sigemptyset(&set);
//将SIGINT加入set集合
sigaddset(&set, SIGINT);
//将SIGISTP加入set集合
sigaddset(&set, SIGTSTP);
//判断SIGINT是否在set中
if(sigismember(&set, SIGINT))
{
printf("SIGINT在set集合中\n");
}
return 0;
}
6.3 信号阻塞集
信号阻塞集也称信号屏蔽集、信号掩码。每个进程都有一个阻塞集,创建子进程时子进程将继承父进程的阻塞集。信号阻塞集用来描述哪些信号传递到该进程的时候被阻塞(在信号发生时记住它,直到进程准备好时再将信号通知进程)。所谓阻塞并不是禁止传送信号,而是暂缓信号的传送。若将被阻塞的信号从信号阻塞集中删除,且对应的信号在被阻塞时发生了,进程将会收到相应的信号。我们可以通过
sigprocmask()
修改当前的信号掩码来改变信号的阻塞情况。
6.3.1 sigprocmask 函数
检查或修改信号阻塞集,根据 how 指定的方法对进程的阻塞集和进行修改,新的信
号阻塞集由set指定,而原先的信号阻塞集合由oldset保存。
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
参数:
how:信号阻塞集合的修改方法,有3种情况:
1)SIG_BLOCK:向信号阻塞集合中添加set信号集,新的信号掩码是set和旧信号掩码的并集。相当于mask=mask|set。
2)SIG_UNBLOCK:从信号阻塞集合中删除set信号集,从当前信号掩码中去除set中的信号。相当于mask=mask&~set。
3)SIG_SETMASK:讲信号阻塞集合设为set信号集,相当于原来信号阻塞集的内容清空,然后按照set中的信号重新设置信号阻塞集。相当于mask=set。
set:要操作的信号集地址
若set为NULL,则不改变信号阻塞集合,函数只把当前信号阻塞集合保存到oldset中。
oldset:保存原先信号阻塞集地址。
返回值:
成功:0
失败:-1,失败时错误代码只可能是EINVAL,表示参数how不合法。
例:5秒内按下ctrl + c不结束进程(按下ctrl+c会将信号加入到未决信号集,但是ctrl + c信号阻塞了,不会结束进程,等5秒过后把ctrl + c信号从阻塞信号中删除,就会立马去执行ctrl + c信号,结束进程)
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/time.h>
int main(int argc, char const *argv[])
{
//定义一个信号集合
sigset_t set;
//清空信号集合
sigemptyset(&set);
//将SIGINT加入set集合
sigaddset(&set, SIGINT);
//将set集合添加到阻塞集中
sigprocmask(SIG_BLOCK, &set, NULL);
printf("5秒后SIGINT将从阻塞集中删除\n");
sleep(5);
//将set集合从阻塞集删除
sigprocmask(SIG_UNBLOCK, &set, NULL);
getchar();
return 0;
}
6.4 未决信号集
6.4.1 sigpending 函数
读取当前进程的未决信号集
#include <signal.h>
int sigpending(sigset_t *set);
参数:
set:未决信号集
返回值:
成功:0
失败:-1
例:将信号阻塞,看信号是否在未决信号集中
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/time.h>
int main(int argc, char const *argv[])
{
//定义一个信号集合
sigset_t set;
//清空信号集合
sigemptyset(&set);
//将SIGINT加入set集合
sigaddset(&set, SIGINT);
//将set集合添加到阻塞集中
sigprocmask(SIG_BLOCK, &set, NULL);
printf("5秒后判断SIGINT是否在未决信号集中\n");
sleep(5);
sigset_t set2;
sigemptyset(&set2);
//读取未决信号集
sigpending(set2);
if(sigismember(&set2,SIGINT))
{
printf("SIGINT在未决信号集中\n");
}
//将set集合从阻塞集删除
sigprocmask(SIG_UNBLOCK, &set, NULL);
getchar();
return 0;
}
7 进程间通信方式总结
进程间通信方式有7种通信方式:
同一主机的进程通信:
- 无名管道
- 有名管道(命令管道)
- 消息队列
- mmap(存储映射)
- 共享内存
- 信号
不同主机的进程通信:
7. socket(网络通信)
7.1 通信的特点
7.1.1 无名管道
血缘关系、半双工、一对一、先进先出、无格式、数据读取后就丢弃(内存中)
7.1.2 有名管道
有/无血缘、半双工、一对一、先进先出、无格式、数据读取后就丢弃(内存抽象成文件名)
7.1.3 消息队列
有/无血缘、全双工、多对多、按消息类型收取、同类型先进先出、有格式、数据读取后丢失(内存中)
7.1.4 mmap(存储映射)
多对多、无格式、数据读取后数据存在、写入覆盖以前数据(磁盘中)
7.1.5 共享内存
多对多、无格式、数据读取后数据存在、写入覆盖以前数据(物理内存中)
7.1.6 信号
简单、不能携带大量信息、满足某个特设条件才发送
7.1.7 socket
不同主机间的进程通信
五. 文件描述符复制
让新的文件描述符指向旧的文件描述符(新旧文件描述符指向同一个文件)
使用函数dup
、dup2
1 dup函数
系统调用从系统寻找最小可用的文件描述符作为oldfd的副本,新文件描述符通过dup的返回值返回
#include <unistd.h>
int dup(int oldfd);
参数:
oldfd:需要复制的文件描述符
返回值:
返回成功复制后的文件描述符
例
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char const *argv[])
{
//复制描述符1(输出终端)
int fd = dup(1);
printf("fd = %d\n", fd); //3
//输出字符串
printf("hello dup\n");
write(fd, "hello dup hehe",strlen("hello dup hehe"));
close(fd);
return 0;
}
2 dup2函数
指定一个文件描述符做另一个文件描述符的副本
dup2
与dup
基本一致,没什么区别
#include <unistd.h>
int dup2(int oldfd, int newfd);
参数:
oldfd:需要拷贝的文件描述符
newfd:需要成为副本的文件描述符
(将newfd作为oldfd的副本)
例
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char const *argv[])
{
//复制描述符1(输出终端)
dup2(1, 4);
//输出字符串
printf("hello dup\n");
write(4, "hello dup hehe",strlen("hello dup hehe"));
close(4);
return 0;
}
注:如果newfd
事先存在,dup2
会先close(newfd)
,然后将newfd
作为oldfd
的副本
六. 无名管道
1 无名管道概述
管道(pipe)又称无名管道。无名管道是一种特殊类型的文件,在应用层体现为两个打开的文件描述符。
管道是最古老的UNIX IPC(进程间通信方式)方式,其特点是:
- 半双工,数据在同一时刻只能在一个方向上流动。
- 数据只能从管道的一端写入,从另一端读出。
- 写入管道中的数据遵循先入先出的规则。
- 管道所传送的数据是无格式的,这要求管道的读出方与写入方必须事先约定好数据的格式,如多少字节算一个消息等。
- 管道不是普通的文件,不属于某个文件系统,其只存在于内存(内核内存中,而不是进程内存中)中。
- 管道在内存中对应一个缓冲区。不同的系统其大小不一定相同。
- 从管道读数据是一次性操作,数据一旦被读走,它就从管道中被抛弃,释放空间以便写更多的数据。
- 管道没有名字,只能在具有血缘关系(公共祖先)的进程之间使用。
2 无名管道的创建
经由参数
filedes
返回两个文件描述符
#include <unistd.h>
int pipe(int filedes[2]);
参数:
filedes为int型数组的首地址,其存放了管道的文件描述符fd[0]、fd[1]
filedes[0]为读而打开,filedes[1]为写而打开管道
返回值:
成功:0
失败:-1
注:在使用无名管道的时候,必须事先确定谁发,谁收
例:父进程发、子进程收
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
int main(int argc, char const *argv[])
{
//创建一个无名管道
//父进程发、子进程收
int fd[2];
pipe(fd);
//创建一个子进程
pid_t pid = fork();
if(pid == 0) //子进程
{
//子进程的写端无意义(可以事先关闭)
close(fd[1]);
//子进程接受父进程消息
printf("子进程%d正在等待父进程的消息\n",getpid());
unsigned char buf[128] = "";
read(fd[0], buf, sizeof(buf));
printf("子进程%d读到的消息为:%s\n", getpid(), buf);
//子进程读完数据,应该关闭读端
close(fd[0]);
//显示退出
_exit(-1);
}
else if(pid > 0) //父进程
{
//父进程的读端无意义(可以事先关闭)
close(fd[0]);
//写段写入数据
printf("父进程:%d将3秒后写入数据hello pipe\n", getpid());
sleep(3);
write(fd[1], "hello pipe", strlen("hello pipe"));
printf("父进程:%d完成写入\n", getpid());
//通信完成,应该关闭写端
close(fd[1]);
//等待子进程退出
wait(NULL);
}
return 0;
}
注:为什么无名管道不需要名字,且只能血缘关系通信?
因为父进程创建好管道后再创建子进程,子进程会连同管道一起复制,这样父子进程就能操作同一管道了。
3 无名管道读写的特点
- 默认用read函数从管道中读数据是阻塞的。
- 调用write函数向管道里写数据,当缓冲区已满时write也会阻塞。
- 通信过程中,读端口全部关闭后,写进程向管道内写数据时,写进程会(收到SIGPIPE(管道退出)信号)退出。|
例:管道写数据,缓冲区满时阻塞
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
int main(int argc, char const *argv[])
{
//创建一个无名管道
//父进程发、子进程收
int fd[2];
pipe(fd);
//创建一个子进程
pid_t pid = fork();
if(pid == 0) //子进程
{
getchar();
//显示退出
_exit(-1);
}
else if(pid > 0) //父进程
{
int i = 0;
for(i = 0; i < 1000; i++)
{
char buf[128] = "";
write(fd[1], buf, 128);
printf("i=%d\n",i+1);
}
//等待子进程退出
wait(NULL);
}
return 0;
}
例:读端口全部关闭后,写进程向管道内写数据时,写进程会(收到SIGPIPE(管道退出)信号)退出
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
int main(int argc, char const *argv[])
{
//创建一个无名管道
//父进程发、子进程收
int fd[2];
pipe(fd);
//创建一个子进程
pid_t pid = fork();
if(pid == 0) //子进程
{
//子进程的写端无意义(可以事先关闭)
close(fd[1]);
int i = 0;
while(1)
{
//子进程接受父进程消息
printf("子进程%d正在等待父进程的消息\n",getpid());
unsigned char buf[128] = "";
read(fd[0], buf, sizeof(buf));
printf("子进程%d读到的消息为:%s\n", getpid(), buf);
if(++i == 3)
{
break;
}
}
//子进程读完数据,应该关闭读端
close(fd[0]);
//显示退出
_exit(-1);
}
else if(pid > 0) //父进程
{
//父进程的读端无意义(可以事先关闭)
close(fd[0]);
while(1)
{
//写段写入数据
printf("父进程:%d将3秒后写入数据hello pipe\n", getpid());
sleep(3);
write(fd[1], "hello pipe", strlen("hello pipe"));
printf("父进程:%d完成写入\n", getpid());
}
//通信完成,应该关闭写端
close(fd[1]);
//等待子进程退出
wait(NULL);
}
return 0;
}
4 无名管道综合案例
例:完成 ps -A | grep bash命令
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
int main(int argc, char const *argv[])
{
//创建无名管道
int fd[2];
pipe(fd);
//创建两个子进程
int i = 0;
for(i = 0; i < 2; i++)
{
pid_t pid = fork();
if( pid == 0)
break;
}
if(i == 0) //子进程1
{
// ps -A 写段
//读端无意义,可以事先关闭
close(fd[0]);
//1要作为fd[1]的副本
dup2(fd[1],1);
//执行ps -A
execlp("ps","ps","-A",NULL);
_exit(-1)
}
else if(i == 1) //子进程2
{
//grap bash 读端
//写端无意义 可以事先关闭
close(fd[1]);
//0作为fd[0]的副本
dup2(fd[0],0);
//执行grep bash
execlp("grep","grep","bash",NULL);
_exit(-1);
}
else if(i ==2) //父进程
{
//关闭管道读写端
close(fd[0]);
close(fd[1]);
while(1)
{
pid_t pid = waitpid(-1, NULL, WNOHANG);
if(pid > 0)
{
printf("子进程%d退出了\n", pid);
}
else if(pid == 0)
{
continue;
}
else if(pid < 0)
{
break;
}
}
}
rturn 0;
}
七. 有名管道(命名管道)
主要用于没有血缘关系的进程间通信。
1 有名管道的特点
命名管道(FIFO)和管道(pipe)基本相同,但也有一些显著的不同,其特点是:
- 半双工,数据在同一时刻只能在一个方向上流动。
- 写入FIFO中的数据遵循先入先出的规则。
- FIFO所传送的数据是无格式的,这要求FIFO的读出方与写入方必须事先约定好数据的格式,如多少字节算一个消息等。
- FIFO在文件系统中作为一个特殊的文件而存在,但FIFO中的内容却存放在内存中。
- 管道在内存中对应一个缓冲区。不同的系统其大小不一定相同。
- 从FIFO读数据是一次性操作数据一旦被读,它就从FIFO中被抛弃,释放空间以便写更多的数据。
- 当使用FIFO的进程退出后,FIFO文件将继续保存在文件系统中以便以后使用。
- FIFO有名字,不相关的进程可以通过打开命名管道进程通信(重要)。
2 有名管道的创建
FIFO文件的创建
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
参数:
pathname:FIFO的路劲名 + 文件名
mode:mode_t类型的权限描述符
返回值:
成功:0
失败:如果文件已经存在,则会出错且返回-1
例
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
int main(int argc, char const *argv[])
{
//创建有名管道(两个进程要通信,必须保证两个进程访问的有名管道是同一路劲、名)
mkfifo("my_fifo", 0666);
return 0;
}
3 有名管道的使用案例
3.1 两个程序,一个写FIFO,一个读FIFO
3.1 写FIFO
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
//创建有名管道(两个进程要通信,必须保证两个进程访问的有名管道是同一路劲、名)
mkfifo("my_fifo", 0666);
//open以写的方式打开有名管道(阻塞到有进程以读的方式打开)
int fd = open("my_fifo", O_WRONLY);
if(fd < 0)
{
perror("open");
return 0;
}
printf("写端open成功了\n");
//循环写入数据
while(1)
{
//获取键盘输入
char buf[128] = "";
printf("请输入需要发送的数据:");
fgets(buf, sizeof(buf), stdin);
buf(strlen(buf)-1] = 0;
//发送数据
write(fd, buf, strlen(buf));
//退出循环
if(strcmp(buf, "bye") == 0)
{
break;
}
}
close(fd);
return 0;
}
注:open 打开有名管代是带阻塞的,直到有进程以读的方式打开此有名管道
3.2 读FIFO
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
//创建有名管道(两个进程要通信,必须保证两个进程访问的有名管道是同一路劲、名)
mkfifo("my_fifo", 0666);
//open以写的方式打开有名管道(阻塞到有进程以写的方式打开)
int fd = open("my_fifo", O_RDONLY);
if(fd < 0)
{
perror("open");
return 0;
}
printf("读端open成功了\n");
//循环的读取数据
while(1)
{
//接受数据
char buf[128] = "";
read(fd, buf, sizeof(buf));
printf("收到数据为:%s\n",buf);
//退出循环
if(strcmp(buf, "bye") == 0)
{
break;
}
}
close(fd);
return 0;
}
注:open 打开有名管代是带阻塞的,直到有进程以写的方式打开此有名管道
3.2 将收、发FIFO合并成一个代码
编译时加入宏名
-D 宏名
如:gcc 01_code.c -o test -D WRITE
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
//创建有名管道(两个进程要通信,必须保证两个进程访问的有名管道是同一路劲、名)
mkfifo("my_fifo", 0666);
#ifdef WRITE
int fd = open("my_fifo", O_WRONLY);
#endif
#ifdef READ
int fd = open("my_fifo", O_RDONLY);
#endif
if(fd < 0)
{
perror("open");
return 0;
}
printf("open成功了\n");
#ifdef WRITE
while(1)
{
//获取键盘输入
char buf[128] = "";
printf("请输入需要发送的数据:");
fgets(buf, sizeof(buf), stdin);
buf(strlen(buf)-1] = 0;
//发送数据
write(fd, buf, strlen(buf));
//退出循环
if(strcmp(buf, "bye") == 0)
{
break;
}
}
#endif
#ifdef READ
while(1)
{
//接受数据
char buf[128] = "";
read(fd, buf, sizeof(buf));
printf("收到数据为:%s\n",buf);
//退出循环
if(strcmp(buf, "bye") == 0)
{
break;
}
}
#endif
return 0;
}
4 有名管道读写的特点
操作FIFO文件时的特点,系统调用的I/O函数都可以作用于FIFO,如open、close、read、write等。
4.1 打开FIFO时,非阻寨标志(O_NONBLOCK)产生下列影响
4.1.1 以阻塞方式打开管道
不指定O_NONBLOCK(即open没有位或O_NONBLOCK)
- open 以只读方式打开FIFO时,要阻塞到某个进程为写而打开此FIFO
- open 以只写方式打开FIFO时,要阻塞到某个进程为读而打开此FIFO
- open 以只读、只写方式打开FIFO时会阻塞,调用read函数从FIFO里读数据时read也会阻塞
- 通信过程中若写进程先退出了,则调用read函数从FIFO里读数据时不阻塞;若写进程又重新运行,则调用read 函数从FIFO里读数据时又恢复阻塞
- 通信过程中,读进程退出后,写进程向有名管道内写数据时,写进程也会(收到SIGPIPE信号)退出
- 调用write函数向FIFO里写数据,当缓冲区已满时write也会阻塞
4.1.2 以非阻塞方式打开管道
指定O_NONBLOCK(即open没有位或O_NONBLOCK)
- 先以只读方式打开:如果没有进程已经为写而打开一个FIFO,只读open成功,并且open不阻塞
- 先以只写方式打开:如果没有进程已经为读而打开一个FIFO,只写open将出错返回-1
- read、write读写有名管道中读数据时不阻塞
- 通信过程中,读进程退出后,写进程向有名管道内写数据时,写进程也会(收到SIGPIPE信号)退出。
注:open函数以可读可写方式打开FIFO文件时的特点:
- open不阻塞
- 调用read函数从FIFO里读数据时read会阻塞
- 调用write函数向FIFO里写数据,当缓冲区已满时write也会阻塞
5 单机QQ聊天程序
实现单机QQ聊天:父进程创建子进程,实现多任务。父进程负责发信息(向FIFO里写数据),子进程负责接受信息(从FIFO里读数据)。打开有名管道的用阻塞的方法。
5.1 两个程序实现单机聊天
5.1.1 用户 bob
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
//创建两个有名管道
mkfifo("bob_to_lucy", 0666);
mkfifo("lucy_to_bob", 0666);
int i = 0;
for(; i < 2; i++)
{
pid_t pid = fork();
if(pid == 0)
break;
}
if(i == 0) //子进程1 负责发消息 (bod 发给lucy)
{
int fd = open("bob_to_lucy", O_WRONLY);
if(fd < 0)
{
perror("open");
_exit(-1)
}
//获取键盘输入
while(1)
{
//获取键盘输入
char buf[128] = "";
printf("\rbob:");
fgets(buf, sizeof(buf), stdin);
buf(strlen(buf)-1] = 0;
//发送数据
write(fd, buf, strlen(buf));
//退出循环
if(strcmp(buf, "bye") == 0)
{
break;
}
}
close(fd);
//退出进程
_exit(-1);
}
else if(i == 1) //子进程2 负责收消息 (lucy 发给 bob)
{
int fd = open("lucy_to_bob", O_RDONLY);
if(fd < 0)
{
perror("open");
_exit(-1)
}
//循环的读取数据
while(1)
{
//接受数据
char buf[128] = "";
read(fd, buf, sizeof(buf));
printf("\rlucy:%s\n\rbob:",buf);
//退出循环
if(strcmp(buf, "bye") == 0)
{
break;
}
}
close(fd);
return 0;
}
else if(i == 2) //父进程负责回收资源
{
while(1)
{
pid_t pid = waitpid(-1, NULL, WNOHANG);
if(pid > 0)
{
printf("子进程%d退出了\n",pid);
}
else if(pid == 0)
{
continue;
}
else if(pid < 0)
{
break;
}
}
}
return 0;
}
5.1.2 用户 lucy
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
//创建两个有名管道
mkfifo("bob_to_lucy", 0666);
mkfifo("lucy_to_bob", 0666);
int i = 0;
for(; i < 2; i++)
{
pid_t pid = fork();
if(pid == 0)
break;
}
if(i == 0) //子进程1 负责发消息 (lucy 发给 bob)
{
int fd = open("lucy_to_bob", O_WRONLY);
if(fd < 0)
{
perror("open");
_exit(-1)
}
//获取键盘输入
while(1)
{
//获取键盘输入
char buf[128] = "";
printf("\rlucy:");
fgets(buf, sizeof(buf), stdin);
buf(strlen(buf)-1] = 0;
//发送数据
write(fd, buf, strlen(buf));
//退出循环
if(strcmp(buf, "bye") == 0)
{
break;
}
}
close(fd);
//退出进程
_exit(-1);
}
else if(i == 1) //子进程2 负责收消息 (bob 发给 lucy)
{
int fd = open("bob_to_lucy", O_RDONLY);
if(fd < 0)
{
perror("open");
_exit(-1)
}
//循环的读取数据
while(1)
{
//接受数据
char buf[128] = "";
read(fd, buf, sizeof(buf));
printf("\rbob:%s\n\rlucy:",buf);
//退出循环
if(strcmp(buf, "bye") == 0)
{
break;
}
}
close(fd);
return 0;
}
else if(i == 2) //父进程负责回收资源
{
while(1)
{
pid_t pid = waitpid(-1, NULL, WNOHANG);
if(pid > 0)
{
printf("子进程%d退出了\n",pid);
}
else if(pid == 0)
{
continue;
}
else if(pid < 0)
{
break;
}
}
}
return 0;
}
5.2 两个程序合并为一个实现单机聊天
同3.2节
八. 消息队列
1 消息队列概述
消息队列是消息的链表,存放在内存中,由内核维护消息队列的特点:
- 消息队列中的消息是有类型的
- 消息队列中的消息是有格式的
- 消息队列可以实现消息的随机查询。消息不一定要以先进先出的次序读取,编程时可以按消息的类型读取
- 消息队列允许一个或多个进程向它写入或者读取消息
- 与无名管道、命名管道一样,从消息队列中读出消息,消息队列中对应的数据都会被删除
- 每个消息队列都有消息队列标识符,消息队列的标识符在整个系统中是唯一的
- 只有内核重启或人工删除消息队列时,该消息队列才会被删除。若不人工删除消息队列,消息队列会一直存在于系统中
在ubuntu某些版本中消息队列限制值如下: |
---|
每个消息内容最多为8K字节 |
每个消息队列容量最多为16K字节 |
系统中消息队列个数最多为1609个 |
系统中消息个数最多为16384个 |
2 消息队列的API
SystemV提供的IPC通信机制需要一个key值,通过key值就可在系统内获得一个唯一的消息队列标识符。key值可以是人为指定的,也可以通过ftok 函数获得。
2.1 获取唯一的key值
获得项目相关的唯一的IPC键值
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
参数:
pathname:路径名
proj_id:项目ID,非0整数(只有低8位有效)
返回值:
成功返回key值,失败返回-1
例:
bob.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
int main(int argc, char const *argv[])
{
//获取IPC的唯一KEY值
key_t key = ftok("/",2021);
printf("key=%d\n",key);
return 0;
}
lucy.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
int main(int argc, char const *argv[])
{
//获取IPC的唯一KEY值
key_t key = ftok("/",2021);
printf("key=%d\n",key);
return 0;
}
tom.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
int main(int argc, char const *argv[])
{
//获取IPC的唯一KEY值
key_t key = ftok("/",2021);
printf("key=%d\n",key);
return 0;
}
2.2 创建消息队列
创建一个新的或打开一个已经存在的消息队列。不同的进程调用此函数,只要用相同的key值就能得到同一个消息队列的标识符
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
参数:
key:IPC键值
msgflg:标识函数的行为及消息队列的权限
msgflg的取值:
IPC_CREAT:创建消息队列
IPC_EXCL:检测消息队列是否存在
位或权限位:消息队列位或权限位后可以设置消息队列的访问权限,格式和 open函数的mode_t一样,但可执行权限未使用。
返回值:
成功:消息队列的标识符
失败:-1
注:使用shell命令操作消息队列:
查看消息队列:ipcs -q
删除消息队列:ipcrm -q msqid
例
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
int main(int argc, char const *argv[])
{
//获取IPC的唯一KEY值
key_t key = ftok("/",2021);
printf("key=%#x\n",key);
//创建一个消息队列
int msg_id = msgget(key, IPC_CREAT|0666);
printf("msg_id=%d\n",msg_id);
return 0;
}
3 消息队列的信息格式定义
typedef struct _msg
{
long mtype; //消息类型(必须是第一个成员,必须是long类型)
char mtext[100]; //消息正文
... //消息的正文可以有多个成员
}MSG;
消息类型必须是长整型的,而且必须是结构体类型的第一个成员,类型下面是消息正文,正文可以有多个成员(正文成员可以是任意数据类型)
4 发送消息
将新消息添加到消息队列
#include <sys/msg.h>
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
参数:
msqid:消息队列的标识符
msgp:待发送消息结构体的地址
msgsz:消息正文的字节数 //一般为sizeof(MSG)-sizeof(long)
msgflg:函数的控制属性
0:msgsnd调用阻塞直到条件满足为止
IPC_NOWAIT:若消息没有立即发送则调用该函数的进程会立即返回
返回值:
成功:0
失败:-1
例:bob给luny发消息
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <string.h>
//定义一个消息类型结构体
typedef struct MyStruct
{
long mtype; //消息类型
char mtext[64]; //消息正文
char name[32]; //发送者的姓名
}MSG;
int main(int argc, char const *argv[])
{
//获取IPC的唯一KEY值
key_t key = ftok("/",2021);
printf("key=%#x\n",key);
//创建一个消息队列
int msg_id = msgget(key, IPC_CREAT|0666);
printf("msg_id=%d\n",msg_id);
//发送消息 给lucy(lucy只接受20类型的数据)发消息
MSG msg;
memset(&msg, 0, sizeof(msg));
msg.mtype = 20;
strcpy(msg.name,"bob");
strcpy(msg.mtext, "hello msg");
msgsnd(msg_id, &msg, sizeof(MSG)-sizeof(long), 0);
return 0;
}
5 接受消息
从标识符为msqid的消息队列中接受一个消息。一旦接受消息成功,则消息在消息队列中删除
#include <sys/msg.h>
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
参数:
msqid:消息队列的标识符,代表要从哪个消息队列中获取消息
msgp:存放消息结构体的地址
msgsz:消息正文的字节数
msgtyp:感兴趣消息的类型、可以有以下几种类型
msgtyp = 0:返回队列中的第一个消息
msgtyp > 0:返回队列中消息类型为msgtyp的消息
msgtyp < 0:返回队列中消息类型小于或等于msgtyp绝对值的消息,如果这种消息有若干个,则取类型值最小的消息
注:若消息队列中有多种类型的消息,msgrcv获取消息的时候按消息类型获取,不是先进先出的<br />
在获取某类型消息的时候,若队列中有多条此类型的消息,则获取最先添加的消息,即先进先出原则
msgflg:函数的控制属性
0:msgrcv调用阻塞直到接受消息为止
MSG_NOERROR:若返回的消息字节数比nbytes字节数多,则消息就会截断到nbytes字节,且不通知消息发送进程。
IPC_NOWAIT:调用进程会立即返回。若没有收到消息则立即返回-1.
返回值:
成功:返回读取消息的长度
失败:-1
例
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <string.h>
//定义一个消息类型结构体
typedef struct MyStruct
{
long mtype; //消息类型
char mtext[64]; //消息正文
char name[32]; //发送者的姓名
}MSG;
int main(int argc, char const *argv[])
{
//获取IPC的唯一KEY值
key_t key = ftok("/",2021);
printf("key=%#x\n",key);
//创建一个消息队列
int msg_id = msgget(key, IPC_CREAT|0666);
printf("msg_id=%d\n",msg_id);
//接受消息
MSG msg;
memset(&msg, 0, sizeof(msg));
msgrcv(msg_id, &msg, sizeof(MSG)-sizeof(long), 20, 0);
printf("发送者:%s\n",msg.name);
printf("消息:%s\n",msg.mtext);
return 0;
}
6 消息队列的控制:
对消息队列进行各种控制,如修改消息队列的属性,或删除消息消息队列
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
参数:
msqid:消息队列的标识符
cmd:函数功能的控制
IPC_RMID:删除由msqid指示的消息队列,将它从系统中删除并破坏相关数据结构
IPC_STAT:将msqid相关的数据结构中各个元素的当前值存入到由buf指向的结构中
IPC_SET:将msqid相关的数据结构中的元素设置为由buf指向的结构中的对应值
buf:msqid_ds数据类型的地址,用来存放或更改消息队列的属性
返回值:
成功:0
失败:-1
7 总结
不管是发送者还是接受者都需要
ftok
得到唯一的keymsgget
创建消息队列发送者:
MSG msg
msg.mtype = 接受感兴趣的类型值
msgsnd(msg_id, &msg, sizeof(MSG) - sizeof(long), 0)
//发送消息到消息队列接受者
MSG msg
msgrcv(msg_id, &msg, sizeof(MSG) - sizeof(long), 接受感兴趣的类型值, 0)
九. mmap
1 mmap原理
存储映射I/O (Memory-mapped I/O)使一个磁盘文件与存储空间中的一个缓冲区相映射。于是当从缓冲区中取数据,就相当于读文件中的相应字节。于此类似,将数据存入缓冲区,则相应的字节就自动写入文件。这样,就可在不适用read和write函数的情况下,使用地址(指针)完成I/O操作。使用存储映射这种方法,首先应通知内核,将一个指定文件映射到存储区域中。这个映射工作可以通过mmap函数来实现。
2 mmap的API
2.1 建立文件和内存的映射
#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:可写
flags:标志位
MAP_SHARED:共享的--对映射区的修改会影响源文件
MAP_PRIVATE:私有的
MAP_ANONYMOUS:匿名映射,映射不受任何文件的支持,它的内容被初始化为零
fd和offset参数被忽略,如果指定MAP_ANONYMOUS,有些实现要求fd为-1
Linux从2.4内核开始支持MAP_ANONYMOUS与MAP_SHARED结合使用
fd:文件描述符,需要打开一个文件
offset:指定一个偏移位置,从该位置开始映射
返回值:
成功:返回映射区的首地址
失败:返回MAP_FAILED((void *)-1)
2.2 扩展文件大小
#include <unistd.h>
#include <sys/types.h>
int truncate(const char *path, off_t length);
参数:
path:要扩展的文件
length:要扩展的长度
2.3 释放映射区域
int munmap(void *addr, size_t length);
参数:
addr:映射区的首地址
length:映射区的长度
返回值:
成功:0
失败:-1
2.4 例:mmap的实用
2.4.1 write.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <string.h>
int main(int argc, char const *argv[])
{
//通过open事先打开文件
int fd = open("tmp", O_RDWR | O_CREAT, 0666);
if(fd < 0)
{
perroe("open");
return 0;
}
//扩展文件大小
truncate("tmp", 16);
//建立映射
char *buf = (char *)mmap(NULL, 16, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
//使用区域
strcpy(buf, "hello mmap");
//断开映射
munmap(buf, 16);
return 0;
}
2.4.2 read.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
int main(int argc, char const *argv[])
{
//通过open事先打开文件
int fd = open("tmp", O_RDWR | O_CREAT, 0666);
if(fd < 0)
{
perroe("open");
return 0;
}
//扩展文件大小
truncate("tmp", 16);
//建立映射
char *buf = (char *)mmap(NULL, 16, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
//使用区域
printf("%s\n", buf);
//断开映射
munmap(buf, 16);
return 0;
}
十. 共享内存
1 共享内存概述
共享内存允许两个或多个进程共享给定的存储区域
共享内存的特点:
- 共享内存是进程间共享数据的一种最快的方法。一个进程向共享的内存区域写入了数据,共享这个内存区域的所有进程就可以立刻看到其中的内容。
- 使用共享内存要注意的是多个进程之间对一个给定存储区访问的互斥。若一个进程正在向共享内存区写数据,则在它做完这一步操作前,别的进程不应当去读、写这些数据。
在ubuntu部分版本中共享内存限制值如下 |
---|
共享存储区的最小字节数:1 |
共享存储区的最大字节数:32M |
共享存储区的最大个数:4096 |
每个进程最多能映射的共享存储区的个数:4096 |
2 共享内存的创建或使用
2.1 获得一个唯一共享存储标识符(创建或打开一个共享内存区)
创建或打开一个共享内存区
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
参数:
key:IPC键值
size:该共享存储段的长度(字节)
shmflg:标识函数的行为及共享内存的权限
IPC_CREAT:如果不存在就创建
IPC_EXCL:如果已经存在则返回失败
位或权限位:共享内存位或权限后可以设置共享内存的访问权限,格式和open函数的mode_t一样,但可执行权限未使用
返回值:
成功:返回共享内存标识符
失败:-1
使用shell命令操作共享内存:
查看共享内存:ipcs -m
删除共享内存:ipcrm -1m shmid
2.2 建立进程虚拟内存和物理内存的映射
将一个共享内存段映射到调用进程的数据段中
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数:
shmid:共享内存标识符
shmaddr:共享内存映射地址(若为NULL则由系统自动指定,推荐使用NULL)
shmflg:共享内存段的访问权限和映射条件
0:共享内存具有可读可写权限
SHM_RDONLY:可读
SHM_RND:(shmaddr非空时才有效)
没有指定SHM_RND则此段连接到shmaddr所指定的地址上(shmaddr必须页对齐)
指定了SHM_RND则此段连接到shmaddr-shmaddr%SHMLBA所表示的地址上
返回值:
成功:返回共享内存段映射地址
失败:-1
注:shmat函数使用的时候第二个和第三个参数一般为NULL和0,即系统自动指定共享内存地址,并且共享内存可读可写
2.3 接触共享映射区
将共享内存和当前进程分离(仅仅是断开联系并不删除共享内存)
#include <sys/types.h>
#include <sys/shm.h>
int shmdt(const void *shmaddr);
参数:
shmaddr:共享内存映射地址
返回值:
成功:0
失败:-1
2.4 共享内存控制
共享内存空间的控制
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数:
shmid:共享内存标识符
cmd:函数功能的控制
IPC_RMID:删除
IPC_SET:设置shmid_ds参数
IPC_STAT:保存shmid_ds参数
SHM_LOCK:锁定共享内存段(超级用户)
SHM_UNLOCK:解锁共享内存段
buf:shmid_ds数据类型的地址,用来存放或修改共享内存的属性
返回值:
成功:0
失败:-1
注:SHM_LOCK用于锁定内存,禁止内存交换。并不代表共享内存被锁定后禁止其他进程访问。其真正的意义是:被锁定的内存不允许被交换到虚拟内存中。这样做的优势在于让共享内存一直处于内存中,从而提高程序性能。
2.5 例:共享内存实用
2.5.1 write.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>
int main(int argc, char const *argv[])
{
//获取唯一的key
key_t key = ftok("/", 2022);
//获取共享内存的标识(分配物理内存)
int shm_id = shmget(key, 32, IPC_CREAT|0666);
//将虚拟内存和物理内存建立映射
char *buf = (char *)shmat(shm_id, NULL, 0);
//操作虚拟内存
strcpy(buf, "hello shm");
//释放映射
shmdt(buf);
return 0;
}
2.5.2 read.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int main(int argc, char const *argv[])
{
//获取唯一的key
key_t key = ftok("/", 2022);
//获取共享内存的标识(分配物理内存)
int shm_id = shmget(key, 32, IPC_CREAT|0666);
//将虚拟内存和物理内存建立映射
char *buf = (char *)shmat(shm_id, NULL, 0);
//操作虚拟内存
printf("%s\n", buf);
//释放映射
shmdt(buf);
return 0;
}
十一. 线程
1 线程概述
在许多经典的操作系统教科书中,总是把进程定义为程序的执行实例,它并不执行什么,只是维护应用程序所需的各种资源,而线程则是真正的执行实体。所以,线程是轻量级的进程(LWP:light weight process),在Linux环境下线程的本质伋是进程。为了让进程完成一定的工作,进程必须至少包含一个线程。
进程是系统分配资源的基本单位
线程是CPU执行调度的基本单位
线程依赖于进程,线程共享进程的资源,线程有独立的资源(计算器,一组寄存器和栈等)
进程结束,当前进程的所有线程都将立即结束
进程,直观点说,保存在硬盘上的程序运行以后,会在内存空间里形成一个独立的内存体,这个内存体有自己的地址空间,有自己的堆,上级挂靠单位是操作系统。操作系统会以进程为单位,分配系统资源,所以我们也说,进程是系统分配资源的最小单位。线程存在于进程当中(进程可以认为是线程的容器),是CPU调度执行的最小单位。说通俗点,线程就是干活的。进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配的一个独立单位。
线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。如果说进程是一个资源管家负责从主人那里要资源的话,那么线程就是干活的苦力。一个管家必须完成一项工作,就需要最少一个苦力,也就是说,一个进程最少包含一个线程,也可以包含多个线程。苦力要干活,就需要依托于管家,所以说一个线程,必须属于某一个进程。进程有自己的地址空间,线程使用进程的地址空间,也就是说,进程里的资源,线程都是有权访问的,比如说堆啊,栈啊,静态存储区什么的。
2 线程函数列表安装
命令:
manpages-posix-dev
说明:
manpages-posix-dev
包含POSIX
的header files
和library calls
的用法
查看:
man -k pthred
注:线程为第三方库,不是C语言自带的库
3 线程的特点
类Unix系统中,早起是没有“线程”概念的,80年代才引入,借助进程机制实现出了线程的概念。
因此在这类系统中,进程和线程关系密切:
- 线程是轻量级进程(light-weight process),也有PCB,创建线程使用的底层函数和进程一样,都是clone
- 从内核里看进程和线程是一样的,都有各自不同的PCB
- 进程可以脱变成线程
- 在Linux下,线程是最小的执行单位,进程是最小的分配资源单位
查看指定进程的LWP号:
ps -Lf pid
实际上,无论是创建进程的fork,还是创建线程的pthread_create
,底层实现都是调用同一个内核函数的clone
。
- 如果复制对方的地址空间,那么就产出一个“进程”;
- 如果共享对方的地址空间,就产生一个“线程”;
- Linux内核是不区分进程和线程的,只是用户层面上进程区分;
- 所以,线程所有操作函数
pthread*
是库函数,而非系统调用。
3.1 线程共享资源
- 文件描述符
- 每种信号的处理方式
- 当前工作目录
- 用户ID和组ID内存地址空间(.text/ .data/ .bss/ heap/ 共享库)
- 信号屏蔽字
- 调度优先级
3.2 线程非共享资源
- 线程ID
- 处理器现场和栈指针(内核栈)
- 独立的栈空间(用户空间栈)
- errno 变量
- 信号屏蔽字
- 调度优先级
3.3 线程的优缺点
优点:
- 提高程序并发性
- 开销小
- 数据通信、共享数据方便
缺点:
- 库函数,不稳定
- 调试、编写困难,gdb支持
- 对信号支持不好
总结:
- 优点相对突出,缺点均不是硬伤。Linux下由于实现方法导致进程、线程差别不是很大
2 线程的API
2.1 获取线程号
获取线程号
#include <pthread.h>
pthread_t pthrea_self(void);
参数:
无
返回值:
调用线程的线程ID(pthread_t类型:unsigned long int)
2.1.1 代码示例:获取线程号
#include <stdio.h>
#include <pthread.h>
int main(int argc, char const *argv[])
{
//查看线程号
printf("线程号:%d\n", pthread_self());
getchar();
return 0;
}
注:使用<pthread.h>库,编译时需要加-lpthread
2.2 线程的创建
创建一个线程
#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
注:进程结束,线程就结束
2.2.1 代码示例:每个线程有独立的线程函数
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void *pthread_fun01(void *arg)
{
int i = 0;
while(1)
{
printf("%s---------i=%d\n",(char *)arg,i++);
sleep(1);
}
return NULL;
}
void *pthread_fun02(void *arg)
{
int i = 0;
while(1)
{
printf("%s---------i=%d\n",(char *)arg,i++);
sleep(1);
}
return NULL;
}
int main(int argc, char const *argv[])
{
//创建两个线程
pthread_t tidi, tid2;
pthread_create(&tid1, NULL, pthread_fun01, "任务01");
pthread_create(&tid2, NULL, pthread_fun02, "任务02");
printf("tid1=%lu\n", tid1);
printf("tid2=%lu\n", tid2);
getchar();
return 0;
}
2.2.2 代码示例:每个线程共用同一线程函数
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void *pthread_fun(void *arg)
{
if(strcmp(arg, "任务A") == 0)
{
int i = 0;
while(1)
{
printf("%s---------i=%d\n",(char *)arg,i++);
sleep(1);
}
}
else if(strcmp(arg, "任务B") == 0)
{
int i = 0;
while(1)
{
printf("%s---------i=%d\n",(char *)arg,i++);
sleep(1);
}
}
return NULL;
}
int main(int argc, char const *argv[])
{
//创建两个线程
pthread_t tidi, tid2;
pthread_create(&tid1, NULL, pthread_fun, "任务01");
pthread_create(&tid2, NULL, pthread_fun, "任务02");
printf("tid1=%lu\n", tid1);
printf("tid2=%lu\n", tid2);
getchar();
return 0;
}
2.3 回收线程资源(阻塞)
等待线程结束(此函数会阻塞),并回收线程资源,类似进程的
wait()
函数。如果线程已经结束,那么该函数会立即返回
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
参数:
thread:被等待的线程号
retval:用来存储线程退出状态的指针的地址
返回值:
成功:0
失败:非0
2.3.1 代码示例:回收线程资源
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void *pthread_fun01(void *arg)
{
int i = 0;
for(; i < 5; i++)
{
printf("%s---------i=%d\n",(char *)arg,i++);
sleep(1);
}
return NULL;
}
void *pthread_fun02(void *arg)
{
int i = 0;
for(; i < 3; i++)
{
printf("%s---------i=%d\n",(char *)arg,i++);
sleep(1);
}
return NULL;
}
int main(int argc, char const *argv[])
{
//创建两个线程
pthread_t tidi, tid2;
pthread_create(&tid1, NULL, pthread_fun01, "任务A");
pthread_create(&tid2, NULL, pthread_fun02, "任务B");
//回收线程资源(带阻塞)
pthread_join(tid1, NULL); //不关心返回值
printf("tid1结束了\n");
pthread_join(tid2, NULL); //不关心返回值
printf("tid2结束了\n");
getchar();
return 0;
}
2.3.2 代码示例:回收线程资源并获取线程返回值的值
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void *pthread_fun01(void *arg)
{
int i = 0;
for(; i < 5; i++)
{
printf("%s---------i=%d\n",(char *)arg,i++);
sleep(1);
}
return (void *)"任务A";
}
void *pthread_fun02(void *arg)
{
int i = 0;
for(; i < 3; i++)
{
printf("%s---------i=%d\n",(char *)arg,i++);
sleep(1);
}
return (void *)"任务B";
}
int main(int argc, char const *argv[])
{
//创建两个线程
pthread_t tidi, tid2;
pthread_create(&tid1, NULL, pthread_fun01, "任务A");
pthread_create(&tid2, NULL, pthread_fun02, "任务B");
//回收线程资源(带阻塞)
void *p1 = NULL;
pthread_join(tid1, &p1); //不关心返回值
printf("tid1结束了,返回值为%s\n", (char *)p1);
pthread_join(tid2, &p1); //不关心返回值
printf("tid2结束了,返回值为%s\n", (char *)p2);
getchar();
return 0;
}
2.4 线程分离(不阻塞)
一般情况下,线程终止后,其终止状态一直保留到其它线程调用
pthread _join
获取它的状态为止。但是线程也可以被置为detach
状态,这样的线程一旦终止就立刻回收它占用的所有资源,而不保留终止状态。不能对一个已经处于detach
状态的线程调pthread_join
,这样的调用将返回EINVAL 错误。也就是说,如果已经对一个线程调用了pthread_detach
就不能再调用pthread_join
了。
使调用线程与当前进程分离,分离后不代表此线程不依赖与当前进程,线程分离的目的是将线程资源的回收工作交由系统自动来完成,也就是说当被分离的线程结束之后,系
统会自动回收它的资源。所以,此函数不会阻塞
#include <pthread.h>
int pthread_detach(pthread_t thread);
参数:
thread:线程号
返回值:
成功:0
失败:非0
2.4.1 代码示例:线程分离
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void *pthread_fun01(void *arg)
{
int i = 0;
for(; i < 5; i++)
{
printf("%s---------i=%d\n",(char *)arg,i++);
sleep(1);
}
return (void *)"任务A";
}
int main(int argc, char const *argv[])
{
//创建两个线程
pthread_t tidi, tid2;
pthread_create(&tid1, NULL, pthread_fun01, "任务A");
//线程分离
pthread_detach(tid1);
//主函数也是个线程
int i = 0;
while(1)
{
printf("%s---------i=%d\n", "任务B", i++);
sleep(1);
}
return 0;
}
2.5 线程的退出
退出调用线程。一个进程中的多个线程是共享该进程的数据段,因此,通常线程退出后占用的资源并不会释放。
#include <pthread.h>
void pthread_exit(void *retval);
参数:
retval:存储线程退出状态的指针
返回值:
无
2.5.1 代码示例:线程的退出
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void *pthread_fun01(void *arg)
{
int i = 0;
while(1)
{
printf("%s---------i=%d\n",(char *)arg,i++);
if(i == 5)
{
pthread_exit(NULL);
}
sleep(1);
}
return NULL;
}
int main(int argc, char const *argv[])
{
//创建两个线程
pthread_t tidi, tid2;
pthread_create(&tid1, NULL, pthread_fun01, "任务A");
pthread_join(tid1, NULL);
printf("任务A结束了\n");
return 0;
}
2.6 线程的取消
取消自己,也可以取消当前进程的其他线程,杀死线程
注:线程的取消并不是实时的,而有一定的延时。需要等待线程到达某个取消点(检查点)。类似于玩游戏存档,必须到达指定的场所(存档点,如:客栈、仓库、城里等)才能存储进度。杀死线程也不是立刻就能完成,必须要到达取消点。取消点:是线程检查是否被取消,并按请求进行动作的一个位置。通常是一些系统调用creat,open
,pause
,close
,read
,write
…执行命令man 7 pthreads
可以查看具备这些取消点的系统调用表。可粗略认为一个系统调用(进入内核)即为一个取消点。
#include <pthread.h>
int pthread_cancel(pthread_t thread);
参数:
thread:目标线程ID
返回值:
成功:0
失败:出错编号
2.6.1 代码示例:线程的取消
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void *pthread_fun01(void *arg)
{
int i = 0;
while(1)
{
printf("%s---------i=%d\n",(char *)arg,i++);
if(i == 5)
{
pthread_exit(NULL);
}
sleep(1); //取消点
}
return NULL;
}
int main(int argc, char const *argv[])
{
//创建两个线程
pthread_t tidi, tid2;
pthread_create(&tid1, NULL, pthread_fun01, "任务A");
//线程分离
pthread_detach(tid1);
printf("5秒后结束任务A\n");
sleep(5);
//取消线程
pthread_detach(tid1);
getchar();
return 0;
}
3 线程的属性
Linux下线程的属性是可以根据实际项目需要,进行设置,之前我们讨论的线程都是采用线程的默认属性,默认属性已经可以解决绝大多数开发时遇到的问题。如我们对程序的性能提出更高的要求那么需要设置线程属性,比如可以通过设置线程栈的大小来降低内存的使用,增加最大线程个数。
主要结构成员:
- 线程分离状态
- 线程栈大小(默认平均分配)
- 线程栈警戒缓冲区大小(位于栈末尾)
- 线程栈最低地址
属性值不能直接设置,必须使用相关函数进程操作,初始化的函数为
pthread_attr_init
,这个函数必须在pthread_create
函数之前调用。之后须用pthread_attr_destroy
函数来释放资源。线程属性主要包括如下属性:作用域(scope)、栈尺寸(stack size)、栈地址(stack address)、优先级(priority)、分离的状态(detached state)、调度策略和参数(scheduling policy and parameters)。默认的属性为非绑定、非分离,缺省的堆栈与父进程同样级别的优先级。
3.1 线程属性初始化
注:应先初始化线程属性,在
pthread_create
创建线程
int pthread_init(pthread_attr_t *attr);
参数:
attr:
typedef struct
{
int etachstate; //线程的分离状态
int schdpolicy; //线程调度策略
struct schedparam; //线程的调度参数
int inheritsched; //线程的继承性
int scope; //线程的作用域
size_t guardsize; //线程栈末尾的警械缓冲区大小
int stackaddr_set; //线程的栈设置
void* stackaddr; //线程栈的位置
size_t stacksize; //线程栈的大小
}pthread_attr_t;
返回值:
成功:0
失败:错误号
3.2 销毁线程属性所占用的资源
int pthread_attr_destroy(pthread_attr_t *attr);
参数:
attr
返回值:
成功:0
失败:错误号
3.3 设置线程属性中的分离状态
设置线程属性,分离或非分离
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstat);
参数:
attr:已初始化的线程属性
detachstate:
分离状态:PTHREAD_CREATE_DETACHED(分离线程)
PTHREAD_CREATE_JOINABLE(非分离线程)
3.4 获取线程属性中的分离状态
获取线程属性,分离状态为分离或非分离
int pthread_attr_getdetachstate(pthread_attr_t *attr, int detachstat);
参数:
attr:已初始化的线程属性
detachstate:
分离状态:PTHREAD_CREATE_DETACHED(分离线程)
PTHREAD_CREATE_JOINABLE(非分离线程)
3.1 代码示例:使用线程的属性使线程分离
以前的方案
注:如果线程先结束,pthread_detach后执行,就存在问题
pthread_t tid1;
pthread_create(&tid1, NULL, pthread_fun01, "任务A");
//线程分离
pthread_detach(tid1);
现在的方案
注:创建线程的时候,通过线程属性设置线程分离,就定义保存,先分离后执行线程(解决了上面的问题)
4 创建多线程
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
typedef struct
{
char task_name[32];
int time;
}MSG;
void *deal_fun(void *arg)
{
MSG p = *(MSG *)arg;
int i = 0;
for( i = msg.time; i > 0; i--)
{
printf("%s剩余时间%d\n",msg.task_name, i);
sleep(1);
}
return NULL;
}
int main(int argc, char const *argcv[])
{
while(1)
{
MSG msg;
printf("输入新增的任务名:");
fgets(msg.task_name, sizeof(msg.task_name), stdin);
msg.task_name[strlen(msg.task_name)-1] = 0;
printf("输入运行时间:");
scanf("%d", &msg.time);
getchar(); // 获取换行符
//创建线程 注:此tid只是用来记录当时创建线程的编号
pthread_t tid;
pthread_create(&tid, NULL, deal_fun, (void *)&msg);
pthread_detach(tid); //现场分离
}
return 0;
}
十二. 线程的同步与互斥
1 同步与互斥的概述
现代操作系统基本都是多任务操作系统,即同时有大量可调度实体在运行。在多任务操作系统中,同时运行的多个任务可能:都需要访问/使用同一种资源多个任务之间有依赖关系,某个任务的运行依赖于另一个任务这两种情形是多任务编程中遇到的最基本的问题,也是多任务编程中的核心问题,同步和互斥就是用于解决这两个问题的。
1.1互斥
是指散步在不同任务之间的若干程序片断,当某个任务运行其中一个程序片段时,其它任务就不能运行它们之中的任一程序片段,只能等到该任务运行完这个程序片段后才可以运行。最基本的场景就是:一个公共资源同一时刻只能被一个进程或线程使用,多个进程或线程不能同时使用公共资源。
同一时间,只能一个任务(进程或线程)执行,谁先运行不确定。
1.2 同步
是指散步在不同任务之间的若干程序片断,它们的运行必须严格按照规定的某种先后次序来运行,这种先后次序依赖于要完成的特定的任务。最基本的场景就是:两个或两个以上的进程或线程在运行过程中协同步调,按预定的先后次序运行。比如A任务的运行依赖于B任务产生的数据。显然,同步是一种更为复杂的互斥,而互斥是一种特殊的同步。也就是说互斥是两个任务之间不可以同时运行,他们会相互排斥,必须等待一个线程运行完毕,另一个才能运行,而同步也是不能同时运行,但他是必须要按照某种次序来运行相应的线程(也是一种互斥)! 因此互斥具有唯一性和排它性,但互斥并不限制任务的运行顺序,即任务是无序的,而同步的任务之间则有顺序关系。
同一时间,只能一个任务(进程或线程)执行,有顺序的运行,是特殊的互斥。
2 互斥锁
互斥锁(mutex),也叫互斥量,互斥锁是一种简单的加锁的方法来控制对共享资源的访问,互斥锁只有两种状态,即加锁(lock)和解锁(unlock)。
2.1 互斥锁的操作流程
- 在访问共享资源临界区域前,对互斥锁进程加锁
- 在访问完成后释放互斥锁上的锁
- 在互斥锁进程加锁后,任何其他试图再次对互斥锁加锁的线程将会阻塞,直到锁被释放
互斥锁的数据类型是:pthread_mutex_t
安装对应帮助手册:$ sudo apt-get install manpages-posix-dev
2.2 互斥锁的API
2.2.1 初始化互斥锁
初始化一个互斥锁
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *arrt);
参数:
mutex:互斥锁地址(类型:pthread_mutex_t)
attr:设置互斥量的属性,通常可采样默认属性,即可将attr设为NULL。
可以使用宏PTHREAD_MUTEX_INITIALIZER静态初始化互斥锁。
比如:pthread_mutex_t mutex = PTHREAD_MUTEX_initializer;
这种方法等价于使用NULL指定的attr参数调用pthread_mutex_init()来完成动态初始化,不同之处在于PTHREAD_MUTEX_INITIALIZER宏不进行错误检查
返回值:
成功:0,成功申请的默认是打开的
失败:非0 ,错误码
2.2.2 销毁互斥锁
销毁指定的一个互斥锁。互斥锁在使用完毕后,必须要对互斥锁进程销毁,以释放资源。
#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);
参数:
mutex:互斥锁地址
返回值:
成功:0
失败:非0,错误码
2.2.3 申请上锁
对互斥锁上锁,若互斥锁已经上锁,则调用者阻塞,知道互斥锁解锁后再上锁。
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
参数:
mutex:互斥锁地址
返回值:
成功:0
失败:非0,错误码
2.2.4 试着上锁
调用该函数时,若互斥锁未加锁,则上锁,返回0;若互斥锁已加锁,则函数直接返回失败,即EBUSY。
#include <pthread.h>
int pthread_mutex_trylock(pthread_mutex_t *mutex);
参数:
mutex:互斥锁地址
返回值:
成功:0
失败:非0,错误码
2.2.5 解锁
对指定的互斥锁解锁
#include <pthread.h>
int pthread_mutex_unlock(pthread_mutex_t *mutex);
参数:
mutex:互斥锁地址
返回值:
成功:0
失败:非0,错误码
2.3 代码示例
2.3.1 没有互斥锁多任务的运行情况
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
void *deal_fun01(void *arg)
{
char *str = (char *)arg;
int i = 0;
while(str [i] != '\0')
{
printf("%c", str[i++]);
fflush(stdout); //强制刷新
sleep(1);
}
return NULL;
}
void *deal_fun02(void *arg)
{
char *str = (char *)arg;
int i = 0;
while(str [i] != '\0')
{
printf("%c", str[i++]);
fflush(stdout); //强制刷新
sleep(1);
}
return NULL;
}
int main(int argc, char const *argv[])
{
//创建两个线程
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, deal_fun01, "hello");
pthread_create(&tid2, NULL, deal_fun02, "world");
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
return 0;
}
2.3.2 有互斥锁多任务的运行情况
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
//定义一把锁
pthread_mutex_t mutex;
void *deal_fun01(void *arg)
{
char *str = (char *)arg;
int i = 0;
//上锁
pthread_mutex_lock(&mutex);
while(str [i] != '\0')
{
printf("%c", str[i++]);
fflush(stdout); //强制刷新
sleep(1);
}
//解锁
pthread_mutex_unlock(&mutex);
return NULL;
}
void *deal_fun02(void *arg)
{
char *str = (char *)arg;
int i = 0;
//上锁
pthread_mutex_lock(&mutex);
while(str [i] != '\0')
{
printf("%c", str[i++]);
fflush(stdout); //强制刷新
sleep(1);
}
//解锁
pthread_mutex_unlock(&mutex);
return NULL;
}
int main(int argc, char const *argv[])
{
//初始化一把锁
pthread_mutex_init(&mutex, NULL);
//创建两个线程
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, deal_fun01, "hello");
pthread_create(&tid2, NULL, deal_fun02, "world");
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
//销毁锁
pthread_mutex_destroy(&mutex);
return 0;
}
2.3.3 代码示例总结
如果是互斥,不管有多少个任务,只需要一把锁,所有任务的流程:上锁->访问资源->解锁
3 死锁
互斥条件,某资源只能被一个进程使用,其他进程请求该资源时,只能等待,直到资源使用完毕后释放资源。请求和保持条件程序已经保持了至少一个资源,但是又提出了新要求,而这个资源被其他进程占用,自己占用资源却保持不放。不可抢占条件进程已获得的资源没有使用完,不能被抢占。循环等待条件必然存在一个循环链。
出现死锁的情况
情况1:上完锁,未解锁
- 解决方式:上锁和解锁一一对应
情况2:多把锁的上锁顺序问题,导致死锁
- 解决方式:规定好上锁和解锁的顺序
情况3:任务中的阻塞,导致无法解锁,形成死锁
- 解决方式:修改任务为非阻塞
4 读写锁
读写锁的特点:
- 如果有其他线程读数据,则允许其他线程读操作,但不允许写操作
- 如果有其他线程写数据,则其它线程都不允许读、写操作
读写锁分为读锁和写锁,规则如下
- 如果某线程申请了读锁,其他线程可以再申请读锁,但不能申请写锁
- 如果某线程申请了写锁,其他线程不能申请读锁,也不能申请写锁
POSIX定义的读写锁的数据类型是:pthread_rwlock_t
4.1 初始化读写锁
用来初始化rwlock所指向的读写锁
#include <pthread.h>
int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);
参数:
rwlock:指向要初始化的读写锁指针
attr:读写锁的属性指针。如果attr为NULL则会使用默认的属性初始化读写锁,否则使用指定的attr初始化读写锁
可以使用宏PTHREAD_RWLOCK_INITIALIZER静态初始化读写锁,比如:
pthread_rwlock_t my_rwlock = PTHREAD_RWLOCK_INITIALIZER;
这种方法等价于使用NULL指定的attr参数调用pthread_rwlock_init()来完成动态初始化,不同之处在于PTHREAD_RWLOCK_INITIALIZER宏不进行错误检查
返回值:
成功:0,读写锁的状态将成为已初始化和已解锁
失败:非0,错误码
4.2 销毁读写锁
用于销毁一个读写锁,并释放所有相关联的资源(所谓的所有指的是由
pthread_rwlock_init()
自动申请的资源)
#include <pthread.h>
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
参数:
rwlock:读写锁指针
返回值:
成功:0
失败:非0,错误码
4.3 申请读锁
以阻塞方式在读写锁上获取读锁(读锁定)。
如果没有写者持有该锁,并且没有写者阻塞在该锁上,则调用线程会获取读锁。
如果调用线程未获取读锁,则它将阻塞直到它获取了该锁。一个线程可以在一个读写锁上多次执行读锁定。
线程可以成功调用pthread_rwlock_rdlock()
函数n次,但是之后该线程必须调用pthread_rwlock_unlock()
函数n次才能解除锁定。
#include <pthread.h>
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
参数:
rwlock:读写锁指针
返回值:
成功:0
失败:非0,错误码
4.4 申请写锁
在读写锁上获取写锁(写锁定)。
如果没有写者持有该锁,并且没有写者读者持有该锁,则调用线程会获取写锁。
如果调用线程未获取写锁,则它将阻塞直到它获取了该锁。
#include <pthread.h>
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
参数:
rwlock:读写锁指针
返回值:
成功:0
失败:非0,错误码
4.5 尝试申请写锁
用于尝试以非阻塞的方式来在读写锁上获取写锁
如果没有任何的读者或写者持有该锁,则立即失败返回。
#include <pthread.h>
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
参数:
rwlock:读写锁指针
返回值:
成功:0
失败:非0,错误码
4.6 释放读写锁
无论是读锁或写锁,都可以通过此函数解锁
#include <pthread.h>
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
参数:
rwlock:读写锁指针
返回值:
成功:0
失败:非0,错误码
4.7 代码示例:两个任务读,一个任务写
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
//定义一把读写锁
pthread_rwlock_t rwlock;
void *read_data01(void *arg)
{
int *p = (int *)arg;
while(1)
{
//申请上读锁
pthread_rwlock_rdlock(&rwlock);
printf("任务A:num=%d\n",*p);
//解锁写锁
pthread_rwlock_unlock(&rwlock);
sleep(1);
}
return NULL;
}
void *read_data02(void *arg)
{
int *p = (int *)arg;
while(1)
{
//申请上读锁
pthread_rwlock_rdlock(&rwlock);
printf("任务B:num=%d\n",*p);
//解锁写锁
pthread_rwlock_unlock(&rwlock);
sleep(1);
}
return NULL;
}
void *write_data(void *arg)
{
int *p = (int *)arg;
while(1)
{
//申请写锁
pthread_rwlock_wrlock(&rwlock);
(*p)++;
//解锁写锁
pthread_rwlock_unlock(&rwlock);
printf("任务C:写入num=%d\n",*p);
sleep(2);
}
return NULL;
}
int main(int argc, char const *argv[])
{
//定义公共资源
int num = 0;
//初始化读写锁
pthread_rwlock_init(&rwlock, NULL);
//创建两个线程
pthread_t tid1, tid2, tid3;
pthread_create(&tid1, NULL, read_data01, (void *)# //读
pthread_create(&tid2, NULL, read_data02, (void *)&num); //读
pthread_create(&tid3, NULL, write_data, (void *)&num); //写
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
pthread_join(tid3, NULL);
//销毁锁
pthread_rwlock_destroy(&rwlock);
return 0;
}
十三. 条件变量
与互斥锁不同,条件变量是用来等待而不是用来上锁的,条件变量本身不是锁!条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。通常条件变量和互斥锁同时使用。
条件变量的两个动作:条件不满,阻塞线程;当条件满足,通知阻塞的线程开始工作。条件变量的类型:
pthread_cond_t
1 条件变量API
1.1 条件变量初始化
初始化一个条件变量
#include <pthread.h>
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
参数:
cond:指向要初始化的条件变量指针
attr:条件变量属性,通常为默认值,传NULL即可
也可以使用静态初始化的方法,初始化条件变量:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
返回值:
成功:0
失败:非0,错误吗
1.2 释放条件变量
销毁一个条件变量
#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);
参数:
cond:指向要初始化的条件变量指针
返回值:
成功:0
失败:非0,错误号
1.3 等待条件
阻塞等待一个条件变量
- a) 阻塞等待条件变量cond(参1)满足
- b) 释放已掌握的互斥锁(解锁互斥量)相当于pthread_mutex_unlock(&mutex);
- c) 当被唤醒,pthread_cond_wait函数返回时,解除阻塞并重新申请获取互斥锁pthread_mutex_lock(&mutex);
注:a)、b)两步为一个原子操作。
先阻塞变量cond,再解锁mutex,等待cond满足后对mutex再上锁
#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
参数:
cond:指向要初始化的条件变量指针
mutex:互斥锁
返回值:
成功:0
失败:非0,错误码
1.4 限时等待一个条件变量
限时等待一个条件变量
#include <pthread.h>
int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct *abstime);
参数:
cond:指向要初始化的条件变量指针
mutex:互斥锁
abstime:绝对时间
返回值:
成功:0
失败:非0,错误吗
1.5 唤醒等待在条件变量上的线程
1.5.1 唤醒至少一个阻塞在条件变量上的线程
唤醒至少一个阻塞在条件变量上的线程
#include <pthread.h>
int pthread_cond_signal(pthread_cond_t *cond);
参数:
cond:指向要初始化的条件变量值
返回值:
成功:0
失败:非0,错误号
1.5.2 唤醒全部阻塞在条件变量上的线程
唤醒全部阻塞在条件变量上的线程
#include <pthread.h>
int pthread_cond_broadcast(pthread_cond_t *cond);
参数:
cond:指向要初始化的条件变量指针
返回值:
成功:0
失败:非0,错误号
十四. 生产者和消费者
生产者消费者问题(英语:Producer-consumer problem),也称有限缓冲问题(英语:Bounded-buffer problem),是一个多线程同步问题的经典案例。该问题描述了共享固定大小缓冲区的两个线程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。
例: 仓库默认产品为3个,同一时刻只能生产者或消费者中的一个进入仓库(互斥)
如果仓库的产品数量为0,消费者不允许进入仓库购买(条件变量)
1 代码示例
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <time.h>
#include <stdlib.h>
//定义互斥锁
pthread_mutex_t mutex;
//定义条件变量
pthread_cond_t cond;
//定义一个仓库,默认有3个产品
int num = 3;
void *consumption_function(void *arg) //消费
{
while(1)
{
//申请上锁
pthread_mutex_lock(&mutex);
//判断仓库是否为空,如果为空,等待条件变量满足
if(0 == num) //仓库为空
{
printf("%s发现仓库为空,等待生产\n", (char *)arg);
pthread_cond_wait(&cond, &mutex); //如果为空,阻塞
}
//进入仓库购买产品
int is_shopping = 0;
if(num > 0)
{
is_shopping = 1;
--num;
printf("%s购买了一个产品,仓库剩余%d个\n", (char *)arg, num);
}
//解锁
pthread_mutex_unlock(&mutex);
//使用产品
if(is_shopping == 1)
{
printf("%s正在使用产品\n",(char *)arg);
sleep(rand()%5);
}
}
return NULL;
}
void *production_function(void *arg) //生产
{
while(1)
{
//生产一个产品
sleep(rand()%5);
//上锁,进入仓库
pthread_mutex_lock(&mutex);
//将产品放入仓库
num++;
printf("%s放入一个产品,仓库剩余%d个\n",(char *)arg, num);
//通知条件变量阻塞的线程
pthread_cond_broadcast(&cond);
//解锁
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main(int argc, char const *argv[])
{
//设置随机数种子
srand(time(NULL));
//初始化锁
pthread_mutex_init(&mutex, NULL);
//初始化条件变量
pthread_cond_init(&cond, NULL);
pthread_t tid1, tid2, tid3;
pthread_create(&tid1, NULL, consumption_function, "消费者A");
pthread_create(&tid2, NULL, consumption_function, "消费者B");
pthread_create(&tid3, NULL, production_function, "生产者A");
//等待线程结束
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
pthread_join(tid3, NULL);
//销毁锁
pthread_mutex_destroy(&mutex);
//销毁条件变量
pthread_cond_destroy(&cond);
return 0;
}
十五. 信号量
1 信号量
1.1 信号量的概述
信号量广泛用于进程或线程间的同步和互斥,信号量本质上是一个非负的整数计数器,它被用来控制对公共资源的访问。编程时可根据操作信号量值的结果判断是否对公共资源具有访问的权限,当信号量值大于0时,则可以访问,否则将阻塞。PV原语是对信号量的操作,一次Р操作使信号量减1,一次V操作使信号量加1。信号量主要用于进程或线程间的同步和互斥这两种典型情况。
信号量数据类型为:sem_t
。
信号量用于互斥:
不管多少个任务互斥,只需要一个信号量。先P操作,再V操作
信号量用于同步:
有多少个任务,就需要多少个信号量。最先执行的任务对应的信号量为1,其他信号量全部为0。
执行任务时,先P执行任务,执行完再V下一个执行任务
1.2 信号量的API
1.2.1 初始化信号量
创建一个信号量并初始化它的值。一个无名信号量在被使用前必须先初始化
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value)
参数:
sem:信号量的地址
pshared:等于0,信号量在线程间共享(常用);
不等于0,信号量在进程共享。
value:信号量的初始值
返回值:
成功:0
失败:-1
1.2.2 信号量减一(P操作)
将信号量减一,如果信号量的值为0则阻塞,大于0可以减一
#include <semaphore.h>
int sem_wait(sem_t *sem);
参数:
sem:信号量的地址
返回值:
成功:0
失败:-1
1.2.3 尝试对信号量减一
尝试将信号量减一,如果信号量的值为0,不阻塞,立即返回,大于0可以减一
#include <semaphore.h>
int sem_trywait(sem_t *sem);
参数:
信号量的地址
返回值:
成功:0
失败:-1
1.2.4 信号量加一(V操作)
将信号量加一
#include <semaphore.h>
int sem_post(sem_t *sem);
参数:
信号量的地址
返回值:
成功:0
失败:-1
1.2.5 销毁信号量
销毁信号量
#include <semaphore.h>
int sem_destroy(sem_t *sem);
参数:
信号量的地址
返回值:
成功:0
失败:-1
1.3 代码示例
1.3.1 信号量用于线程的互斥
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
//定义一个信号量(用于互斥)
sem_t sem;
void my_printf(char *str)
{
int i = 0;
while(str[i] != '\0')
{
printf("%c", str[i++]);
fflush(stdout);
sleep(1);
}
return NULL;
}
void *task_fun01(void *arg)
{
// P 操作
sem_wait(&sem);
my_printf((char *)arg);
// V 操作
sem_post(&sem);
return NULL;
}
void *task_fun02(void *arg)
{
// P 操作
sem_wait(&sem);
my_printf((char *)arg);
// V 操作
sem_post(&sem);
return NULL;
}
void *task_fun03(void *arg)
{
// P 操作
sem_wait(&sem);
my_printf((char *)arg);
// V 操作
sem_post(&sem);
return NULL;
}
int main(int argc, char const *argv[])
{
//信号量初始化为1,第二参数0表示用于线程
sem_init(&sem, 0, 1);
pthread_t tid1, tid2, tid3;
pthread_create(&tid1, NULL, task_fun01, "hello");
pthread_create(&tid2, NULL, task_fun02, "world");
pthread_create(&tid3, NULL, task_fun03, "beijing");
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
pthread_join(tid3, NULL);
//销毁信号量
sem_destroy(&sem);
return 0;
}
1.3.2 信号量用于线程的同步
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
//定义三个信号量(用于同步)
sem_t sem1;
sem_t sem2;
sem_t sem3;
void my_printf(char *str)
{
int i = 0;
while(str[i] != '\0')
{
printf("%c", str[i++]);
fflush(stdout);
sleep(1);
}
return NULL;
}
void *task_fun01(void *arg)
{
// P 操作
sem_wait(&sem1);
my_printf((char *)arg);
// V 操作
sem_post(&sem2);
return NULL;
}
void *task_fun02(void *arg)
{
// P 操作
sem_wait(&sem2);
my_printf((char *)arg);
// V 操作
sem_post(&sem3);
return NULL;
}
void *task_fun03(void *arg)
{
// P 操作
sem_wait(&sem3);
my_printf((char *)arg);
// V 操作
sem_post(&sem1);
return NULL;
}
int main(int argc, char const *argv[])
{
//信号量初始化,第二参数0表示用于线程
sem_init(&sem1, 0, 1);
sem_init(&sem2, 0, 0);
sem_init(&sem3, 0, 0);
pthread_t tid1, tid2, tid3;
pthread_create(&tid1, NULL, task_fun01, "hello");
pthread_create(&tid2, NULL, task_fun02, "world");
pthread_create(&tid3, NULL, task_fun03, "beijing");
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
pthread_join(tid3, NULL);
//销毁信号量
sem_destroy(&sem1);
sem_destroy(&sem2);
sem_destroy(&sem3);
return 0;
}
2 无名信号量用于有血缘关系的进程间互斥与同步
使用mmap完成无名信号量的定义
2.1 代码示例
2.1.1 无名信号量用于有血缘关系的进程间互斥
#include <stdio.h>
#include <semaphore.h>
#include <unistd.h>
#include <sys/mman.h>
void my_printf(char *str)
{
int i = 0;
while(str[i] != '\0')
{
printf("%c", str[i++]);
fflush(stdout);
sleep(1);
}
return NULL;
}
int main(int argc, char const *argv[])
{
//定义一个无名信号量
//MAP_ANONYMOUS匿名映射 -1不需要文件描述符
sem_t *sem = mmap(NULL, sizeof(sem_t), PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
//无名信号量的初始化,参数2中的1表示进程,参数3中的1表示初始化值为1
sem_init(sem, 1, 1);
pid_t pid = fork();
if(pid == 0)//子进程
{
// P 操作
sem_wait(sem);
my_printf("hello");
// V 操作
sem_post(sem);
}
else if(pid > 0)//父进程
{
// P 操作
sem_wait(sem);
my_printf("world");
// V 操作
sem_post(sem);
}
//销毁信号量
sem_destroy(sem);
return 0;
}
2.1.2 无名信号量用于有血缘关系的进程间同步
#include <stdio.h>
#include <semaphore.h>
#include <unistd.h>
#include <sys/mman.h>
void my_printf(char *str)
{
int i = 0;
while(str[i] != '\0')
{
printf("%c", str[i++]);
fflush(stdout);
sleep(1);
}
return NULL;
}
int main(int argc, char const *argv[])
{
//定义一个无名信号量
//MAP_ANONYMOUS匿名映射 -1不需要文件描述符
sem_t *sem1 = mmap(NULL, sizeof(sem_t), PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
sem_t *sem2 = mmap(NULL, sizeof(sem_t), PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
//无名信号量的初始化,参数2中的1表示进程,参数3中的1表示初始化值为1
sem_init(sem1, 1, 1);
sem_init(sem2, 1, 0);
pid_t pid = fork();
if(pid == 0)//子进程
{
// P 操作
sem_wait(sem1);
my_printf("hello");
// V 操作
sem_post(sem2);
}
else if(pid > 0)//父进程
{
// P 操作
sem_wait(sem2);
my_printf("world");
// V 操作
sem_post(sem1);
}
//销毁信号量
sem_destroy(sem1);
sem_destroy(sem2);
return 0;
}
3 有名信号量用于无血缘关系的进程间互斥与同步
3.1 有名信号量的API
3.1.1 创建一个有名信号量
创建一个有名信号量
#include <fcntl.h>
#include <sys/stat.h>
#include <semaphore.h>
情况一:信号量存在
sem_t *sem_open(const char *name, int oflag);
情况二:信号量不存在
sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value);
参数:
name:信号量的名字
oflag:sem_open函数的权限标志
mode:文件权限(可读、可写、可执行0777)的设置
value:信号量的初始值
返回值:
成功:信号量的地址
失败:SEM_FALIED
3.1.2 信号量的关闭
关闭信号量
#include <fcntl.h>
#include <sys/stat.h>
#include <semaphore.h>
int sem_close(sem_t *sem);
参数:
信号量的地址
返回值:
成功:0
失败:-1
3.1.3 信号量文件的删除
删除信号量的文件
#include <fcntl.h>
#include <sys/stat.h>
#include <semaphore.h>
int sem_unlink(const char *name);
参数:
信号量的文件名
返回值:
成功:0
失败:-1
3.2 代码示例
3.2.1 有名信号量用于无血缘关系的进程间互斥
进程A
#include <stdio.h>
#include <semapore.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
void my_printf(char *str)
{
int i = 0;
while(str[i] != '\0')
{
printf("%c", str[i++]);
fflush(stdout);
sleep(1);
}
return NULL;
}
int main(int argc, char const *argv[])
{
//创建一个有名信号量 sem_open
sem_t *sem = sem_open("sem", O_RDWR|O_CREAT, 0666, 1);
// P 操作
sem_wait(sem);
//任务
my_printf("hello world");
// V 操作
sem_post(sem);
//关闭信号量
sem_close(sem);
//销毁信号量
sem_destroy(sem);
return 0;
}
进程B
#include <stdio.h>
#include <semapore.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
void my_printf(char *str)
{
int i = 0;
while(str[i] != '\0')
{
printf("%c", str[i++]);
fflush(stdout);
sleep(1);
}
return NULL;
}
int main(int argc, char const *argv[])
{
//创建一个有名信号量 sem_open
sem_t *sem = sem_open("sem", O_RDWR|O_CREAT, 0666, 1);
// P 操作
sem_wait(sem);
//任务
my_printf("beijing 2022");
// V 操作
sem_post(sem);
//关闭信号量
sem_close(sem);
//销毁信号量
sem_destroy(sem);
return 0;
}
3.2.2 有名信号量用于无血缘关系的进程间同步
进程A
#include <stdio.h>
#include <semapore.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
void my_printf(char *str)
{
int i = 0;
while(str[i] != '\0')
{
printf("%c", str[i++]);
fflush(stdout);
sleep(1);
}
return NULL;
}
int main(int argc, char const *argv[])
{
//创建两个有名信号量
sem_t *sem1 = sem_open("sem1", O_RDWR|O_CREAT, 0666, 1);
sem_t *sem2 = sem_open("sem2", O_RDWR|O_CREAT, 0666, 0);
// P 操作
sem_wait(sem1);
//任务
my_printf("hello world");
// V 操作
sem_post(sem2);
//关闭信号量
sem_close(sem1);
sem_close(sem2);
//销毁信号量
sem_destroy(sem1);
sem_destroy(sem2);
return 0;
}
进程B
#include <stdio.h>
#include <semapore.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
void my_printf(char *str)
{
int i = 0;
while(str[i] != '\0')
{
printf("%c", str[i++]);
fflush(stdout);
sleep(1);
}
return NULL;
}
int main(int argc, char const *argv[])
{
//创建两个有名信号量 sem_open
sem_t *sem1 = sem_open("sem1", O_RDWR|O_CREAT, 0666, 1);
sem_t *sem2 = sem_open("sem2", O_RDWR|O_CREAT, 0666, 0);
// P 操作
sem_wait(sem2);
//任务
my_printf("beijing 2022");
// V 操作
sem_post(sem1);
//关闭信号量
sem_close(sem1);
sem_close(sem2);
//销毁信号量
sem_destroy(sem1);
sem_destroy(sem2);
return 0;
}