系统编程
shell
一.基本概念
shell脚本概念:解释性语言,shell命令的有序集合
1.模式图
cat:shell操作使用屏幕(shell)
stdout:标准输出调用输出到屏幕(库)
系统调用:直接
2介绍
shell:是应用程序也是脚本语言
作为应用程序:shell解析器(解析shell命令)
脚本语言:批量处理shell命令
Linux下常用解析器:bash
二.创建shell文件
1.脚本创建
xx.sh(包含多行操作系统命令或者shell命令的文本文件)
赋予shell文件可读可写可执行权限–》chmod + 0xxx(二进制控制读写权限 本用户 组用户 所有用户 rwx(每一个一个bit位) (x代表一个八进制位))
2shell脚本拓展 (1)脚本中变量
定义变量:变量名=变量值 取变量的值:$变量名(终端可用)
shell 建立变量不支持数据类型,所赋值解释为一串字符
注意:不能乱加空格 打印echo
(2)脚本变量定义的注意事项
1.在shell脚本中 通常使用全大写的变量 ----> 方便识别
2.unset +变量名 删除变量的赋值
3.export 命令可以让我们的环境变量或者函数在不同进程之间共享
eg:export VARIABLE_NAME(变量名)=value(值)
【export 声明的变量(仅会话有效,unset删除)会被导入环境变量中 -----> 注意一般的shell中 export需要使用source 执行该脚本才生效】
4.env (直接用)可以查看到该环境变量
5.注意echo 中 “” 双引号会解析变量的值 ‘ ’ 单引号不会解析变量的值
3、系统脚本文件分类
1.系统进行调用
这类脚本无需用户调用,系统在运行时,会在合适的时候进行调用: /etc/profile ~/.bashrc
/etc/profile ----> 这个脚本文件 是系统为每个用户设置的环境信息,当用户第一次登陆时,该文件被执行。系统中公共环境变量在这里设置!!!
开机自启动的程序—> 一般都是在这里设置的
~/.bashrc ----> 在用户目录中,登陆时会被调用,打开任意的终端也会自动调用 ----> 一般设置用户相关的环境变量 交叉编译工具链的路径等
2.用户调用用的脚本: xx.sh (用户自定义) 三、shell脚本标准化编写 1、shell脚本格式
shell 脚本 ----> 由0或多条shell命令构成的
shell的语句包含三大类: 说明性语句 功能性语句 结构性语句
说明性语句: 以#开始 到本行结束 -----> 不被解释执行 -----> 可以理解为 注释
功能性语句: 任意的shell命令、用户程序、其他的shell程序 -----> 能够被解释执行
结构性语句: 条件测试语句 多路分支语句 循环语句 循环控制语句 等
注意shell 脚本 第一行 表示用什么类型的shell解释器(bash sh csh ksh zsh …)
#!/bin/bash
2.shell 简单demo1
3子shell4、 补充
shell 脚本如果有错误 解释器会顺序往下最新 只有错误的哪一行不会解析!!!
注:
sed -i 's/\r / / ′ t e s t 1 1 . s h 把 M //' test1_1.sh 把^M //′test11.sh把M去掉就好
报错: -bash: ./test1_1.sh: /bin/bash^M: 坏的解释器: 没有那个文件或目录
方法:
sudo apt-get install dos2unix
dos2unix 1.sh
./1.sh
四、shell语句 1、常用的功能性语句 --- 命令 (1)位置变量 -----> 参数
$0 与键入的命令行一样,包含脚本文件名
$1,$2,……$9 分别包含第一个到第九个命令行参数 ------ 如果是 第11个参数 使用 ${11}
$# 包含命令行参数的个数
$@ 包含所有命令行参数:“$1,$2,……$9”
$? 包含前一个命令的退出状态
$* 包含所有命令行参数:“$1,$2,……$9”
包含正在执行进程的 I D 号 " 包含正在执行进程的ID号 " 包含正在执行进程的ID号""变量最常见的用途是用作临时文件的名字以保证临时文件不会重复
(2)命令的有效周期 (3)read
从标准输入中读取 一行 并且 赋值给后边的变量
read var
read var1 var2 var3 ----> 可以这样第一个单词给 var1 第二个给 var2 后面的都给 var3
(4)expr --- 运算
要使用 空格 隔开 运算符
算术运算命令 expr 主要用来进行简单的整数运算 ,算术运算 + - * / % 等
expr 12 + 55 * 3 -----> 这个是错的 * 作为 元字符 ---- 表示通配符
expr 12 + 55 * 3
(5)declare
shell 中所有的变量 默认都是 字符串类型 如果想要特殊的类型进行运算 要使用 declare 命令
选项:
-: 给变量设定类型属性。
+: 取消变量的类型属性。
-а: 将变量声明为数组型。
-i: 将变量声明为整数型(integer)。
-r: 将变量声明为只读变量。
注意,一旦设置为只读变量,既不能修改变量的值,也不能删除变量,
甚至不能通过+r取消只读属性。
-x: 将变量声明为环境变量。
-p: 显示指定变量的被声明的类型。
(6)test -----> 关系运算 用作比较
test 常用于 算术运算的比较 ---->用在结构语句中
test 的格式 :
整数比较:
test $num -eq 10 ----> 比较 $num的 值 是否和 10 相等
[ n u m − e q 10 ] − − − − − − > [ ] 就等同于 t e s t 命令!!!! t e s t 语句可以比较三种对象:字符串整数文件 d e m o : n a m e = " l o c o " t e s t " num -eq 10 ] ------> [ ] 就等同于 test命令!!!! test 语句可以比较三种对象: 字符串 整数 文件 demo: name="loco" test " num−eq10]−−−−−−>[]就等同于test命令!!!!test语句可以比较三种对象:字符串整数文件demo:name="loco"test"name" = “loco” -----> 判断字符串的值 是否是 loco
num=10
test $num -eq 10 ----->判断num是否是整数10
test -d tmp ----> 判断 tmp 是否是一个目录
test 用法整理
字符串测试
s1 = s2 测试两个字符串的内容是否完全一样
s1 != s2 测试两个字符串的内容是否有差异
-z s1 测试s1 字符串的长度是否为0
-n s1 测试s1 字符串的长度是否不为0
整数测试
a -eq b 测试a 与b 是否相等
a -ne b 测试a 与b 是否不相等
a -gt b 测试a 是否大于b
a -ge b 测试a 是否大于等于b
a -lt b 测试a 是否小于b
a -le b 测试a 是否小于等于b
逻辑运算
-a 与
-o 或
! 非
文件测试
-d name 测试name 是否为一个目录
-e name 测试一个文件是否存在
-f name 测试name 是否为普通文件
-L name 测试name 是否为符号链接
-r name 测试name 文件是否存在且为可读
-w name 测试name 文件是否存在且为可写
-x name 测试name 文件是否存在且为可执行
-s name 测试name 文件是否存在且其长度不为0
f1 -nt f2 测试文件f1 是否比文件f2 更新
f1 -ot f2 测试文件f1 是否比文件f2 更旧
2、结构性语句
注意:continue 2表示跳过当前循环和外层的一个循环,然后继续执行下一个循环。
break 2跳出当前循环和外层的循环
(1)条件测试语句 (单分支 if )
语法结构:
if 表达式
then
命令表(命令表 可以是 一条命令也可以是多条命令)
fi
如果表达式为真, 则执行命令表中的命令 否则退出if语句 ,执行fi 后面的语句
if 表达式
then
命令表1(命令表 可以是 一条命令也可以是多条命令)
else
命令表2
fi
if 表达式1
then
命令表1(命令表 可以是 一条命令也可以是多条命令)
elif 表达式2
then
命令表2
elif 表达式3
then
命令表3
…
else
命令表n
fi
(2)多分支
语法结构:
case 字符串变量 in
模式1) -----------》 case 语句只能测试字符串变量
命令表1
;;
模式2|模式3) -----------》 一次可以匹配多个模式 用| 隔开
命令表2
;; -----------> 命令表结束 以 双分号 表示结束 退出case语句
…
模式n) ------------> 模式n 常用通配符 * 表示其他所有模式
命令表n
;;
esac ----> case 语句模块结束
3)for循环语句
for…do…done
当循环次数已知或确定是,使用 for循环语句 来执行 一条或多条命令。
循环语句的括号 有 do done 来限定
for 变量名 in 单词表 ----> 变量依次取单词表各个单词,每取一次单词就一次循环语句中的命令,所以循环的次数由单词表的单词数决定
do
命令表
done
如果单词表是命令行参数,可以在 for 语句中省略 “ in 单词表 ”
(4)seq序列
seq squeue序列 的缩写
#seq 主要是输出一个序列
seq: squeue 是一个序列的缩写,主要用来输出序列化的东西
seq常见命令参数
用法:seq [选项]… 尾数
或:seq [选项]… 首数 尾数
或:seq [选项]… 首数 增量 尾数
以指定增量从首数开始打印数字到尾数。
-f, --format=格式 使用printf 样式的浮点格式
-s, --separator=字符串 使用指定字符串分隔数字(默认使用:\n)
-w, --equal-width 在列前添加0 使得宽度相同【自动补位】
–help 显示此帮助信息并退出
–version 显示版本信息并退出
seq -s ‘#’ 5 -----> 默认从 1 开始 到 5 结束 每个数之间 用 # 隔开
1#2#3#4#5
seq -s ’ ’ 10
1 2 3 4 5 6 7 8 9 10
5)while循环
while 命令或表达式 -----> while 后面的表达式一般为 test
do
命令表
done
while语句首先测试其后的命令或表达式的值,如果为真 就执行一次循环
然后再测试命令和表达式的值 直到 命令或表达式为 假 才退出循环
如果while表达式一直为 真 死循环
(6)until
until 和while 正好相反 ---->判断条件不成立时就自行循环 一旦条件成立 循环结束
until [条件测试]
do
命令表
done
五、shell的函数 1、shell函数定义
在shell编程中,把固定功能进行封装,方便调用 ---->函数调用前 必须先定义,shell中 顺序上函数说明必须放在调用程序前面
调用函数可以传递参数给函数,函数可以使用 return 返回结果 给调用的程序。
函数只在当前shell 中有效/起作用,不能输出到子shell
function_name()
{
命令表
}
function function_name()
{
命令表
return xxx
}
函数调用:
函数的所有标准输出都传递给主程序的变量
value_name= function_name arg1 arg2 arg3 …
获取函数返回状态
function_name arg1 arg2 …
echo $?
#!/bin/bash
#查看文件的行数
show_line()
{
line_num=cat $1 | wc -l
($1函数参数)
echo $line_num
return $line_num
}
show_line $1 ($1 命令行传给函数)
echo $?
函数变量的作用:
全局作用域:在脚本的任何地方都可以访问的变量
局部作用域: 只能在声明变量的作用域内有效
local val=xxx 这种变量才是局部变量 其他的变量都是 全局变量
文件IO 一、系统调用 1.系统调用的介绍
系统调用 是操作系统 提供给用户 访问 内核的 一组 函数接口 ----> 是操作系统提供给用户的特殊接口
用户程序就可以通过 这一组 特殊的接口 取访问系统内核,从而获得操作系统内核提供的服务
比如1: 用户可以通过文件系统相关的调用请求操作系统打开文件、关闭文件、读写文件
比如2:通过时间相关的系统调用获取系统时间设置定时器
2、系统调用和库调用的关系
系统调用需要消耗时间,如果一个程序频繁的调用系统调用,会降低程序的运行效率。
当运行内核代码,CPU 工作在内核态的,在系统调用发生前,需要保存好用户态的栈空间和内存环境,然后转如内核态工作
系统调用结束后,又要切换回用户态,这种环境的切换 会消耗许多时间。
所以注意:在保障访问内核资源的前提下,频繁调用系统调用不是一件好事!!!!
所以我们要减少系统调用的次数,(比如往硬盘里面写入文件的时候,进一次写一个 和进一次 写完)(缓冲区的概念!!)
二、文件IO 1、文件描述符(唯一标识打开文件的一个非负整数 )
操作这个文件描述符 就等于 操作这个文件!!!!
文件IO ----> 系统调用 / 没有缓冲
程序运行起来之后(每个进程)都有一张文件描述符表!!! 标准输入 标准输出 标准错误输出 被默认打开的 对应的文件描述符 0 1 2 系统为了维护打开的文件,会维护三个表,分别是:
进程级的文件描述符表
系统级的文件描述符表
文件系统的i-node表
1、一个进程能够同时打开多个文件,对应需要多个文件描述符,所以需要用一个文件描述符表对文件描述符进行管理;通常默认大小为1024,也即能容纳1024个文件描述符;
2、文件描述符表中0、1、2三个位置对应的文件描述符固定不变,标准输入、标准输出、标准错误;
3、当打开一个文件时,内核会自动在文件描述符表中寻找一个空闲且最小的文件描述符;
4、同一个文件可以被多次打开,但是每打开一次都需要一个新的文件描述符;
5、已经被占用的文件描述符在被释放后,可以后重新被占用;注意:
文件描述符、文件、进程间的关系:
(1)每个文件描述符会与一个打开的文件相对应;
(2)不同的文件描述符也可能指向同一个文件;
(3)相同的文件可以被不同的进程打开,也可以在同一个进程被多次打开;
2、文件系统相关查看
1查看当前系统允许打开的最大文件个数
cat /proc/sys/fs/file-max
2修改默认设置打开的最大的文件个数
ulimit -a
ulimit -n 4096 ----> 修改默认设置最大的打开文件个数为4096
//以上修改不一定生效
三、文件的操作 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); -----> 用于不确定是否存在 需要创建的时候
int creat(const char *pathname, mode_t mode);
int open(const char *pathname, int flags);
参数:
pathname: 打开文件的路径名
flags: 打开的模式或者标志 可以设置为以下选项
O_RDONLY:只读模式
O_WRONLY:只写模式
O_RDWR:可读可写模式
以上三个选项不能重复,不能同时使用出现,但是必须有一种!!! 以下的参数可选的
O_APPEND:追加 ----> 每次写文件时都会将当前文件的偏移量设置到文件的末尾!!!
O_CREAT: 创建 ----> 如果文件不存在就创建
O_EXCL: 如果新打开的文件已经存在 就报错 —> 一般和 O_CREAT 联用
O_TRUNC:清空 ----> 打开文件的同时将文件里面的内容清空
O_SYNC:使每次的write 都等到物理I/O操作完成
O_NOCTTY:如果打开的文件是一个终端的话,则不将此设备设置为进程的控制终端
O_NONBLOCK:如果打开的文件是一个管道,一个块设备,或一个字符设备文件,则后续的I/O操作都是设置为非阻塞的
int open(const char *pathname, int flags, mode_t mode);
多了mode: 用来设置创建文件权限 rwx ----> flags 参数中 有O_CREAT才有效
权限用八进制表示: rwx rwx rwx r—4 w----2 x----1
eg:0777
返回值:
成功: 返回一个文件描述符 fd —> 当前可用的最小描述符
失败: -1 并且修改errno
errno:系统调用错误码 —> 可以通过错误码查找错误原因
打印错误信息:
#include <string.h>
char *strerror(int errnum);
#include <stdio.h>
void perror(const char *msg); -----> 会自动打印当前错误信息
文件类型
001 ----fifo文件 002 —字符设备 004—目录文件 006 ---- 块设备 010 ---- 普通文件
S_IFREG S_IFDIR S_IFLNK S_IFIFO
mode —> 模式补充说明
文件创建的时候最终权限: mode & umask
shell 进程中umask 可以查看当前的掩码(补码) ----> 设置一个目录或者文件的初始权限
取值八进制含义
S_IRWXU 00700 文件所有者的读、写、可执行权限
S_IRUSR 00400 文件所有者的读权限
S_IWUSR 00200 文件所有者的写权限
S_IXUSR 00100 文件所有者的可执行权限
S_IRWXG 00070 文件所有者同组用户的读、写、可执行权限
S_IRGRP 00040 文件所有者同组用户的读权限
S_IWGRP 00020 文件所有者同组用户的写权限
S_IXGRP 00010 文件所有者同组用户的可执行权限
S_IRWXO 00007 其他组用户的读、写、可执行权限
S_IROTH 00004 其他组用户的读权限
S_IWOTH 00002 其他组用户的写权限
S_IXOTH 00001 其他组用户的可执行权限
umask 0002 ---- 0022
chmod 创建一个文件 初始值 ----> 0664 ----> 0666 0002 ---->0664
2、文件打开demo
3、文件关闭 ----> close
文件有打开 就一定有 关闭
#include <unistd.h>
int close(int fd);
参数:
fd 需要关闭的文件描述符
返回值:
成功 0
失败 -1 并设置errno 可以通过perror 查看原因
4、文件读 ----> read
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
功能:
从 文件fd 中 读出指定数量count 个数据 到 缓冲区buf
参数:
fd: 文件描述符 —> 打开的文件
buf: 输出缓冲区 首地址
count: 想要读出数据的长度 ----> count 不应该超过缓冲区长度!
返回值:
成功: 实际读取到的长度
失败: -1 并且设置errno
读到 EOF (end of file) -->读到文件末尾时候 返回 0!!
5、文件写 ---- write
#include <unistd.h>
ssize_t write(int fd, void *buf, size_t count);
功能:
从 缓冲区buf 中 写入指定数量count 个数据 到 文件fd
参数:
fd: 文件描述符 —> 打开的文件
buf: 输出缓冲区 首地址
count: 想要读出数据的长度 ----> count 不应该超过缓冲区长度!
返回值:
成功: 实际写入到的长度
失败: -1 并且设置errno
没有写入任何数据时 返回 0!!
6、文件的定位 ---- lseek
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
功能:
每一个打开的文件 都有一个读写位置,当打开一个文件时,其读写位置默认是指向文件开头的!!
如果以追加O_APPEND的方法打开一个文件,那读写位置默认切换到文件末尾
当read write时 读写位置会随之增加, lseek 专门用于修改读写位置的
参数:
fd: 文件描述符
offset: 偏移量 根据whence来增加或减少偏移量 这个值 可正可负
whence: 当前位置 可以设置以下的值
SEEK_SET 把读写位置从文件头移动 offset个 就开始读写
SEEK_CUR 当前读写位置偏移 offset个位移量
SEEK_END 从文件末尾偏移offset
注意 当whence为 SEEK_CUR 和 SEEK_END 时 offset 可以为负数
欲将读写位置移到文件开头时:lseek(fd, 0, SEEK_SET);
欲将读写位置移到文件尾时:lseek(fd, 0, SEEK_END);
想要取得目前文件位置时:lseek(fd, 0, SEEK_CUR);
返回值:
返回值 返回当前读写位置 ----> 当前读写位置 到文件头 有多少个字节
失败 -1 设置errno
库函数
#include <stdio.h>
int remove(const char *pathname); -----> 可以删除文件和目录
系统调用
删除文件
#include <unistd.h>
int unlink(const char *pathname);
删除文件夹
#include <unistd.h>
int rmdir(const char *pathname);
demo列子
mycat–demo
mycat进阶版-demo 实现Mycp
cp 源文件 目标文件 -----> 源文件 必须存在 目标文件可以不存在,不存在 就新建 存在 清空
open(“源文件”,O_RDONLY);
open(“目标文件”, O_CREAT | O_WRONLY | O_TRUNC,mode);
四、实现ls -l 1、文件状态获取 一属性 二获取单个文件的属性 demo 2、目录的操作方法 #include <sys/types.h>
#include <dirent.h>
DIR *opendir(const char *name);
函数说明: 打开参数name指定的目录,并且返回一个DIR *的目录流 ----> 操作和open fopen 类似
返回值:
成功 返回打开目录流
失败 NULL 设置errno
#include <dirent.h>
struct dirent *readdir(DIR *dirp);
函数说明: 读取 打开的目录流dirp
返回值:
成功:目录信息结构体 —> 下一个目录的进入点
struct dirent
{
ino_t d_ino; //d_ino 此目录进入点的inode
ff_t d_off; //d_off 目录文件开头至此目录进入点的位移
signed short int d_reclen; //d_reclen _name的长度,不包含NULL字符
unsigned char d_type; //d_type d_name 所指的文件类型
char d_name[256]; //d_name 文件名
};
失败: NULL
使用方法:
readdir 读目录 是一个一个读的 ----> 每次调用readdir 返回一个目录下的文件!!!
struct dirent *dir;
while( (dir = readdir(dp)) != NULL )
{
// 不打印隐藏文件
if(dir->d_name[0] != ‘.’)
{
printf(“%s\t”,dir->d_name);
}
}
#include <sys/types.h>
#include <dirent.h>
int closedir(DIR *dirp);
功能描述 : 关闭打开的目录流
返回值:
成功 0
失败 -1 设置errno
3、实现Myls_l
进程 一、并发 和 并行
并行: 多核 指在同一时刻,有多条指令同时在处理器上执行!!!
并发: 单个处理器(单核),按某种调度方式 运行, 宏观上 ---->并行 微观上 ----- 串行 (因为CPU 调度速度很快 感觉是并行)
二、进程的相关操作 1.进程号
每个进程都有自己的PID 进程号, 进程号在程序中: 类型 pid_t
PID : 当前进程的进程号
PPID: 当前进程的父进程ID
PGID: 进程组ID
头文件 sys/types.h unistd.h
pid_t代表进程号类型,唯一但可重用,大多数系统为int,0代表父进程,
pid_t getpid(void)//返回当前进程id
pid_t getppid(void)//返回当前进程父进程id
pid_t setpgid(pid_t pid, pid_t pgid);//设置组进程id
pid_t getgpid(pid_t pid)//获取指定进程的进程组id,当pid为0时,返回当前进程的进程组id
2、创建进程 --- fork (1)父子进程
系统中允许 一个进程创建的新进程就称为子进程
(2)fork 创建进程
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
功能:
用于在一个已经存在的进程中创建一个新的进程,新进程 就称为 子进程,原进程称为 父进程
参数:
无
返回值: ----> 执行fork之后 会完成内存拷贝
子进程中 返回 0
父进程 得到 子进程的ID
失败 -1;
(3) fork的父子进程的关系
当前进程 使用fork 函数 —> 创建子进程 ----> 这个子进程 是父进程的复制品,它从父进程继承了整个地址空间
地址空间: 包括进程上下文,进程的堆栈 打开的文件描述符!!! 信号控制 进程优先级(nice 修改 优先级 优先级: -19-20) 进程组号 !!
子进程独有的只有它自己的进程ID -----> fork 函数的使用代价是很大的 内存开销大!!
(4) fork的创建案例
fork很奇妙 ,感觉调用了1次 返回两个值--------》 fork 执行完成 出现了两个进程 每个进程都有返回 所以是两个值!!
fork 特点 ----> 它的执行 没有先后顺序 (创建,cpu调度) -----> 两个进程 执行没有固定的顺序, 那个进程先执行 由系统进程调度策略决定!!!
孤儿进程 僵尸进程 3、exit ----> 结束当前进程
#include <stdlib.h>
void exit(int status);
#include <unistd.h>
void _exit(int status);
以上两个函数 都表示 结束当前进程!!!!
参数 结束进程时 返回的进程状态 正常结束 0 异常 负数!!!
exit 库调用 -----> 本质 也是调用 _exit -------> 会强制刷新缓冲区之后才 销毁进程资源
_exit 系统调用 ----> 直接结束 直接销毁进程资源 不管缓冲区有无内容
4、wait ----> 回收子进程资源 #include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *wstatus);
功能:
等待子进程 结束,如果子进程 结束可,此函数 会返回回收的子进程ID
这个函数 会阻塞,直到它的一个子进程退出或者收到不能被忽视的信号才会被唤醒。
如果调用的进程 没有子进程或者 子进程已经结束,该函数立刻返回!!
参数:
status: 参数包含子进程退出时的状态!!!
如果传入的参数 不是NULL ,wait就会将退出的状态 存在该变量中!!
这是一个整数值(int) 指出子进程时正常退出 还是异常结束!!
子进程的退出状态 在 int 里面 包含多个字段 用宏定义可以取出每个字段!
取出子进程的退出信息 WIFEXITED(status) 如果子进程正常结束 取出的字段 非0
返回子进程的退出状态 WEXITSTATUS(status) ,退出状态保存在 status 变量的8~16 位。 子进程调用exit 才能用它获取结束值!
返回值:
成功: 已经结束的子进程的ID
失败: -1 错误原因存于errno
pid_t waitpid(pid_t pid, int *wstatus, int options);
功能:
等待子进程 结束,如果子进程 结束可,此函数 会返回回收的子进程ID
参数:
pid:
pid > 0 等待子进程的子进程ID
pid = 0 当前进程组中的子进程 —> 如果子进程加入其他进程组 不会等待!!
pid = -1 任意子进程 ----> 等同wait
pid < -1 可以制定任意进程组 ----> 绝对值
status:
进程退出状态 等同wait 的status
options:
0 —> 等同wait 阻塞父进程 等待子进程退出
WNOHANG : 没有任何子进程退出时 也立即返回 ---- 非阻塞
WUNTRACED: 如果子进程暂停了则此函数马上返回,并且不予以理会子进程的结束状态。(由于涉及到一
些跟踪调试方面的知识,加之极少用到)
返回值:
1) 当正常返回的时候,waitpid() 返回收集到的已经回收子进程的进程号;
2) 如果设置了选项 WNOHANG,而调用中 waitpid() 还有子进程在运行,且没有子进程退出,返回
0;
父进程的所有子进程都已经退出了 返回-1; 返回>0表示等到一个子进程退出;
3) 如果调用中出错,则返回-1,这时 errno 会被设置成相应的值以指示错误所在,
如:当 pid 所对应的子进程不存在,或此进程存在,但不是调用进程的子进程,
waitpid() 就会
出错返回,这时 errno 被设置为ECHILD
(1)wait demo
wait 阻塞等待 及其 返回值
wait获取退出状态
(2)waitpid demo
等同wait的用法 —> 阻塞
(3)waitpid demo
等同wait的用法 —> 非阻塞
5、创建4个子进程
5、创建4个子进程
1.创建4个子进程 父进程最外侧执行
注意 : 因为fork 会 复制空间 —> 父进程 的 变量 和子进程变量不是同一个
// 创建4个子进程
int main(int argc, char const *argv[])
{
int i = 0;
pid_t pid;
// 正确用法
for (i = 0; i < 4; i++)
{
pid = fork();
if (pid < 0)
{
perror(“fork error:”);
return -1;
}
else if (pid == 0)
{
// 子进程 不需要 继续fork
// 所以break 结束循环语句
break;
}
}
while (1)
;
return 0;
}
}
2回收资源 完美创建
int main(int argc, char const *argv[])
{
int i = 0;
pid_t pid;
for (i = 0; i < 4; i++)
{
pid = fork();
if (pid < 0)
{
perror(“fork error:”);
return -1;
}
else if (pid == 0)
{
// 子进程 不需要 继续fork
// 所以break 结束循环语句
break;
}
}
if (i == 0)
{
// 子进程1
printf(“子进程1: %d\n”, getpid());
exit(1);
}
else if (i == 1)
{
// 子进程2
printf(“子进程2: %d\n”, getpid());
sleep(5);
exit(2);
}
else if (i == 2)
{
// 子进程3
printf(“子进程3: %d\n”, getpid());
sleep(2);
exit(3);
}
else if (i == 3)
{
// 子进程4
printf(“子进程4: %d\n”, getpid());
sleep(7);
exit(4);
}
else if (i == 4)
{
// 父进程
printf(“父进程: %d\n”, getpid());
#if 0
// 循环回收资源
pid_t wpid;
while ((wpid = wait(NULL)) > 0)
{
printf(“—已回收子进程:%d\n”, wpid);
}
#else
// 循环回收资源
pid_t wpid;
// 阻塞 等同与wait
// while ((wpid = waitpid(-1,NULL,0)) > 0)
// 非阻塞
while ((wpid = waitpid(-1, NULL, WNOHANG)) != -1)
{
// 只答应 退出进程的PID 没等到进程退出时候的0 不打印
if (wpid > 0)
{
printf(“—已回收子进程:%d\n”, wpid);
}
}
#endif
}
return 0;
}
三 vfork创建进程的另一种方式 1vfork---确保子进程先执行 1.分析
#include <sys/types.h>
#include <unistd.h>
pid_t vfork(void);
功能:
也是创建子进程 和 fork一样!!!
返回值:
子进程中 返回 0
父进程 得到 子进程的ID
失败 -1;
注意事项:
因为vfork 创建子进程 和父进程 使用同一空间!! 所以子进程 如果想要实现 独立的功能 只能通过 exec 函数簇
fork中使用exec是替换子进程空间执行新程序
2、vfork创建子进程特点
vfork创建的子进程与父进程 共享同一块空间。所有保证子进程先运行。
2、fork和vfork的区别
fork创建的子进程,父子进程有各自独立的4G空间,父子进程的执行顺序不确定
vfork创建的子进程,父子进程共享一个4G空间(子进程可以使用整片空间),确保子进程先执行,
1.如果子进程调用 了exit结束,
2.或者调用 了exec函数族(新进程映射到子进程,只能使用一部分)
父进程才能得到执行
解析:因为在使用 vfork 创建的子进程中,子进程与父进程共享同一份地址空间,所以对地址空间的任何修改都会影响到父进程。
exit 和 exec 函数族中的函数都会进行一些操作,导致子进程的地址空间发生变化,分别是释放资源和加载新程序。这些操作会影响到子进程的内存布局,但不会直接影响到父进程的地址空间。
四 exec函数族
exec 函数簇(开辟新内存地址空间执行,exec后面代码不执行) ----> 提供了一种在一个进程中启动另一个程序执行的方法 ----> 偷梁换柱
一底层系统调用
//exeve 是系统调用
#include <unistd.h>
int execve(const char *filename, char *const argv[],char *const envp[]);
参数:
filename: 要执行的程序文件路径
argv: 字符指针数组 数组元素 就是要执行的参数 // 类似于命令行参数 eg: ls -l
envp: 指针数组 环境变量 自定义环境参数
注意: argv envp 指针数组 都有结尾标志 NULL
返回值:
失败 -1 并且设置 errno
二库函数
如果调用c语言程序
exec函数族(可执行文件xx(可带路径),参数1,参数2,NULL)
开辟新内存空间执行程序
//以下都库调用
‘l’ ----> 表示以列表形式传输
‘e’ -----> env 可以添加自定义环境参数
‘v’ ----> 表示以数组方式传递
‘p’ ----> 不需要给路径名, 只需要给文件名 会自己在系统路径中去找
#include <unistd.h>
int execl(const char *path, const char *arg, …);
参数:
path: 执行的程序(文件) 的 路径
arg: 参数列表 结尾 必须加上NULL 默认采用系统环境!
int execle(const char *path, const char *arg, …, char *const envp[]);
参数:
path: 执行的程序(文件) 的 路径
arg: 参数列表 结尾 必须加上NULL
envp: 自定义环境
int execv(const char *path, char *const argv[]);
参数:
path: 执行的程序(文件) 的 路径
argv: 参数数组 默认采用系统环境!
int execlp(const char *file, const char *arg, …);
参数:
file: 文件名
arg: 参数列表 结尾 必须加上NULL 默认采用系统环境!
}
int execvp(const char *file, char *const argv[]);
参数:
file:文件名
argv: 参数数组 默认采用系统环境!
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
int main(int argc, char const *argv[])
{
char *argv1[] = {
“ls”,
“-l”,
NULL};
// 运行环境
char *envp[] = {
“PATH=/home/edu/2401”,
“sadhahsdhasjj0dj”,
NULL};
char *envp1[] = {
NULL};
pid_t pid = vfork(); if (pid < 0) { perror("vfork error:"); return -1; } else if (pid > 0) { while (1) { printf("hello world!\n"); sleep(3); } } else if (pid == 0) {
#if 0
// execve 系统调用
if (execve(“/bin/ls”, argv1, envp) < 0)
{
perror(“execve error”);
exit(0);
}
#endif
// 库调用
#if 0
if (execl(“/bin/ls”, “ls”, “-l”, “-h”, NULL) < 0)
{
perror(“enecl error”);
return -1;
}
#endif
#if 0
if (execle(“/bin/ls”, “ls”, NULL, envp1) < 0)
{
perror(“enecle error”);
return -1;
}
#endif
#if 0
if (execv(“/bin/ls”, argv1) < 0)
{
perror(“enecv error”);
return -1;
}
#endif
#if 1
// 第一个参数 只传递文件名
if (execlp(“ls”, “ls”, “-l”, “-h”, NULL) < 0)
{
perror(“eneclp error”);
return -1;
}
#endif
#if 0
if (execvp(“ls”, argv1) < 0)
{
perror(“enecvp error”);
return -1;
}
#endif
}
return 0;
}
五system函数 1.介绍
#include <stdlib.h>
int system(const char *command); ----> 执行系统命令
功能:
system 这个函数 1.会调用 fork 函数产生子进程,2.在子进程中调用exec
启用 /bin/sh -c command 来执行 参数 command字符串所代表的命令,执行完成之后返回原调用进程
参数:
command: 要执行的命令字符串
返回值:
如果command 为 NULL ,则system() 返回非0 一般是 1
如果在system() 在调用 /bin/sh 失败 127,其他失败 -1;
注意: system 调用shell 命令 成功后返回shell 命令的返回值
它会设置 errno 错误码
创造 mysystem 六、终端 1、shell进程和终端
每个虚拟机终端连接到ubuntu虚拟机系统上,都会打开一个bash进程 ----> 用于解析命令
虚拟终端 ctrl+alt + F(1~6)
pts ---> 伪终端 对于Linux系统而言,
终端主要分为两类:由用户在本地所打开的终端称为虚拟终端tty,由用户在远程所打开的终端称为伪终端pts。
简而言之: shell进程 和终端 就像一个传话筒,人们通过 shell 进程 说话, 传话筒 把声音(命令)给传递出去,并且将回话(执行结果显示给)人们(用户)
2、终端demo
#include <unistd.h>
char *ttyname(int fd);
功能:
由文件描述符查出对应的文件名
参数:
fd:文件描述符
返回值:
成功:终端名
失败:NULL
在unix操作系统中,用户通过终端登录之后 得到一个shell进程,这个终端的shell 进程 的控制终端
进程中 终端的控制信息 保存在 进程的 PCB 中,fork 会 复制 PCB ,所以shell进程启动的其他进程 的控制终端也是这个终端
{ perror("fork error:"); return -1; } else if (pid == 0) { sleep(5); int num = 0; scanf("%d", &num); printf("son: pid = %d num = %d\n", getpid(), num); printf("s使用的标准输出为:%s\n", ttyname(0)); } else if (pid > 0) { int num = 0; scanf("%d", &num); printf("farther: pid = %d num = %d\n", getpid(), num); printf("f使用的标准输出为:%s\n", ttyname(0)); } return 0;
}
3、进程组
是一个或多个进程的集合。
通常,与同一作业相关联,可以接收来自同一终端的各种信号。
4、会话 ---- SID
是一个或多个进程组的集合。
一个会话可以有一个控制终端。建立与控制终端连接的会话首进程被称为控制进程。
如果进程ID进程组ID会话ID,那么该进程为会话首进程(会长)。
获取会话ID
#include <unistd.h>
pid_t getsid(pid_t pid)
功能:
获取进程所属的会话 ID
参数:
pid:进程号,pid 为 0 表示查看当前进程 session ID
返回值:
成功:返回调用进程的会话 ID
失败:-1
组长进程不一定为新会话首进程,新会话首进程必定会成为组长进程。
#include <unistd.h>
pid_t setsid(void);
功能:
创建一个会话,并以自己的 ID 设置进程组 ID,同时也是新会话的 ID。
调用了 setsid 函数的进程,既是
新的会长,也是新的组长。
参数:无
返回值:
成功:返回调用进程的会话 ID
失败:-1
5、守护进程
脱离终端,是特殊的孤儿进程,在后台运行,执行周期性服务的进程
查看守护进程 ---- ps -axj | grep -E ‘d$’
(1)创建守护进程
1、fork 父进程退出 子进程就变孤儿进程
2、子进程新建会话 setsid
3、修改工作目录 到 根目录
4、关闭不必要的文件描述符
5、忽略 SIGCHLD 17号信号 ---- 子进程结束 传递给父进程的 信号 父进程 用wait 就是在等这个型号
创建守护进程模型
创建子进程,父进程退出(必须) 所有工作在子进程中进行 形式上脱离了控制终端 — 孤儿进程
在子进程中创建新会话(必须) setsid()函数 使子进程完 全独立出来,脱离控制
改变当前目录为根目录(不是必须) chdir()函数 防止占 用可卸载的文件系统 也可以换成其它路径
重设文件权限掩码(不是必须) umask()函数 防止继承的文件创建屏蔽字拒绝某些权限 增加守护进程灵活性
关闭文件描述符(不是必须) 继承的打开文件不会用到,浪费系统资源,无法卸载
开始执行守护进程核心工作(必须) 守护进程退出处理程序模型
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<sys/wait.h>
#include<stdlib.h>
#include <sys/stat.h>
int main(int argc, char const *argv[])
{
//1、创建子进程,父进程必须退出,让子进程从形式上脱离终端
pid_t pid=fork();
if(pid>0)//父进程代码段
{
exit(0);
}
//2、必须创建会话,让子进程真正(实际上)脱离终端
setsid();
//3、改变目录(可选项) chdir("/"); //4、设置掩码(可选项),增强进程的灵活性(权限) umask(0002); //5、关闭文件描述(可选项,不需要) close(STDIN_FILENO);//标准输入0 close(STDOUT_FILENO);//标准输出1 close(STDERR_FILENO);//标准错误2 //6、守护进程的核心功能 while(1) { //实现守护进程的功能代码 ; } return 0;
}
七 进程通讯 1、早期的进程间通讯方法:
无名管道 -----> 有血缘关系,半双工, 一对一 ,先进先出 无格式,数据读取一次性,在内存中,O_NONBLOCK非阻塞
有名管道 -----> 无血缘关系,半双工, 一对一 ,先进先出 无格式,数据读取一次性,在文件系统中
信号 -----> 信息简单 不能携带大量信息,满足某个条件才能发出
system V IPC
共享内存 ------> 多对多、无格式、读取数据后还存在、写入时覆盖原有数据、读写速度快、物理内存中。
消息队列 ------> 多对多、按消息类型读取、同类型的消息遵循先进先出、有格式、数据读取一次性,内存中。
信号灯集 ------->也叫信号量。它是不同进程间或一个给定进程内部不同线程间同步的机制
mmap ------> mmap磁盘映射 多对多、无格式、数据读取后还存在、写入时覆盖原有数据、磁盘中。
BSD
套接字 ----> 网络编程 不同主机间的进程通信
2、管道
<sys/stat.h>
1、查看管道
2、管道的特点
3、无名管道(亲缘进程可用) 1.无名管道特点 1.不存在于文件系统中 ----> 只存在于内存中!!
特点1:
- 只能用于亲缘进程间通讯
- 半双工的通讯方法,具有固定的读端和写端 ----> 单工
可以把管道看做一种特殊文件,对于它的读写可以用 read 、 write函数!!
管道时 基于文件描述符的通讯方式。
当创建一个无名管道时 —> 它会创建两个文件描述符 fd[0] fd[1] , -----> fd[0] 读端 fd[1] 写端
无名管道 2个文件描述符来操作, fd[0] 读端—> 从管道中读出 fd[1] 写端 —> 写入到管道中
注:
1、半双工,数据在同一时刻只能在一个方向上流动。
2、数据只能从管道的一端写入,从另一端读出。
3、写入管道中的数据遵循先入先出的规则。
4、管道所传送的数据是无格式的,这要求管道的读出方与写入方必须事先约定好数据的格式,如多少字节算一个消息等。
5、管道不是普通的文件,不属于某个文件系统,其只存在于内存缓冲区中。
6、管道在内存中对应一个缓冲区。不同的系统其大小不一定相同。
7、从管道读数据是一次性操作,数据一旦被读走,它就从管道中被抛弃,释放空间以便写更多的数据。
8、管道没有名字,只能在具有公共祖先的进程之间使用
无名管道是一种特殊类型的文件,在应用层体现为两个打开的文件描述符。
写端:fd[1] , 读端:fd[0]
dup dup2—>描述符号重定向 --》dup手动关闭,选最小–》dup2 自己设定 先关闭再使用
2.:使用函数
#include <unistd.h>
int pipe(int pipefd[2]);
参数:
// 参数传递 2个 int 元素的数组名
pipefd[2] —》 pipefd代表两个元素的数组 把打开的管道 描述符 传递进去!!
fd[0] 读端 fd[1] 写端
返回值:
成功: 0
失败 -1
2创建管道 方案一:
先创建进程,再创建管道 父子进程里的管道相互不认识(不同的内存地址)
方案二: 先创建管道 再创建进程 3 在亲缘进程间通讯分析 #include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(int argc, char const *argv[])
{
// 创建管道
int fd[2];
// fd[0] 读端 fd[1] 写端
if (pipe(fd) < 0)
{
perror(“pipe error:”);
return -1;
}
// 创建亲缘进程
pid_t pid = fork();
if (pid < 0)
{
perror(“fork error:”);
return -1;
}
else if (pid == 0) // 子进程 写
{
// 关闭读端
close(fd[0]);
char buf[128] = {“hello I am son!\n”};
write(fd[1], buf, strlen(buf));
// 程序结束关闭写端 close(fd[1]); exit(0); } else // 父进程读 { // 关闭写端 close(fd[1]); char buf[128] = {0}; read(fd[0], buf, sizeof(buf)); printf("读到的数据是:%s", buf); wait(NULL); // 程序结束关闭读端 close(fd[0]); } return 0;
}
4、有名管道(非亲缘进程也可用)
1.有名管道特点
基于文件系统 -----> 是一个文件!!! 管道文件 -----> 可以用于不是亲缘进程间通信
2、有名管道命令创建 先open
有名管道 是一种特殊类型的文件,在应用层体现为一个管道文件
注意: 有名管道只能创建在纯linux文件系统下
以只读 或只写 方式打开管道 读写操作会阻塞
mkfifo创建管道文件
3.程序实现有名管道 4、有名管道案例 1.写管道 2.读管道 5、两根管道实现 收发
注1:创建多个进程for(int i=0;i<len;i++){ fork()创建子进程 } 中在for外部 执行父子进程(防止创建一部分被父进程退出)
注2:while(wait(NULL)>0) {} //等待所有子进程退出
(1)条件编译 — 收发一体 在一个文件里面
子主题 1 (2)使用两根管道 实现独立的收发 6、有名读写的特点 (1) open 打开管道时 不使用 O_NONBLOCK(阻塞)
open(“./myfifo”,O_RDONLY); open(“./myfifo”,O_WRONLY);
open以 只读方式打开FIFO时,要阻塞到 以写的方式 打开的 FIFO的 进程运行
open以 只写方式打开FIFO时,要阻塞到 以读的方式 打开的 FIFO的 进程运行
open 以只读 只写的方式打开FIFO,都会阻塞。
通讯过程中 如果 写进程先退出, 则调用 read 函数读FIFO 的进程—> read 变成非阻塞 —> 如果重新运行 写进程 read 又阻塞了!
通讯过程中 如果 读进程先退出, 写进程向管道写数据是 会收到 SIGPIPE 信号。
(2)open 非阻塞打开管道 加参数 O_NONBLOCK
open(“./myfifo”,O_RDONLY | O_NONBLOCK ); open(“./myfifo”,O_WRONLY | O_NONBLOCK);
3、信号
本质上是正整数 ----软中断
<signal.h>
kill -l 查看所有信号
1 kill
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
pid: 正数:要接收信号的进程的进程号 getpid();
pid>0: 将信号传送给进程ID为pid的进程。
pid=0: 将信号传送给当前进程所在进程组中的所有进程。
pid=-1: 将信号传送给系统内所有的进程。
pid<-1: 将信号传给指定进程组的所有进程。这个进程组号等于pid的绝对值。
0:信号被发送到所有和pid进程在同一个进程组的进程
-1:信号发给所有的进程表中的进程 (除了进程号最大的进程外 1 )
sig:
kill -l 列表中的信号
常用
ctrl + C ----> 2 ----> SIGINT ----> 终止前台进程!!!
ctrl + \ ----> 3 -----> SIGQUIT ----> 一般是错误的时候退出
ctrl + z -----> 20 —> SIGTSTP ----> 暂停进程 放到后台
SIGKILL —> 9 -----> 立即结束 不能处理 不能阻塞 不能 忽略!
SIGSTOP ----> 19 ----> 这个信号 不能处理 不能阻塞 不能 忽略! —> 暂停
SIGALRM ----> 14 ----> 定时器的
SIGABRT—>6 —>异常终止,不可阻塞
kill 命令的缺省信号 ----> SIGTERM ----> 15 -----> 程序结束
2 raise
int raise(int sig);
默认给自己发送信号
参数:
sig:信号的编号。
返回值:
成功返回 0,失败返回 -1。
------------------------------------> 等价于 kill(getpid(), sig);
3abort函数
#include <stdlib.h>
void abort(void);
功能:
向进程发送一个SIGABRT信号,默认情况下进程会退出。
//给自己发送异常终止信号 6) SIGABRT,并产生 core 文件
等价于 kill(getpid(), SIGABRT);
注意:
即使SIGABRT信号被加入阻塞集,一旦进程调用了abort函数,进程也还是会被终止,
且在终止前会刷新缓冲区,关文件描述符。
4、alarm函数
#include <unistd.h>
unsigned int alarm(unsigned int seconds)
功能:
seconds: 秒数 //时间一到 就发送 SIGALRM
在seconds秒后,向调用进程发送一个14)SIGALRM信号,
SIGALRM信号的默认动作是 终止调用alarm函数的进程。
每个进程都有且只有唯一的一个定时器。
返回值:
若以前没有设置过定时器,或设置的定时器已超时,返回0;
否则返回定时器剩余的秒数,并重新设定定时器。
注意:
定时,与进程状态无关(自然定时法)!
就绪、运行、挂起(阻塞、暂停)、终止、僵 尸……无论进程处于
何种状态,alarm 都计时。
5 setimer
#include <sys/time.h>
int setitimer(int which, const struct itimerval *new_value, struct itimerval
*old_value);
功能:
设置定时器(闹钟)。 可代替 alarm 函数。精度微秒 us,可以实现周期定时。
参数:
which:指定定时方式
a) 自然定时:ITIMER_REAL → 14)SIGALRM 计算自然时间
b) 虚拟空间计时(用户空间):ITIMER_VIRTUAL → 26)SIGVTALRM 只计算进程占用 cpu 的
时间
c) 运行时计时(用户 + 内核):ITIMER_PROF → 27)SIGPROF 计算占用 cpu 及执行系统调用
的时间
new_value: struct itimerval, 负责设定时:
struct itimerval {
struct timerval it_interval; // 闹钟触发周期 第一次以后的每次时间周期
struct timerval it_value; // 闹钟触发时间 第一次执行的时间
};
struct timerval结构体:
struct timeval {
long tv_sec; // 秒
long tv_usec; // 微秒
};
it_value为计时时长,it_interval为定时器的间隔,
即第一次计时it_value时长发送信号,再往后的信号每隔一个it_interval发送一次。
old_value: 存放旧的 timeout 值,一般指定为NULL
表示上一次定时剩余的时间,例如第一次定时10s,但是过了3s后,而上一次定时的剩余时间则为7s。
再次用setitimer函数定时,此时第二次的计时会将第一次计时覆盖,
6、pause函数 ----> 捕获信号
int pause(void);
功能: —> 进程一直挂起(阻塞)
将调用进程挂起直至捕捉到信号为止。
这个函数通常用于判断信号是否已到。
返回值:
直到捕获到信号,pause函数才返回-1,且errno被设置成EINTR。
7、signal注册信号函数
信号的操作 默认 忽略 自定义 —> 9SIGKILL 19SIGSTOP 不可忽略 不可处理 捕获
捕捉信号并且信号信号的处理方式有两个函数,signal 和sigaction。
—> 信号发起者 和信号接收者 必须要属于同一用户!!!!
(1)signal函数
#include <signal.h>
typedef void (*sighandler_t)(int);//返回值void的函数指针
sighandler_t signal(int signum,sighandler_t handler); // 回调
功能:
注册信号处理函数(不可用于SIGKILL、SIGSTOP信号),即确定收到信号后处理函数的入口地址。
参数:
signum:信号编号
handler的取值:
忽略该信号:SIG_IGN
执行系统默认动作:SIG_DFL
自定义信号处理函数:信号处理函数名
返回值:
成功:返回函数地址,该地址为此信号上一次注册的信号处理函数的地址。
失败:返回SIG_ERR
(2)sigaction函数
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction
*oldact);
功能:
检查或修改指定信号的设置(或同时执行这两种操作)。
参数:
signum:要操作的信号。
act: 要设置的对信号的新处理方式(传入参数)。
oldact:原来对信号的处理方式(传出参数)。
如果 act 指针非空,则要改变指定信号的处理方式(设置),如果 oldact 指针非
空,
则系统将此前指定信号的处理方式存入 oldact。
返回值:
成功:0
失败:-1
struct sigaction:结构体
struct sigaction{
void (*sa_handler)(int); //旧的信号处理函数指针
void (*sa_sigaction)(int, siginfo_t *, void *); //新的信号处理函数指针
sigset_t sa_mask; //信号阻塞集
int sa_flags; //信号处理的方式
void (*sa_restorer)(void); //已弃用
};
sa_handler、sa_sigaction:信号处理函数指针,和 signal() 里的函数指针用法一样,
应根据情况
给 sa_sigaction、sa_handler 两者之一赋值,其取值如下:
a)SIGIGN:忽略该信号
b) SIGDFL:执行系统默认动作
c) 处理函数名:自定义信号处理函数
sa_mask:信号阻塞集,在信号处理函数执行过程中,临时屏蔽指定的信号。
sa_flags:用于指定信号处理的行为,通常设置为 0,表使用默认属性。它可以是以下值的“按位
或”组合:
SA_RESTART:使被信号打断的系统调用自动重新发起(已经废弃)
SA_NOCLDSTOP:使父进程在它的子进程暂停或继续运行时不会收到 SIGCHLD 信号。
SA_NOCLDWAIT:使父进程在它的子进程退出时不会收到 SIGCHLD 信号,这时子进程如果退出
也不会成为僵尸进程。
SA_NODEFER:使对信号的屏蔽无效,即在信号处理函数执行期间仍能发出这个信号。
SA_RESETHAND:信号处理之后重新设置为默认的处理方式。
SA_SIGINFO:使用 sa_sigaction 成员而不是 sa_handler 作为信号处理函数
sa_sigaction 处理函数 ----> 新的信号处理函数
void(*sa_sigaction)(int signum, siginfo_t *info, void *context);
参数说明:
signum:信号的编号。
info:记录信号发送进程信息的结构体。
context:可以赋给指向 ucontext_t 类型的一个对象的指针,
以引用在传递信号时被中断的接收进程
或线程的上下文。
4、未决信号集和信号阻塞集 1、理论
未决信号集: 未解决的信号集合
信号阻塞集: 将某些信号 加入这个集合,对他们设置屏蔽。 当屏蔽x 信号后, 再收到这个信号,这个信号的处理 被暂缓!!!
阻塞信号集合,在阻塞信号集合里的信号将暂缓执行(特殊信号除外,eg: SIGABRT)
所谓阻塞并不是禁止传送信号, 而是暂缓信号的传送。
若将被阻塞的信号从信号阻塞集中删除,且对应的信号在被阻塞时发生了,进程将会收到相应的信号。
这两个集合都是存储在内核的PCB中
1.某信号发生,先加入未决信号集合(置1)表示未决状态
2.再在阻塞信号集查看该信号是否被阻塞(阻塞状态为1)
3阻塞,(若在信号阻塞时有发出该信号即未决状态1)则等待阻塞解除(置0)后响应该信号,随后未决信号集该信号置0
2、信号集
这两个信号集都是内核使用位图机制来实现的。但操作系统不允许我们直接对其进行位操作。而需自定义另外一个集合,借助信号集操作函数来对 PCB中的这两个信号集进行修改。
//信号集相关函数
sigset_t 类型的本质是位图
int sigemptyset(sigset_t *set);
函数说明:将set信号集清0
函数返回值:成功:0;失败:-1,设置errno
int sigfillset(sigset_t *set);
函数说明:将set信号集置1
函数返回值:成功:0;失败:-1,设置errno
int sigaddset(sigset_t *set, int signum);
函数说明:将signum信号加入set信号集合中
函数返回值:成功:0;失败:-1,设置errno
int sigdelset(sigset_t *set, int signum);
函数说明:将signum信号从set信号集中清出
函数返回值:成功:0;失败:-1,设置errno
int sigismember(const sigset_t *set, int signum);
函数说明:判断signum信号是否在set信号集中
函数返回值: 在:1;不在:0;出错:-1,设置errno
3、阻塞信号集和未决信号集
sigprocmask函数: ----> 此函数用于修改阻塞信号集。
函数说明:用来屏蔽信号、解除屏蔽也使用该函数。
其本质,读取或修改进程控制块中的信号屏蔽字(阻塞信号集)。
特别注意,屏蔽信号只是将信号处理延后执行(延至解除屏蔽);而忽略表示将信号丢弃处理。
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
函数返回值:
成功:0;失败:-1,设置errno
函数参数:
how参数取值:假设当前的信号屏蔽字为mask
SIG_BLOCK: 当how设置为此值,set表示需要屏蔽的信号。
SIG_UNBLOCK: 当how设置为此,set表示需要解除屏蔽的信号。
SIG_SETMASK:按照参数 set 提供的信号设置重新设置系统信号设置。
set:传入参数,是一个自定义信号集合。由参数how来指示如何修改当前信号屏蔽字
oldset:传出参数,保存旧的信号屏蔽字。
sigpending函数 ----> 你可以使用 sigpending 函数获取未决信号集
int sigpending(sigset_t *set);
函数说明:读取当前进程的未决信号集函数参数:set传出参数
函数返回值:成功:0;失败:-1,设置errno
5、消息队列
特点:1.消息有格式、有类型,队列中消息不遵循先进先出,同类型消息遵循
2.每个消息队列具有唯一标识 key_t ftok(const char *,int)获得(第二参数仅低八位有效,即最后两位数),只能手动删除
3.命令 ipcs [参数] --》查看system V 的所有 IPC 参数 -q(消息队列queue) -m(共享内存memoray) -s(信号量数组sem)
4.命令 ipcrm 【参数】【指定标识msqid】 --》删除system V 的所有 IPC 参数 -q(消息队列queue) -m(共享内存memoray) -s(信号量数组sem)
5.IPC通信机制都具有唯一标识
本质:在内存中的链表,内核管理
二、消息队列读写
1、msgsnd ----> 向消息队列发送消息
既是发送消息到消息队列!!! ----> send!!
#include <sys/msg.h>
int msgsnd(int msqid, const void msgp,size_t msgsz, int msgflg);
功能:
将msgp消息写入到标识符为msqid的消息队列
参数:
msqid: 消息队列标识符 ----> msgget函数返回值
msgp:发送给队列的消息。指向消息的指针。
//msgp可以是任何类型的结构体,
//但第一个字段必须为long类型,即表明此发送消息的类型,
//msgrcv根据此接收消息。
msgp定义的参照格式如下:
struct s_msg{ /msgp定义的参照格式/
long type; / 必须大于0,消息类型 */
char mtext[256]; /消息正文,可以是其他任何类型/
} msgp;
size: 要发送消息的大小,不含消息类型占用的4个字节,即mtext的长度
flag: 0:当消息队列满时,msgsnd将会阻塞,直到消息能写进消息队列
//IPC_NOWAIT:当消息队列已满的时候,msgsnd函数不等待立即返回
//IPC_NOERROR:若发送的消息大于size字节,则把该消息截断,
//截断部分将被丢弃,且不通知发送进程。
返回值:
成功 0 出错 -1
2、msgrcv---从消息队列读取消息
#include <sys/msg.h>
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
功能:
从标识符为msqid的消息队列读取消息并存于msgp中,读取后把此消息从消息队列中删除
参数:
msgid:消息队列标识符
msgp:存放消息的结构体,结构体类型要与msgsnd函数发送的类型相同
msgsz:要接收消息的大小,不含消息类型long占用的4个字节
msgtype: 0:返回队列中的第一个消息
>0:返回队列中消息类型为msgtyp的消息
<0:返回队列中消息类型值小于或等于msgtyp绝对值的消息,如果这种消息有若干个,则取类型值最小的消息。
msgflg: 函数的控制属性
0: 阻塞式接收消息,没有该类型的消息msgrcv函数一直阻塞等待
IPC_NOWAIT:如果没有返回条件的消息调用立即返回,此时错误码为ENOMSG
IPC_EXCEPT:与msgtype配合使用返回队列中第一个类型不为msgtype的消息
MSG_NOERROR:如果队列中满足条件的消息内容大于所请求的size字节,
则把该消息截断,截断部分将被丢弃,且不通知消息发送进程
返回:
成功: 实际读取到的消息数据长度
出错 -1
3、msgctl---消息队列控制
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds buf);
功能: 获取或设置消息队列属性
对消息队列进行各种控制,如修改消息队列的属性,或删除消息消息队列。
参数:
msqid:消息队列的标识符。
cmd:函数功能的控制
IPC_RMID:删除由msqid指示的消息队列,将它从系统中删除并破坏相关数据结构。 ----> 从系统中删除消息队列。
IPC_STAT:将msqid相关的数据结构中各个元素的当前值存入到由buf指向的结构中。 --> 获得msgid的消息队列头数据到buf中
IPC_SET:将msqid相关的数据结构中的元素设置为由buf指向的结构中的对应值。
可设置的属性包括:msg_perm.uid、msg_perm.gid、msg_perm.mode以及msg_qbytes
buf:msqid_ds数据类型的地址,用来存放或更改消息队列的属性。
消息队列管理结构体,请参见消息队列内核结构说明部分
struct msqid_ds {
struct ipc_perm msg_perm; / Ownership and permissions /
time_t msg_stime; / Time of last msgsnd(2) /
time_t msg_rtime; / Time of last msgrcv(2) /
time_t msg_ctime; / Time of last change /
unsigned long __msg_cbytes; / Current number of bytes inqueue (nonstandard) /
msgqnum_t msg_qnum; / Current number of messages in queue /
msglen_t msg_qbytes; / Maximum number of bytes allowed in queue /
pid_t msg_lspid; / PID of last msgsnd(2) /
pid_t msg_lrpid; / PID of last msgrcv(2) */
};
返回值:
成功:返回 0;失败:返回 -1
//控制消息队列
msgctl(msgid,IPC_RMID,NULL); //删除消息队列
struct msqid_ds get_ds; // 不用赋值!!
msgctl(msgid,IPC_STAT,&getds); //将指定消息队列的信息 获取到 get_ds 结构体中
struct msqid_ds set_ds;
set_ds.msg_lspid = 32103;
msgctl(msgid,IPC_SET,&set_ds); //将自定义的消息队列属性 set_ds 中的信息传到指定消息队列中 并设置改消息队列
4、作业
两个同学通讯 A B ----> 可以互相收发消息!!!! —> 通过消息队列!!!
A代码 B代码 C代码 6、mmap磁盘存储映射
mmap—存储映射IO(memeory mappend i/o)–>是磁盘文件 与 内存的一个缓冲区映射
1.mmap映射
子主题 1这个磁盘空间就是两个进程间通讯的桥梁!!! -----> 不要用来存放 音视频等 大的数据!!
2、mmap函数
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);
功能:
将磁盘文件映射到当前进程的虚拟内存中
参数:
addr:虚拟内存的地址,默认一般为NULL,由系统指定一块合适的内存
len :映射内存的大小 —> 要申请的映射区的长度
prot:可读、可写、可执行、不可访问
PROT_READ 可读
PROT_WRITE 可写
flags 标志位:
MAP_SHARED 共享的 —> 虚拟内存内的数据改变会立即同步到磁盘文件
MAP_PRIVATE 私有的 —> 虚拟内存内的数据的改变不会同步到磁盘文件
fd :
文件描述符需要打开一个文件
offset:
指定一个偏移位置 ,默认为0(从文件开始位置映射)
返回值
成功 返回映射区的首地址
失败 返回 MAP_FAILED ((void *) -1)
3、释放映射区
#include <sys/mman.h>
int munmap(void *addr, size_t len);
功能:
取消磁盘文件和当前进程的虚拟内存的映射关系
参数:
addr:内存首地址
len: 内存大小
返回值:
成功:0
失败:-1
4、拓展文件大小
每次需要映射文件 都需要 新建!!!! ----> 如果里面有数据 每次都会被覆盖
每次新建文件的大小都是 0字节 ----> 0的话装不了数据 -----> 所以新文件需要先拓展文件大小 把空间先占着!!!
#include <unistd.h>
#include <sys/types.h>
int truncate(const char *path, off_t length);
功能:
给文件一个大小
参数:
path:文件路径名
length:文件大小
返回值:
成功:0
失败:-1
mmap demo案例 7共享内存 1.概念
|共享内存 是 进程间通讯(IPC) 中效率最高的 最快的!
特点:互斥访问
共享内存允许两个或多个进程共享给定的内存区域!
2、shm有特点
ubuntu 中共享内存限制如下: 系统之间可能有差异
共享存储区的最小字节数:1
共享存 储区的最大字节数:32M
共享存储区的最大个数:4096
每个进程最多能映射的 共享存储区的个数:4096
3、shm —共享内存操作
1)shm 也是属于 SYSTEM V 的IPC创建key 值!!
key_t key = ftok(“/home/edu”,2401);
(2)shmget — 获取一块共享内存
#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:与IPC_CREAT共同使用,不存在时创建,存在则报错。
位或权限位:共享内存位或权限位后可以设置共享内存的访问权限,格式和open函数的mode_t一样,但可执行权限未使用。
它们的功能与open()的O_CREAT和O_EXCL相当。
返回值:
成功:返回共享内存标识符。
失败:返回-1。
(3)shmat----共享内存映射(attach)
因为创建的共享内存 是独立内存!!! 而每个进程有自己能够使用的虚拟内存空间!! 所以需要建立两者映射关系!
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
功能:
将一个共享内存段映射到调用进程的数据段中。
参数:
shmid:共享内存id —> shmget创建返回值
shmaddr:共享内存映射地址(若为NULL则由系统自动指定),通常 NULL,系统自己决定。
shmflg:共享内存段的访问权限和映射条件
0:共享内存具有可读可写权限。
SHM_RDONLY:只读。
SHM_RND:自动选择比 shmaddr 小的最大页对齐地址(shmaddr非空时才有效)
shmat( )第二个参数shmaddr不为空时,那么要求 SHM_RND 在 shmflg必须被设置,
这样的话系统将会选择比 shmaddr 小而又最大的页对齐地址 (即为 SHMLBA 的整数倍) 作为共享内存区域的起始地址。
如果没有设置 SHM_RND,那么 shmaddr 必须是严格的页对齐地址。。
返回值:
成功:返回共享内存段映射地址
失败:返回 (void *)(-1)
(4)shmdt ---- 解除共享内存映射 (detach)
int shmdt(const void *shmaddr);
shmaddr: 共享内存起始地址 //类似于free
返回值:
成功 0 失败 -1
//进程结束 会自动撤销映射
(5)shmctl ----共享内存控制
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
功能:
共享内存空间的控制。
参数:
shmid: 共享内存id
cmd:
IPC_STAT (获取对象属性) //得到共享内存的状态,把共享内存的shmid_ds结构复制到buf中
IPC_SET (设置对象属性) //把buf所指的shmid_ds结构中的uid、gid、mode复制到共享内存的shmid_ds结构内
IPC_RMID (删除对象)
IPC_INFO 获得关于共享内存的系统限制值信息
SHM_INFO 获得系统为共享内存消耗的资源信息
SHM_STAT 同 IPC_STAT,
但shmid 为该 SHM 在内核中记录所有 SHM 信息的数组的下标,
因此通过迭代所有的 下标可以获得系统中所有 SHM 的相关信息
SHM_LOCK 禁止系统将该 SHM 交换至 swap 分区
SHM_UNLOCK 允许系统将该 SHM 交换至 swap 分区
buf: 就是用来存放共享内存属性的
//共享内存管理结构体。具体说明参见共享内存内核结构定义部分
返回值:
成功 0 失败 -1
//清除共享内存时: cmd 为: IPC_RMID 第三个参数为NULL;
//SHM_LOCK 用于锁定内存,禁止内存交换。并不代表共享内存被锁定后 禁止其它进程访
问。
//其真正的意义是:被锁定的内存不允许被交换到虚拟内存中。 这样做的优势在于让共享内存一直
处于内存中,从而提高程序性能
1结构体
2控制操作
//控制共享内存
shmctl(shmid,IPC_RMID,NULL); //删除共享内存
struct shmid_ds get_ds; // 不用赋值!!
shmctl(shmid,IPC_STAT,&getds); //将指定共享内存的信息 获取到 get_ds 结构体中
struct shmid_ds set_ds;
set_ds.shm_cpid = 32103;
shmctl(shmid,IPC_SET,&set_ds); //将自定义的共享内存属性 set_ds 中的信息传到指定共享内存中 并设置改共享内存
4、两个进程 收发消息 ----> 共享内存
8信号灯集
1.同步与互斥
同步:由于进程推进速度差异确保多个进程或线程之间按照一定的顺序和规则进行执行,以避免数据竞争和不确定性。是时间上的次序。
互斥:在一个时间只允许有一个进程访问临界资源
2信号灯集
作用:控制进程共享资源
本质:内存中的标志,所有进程可以看见并且修改
信号灯:
1.二值信号灯0、1,类似互斥锁。区别:(1)信号灯强调共享资源,只要共享资源可用,其它进程同样可以修改信号灯值(2)互斥锁强调进程,占用资源的进程使用完资源后,由进程本身解锁
临界资源:进程之间的共享资源如打印机、磁带机缓冲区等,同一时刻,只有一个使用
临界区:访问临界资源那段代码
信号量:记录空闲资源数量,控制访问资源
3、pv操作 (原子操作,不可中断,不可重入)
可重入:指的是一个系统或者函数可以安全地被多个任务或线程同时调用,而不会产生不确定的结果或者破坏系统状态。
p操作(wait):尝试获取资源,若信号量>0,信号量-1,进程继续执行;如果信号量等于0,表示没有空闲资源,进程阻塞。
v操作(signal):释放资源,信号量+1
4信号灯集的操作
1、semget ----- 创建一个信号灯集
//得到一个信号量集标识符或创建一个信号量集对象并返回信号量集标识符
int semget(key_t key, int nsems, int semflg);
key: 0(IPC_PRIVATE):会建立新信号量集对象
nsems: 创建信号量集中信号量的个数,该参数只在创建信号量集时有效
semflg: 0:取信号量集标识符,若不存在则函数会报错
//信号灯集的访问权限,通常为IPC_CREAT | 0666 | IPC_EXCL
IPC_CREAT 如果不存在,则创建一个共享内存,否则打开操作。
IPC_EXCL:与IPC_CREAT共同使用,不存在时创建,存在则报错。
EACCESS:没有权限
EEXIST:信号量集已经存在,无法创建
EIDRM:信号量集已经删除
ENOENT:信号量集不存在,同时semflg没有设置IPC_CREAT标志
ENOMEM:没有足够的内存创建新的信号量集
ENOSPC:超出限制
返回:
成功: 信号灯集标识符 ID
出错: -1
2、semop ----- 信号量的操作 PV 操作
//semop(完成对信号量的P操作或V操作)
int semop ( int semid, struct sembuf *opsptr, size_t nops);
semid: 信号灯集标识符
opsptr: 指向进行操作的信号量集结构体数组的首地址
struct sembuf {
short sem_num; // 要操作的信号灯的编号 0代表第一个信号量
short sem_op; // 0 : 等待,直到信号灯的值变成0
// 1 : 释放资源,V操作
// -1 : 分配资源,P操作
short sem_flg; // 0设置信号量的默认操作
//IPC_NOWAIT设置信号量操作不等待
//SEM_UNDO 选项会让内核记录一个与调用进程相关的UNDO记录,
//如果该进程崩溃,则根据这个进程的UNDO记录自动恢复相应信号量的计数值
};
nsops: 进行操作信号量的个数,即opsptr结构变量的个数,需大于或等于1。
//最常见设置此值等于1,只完成对一个信号量的操作
返回:
成功 0 出错 -1
3、semctl — 信号量控制
int semctl ( int semid, int semnum, int cmd…/union semun arg/);
功能:
控制信号量,删除信号量或初始化信号量
参数:
semid: 信号灯集标识符
semnum: 信号量集数组上的下标,表示某一个信号量
cmd: IPC_STAT读取一个信号量集的数据结构semid_ds,并将其存储在semun中的buf参数中。
IPC_SET设置信号量集的数据结构semid_ds中的元素ipc_perm,其值取自semun中的buf参数。
//IPC_RMID将信号量集从内存中删除。
GETALL用于读取信号量集中的所有信号量的值。
GETNCNT返回正在等待资源的进程数目。
GETPID返回最后一个执行semop操作的进程的PID。
//GETVAL返回信号量集中的一个单个的信号量的值。
GETZCNT返回这在等待完全空闲的资源的进程数目。
SETALL设置信号量集中的所有的信号量的值。
//SETVAL设置信号量集中的一个单独的信号量的值。
arg: 可变参函数: cmd为GETVAL或SETVAL时
union semun {
int val;
struct semid_ds *buf;
unsigned short *arrary;
};
返回值:
成功 0 失败 -1
4demo
p原语操作的动作是;
(1)sem减1;
(2) 若(1)操作后仍大于0或者1,进程继续
(3)若(1)后小于0,进程进入相应阻塞队列,然后转进程调度
v原语操作的动作是;
(1)sem加1;
(2) 若相加结果大于零,则进程继续执行;
(3) 若相加结果小于或等于零,则从该信号的等待队列中唤醒一等待进程,然后再返回原进程继续执行或转进程调度。
write read 线程 1、线程的概述 1.进程和线程的关系: 一个进程包含一个或者多个进程
1.进程:程序的一次执行过程包括创建 调度 消亡 资源分配的基本单位
2.线程:微量级进程(LWP:light weight process ) 资源调度的基本单位
3.PCB -->存的进程标识符表 进程ID
在linux环境下 线程的本质仍然是进程!线程共享进程的资源,所以线程很少用信号量,同时每个线程有自己独立的栈区
4.一般一个进程只有一个控制线程
图解
线程作用:使用多个控制线程(或者简单地说就是线程〉在单进程环境中执行多个任务。
特点:1.多个线程自动地可以访问相同的存储地址空间和文件描述符,共享
2.在只有一个控制线程的情况下,一个单线程进程要完成多个任务,只需要把这些任务串行化
3.使用多线程来改善响应时间
2、线程的标识
每个线程也像进程一样有标识 —> 线程ID LWP -----> 进程ID PID 唯一 (线程ID不同,它只有在 它所属的进程上下文才有意义!!)
进程ID 类型 pid_t ----> 非负整数!!!
线程ID 类型 pthread_t ----> 是一个结构体 ----> 在可移植操作系统中不能把它当做整数处理!!
线程ID判断 必须使用函数来比较!!!!
3、线程的属性 提要:Linux 下线程的属性是可以根据实际项目需要,进行设置,之前我们讨论的线程都是采用线程的默认属性,
默认属性已经可以解决绝大多数开发时遇到的问题。
----> 线程创建时 的第二个参数 pthread_create(,NULL) —> NULL 代表默认属性
1、线程属性结构体 —>pthread_attr_t
typedef struct
{
int etachstate; //线程的分离状态
int schedpolicy; //线程调度策略
struct sched_param schedparam; //线程的调度参数
int inheritsched; //线程的继承性
int scope; //线程的作用域
size_t guardsize; //线程栈末尾的警戒缓冲区大小
int stackaddr_set; //线程的栈设置
void* stackaddr; //线程栈的位置
size_t stacksize; //线程栈的大小
} pthread_attr_t;
1主要成员:
线程分离状态
线程栈大小 ----> 默认平均分配
线程栈末尾的警戒缓冲区大小 ----- 栈末尾
线程栈的最低地址 属性值 不能直接设置, ----> 通过函数设置线程属性 pthread_attr_init -----> 在pthread_create之前调用!!!
--------> 之后使用 pthread_attr_destory 来释放资源
2线程属性初始化
int pthread_attr_init(pthread_attr_t *attr);
函数返回值:
成功:0;
失败:错误号
//线程属性资源销毁
pthread_attr_destroy(pthread_attr_t *attr);
函数返回值:
成功:0;
失败:错误号
2、通过属性进程线程分离(把当前线程从进程分离出来,由系统自动回收资源)
线程分离函数 ----> pthread_detach ------> 把线程和当前进程分离 —> 脱离进程由系统自动释放资源
----> 如果 在分离之前 线程 已经结束,线程资源是释放不了的!!
先设置线程的分离属性、再去创建线程。
1线程的资源:
#include <pthread.h>
void pthread_exit(void *retval);
#include <pthread.h>
int pthread_cancel(pthread_t thread);
使用以上两个函数进行线程的退出,但是相对应的资源(子线程创建时从父线程copy出来的栈内存;子线程内部单独申请的堆内存(malloc、realloc、calloc)和锁资源mutex)并不会被回收,为了防止资源的过度占用造成内存泄漏,在线程回收的时候,或者当线程处于加锁后解锁前的状态时,应当采取相应的措施来回收该线程资源。
2方案
//获取程属性,分离or 非分离
int pthread_attr_getdetachstate(pthread_attr_t *attr, int *detachstat);
参数:
attr:已初始化的线程属性
detachstate:
分离状态
PTHREAD_CREATE_DETACHED(分离线程)
PTHREAD_CREATE_JOINABLE(非分离线程)
demo
3线程资源释放的三大方法
pthread_join 如上5
阻塞,进程释放
pthread_detach 如上6
分离,系统释放
注意:在进程线程分离前,调用pthread_detach 前 线程已经结束, 线程资源得不到释放
//设置分离属性 attr1为线程属性
int pthread_attr_setdetachstate(pthread_attr_t *attr,int *detachstat);
//获取属性 attr1的各项数据
int pthread_attr_getdetachstate(pthread_attr_t *attr,int *detachstat);
参数:
attr:已初始化的线程属性
detachstate:
分离状态
PTHREAD_CREATE_DETACHED(分离线程)
PTHREAD_CREATE_JOINABLE(非分离线程)
属性初始化,分离,系统释放
4、线程的资源调度
在进程中 我们了解了PCB 进程控制块,本质为 task_struct 结构体
Linux内核通过一个被称为进程描述符的task_struct结构体来管理进程,这个结构体包含了一个进程所需的所有信息。它定义在include/linux/sched.h文件中。
谈到task_struct结构体,可以说它是linux内核源码中最复杂的一个结构体了,成员之多,占用内存之大。
task_struct 结构体包含了许多字段,其中一些重要的字段包括:
volatile long state: 进程状态,如运行、就绪、等待等。
struct thread_info *thread_info: 线程信息。
pid_t pid: 进程ID。
struct task_struct *parent: 指向父进程的指针。
struct list_head children: 子进程链表头部。
struct mm_struct *mm: 进程地址空间描述符。
struct files_struct *files: 文件描述符表指针。
首先一切进程至少都有一个执行线程。线程在进程内部运行,本质是在进程地址空间内运行
首先一切进程至少都有一个执行线程。线程在进程内部运行,本质是在进程地址空间内运行
每个进程都有自己独立的进程地址空间和页表,这代表着运行本身就具有独立性。
线程运行的本质就是在这个进程地址空间运行,也就是这个进程的所有资源几乎是所有线程共享的。
单纯从技术角度,这是一定能实现的,因为线程粒度比进程更小,所以它比创建原始进程更轻松。
进程不仅通过task_struct来衡量,还要有进程地址空间、文件、信号等等, ----> 从内核角度来看进程就是系统资源分配的基本单位
线程是资源调度的基本单位。
5、线程的资源
共享资源
一个进程中的线程之间拥有公共区域:共享的
- 可执行的指令
- 静态数据全局变量
- 进程中打开的文件描述符信号处理函数
- 当前工作目录用户ID
- 用户组ID
非共享资源
也有相互独立的空间:
- 线程ID(TID)
- PC(程序计数器)和相关寄存器
- 栈
- 局部变量返回地址
- 错误号(errno)
- 信号掩码和优先级·
- 执行状态和属性
6、线程优缺点
优点:
提高程序并发性
开销小
数据通信、共享数据方便
缺点:
库函数,不稳定 ---- 三方库!!!
调试、编写困难、gdb 不支持
对信号支持不好
优点相对突出,缺点均不是硬伤。Linux 下由于实现方法导致进程、线程差别不是很大。
7、线程的操作
1、安装线程三方库
线程函数列表安装:
sudo apt-get install manpages‐posix‐dev
查看线程函数:
man ‐k pthread
注意:编译时加 -lpthread
2、线程的创建
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine)(void *), void *arg);
功能:
创建一个线程。
参数:
thread: 线程标识符地址 -----> 定义一个pthread_t 的变量 —>将变量地址 传进来
attr: 线程属性 ----> 可以通过该参数设置线程属性 —> 常用NULL
start_routine: 回调函数 —> 线程函数入口 —>函数指针 函数名 —> void *func(void *)
这个回调函数 就是创建的线程想要实现的功能!!
arg: 传递给 回调函数 func 的参数!!!
返回值:
成功 0
失败 非0 errno3、创建两个线程执行独立的功能demo 4、获得当前线程号
#include <pthread.h>
pthread_t pthread_self(void);
功能:
获取线程号。
参数:
无
返回值:
调用线程的线程 ID 。
5、pthread_join 阻塞 ----->回收线程资源
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
功能:
等待线程结束(此函数会阻塞),并回收线程资源,类似进程的 wait() 函数。如果线程已经结
束,那么该函数会立即返回。
参数:
thread:被等待的线程号。
retval:用来存储线程退出状态的指针的地址
返回值:
成功:0
失败:非 0
6、线程分离 --- pthread_detach (非阻塞)
创建一个线程后应回收其资源,但使用pthread_join函数会使调用者阻塞,故Linux提供了线程分离函数:pthread_detach。
线程也可以被置为 detach 状态,这样的线程一旦终止就立刻 回收它占用的所有资源,而不保留终止状态。
不能对一个已经处于 detach 状态的 线程调用 pthread_join,这样的调用将返回 EINVAL错误。
就是 一旦 调用了 pthread_detach 就不能再调用 pthread_join
#include <pthread.h>
int pthread_detach(pthread_t thread);
功能:
回收线程资源,线程结束时,由系统自动回收其资源
参数:
thread:线程号
返回值:
0:成功回收线程
其它:失败
使调用线程与当前进程分离,分离后不代表此线程不依赖与当前进程,
线程分离的目的是将线程资
源的回收工作交由系统自动来完成
,也就是说当被分离的线程结束之后,系统会自动回收它的资源。
所以,此函数不会阻塞
7、线程退出—pthread_exit
#include <pthread.h>
void pthread_exit(void *value_ptr)
功能:
退出调用线程。一个进程中的多个线程是共享该进程的数据段,因此,通常线程退出
后所占用的资源并不会释放。
参数:
value_ptr: 线程退出的线程返回值
demo
8、线程控制 ----->取消线程 cacel
注:对于未设置线程相应属性、没有对线程join、detach,使用cancel是不会释放线程资源的
#include<pthread.h>
int pthread_cancel(pthread_t thread);
功能:
杀死(取消)线程
参数:
thread : 目标线程 ID。
返回值:
成功:0
失败:出错编号
线程可以被 同一 进程中的其他线程 取消(杀死)
注意: 线程的取消不是实时的!! 而是有一定延时性---->需要等到线程达到某个取消点的时候才会取消
----> 取消点: 是线程检查是否被取消,并按请求进行动作的 一个位置。
----> 通常是一些系统调用: open creat pause close read write -----> man 7 pthreads
-----> eg : 单机游戏存档,必须到达指定的场所(存档点)才能存档成功!!
demo结果图
9线程资源释放的三大方法
pthread_join 如上5
阻塞,进程释放
pthread_detach 如上6
分离,系统释放
注意:在进程线程分离前,调用pthread_detach 前 线程已经结束, 线程资源得不到释放
//设置分离属性 attr1为线程属性
int pthread_attr_setdetachstate(pthread_attr_t *attr,int *detachstat);
//获取属性 attr1的各项数据
int pthread_attr_getdetachstate(pthread_attr_t *attr,int *detachstat);
参数:
attr:已初始化的线程属性
detachstate:
分离状态
PTHREAD_CREATE_DETACHED(分离线程)
PTHREAD_CREATE_JOINABLE(非分离线程)
属性初始化,分离,系统释放
10、设置线程的栈空间
当进程的栈地址空间不够用时,我们就需要指定线程使用 malloc 分配的空间 作为 线程自己的栈空间来使用!!!
栈不够,堆来凑 ------> 16K <=栈的大小 <=8M
//设置栈的地址
int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize);
成功:0;失败:错误号
参数:
attr:指向一个线程属性的指针
stackaddr:设置的栈地址
stacksize:设置的栈大小
//得到栈的地址
int pthread_attr_getstack(pthread_attr_t *attr, void **stackaddr, size_t *stacksize);
成功:0;失败:错误号
参数:
attr:指向一个线程属性的指针
stackaddr:返回获取的栈地址
stacksize:返回获取的栈大小
//设置线程所使用的栈空间大小
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
成功:0;失败:错误号
参数:
attr:指向一个线程属性的指针
stacksize:设置堆栈大小
//得到线程所使用的栈空间大小
int pthread_attr_getstacksize(pthread_attr_t *attr, size_t *stacksize);
成功:0;失败:错误号
参数:
attr:指向一个线程属性的指针
stacksize:返回线程的堆栈大小
11。获取已经创建的进程的属性(补充) 8、线程的同步与互斥 1.全局变量资源共享demo 2互斥锁
作用:用于线程间互斥,控制临界资源(共享资源)的访问
两种状态: 加锁(lock) 和解锁(unlock)
同一时刻,只能有一个任务上锁 !!!其他任务阻塞 ---->访问临界资源前上锁(其他任务阻塞),访问结束 解锁(其他任务才能上锁)
死锁,访问前 上锁,访问后 不解锁!!!
3互斥锁的操作流程 加锁
访问共享资源
解锁
4互斥锁类型及操作
互斥锁的数据类型:pthread_mutex_t
(1)定义一把锁
pthread_mutes_t mutex;
(2)初始化锁
静态初始化:
pthread_mutex_t mutex=PTHREAD_MUTEX_INITALIZER;
动态初始化:推荐
pthread_mutex_t mutx;
pthread_mutex_init(&mutx,NULL);
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
功能:
初始化互斥锁
参数:
restrict mutex:互斥锁地址。类型是 pthread_mutex_t 。
restrict attr:设置互斥量的属性,通常可采用默认属性,即可将 attr 设为 NULL。
可以使用宏
PTHREAD_MUTEX_INITIALIZER 静态初始化互斥锁,
比如:pthread_mutex_t mutex =
PTHREAD_MUTEX_INITIALIZER;
这种方法等价于使用 NULL 指定的 attr 参数调用
pthread_mutex_init() 来完成动态初始化,不同之处在于 PTHREAD_MUTEX_INITIALIZER 宏不进行
错误检查。
返回值:
成功返回0,成功申请的锁默认是打开的。
失败返回非0。
(3)销毁互斥锁(仅动态初始化需要,静态只是一个变量)
#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);
功能:
销毁指定的一个互斥锁。互斥锁在使用完毕后,必须要对互斥锁进行销毁,以释放资源。
参数:
mutex:互斥锁地址。
返回值:
成功:0
失败:非 0 错误码
pthread_mutex_destroy(&mutx);
(4)上锁(访问临界资源前)
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
功能:
对互斥锁上锁,若互斥锁已经上锁,则调用者阻塞,直到互斥锁解锁后再上锁。
参数:
mutex:互斥锁地址。
返回值:
成功返回0,
失败返回非0。
(5)试着上锁 非阻塞
#include <pthread.h>
int pthread_mutex_trylock(pthread_mutex_t *mutex);
功能:
调用该函数时,若互斥锁未加锁,则上锁,返回 0;
若互斥锁已加锁,则函数直接返回失败,即 EBUSY
参数:
mutex:互斥锁地址。
返回值:
成功返回0
失败返回非0。
(6)解锁
#include <pthread.h>
int pthread_mutex_unlock(pthread_mutex_t *mutex);
功能:
对指定的互斥锁解锁。
参数:
mutex:互斥锁地址。
返回值:
成功:0
失败:非0 错误码
5互斥锁的案例demo
1多个任务之间抢资源不用互斥锁demo
2多任务之间使用互斥锁demo 3死锁---》不允许 6读写锁
2任务 一读 一写> 互斥锁
3个以上任务 多读,多写 > 读写锁
1.读写锁概念
读读:多读不影响
读写:读写互斥 多读不可写 写不可读
写写:互斥
再简单点:
1)如果某线程申请了读锁,其它线程可以再申请读锁,但不能申请写锁。
2)如果某线程申请了写锁,其它线程不能申请读锁,也不能申请写锁。
POSIX 定义的读写锁的数据类型是: pthread_rwlock_t
2、操作流程及API
(1)定义锁
pthread_rwlock_t rwlock;
(2)初始化读写锁
#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_t my_rwlock = PTHREAD_RWLOCK_INITIALIZER;
这种方法等价于使用 NULL 指定的 attr 参数调用 pthread_rwlock_init() 来完成动态初始化,
不同之
处在于 PTHREAD_RWLOCK_INITIALIZER 宏不进行错误检查。
3销毁读写锁
#include <pthread.h>
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
功能:
用于销毁一个读写锁,并释放所有相关联的资源(所谓的所有指的是由 pthread_rwlock_init() 自
动申请的资源) 。
参数:
rwlock:读写锁指针。
返回值:
成功:0
失败:非 0 错误码
4申请读锁–》写会阻塞
#include <pthread.h>
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
功能:
以阻塞方式在读写锁上获取读锁(读锁定)。
如果没有写者持有该锁,并且没有写者阻塞在该锁上,则调用线程会获取读锁。
如果调用线程未获取读锁,则它将阻塞直到它获取了该锁。一个线程可以在一个读写
锁上多次执行读锁定。
线程可以成功调用 pthread_rwlock_rdlock() 函数 n 次,但是之后该线程必须调用
pthread_rwlock_unlock() 函数 n 次才能解除锁定。
参数:
rwlock:读写锁指针。
返回值:
成功:0
失败:非 0 错误码
5试着申请读锁
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
用于尝试以非阻塞的方式来在读写锁上获取读锁。
如果有任何的写者持有该锁或有写者阻塞在该读写锁上,则立即失败返回。
6申请写锁 ----> 读和写 都阻塞
#include <pthread.h>
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
功能:
在读写锁上获取写锁(写锁定)。
如果没有写者持有该锁,并且没有写者读者持有该锁,则调用线程会获取写锁。
如果调用线程未获取写锁,则它将阻塞直到它获取了该锁。
参数:
rwlock:读写锁指针。
返回值:
成功:0
失败:非 0 错误码
7试着申请写锁
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
用于尝试以非阻塞的方式来在读写锁上获取写锁。
如果有任何的读者或写者持有该锁,则立即失败返回。
8读写锁解锁
#include <pthread.h>
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
功能:
无论是读锁或写锁,都可以通过此函数解锁。
参数:
rwlock:读写锁指针。
返回值:
成功:0
失败:非 0 错误码
3读写锁案例 — demo
9、条件变量 1.条件变量概述
与互斥锁不同,条件变量是用来等待而不是用来上锁的,条件变量本身不是锁!
条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。
通常条件变量和互斥锁同时使用。 条件变量的两个动作:
条件不满, 阻塞线程
当条件满足, 通知阻塞的线程开始工作。
条件变量的类型: pthread_cond_t。
2、条件变量的操作以及API
(1)定义条件变量
pthread_cond_t cond
(2) 条件变量初始化
静态初始化:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
动态初始化
#include <pthread.h>
int pthread_cond_init(pthread_cond_t *cond,
const pthread_condattr_t *restrict attr);
功能:
初始化一个条件变量
参数:
cond:指向要初始化的条件变量指针。
attr:条件变量属性,通常为默认值,传 NULL 即可
返回值:
成功:0
失败:非 0 错误号
(3)销毁条件变量
#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);
功能:
销毁一个条件变量
参数:
cond:指向要初始化的条件变量指针
返回值:
成功:0
失败:非 0 错误
(4) 等待 条件满足
#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *cond,
pthread_mutex_t *mutex);
功能:
阻塞等待一个条件变量
a) 阻塞等待条件变量 cond(参 1)满足
b) 释放已掌握的互斥锁(解锁互斥量)相当于 pthread_mutex_unlock(&mutex);
a) b) 两步为一个原子操作 —> 不可中断。
c) 当被唤醒,pthread_cond_wait 函数返回时,解除阻塞并重新申请获取互斥锁
pthread_mutex_lock(&mutex);
参数:
cond:指向要初始化的条件变量指针
mutex:互斥锁
返回值:
成功:0
失败:非 0 错误号
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex,
const struct timespec *restrict abstime);
功能:
限时等待一个条件变量
参数:
cond:指向要初始化的条件变量指针
mutex:互斥锁
abstime:绝对时间
返回值:
成功:0
失败:非 0 错误号
5)唤醒等待在条件变量上阻塞的线程
#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 错误号
3、条件变量的工作原理
消费者 买货物 生产者生产货物
如果 货物数量为 0 不能买
当 货物 数量超过 50 ,生产者 不生产
------> 生产者 2个 消费者 3个
注意:1.在消费者中使用if可能出现负数,这是因为在条件变量wait中等待到广播的时候,资源已经被消耗掉为0,由于if只执行一次,继续往下执行可能出现负数 while解决
2.爆仓:多个生产者先判断while,如果多个生产者在while中循环,突然条件允许,多个生产者取锁生产。 在while前用信号量解决
10、信号量 1信号量概述
信号由内核空间管理完成进程间通信
信号量由用户空间管理完成进程、线程间通信
信号量广泛应用于进程或线程间通讯的同步与互斥,信号量的本质是一个非负的整数计数器 ----> 控制对临界资源的访问!!!
2信号量的互斥
不管多少个任务(进程、线程),只要是互斥, 只需要1一个信号量 并且初始化为1
3信号量的同步
同步: 按照一定顺序
有几个任务,就创建几个信号量 ----> 先执行的任务信号量为1 其他都为 0 ----> 所有任务 P 自己的信号量, V 下一个任务的信号量
4信号量操作 1 定义信号量
sem_t sem;
2 信号量初始化
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared,unsigned int value);
功能:
创建一个信号量并初始化它的值。一个无名信号量在被使用前必须先初始化。
参数:
sem:信号量的地址。
pshared:等于0,信号量在线程间共享;不等于0,信号量在进程间共享。
value:信号量的初始值。
返回值:
成功返回0,
失败返回-1。
3 p操作 信号量-1–》没有资源可用,会阻塞
#include <semaphore.h>
int sem_wait(sem_t *sem);
功能:
将信号量的值减1,若信号量的值小于0,此函数会引起调用者阻塞。
参数:
sem:信号量地址。
返回值:
成功返回0,失败返回-1。
int sem_trywait(sem_t *sem);
功能:
尝试将信号量减一,如果信号量的值为 0 不阻塞,立即返回 ,大于 0 可以减一
参数:
sem: 信号量的地址
返回值:
成功返回 0
失败返回 -1
4 v操作 信号量+1
#include <semaphore.h>
int sem_post(sem_t *sem);
功能:
将信号量的值加1并发出信号唤醒等待线程。
参数:
sem:信号量地址。
返回值:
成功返回0,失败返回-1。
5获取信号量的计数值
#include <semaphore.h>
int sem_getvalue(sem_t *sem, int *sval);
功能:
获取sem标识的信号量的值,保存在sval中。
参数:
sem:信号量地址。
sval:保存信号量值的地址。
返回值:
成功返回0,失败返回-1。
6销毁信号量
返回值:
成功返回0
失败返回-1。
5信号量的互斥demo
6信号量的同步demo 7无名信号量流程 (用于血缘关系进程) 1.无名信号量概述
无名信号量用于 血缘关系进程间 的同步和互斥
无名信号量用 mmap创建
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);
功能:
将磁盘文件映射到当前进程的虚拟内存中
参数:
addr:虚拟内存的地址,默认一般为NULL,由系统指定一块合适的内存
len :映射内存的大小 —> 要申请的映射区的长度
prot:可读、可写、可执行、不可访问
PROT_READ 可读
PROT_WRITE 可写
flags 标志位:
MAP_SHARED 共享的 —> 虚拟内存内的数据改变会立即同步到磁盘文件
MAP_PRIVATE 私有的 —> 虚拟内存内的数据的改变不会同步到磁盘文件
MAP_ANONYMOUS //匿名映射,映射区不与任何文件关联。
fd :
文件描述符需要打开一个文件
offset:
指定一个偏移位置 ,默认为0(从文件开始位置映射)
返回值
成功 返回映射区的首地址
失败 返回 MAP_FAILED ((void *) -1)
#include <sys/mman.h>
#include <semaphore.h>
//MAP_ANONYMOUS匿名映射 -1不需要文件描述符
sem_t *sem = (sem_t *)mmap(NULL,sizeof(sem_t), PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
sem_init、sem_wait、sem_post、sem_destroy都一样。
由于上面sem已经是信号量指针变量,在使用信号量API中 不要再对sem取地址。
2、血缘进程间互斥demo
3、无名信号量同步demo 4、拓展 使用共享内存实现血缘进程间互斥 demo
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
shmid:
共享内存的IPC对象ID
shmaddr:
若为NULL:共享内存会被attach到一个合适的虚拟地址空间,建议使用NULL;不为NULL:系统会根据参数及地址边界对齐等分配一个合适的地址
shmflg:
0 —> 读写!!
IPC_RDONLY:附加只读权限,不指定的话默认是读写权限;
IPC_REMAP:替换位于shmaddr处的任意既有映射:共享内存段或内存映射;
返回值:
共享内存段的地址
共享内存 --> 涉及 shmid —> 必须先ftok 获取key 再shmget 创建共享内存 才能映射
8有名信号量流程(可用于非血缘关系进程) 记得删除 1.有名信号量创建
#include <fcntl.h> /* For O_* constants /
#include <sys/stat.h> / For mode constants */
#include <semaphore.h>
创建的是POSIX信号量 无法用 ipcs -s 访问,使用sem_getvalue()函数
//信号量存在时 使用
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:和open函数的flag一致 O_CREAT O_RDWR
mode:磁盘权限0666
value:信号量的初始值
返回值:
成功就是信号量的地址
失败为NULL
2.关闭有名信号量
#include <semaphore.h>
int sem_close(sem_t *sem);
功能:
关闭有名信号量。
参数:
sem:指向信号量的指针。
返回值:
成功返回0
失败返回-1。
3删除有名信号量文件
#include <semaphore.h>
int sem_unlink(const char *name);
功能:
删除信号量的文件。
参数:
name:信号量文件名。
返回值:
成功返回0
失败返回-1。
4无血缘关系的互斥
5无血缘关系的同步 6、案例: 使用共享内存 来实现两个进程A B 的通讯 , A 写 B进程读 ----> A先 B后 线程进程总结 1.进程间通讯 同一主机: 原始方式:无名管道pipe(int fd[2])有名管道 mkfifo 信号 siganl system V IPC:都需要key 消息队列 mmap 共享内存 信号灯集 Posix:的有名信号量、无名信号量
不同主机:BSD:套接字
注意: 所以ipcs 不能查
System V的信号量一般用于进程同步, 且是内核持续的, api为
semget
semctl
semop
Posix的有名信号量一般用于进程同步, 有名信号量是内核持续的. 有名信号量的api为
sem_open
sem_close
sem_unlink
Posix的无名信号量一般用于线程同步, 无名信号量是进程持续的, 无名信号量的api为
sem_init
sem_destroy
速记集合 原始方式 无名管道 有名管道 signal system V IPC 都需要key 消息队列 共享内存 mmap映射 信号灯集 POIXS 无名信号量 mmap映射实现 有名信号量 2线程间和进程间的同步与互斥 线程:互斥锁mutex 读写锁rwlock 条件变量+互斥锁cond+mutex 信号量 sem_t sem
进程:血缘进程(无名信号量 sem_t *sem=mmap) 任意进程(有名信号量sem_t *sem=open)
信号量操作 sem_wait sem_post
速记集合
线程
互斥锁
读写锁
信号量
条件变量 +互斥锁
pthread_cond_init()//初始化
pthread_cond_wait()//条件不满足阻塞
pthread_cond_signal()一个或pthread_cond_broadcast()全部
pthread_cond_destroy()
进程
无名信号量
有名信号量