linux系统编程
参考视频:黑马linux系统编程
文章目录
- 1. gcc编译
- 1.1 gcc编译四步骤
- 1.2 gcc常用参数
- 2. 库
- 2.1 静态库和动态库理论比对
- 2.2 制作静态库
- 2.3 静态库使用及头文件对应
- 2.4 动态库制作理论
- 2.5 动态库制作
- 2.5 数据段合并--链接阶段
- 3. gdb调试工具
- 3.1 基础指令
- 3.2 其他指令
- 4. makefile项目管理
- 4.1 基础规则
- 4.2 makefile一个规则
- 4.3 两个函数
- 4.4 三个自动变量和模式规则
- 4.5 实例
- 4.6 实例2
- 5. 文件I/O
- 5.1 系统调用
- 5.2 open和close函数
- 5.3 read和write函数
- 5.4 系统调用和库函数的比较
- 5.5 文件描述符
- 5.6 阻塞和非阻塞
- 5.7 fcntl函数
- 5.8 lseek函数
- 5.9 传入传出参数
- 6. 文件操作
- 6.1 文件存储
- 6.2 stat函数 和 lstat函数
- 6.3 access函数
- 6.4 link和unlink函数
- 6.5 隐式回收
- 6.6readlink函数 和 rename函数
- 7. 目录操作
- 7.1 getcwd和chdir函数
- 7.2 文件、目录权限
- 7.3 目录操作函数
- 7.4 递归遍历目录-实现ls-R
- 7.5 dup和dup2
- 7.6 fcntl1实现dup
- 8. 进程
- 8.1 相关概念
- 8.2 环境变量
- 8.3 fork函数
- 8.4 循环创建多个子进程
- 8.5 父子进程共享
- 8.6 父子进程gdb调试
- 8.7 exec函数族
- 8.8 回收进程
- 8.9 wait函数--回收子进程
- 8.10 waitpid -- 回收子进程(指定进程)
- 8.11 进程间通信常见方式
- 8.12 管道通信(通过内核缓冲区通信)
- 8.13 命名管道FIFO
- 8.14 文件用于进程间通信(外存磁盘通信)
- 8.15 存储映射I/O --- mmap
- 9. 信号
- 9.1 信号的概念和机制
- 9.2 信号相关的事件和状态
- 9.3 信号四要素和常规信号
- 9.4 kill命令和kill函数
- 9.5 alarm函数
- 9.6 setitimer函数
- 9.7 信号集操作函数
- 9.8 signal实现信号捕捉
- 9.9 sigaction实现信号捕捉
- 9.10 信号捕捉的特性
- 9.11 内核实现信号捕捉的过程
- 9.11 借助信号捕捉回收子进程
- 9.12 中断系统调用
- 10. 进程组和会话
- 10.1 概念和特性
- 10.2 会话
- 10.3 守护进程
- 11. 线程
- 11.1 概念
- 11.2 三级映射
- 11.3 线程共享和非共享
- 11.4 创建线程---线程控制原语
- 11.5 循环创建多个子线程
- 11.6 pthread_exit函数 -- 线程退出
- 11.7 pthread_join--线程回收(类似waitpid)
- 11.8 pthread_cancel函数--杀死线程
- 11.9 pthread_detach--线程分离
- 11.10 线程控制原语与进程控制原语对比
- 11.11 线程属性
- 11.12 线程注意事项
- 12 同步与互斥
- 12.1 线程同步
- 12.2 互斥锁的使用
- 12.3 死锁
- 12.4 读写锁
- 12.5 条件变量
- 12.6 条件变量------wait函数
- 12.7 生产者-消费者模型
- 12.8 信号量--PV操作
1. gcc编译
1.1 gcc编译四步骤
注:
- 编译阶段会将.i文件转换为汇编语言的文件
- 链接阶段会将生成的hello.o 以及头文件中包含的 .o文件一起链接,生成可执行文件
1.2 gcc常用参数
例:
-I : 指定头文件目录
-g:增加调试信息,可用gdb调试
-Wall:提示所有警告信息
-D:动态注册宏定义(常用于调试)
2. 库
2.1 静态库和动态库理论比对
静态库: 将库文件与源文件编译成一个可执行文件
动态库: 在使用库函数时,才去调用动态库加载函数
2.2 制作静态库
例:
2.3 静态库使用及头文件对应
(1)创建静态库中函数的声明
(2)添加静态库头文件
(3)输入所有警告信息并运行结果
2.4 动态库制作理论
在链接时,将源代码生成的二进制文件中使用动态库函数的进行位置替换,将源码暂存的位置替换为动态库函数的位置。
例: printf函数是在动态库内,源码生成的汇编文件在对动态库函数进行@plt标记。在链接阶段,通过符号链接找到动态库中的printf函数,进行替换。也就是地址回填。
生成与位置无关的代码:
2.5 动态库制作
(1)生成动态库
(2)运行调用动态库
报错原因:
(3)解决报错方法一:设置链接库环境变量
(4)其余解决方法,第三种不推荐
2.5 数据段合并–链接阶段
一个内存页大小为4k,为了减少空间的浪费,将数据段合并
.rodata和.text 合并为一页,ro(只读数据段)
.bss和 .data 合并为一页, rw(读写数据段)
3. gdb调试工具
3.1 基础指令
3.2 其他指令
随着函数调用而在stack上开辟的一片内存空间,用于存放函数调用时产生的局部变量和临时值。
4. makefile项目管理
用途:
4.1 基础规则
例:
若规则需要的依赖不存在,则查看其他规则是否能生成该依赖
例:规则中依赖的test.o本来不存在,另外的规则可以生成test.o规则
4.2 makefile一个规则
makefile会默认将第一组规则的目标设置为终极目标,完成终极目标则结束。可以使用all 来指定终极目标
例:
- 先编译.o 再 链接的目的是为了在修改某源码时,不会重新编译其他源码,而是直接将其他源码的.o 文件和修改后的文件编译生成的.o文件链接即可,降低了编译运行整个文件的速度(编译阶段最耗时)。
- makefile会根据规则中目标和依赖的修改时间来判断依赖是否修改,进而判断是否需要修改目标文件。
- 可用all指定终极目标。
4.3 两个函数
%格式:模式匹配符,用于定义通用规则时使用。
例如:%.c 代表任何以 .c 结尾的文件,并且可以与模式规则一起使用,以生成相应的目标文件。%.o : %.c 表示任何.o文件都可以由相应的.c文件生成。使用 %.c 时,make 会根据需要自动推导出依赖关系。
例:
obj = $(patsubst %.c, %.o, $(src))
图中的 obj = $(patsubst *.c, *.o, $(src))会出错
执行clean规则:
-n 参数表示显示要执行的命令
makefile中 rm前的 “ - ”表示错误依然执行
4.4 三个自动变量和模式规则
(1)
使用自动变量的原因:便于扩展
例:
测试可扩展性:添加一个乘法
输出结果:
(2)静态模式规则:
例:
(3) 伪目标:为了防止clean ,ALL等目标被当前目录中的同名文件夹影响,使用.PHONY将他们设置为伪目标
(4)参数:
例: make -f m(m是makefile的文件名)
4.5 实例
要求:
- src中保存所有.c文件
- inc中保存所有.h文件
- obj中保存所有.o文件
具体实现:
(1)mymath.h : 实现一个宏,包含头文件和声明
(2)test.c: 引入自定义头文件
(3)makefile
当匹配obj的路径时,若在.c文件前不加 ./src ,则% 匹配的是 ./src,那么后面的 ./obj/ %.o = ./obj/src/*.o 。而加了./src ,则 % .c= * .c。后面也是同理
结果:
4.6 实例2
将当前目录下的.c文件全部编译生成可执行文件
makefile:
5. 文件I/O
5.1 系统调用
5.2 open和close函数
(1)
系统调用中的open函数:(命令模式下输入 K 即可跳转)
参数:
pathname是文件路径,flags表示只读/只写/读写
mode表示文件的权限,在创建新文件的时候使用第二种open
返回值:返回文件描述符——文件打开表中该文件的索引号
(2)
umask是权限掩码,默认为022。而文件的默认权限为 777-022=755,目录的默认权限为666 -022 = 644。
mode &~umask 表示 mode与 ~umask作 位与运算。
注: “ | ” 在c语言中表示按位运算
(3)实例:
(4)常见错误
errno.h 中包含出错时的错误号errno
string.h 中包含streror函数,结果为错误号对应的具体错误
(5) open/close函数总结
5.3 read和write函数
(1)
read函数:
write函数:
(2)使用read和write函数实现复制功能
mycp.c:
makefile:
指定文件运行makefile:
结果:
错误处理:perror
5.4 系统调用和库函数的比较
使用fputc函数时:
buf每次读一个字节,传入程序缓冲区(蓝框),当程序缓冲区的字节数到达一定值(如4096),则将程序缓冲区的所有字节一并使用系统调用传入内核缓冲区
而使用系统调用,设置缓冲区大小为1时:
buf每次读一个字节后就换传入内核缓冲区,没有程序缓冲区的暂缓存。因此降低了程序读写的效率。因此直接使用系统调用并不一定比库函数效率高。
5.5 文件描述符
5.6 阻塞和非阻塞
5.7 fcntl函数
实例:
flags |= O_NONBLOCK;做位或运算,将位图中的O_NONBLOCK位置变为1
5.8 lseek函数
返回值 = 起始偏移量 + 偏移量
例1: 文件读写同一偏移位置:
第21行使用lseek将当前位置重新移到文件起始位置。 如果不使用lseek,则会导致后续代码读的时候的位置在文件末尾(由于前面写文件,将位置移到末尾)
例2:使用lseek获取文件大小
将偏移起始位置设置为文件末尾,lseek返回当前位置距离起始位置(文件起始位置。而非偏移起始位置)的偏移量
例3:使用lseek拓展文件大小
lseek设置偏移位置后必须有I/O操作才能实现文件大小变化。也可以使用truncate来改变文件大小
查看文件的十进制和十六进制:
5.9 传入传出参数
6. 文件操作
6.1 文件存储
(1)inode
(2)目录项dentry
6.2 stat函数 和 lstat函数
头文件 # include<sys/stat.h>
stat结构体内部成员:
st_mode相关函数:
stat和lstat的区别:stat会穿透符号链接,lstat不会
实例:
6.3 access函数
6.4 link和unlink函数
link函数: 给oldpath文件添加新的硬链接newpath。成功返回0,失败返回-.1
unlink函数: 删除文件的一个目录项。成功返回0,失败返回-1,并设置errno
unlink删除目录项会在进程结束后由操作系统择机删除
实例:利用link 和unlink实现MV操作.
myMV.c:
将当前目录的test.c移动到当前目录改名为testMV.c:
6.5 隐式回收
6.6readlink函数 和 rename函数
当读软链接时,输出为软链接指向的文件/目录的路径
7. 目录操作
7.1 getcwd和chdir函数
7.2 文件、目录权限
目录的内容是目录项
7.3 目录操作函数
DIR是目录流
dirent的具体格式: 常用inode和dname
readdir成功返回一个dirent指针,失败返回NULL,并设置errno。若读到底,则返回NULL,不设置errno。
实现ls:
myLS.c
结果:
7.4 递归遍历目录-实现ls-R
mylsr.c
#include<stdio.h>
#include<stdlib.h>
#include<sys/stat.h>
#include<string.h>
#include<dirent.h>
#include<unistd.h>void recursion(char* path){DIR* dp = opendir(path); // 打开目录,返回目录流struct dirent *sdp; // 接收readdir的返回值while(sdp=readdir(dp)){if(sdp->d_name[0] == '.')continue;// 构建完整路径char newpath[256];snprintf(newpath, sizeof(newpath), "%s/%s", path, sdp->d_name);// 使用stat判断是否为目录struct stat buf; // 保存stat的结果int ret = stat(newpath, &buf); // 使用拼接后的路径if(ret == -1){perror("stat error");continue;}if(S_ISDIR(buf.st_mode)){ // 如果是目录,则继续递归recursion(newpath);}printf("%s\t", sdp->d_name);}printf("\n");closedir(dp); // 关闭目录return;
}int main(int argc, char* argv[]){ // argc 是参数的个数recursion(argv[1]);return 0;
}
运行结果
可能出错的地方:
-
未分配内存的指针:在recursion函数中,newpath是一个未初始化的指针,直接使用sprintf写入会导致段错误。
-
错误的stat调用:你在调用stat时使用了目录路径而不是完整的文件路径,这会导致无法正确获取文件信息。
-
路径拼接问题:在递归处理子目录时,路径拼接不正确。
7.5 dup和dup2
dup函数用于将复制一个文件描述符,两个文件描述符可以对同一个文件操作。
实例:将输出重定向到fd1指向的文件
总结:
dup2中newfd赋值为oldfd,即后一个文件重定向为前一个文件描述符
7.6 fcntl1实现dup
实例:
fd1 =3
fd2 =4
fd3 = 7
8. 进程
8.1 相关概念
(1)进程和程序
(2)虚拟内存和物理内存的映射关系
内核区的pcb映射到物理内存的同一块区域。那一块区域会保存多个pcb
(3)pcb
8.2 环境变量
例:
8.3 fork函数
创建子进程;
实例:创建子进程并打印pid
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>int main(int argc, char* argv[]){printf("before fork-1\n");printf("before fork-2\n");printf("before fork-3\n");printf("before fork-4\n");// 创建子进程,在fork时,生成子进程// 父进程返回子进程pid,子进程返回0pid_t pid = fork(); if(pid == -1){perror("fork error");exit(1);}else if(pid == 0){ // 等于0表示为子进程printf("子进程打印~~~~\n");printf("子进程pid=%d,子进程的父进程pid=%d\n",getpid(), getppid());}else if(pid > 0){ // 大于0时,表示其为父进程printf("父进程打印~~~~\n");printf("子进程id=%d,父进程id=%d,父进程的父进程id=%d\n",pid, getpid(),getppid());sleep(1);}printf("================end file\n");return 0;
}
运行结果:
- 在父进程fork时,会创建出子进程,父进程中的pid变量保存子进程的pid,而创建出的子进程中pid变量保存0。父进程可以打印代码中所有打印的,而子进程只能打印fork以后打印的。
- 在打印时父进程需sleep,否则,父进程先结束,则子进程的父进程pid会打印出1。
8.4 循环创建多个子进程
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>int main(int argc, char* argv[]){printf("before fork-1\n");printf("before fork-2\n");printf("before fork-3\n");printf("before fork-4\n");int i = 0;for(i = 0;i<5;i++){pid_t pid = fork(); if(pid == -1){perror("fork error");exit(1);}else if(pid == 0){ // 等于0表示为子进程printf("子进程%d打印~~~~\n", i + 1);printf("子进程pid=%d,子进程的父进程pid=%d\n",getpid(), getppid());break; // 防止子进程再创建子进程}else if(pid > 0){ // 大于0时,表示其为父进程printf("父进程打印~~~~\n");printf("子进程id=%d,父进程id=%d,父进程的父进程id=%d\n",pid, getpid(),getppid());sleep(1);}}printf("================end file\n");return 0;
}
运行结果:
8.5 父子进程共享
父子进程间遵循 “读时共享,写时复制” 的原则
即任一进程修改共享内容时还是会发生写时复制,就是为自己再创建一个副本
共享的内容:代码段,全局变量和静态变量,文件描述符和其他系统资源
不共享的内容:数据段(Data Segment)、堆(Heap)和栈(Stack)—这些在fork时,会产生副本,当时与父进程内容相同但不是同一个;以及进程不同的那些,如pid…
8.6 父子进程gdb调试
例:
8.7 exec函数族
(1)execlp函数:
实例:
exec_fork.c
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>int main(int argc, char* argv[]){int i = 0;pid_t pid = fork(); if(pid == -1){perror("fork error");exit(1);}else if(pid == 0){ // 等于0表示为子进程execlp("ls","ls","-l","-h", NULL);perror("execlp error"); // 只有execlp出错才执行exit(1);}else if(pid > 0){ // 大于0时,表示其为父进程printf("parentID:%d\n", getpid());sleep(1);}printf("================end file\n");return 0;
}
运行结果:
execlp执行系统命令,第一个参数是路径,会在$PATH下找;第二个参数是命令的参数,一般和参数一相同;后续是可变参数
若execlp执行成功,则会将其代码后续换成execlp需执行的命令,不会执行perror;若执行失败,则会执行后续代码,perror执行
参数末尾必须加上NULL
(2)execl函数: 传入可执行文件路径,名字和参数执行
注 : 第一个参数必须是可执行文件的路径,即把.c文件编译后的文件的路径
例:
执行结果:
execl 与execlp的区别是,execl需传入可执行文件的路径,而execl第一个参数传入的文件名,会默认在PATH环境变量下查找该可执行文件。后面的参数类似。
实例:将进程信息保存在文件中,使用dup2,execlp
(3)
(4) exec族一般规律
总结:
8.8 回收进程
(1)孤儿进程
(2)僵尸进程
若子进程变为僵尸进程,kill父进程后,子进程被init进程接管。init进程发现僵尸进程会自动清除
8.9 wait函数–回收子进程
判断status的宏函数:
实例:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>int main(int argc, char* argv[]){pid_t pid, wpid;int status;pid = fork();if(pid == 0){ // 子进程printf("--child, my id = %d, going to sleep\n", getpid());sleep(10);printf("-------child die\n");exit(72);}else if(pid > 0){ //父进程// wpid = wait(NULL); 不关心子进程结束状态的写法wpid = wait(&status); // 如果子进程未终止,父进程会阻塞在这if(wpid == -1){perror("wait error");exit(1);}if(WIFEXITED(status)){ // 判断进程是否正常结束// 使用WEXITSTATUS获取子进程的退出状态(exit的参数)printf("child exit with %d\n", WEXITSTATUS(status));}if(WIFSIGNALED(status)){ // 判断进程是否异常终止// 使用WTERMSIG获取终止进程的信号编号(异常终止均为信号终止)printf("child kill with %d\n", WTERMSIG(status));}}return 0;
}
运行结果:
(1)
(2)
注: kill -数字 进程号 =》 表示使用不同的信号终止进程
总结:
8.10 waitpid – 回收子进程(指定进程)
返回值:
实例:回收第三个子进程
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>int main(int argc, char* argv[]){pid_t pid, tmppid, wpid;int i = 0;for(i = 0; i < 5; i++){pid = fork();if(pid == 0) { // 子进程break;}if(i == 2) { // 指定第三个子进程tmppid = pid; // 记录第三个子进程的pid}}if(i == 5) { // 父进程执行// sleep(5);printf("I am a parent, my child id is %d\n", tmppid);wpid = waitpid(tmppid, NULL, WNOHANG); // 设置非阻塞// wpid = waitpid(tmppid, NULL, 0); // 设置阻塞,等同于waitprintf("wpid = %d\n", wpid);}else{ // 子进程执行sleep(i);printf("I am %d th child\n", i + 1);}return 0;
}
运行结果:
父进程设置sleep
父进程未设置sleep
实例2:回收多个子进程
总结:
注:wpid若设置了WNOHANG,则未回收子进程,不会改变status的值
8.11 进程间通信常见方式
内核空间的pcb存储在同一块物理块
8.12 管道通信(通过内核缓冲区通信)
(1)管道的特质
总结:
(2)管道的基本用法
fd[2]中保存使用管道的文件描述符
pipe函数的图示:创建匿名管道
pipe会创建管道,并保存读端和写端的文件描述符
实例:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>int main(int argc, char* argv[]){int fd[2], ret;pid_t pid;ret = pipe(fd); // 将两个描述符返回在fd中,fd[0]读,fd[1]写if(ret < 0){perror("pipe error");exit(1);}char* str = "hello, pipe\n";char buf[1024]; // 定义缓冲区pid = fork(); // 创建子进程// 父子进程共享文件描述符if(pid > 0){ // 父进程close(fd[0]); // 关闭读端write(fd[1], str, strlen(str));close(fd[1]);}else if(pid == 0) { // 子进程close(fd[1]); // 关闭写端// count保存实际接受的字节数int count = read(fd[0], buf, sizeof(buf)); write(STDOUT_FILENO, buf, count); // 将读到的内容打印close(fd[0]);}return 0;
}
运行结果:
总结:
(3)管道的读写行为
总结:
实例2:使用管道实现" ls | wc -l " =》 输出当前文件夹中的文件数
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>int main(int argc, char* argv[]){int fd[2], ret;pid_t pid;ret = pipe(fd); // 创建管道if(ret == -1){perror("pipe error");exit(1);}pid = fork(); // 创建子进程if(pid == 0){ // 子进程执行ls命令close(fd[0]); // 关闭读端// 将标准输出重定向为管道的写端dup2(fd[1], STDOUT_FILENO); // 执行ls命令execlp("ls", "ls", NULL);perror("ls error");exit(1);}else if(pid > 0){ // 父进程执行wc -l命令close(fd[1]); // 关闭写端// 将标准输入重定向为管道的读端(wc -l默认从标准输入读)dup2(fd[0], STDIN_FILENO);// 执行wc -l命令execlp("wc", "wc", "-l", NULL);perror("wc -l error");exit(1);}return 0;
}
运行结果:
注: 父进程实现读操作,子进程实现写操作。否则,父进程会先结束,使子进程变为孤儿进程
**实例3:使用兄弟进程的管道实现" ls | wc -l ", 父进程回收子进程 **
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<sys/wait.h>int main(int argc, char* argv[]){int fd[2], ret, i;pid_t pid;ret = pipe(fd); // 创建管道if(ret == -1){perror("pipe error");exit(1);}for(i = 0;i < 2; i++){pid = fork(); // 创建子进程if(pid == 0)break;}if(i == 0){ // 兄进程执行ls命令close(fd[0]); // 关闭读端// 将标准输出重定向为管道的写端dup2(fd[1], STDOUT_FILENO); // 执行ls命令execlp("ls", "ls", NULL);perror("ls error");exit(1);}else if(i== 1){ // 弟进程执行wc -l命令close(fd[1]); // 关闭写端// 将标准输入重定向为管道的读端(wc -l默认从标准输入读)dup2(fd[0], STDIN_FILENO);// 执行wc -l命令execlp("wc", "wc", "-l", NULL);perror("wc -l error");exit(1);}else if( i == 2){ // 父进程执行回收子进程close(fd[0]);close(fd[1]);wait(NULL);wait(NULL);}return 0;
}
运行结果:
注: 父进程需要关闭读写两端,保证管道是单向流动的
(4)管道缓冲区大小
(5)管道的优劣
8.13 命名管道FIFO
成功返回0, 失败返回-1; mode是权限设置
实例:创建一个命名管道
实例2: 利用fifo实现两个无血缘关系的进程间通信
写进程:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<fcntl.h>
#include<sys/stat.h>int main(int argc, char* argv[]){int fd;char buf[4096];// 创建管道int ret = mkfifo("testfifo", 0644);if(ret == -1){perror("mkfifo error");exit(1);}fd = open("testfifo", O_WRONLY);if(fd < 0){perror("open error");exit(1);}int i = 0;while(1){sprintf(buf, "hello, %d\n", i++); // 格式化输入bufwrite(fd, buf, strlen(buf));sleep(1);}return 0;
}
读进程:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<sys/stat.h>
#include<fcntl.h>int main(int argc, char* argv[]){int fd, len; // len保存读到的字节数char buf[4096];fd = open("testfifo", O_RDONLY);if(fd < 0){perror("open error");exit(1);}while(1){len = read(fd, buf, sizeof(buf));write(STDOUT_FILENO, buf, len);sleep(1);}return 0;
}
运行结果:
8.14 文件用于进程间通信(外存磁盘通信)
两个进程对同一个文件进行读写操作,实现通信
8.15 存储映射I/O — mmap
(1)mmap的函数原型
mmap的返回值为一个指向共享映射区的指针。具体的指针类型需根据取决于映射内存的用途或访问方式。例: 访问文本/字符串,使用char* ;访问二进制数据,使用int*/float* …
munmap函数:释放共享映射区
传入的参数分别为共享映射区的首地址和映射区大小
总结:
其中,flags的参数中, MAP_SHARED表示修改共享映射区中的内容会同时修改磁盘上的内容;而MAP_PRIVATE则不修改。
MAP_SHARED表示对映射的更新对其他相关进程可见。 映射此文件,并贯穿到基础文件中。
(2) mmap建立映射区
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/mman.h>
#include<string.h>
#include<fcntl.h>int main(int argc, char* argv[]){int fd, ret;char* ptr = NULL; // 用于接收返回值,保存映射区地址// 创建文件fd = open("testmmap", O_RDWR |O_CREAT | O_TRUNC, 0664);// 对文件进行扩容ftruncate(fd, 20); // 也可以使用lseek扩容int len = lseek(fd, 0, SEEK_END); // 使用lseek获取文件长度printf("len = %d\n", len);ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);if(ptr == MAP_FAILED){perror("mmap error");exit(1);}// 使用ptr对文件进行读写操作strcpy(ptr, "hello, mmap");printf("----------%s\n", ptr);// 使用munmap释放共享映射区ret = munmap(ptr, len);if(ret == -1){perror("munmap error");exit(1);}return 0;
}
运行结果:
使用mmap创建共享映射区, 使用munmap释放映射区
(3)mmap的注意事项
注: 不同进程使用同一个文件建立mmap,是一份
(4)mmap的保险写法
(5)父子间通信–mmap
实例:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/mman.h>
#include<string.h>
#include<fcntl.h>// 全局变量,读时共享,写时复制
int var = 100;int main(int argc, char* argv[]){int fd, ret;int* ptr; // 用于接收返回值,保存映射区地址// 创建文件fd = open("test", O_RDWR |O_CREAT | O_TRUNC, 0664);// 对文件进行扩容ftruncate(fd, 4); // 也可以使用lseek扩容int len = lseek(fd, 0, SEEK_END); // 使用lseek获取文件长度printf("len = %d\n", len);// flag必须为MAP_SHARED,保证对映射区的操作多个进程间能共享ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);if(ptr == MAP_FAILED){perror("mmap error");exit(1);}close(fd);// 创建子进程pid_t pid = fork();if(pid <= -1){perror("fork error");exit(1);}else if(pid == 0){ // 子进程写映射区*ptr = 2000;var = 1000;printf("child. *ptr = %d, var = %d\n", *ptr, var);}else if(pid > 0){ // 父进程读映射区sleep(1);printf("parent . *ptr = %d, var = %d\n", *ptr, var);}// 使用munmap释放共享映射区ret = munmap(ptr, len);if(ret == -1){perror("munmap error");exit(1);}return 0;
}
运行结果:
总结:
(6)非血缘关系通信
实例:
写进程:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/mman.h>
#include<string.h>
#include<fcntl.h>typedef struct student{int num;char name[1024];int age;
}student;int main(int argc, char* argv[]){int fd;student* p = NULL;student stu = {1, "xiaoming", 18};fd = open("testwr", O_RDWR|O_CREAT|O_TRUNC, 0664);if(fd == -1){perror("open error");exit(1);}// 对文件进行扩容ftruncate(fd, sizeof(stu));// 创建共享映射区p = mmap(NULL, sizeof(stu), PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);if(p == MAP_FAILED){perror("mmap error");exit(1);}close(fd);// 写映射空间while(1){// 将内存中一块区域复制到共享内存memcpy(p, &stu, sizeof(stu));stu.num++;sleep(1);}return 0;
}
memcpy函数对内存进行操作
读进程:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/mman.h>
#include<string.h>
#include<fcntl.h>typedef struct student{int num;char name[1024];int age;
}student;int main(int argc, char* argv[]){int fd;student* p = NULL;student stu;fd = open("testwr", O_RDWR|O_CREAT|O_TRUNC, 0664);if(fd == -1){perror("open error");exit(1);}// 对文件进行扩容ftruncate(fd, sizeof(stu));// 创建共享映射区p = mmap(NULL, sizeof(stu), PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);if(p == MAP_FAILED){perror("mmap error");exit(1);}close(fd);// 读映射空间while(1){printf("num=%d, name=%s, age=%d\n", p->num, p->name, p->age);sleep(1);}return 0;
}
运行结果:
当多个进程使用 mmap 映射同一个文件(并指定 MAP_SHARED 标志)时,它们最终会访问同一块物理内存(在不同进程的虚拟地址可能不同)
当指定MAP_PRIVATE时,遵循“读时共享,写时复制”
总结:
(7) 匿名映射 – 血缘关系进程间通信
实例:
总结:
9. 信号
9.1 信号的概念和机制
(1)概念
(2)机制
总结:
信号都是由内核产生,人为只能驱使内核产生和处理信号。
9.2 信号相关的事件和状态
(1)产生信号
(2)递达和未决
(3)信号的处理方式
(4)阻塞信号集和未决信号集
图示:
总结:
9.3 信号四要素和常规信号
(1)通过kill -l 查看信号,其中前31个为常规信号,有默认事件
32-64为实时信号,一般会捕捉使用
(2) 信号四要素
只有每个信号的事件发生后,信号才会递送,但不一定递达
默认处理动作:
(3)常见信号一览表
其中,9和19号默认处理动作不能被设置为忽略和捕捉,只能执行
后面的不常用,用的时候再查
9.4 kill命令和kill函数
实例:循环创建五个子进程,父进程使用kill函数终止任一子进程
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<signal.h>int main(int argc, char* argv[]){pid_t childID[5], pid; // 保存子进程idint i = 0;for(i = 0; i < 5; i++){pid = fork();if(pid == -1){perror("fork error");exit(1);}else if(pid == 0){printf("child %d : id = %d\n", i + 1, getpid());while(1){sleep(1);}exit(0);}else{childID[i] = pid; // 保存子进程的id}}sleep(2); // 父进程等待一会儿,确保所有子进程都启动int killNUM = 2; // 设置要杀死的子进程索引printf("Parent is killing child %d (PID: %d)\n", killNUM + 1, childID[killNUM]);int ret = kill(childID[killNUM], SIGKILL);if(ret == -1){perror("kill error");exit(1);}sleep(5); // 保证子进程被完全杀死,SIGKILL 是异步的,操作系统需要时间处理// 父进程等待所有进程退出for(i = 0 ; i < 5; i++){int status; // 设置状态pid_t wpid = waitpid(childID[i], &status, WNOHANG);if (wpid == -1) {perror("waitpid error");} else if (wpid > 0) {if (WIFSIGNALED(status)) {printf("Child %d (PID: %d) was killed by signal %d\n", i+1, wpid, WTERMSIG(status));} else {printf("Child %d (PID: %d) exited normally\n", i+1, wpid);}} else {printf("Child %d (PID: %d) is still running\n", i+1, childID[i]);}}printf("All children have exited. Parent is terminating.\n");return 0;
}
运行结果:
注:其他几个子进程并未结束和回收,需手动结束并回收
总结:
9.5 alarm函数
返回值为当前时间距上一个闹钟设置时间相差的秒数
实例:测试计算机一秒能写多少个数
#include<stdio.h>
#include<unistd.h>int main(int argc, char* argv[]){int i = 0;alarm(1);while(++i){printf("i=%d\n", i);}return 0;
}
运行结果:使用time ./alarm运行
real - user - sys 的剩余时间用于等待,本次是等待屏幕I/O
总结:
用户时间:用户CPU时间是指程序在用户模式下执行时消耗的CPU时间,即执行应用程序代码时所花费的时间
内核时间:系统CPU时间是程序在内核模式下执行时消耗的CPU时间,即执行操作系统内核代码时所花费的时间。
9.6 setitimer函数
使用不同的计时方式,发送的信号不同
参数的结构体类型:
实例:周期5s发送hello,world,定时第一次为2s
#include<stdio.h>
#include<unistd.h>
#include<sys/time.h>
#include<signal.h>void myfunc(int sigo){printf("hello world\n");
}int main(int argc, char* argv[]){signal(SIGALRM, myfunc); // 捕捉信号struct itimerval cur, old;// 设置周期发信号间隔为5.0scur.it_interval.tv_sec = 5;cur.it_interval.tv_usec = 0;// 设置第一次信号发送时间为2.0scur.it_value.tv_sec = 2;cur.it_value.tv_usec = 0;int ret = setitimer(ITIMER_REAL, &cur, &old);while(1);return 0;
}
若间隔设置为0.0s, 则只会发送一次
总结:
9.7 信号集操作函数
(1)信号集设定
(2)sigprocmask函数
(3)sigpending函数
(4) 信号集操作函数使用原理
(5)实例:屏蔽信号2,即SIGINT=按键输入ctrl+c, 查看未决信号集
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<signal.h>
#include<stdlib.h>void print_set(sigset_t* set){int i;for(i = 1; i <= 32; i++){if(sigismember(set, i)) // 查看编号i的信号是否在信号集中putchar('1');elseputchar('0');}printf("\n");return;
}int main(int argc, char* argv[]){sigset_t set, oldset, pedset; // 设置集合int ret = 0;sigemptyset(&set); // 清空集合sigaddset(&set, SIGINT); // 将SIGINT=2信号添加进集合// 将set与信号屏蔽集做位或,添加新的屏蔽,oldset接收原来的信号屏蔽ret = sigprocmask(SIG_BLOCK, &set, &oldset);if(ret == -1){perror("sigprocmask error");exit(1);}while(1){// 查看未决信号集,pedset保存返回值ret = sigpending(&pedset); if(ret == -1){perror("sigpending error");exit(1);}// 打印查询到的未决信号集(前32位)print_set(&pedset);sleep(1);}return 0;
}
运行结果:打印前32个信号的未决信号集,在屏蔽信号2,并出现信号2后,未决信号集出现信号2
此时ctrl + c不能终止进程,发送信号2,使未决信号集添加信号2
屏蔽某信号后,未决信号集不会马上添加该信号,而是发送被屏蔽的信号后才添加(例如本例中的信号2)
总结:
9.8 signal实现信号捕捉
实质是函数让内核捕捉信号
定义了一个函数指针,命名位sighandler,作为传入参数和返回值
返回值为原来的函数指针,handler为新传入的函数指针
signum 作为handler函数的输入
函数名即可表示函数的地址,不需要&
实例:屏蔽信号2,SIGINT,即ctrl +c
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<signal.h>
#include<stdlib.h>void print_catch(int signum){printf("signum = %d\n", signum); // 打印屏蔽的信号return;
}int main(int argc, char* argv[]){signal(SIGINT, print_catch);while(1);return 0;
}
运行结果:每ctrl+c一次,打印一次
9.9 sigaction实现信号捕捉
sigaction结构体:
sa_mask只在捕捉函数处理期间生效。此时捕捉期间会屏蔽mask | sa_mask的所有信号
实例:
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<signal.h>
#include<stdlib.h>void print_catch(int signum){printf("catch you: %d\n", signum); // 打印屏蔽的信号return;
}int main(int argc, char* argv[]){struct sigaction act, oldact;act.sa_handler = print_catch; // 设置回调函数sigemptyset(&act.sa_mask); // 将执行捕捉函数的屏蔽集清空,只在捕捉函数执行间生效act.sa_flags = 0; // 0表示执行期间会屏蔽当前信号int ret = sigaction(SIGINT, &act, &oldact);if(ret == -1){perror("sigaction error");exit(1);}while(1);return 0;}
运行结果:
若信号再被捕捉前已经被屏蔽,则在屏蔽期间无法被捕捉
9.10 信号捕捉的特性
总结:
特性1 应改为 信号屏蔽字为 mask和sa_mask 的并集
特性3中后32个实时信号支持排队,前面的常规信号只保留一个
9.11 内核实现信号捕捉的过程
步骤1中时间片结束也会从用户态-》内核态(但是要等下次调度该进程才处理)
步骤4中处理完回调函数需返回调用者,借用一个特殊的系统调用sigreturn 返回内核空间
9.11 借助信号捕捉回收子进程
(1)SIGCHLD的产生条件
(2)实例:借助SIGCHLD回收子进程
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<signal.h>
#include<stdlib.h>
#include<sys/wait.h>void catch(int signum){int status;pid_t wpid;// 循环wait,保证能清除僵尸进程while((wpid = waitpid(-1, &status, 0)) != -1){if(WIFEXITED(status)){printf("子进程正常结束\n");}}return;
}int main(int argc, char* argv[]){pid_t pid;int i;sigset_t set;sigemptyset(&set);sigaddset(&set, SIGCHLD); // 添加阻塞sigprocmask(SIG_BLOCK, &set, NULL);// 创建15个子进程for(i = 0 ; i < 15 ; i++){pid = fork();if(pid == 0)break;}if(i == 15){ // 父进程,捕捉SIGCHLD信号,回收子进程struct sigaction act;// 初始化actact.sa_handler = catch;sigemptyset(&act.sa_mask);act.sa_flags = 0;// 捕捉SIGCHLD信号sigaction(SIGCHLD, &act, NULL);sigprocmask(SIG_UNBLOCK, &set, NULL);printf("parent process : id = %d\n", getpid());while(1){}}else{ // 子进程sleep(2);printf("child process: id = %d\n", getpid());}return 0;
}
运行结果:
问:若处理回收其中一个子进程时,其他子进程也死亡,但信号被屏蔽不能处理,怎么保证回收了所有子进程
答: 代码12行,使用while循环进行回收,保证回收当前子进程时,其他子进程死亡变为僵尸进程,本次会将所有僵尸进程回收。
问:若在注册捕捉函数前,子进程死亡,则不会被父进程wait回收
答:代码23-26行以及43行,分别对SIGCHLD信号进行阻塞和解除阻塞。保证在注册前死亡的子进程发送的SIGCHLD信号会至少有一个被阻塞,解除阻塞后对该信号进行捕捉。处理当前进程以及前面死亡的僵尸进程。
9.12 中断系统调用
10. 进程组和会话
10.1 概念和特性
会话是多个进程组的集合
10.2 会话
(1)创建会话
新会话无终端
(2)getsid函数
(3)setsid函数
例:
10.3 守护进程
(1)基本概念
(2)守护进程创建模型
总结:
改变工作目录的作用:防止目录被卸载,以及防止守护进程导致目录不能卸载
创建守护进程实例:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<fcntl.h>
#include<sys/stat.h>void sys_err(const char* str){perror(str);exit(1);
}int main(int argc, char* argv[]){pid_t pid;int ret, fd;pid = fork();if(pid == -1)sys_err("fork error");else if(pid > 0)exit(0); // 父进程直接终止pid = setsid(); // 创建新会话if(pid == -1)sys_err("setsid error");ret = chdir("/home/autumn"); // 改变工作目录if(ret == -1)sys_err("chdir error");umask(022); // 重设文件权限掩码,文件权限=777-umask=755close(STDIN_FILENO); // 关闭标准输入-0fd = open("/dev/null", O_RDWR); // 打开空洞文件,fd=0// 将标准输出和标准错误重定向为空洞文件,等同于关闭dup2(fd, STDOUT_FILENO);dup2(fd, STDERR_FILENO);while(1); // 模拟执行守护进程return 0;
}
运行结果:
11. 线程
11.1 概念
图示:cpu执行处理线程
A进程中有三个线程,cpu会独立处理执行三个线程
总结:
线程号 ≠ 线程id
使用ps -LF + id查看线程号
3275号进程的LWP为其线程号,NLWP为线程个数
11.2 三级映射
三级页表指页目录,页表,页面,找到具体的页面
三级页表找物理地址
11.3 线程共享和非共享
(1)共享
虽然信号处理方式共享,但不推荐信号和线程混用
线程间全局变量(除了errno)共享,因为全部变量在.data中
(2)不共享
(3)线程优、缺点
11.4 创建线程—线程控制原语
(1)pthread_self函数
线程ID 是 lu 类型的
(2)pthead_create函数
实例:创建线程
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>void sys_err(const char* str){perror(str);exit(1);
}void* tfunc(void *arg){ // 线程的回调函数printf("thread: id = %lu\n", pthread_self());return NULL;
}int main(int argc, char* argv[]){pthread_t tid;// 创建线程int ret = pthread_create(&tid, NULL, tfunc, NULL);if(ret != 0){ // 若成功返回0,失败则返回errnoprintf("pthread_create error");exit(1);}printf("main thread: id = %ld\n", pthread_self());sleep(1); // 保证主进程不会执行太快,导致线程未执行就结束return 0;
}
运行结果:
编译运行时需添加 -pthread
总结:
11.5 循环创建多个子线程
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>void sys_err(const char* str){perror(str);exit(1);
}void* tfunc(void *arg){ // 线程的回调函数int i = (int)arg; // 强转为intprintf("I'm %dth thread:pid = %d tid = %lu\n", pthread_self());return NULL;
}int main(int argc, char* argv[]){pthread_t tid;int i = 0, ret;// 创建多个线程for(i = 0; i < 5 ; i++){// 传i的值使用值传递,借助强转ret = pthread_create(&tid, NULL, tfunc, (void*)(i+1));if(ret != 0){ // 若成功返回0,失败则返回errno,不会设置errnoprintf("pthread_create err");exit(1);}}printf("main thread:pid = %d id = %ld\n",getpid(), pthread_self());sleep(1); // 保证主进程不会执行太快,导致线程未执行就结束return 0;
}
运行结果:
问:为什么23行采用值传递,而不传指针
答:因为 i 是一个变量,传地址的话,子进程取 i 值时可能 i 已经修改过了
11.6 pthread_exit函数 – 线程退出
实例: 退出第三个线程
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>void sys_err(const char* str){perror(str);exit(1);
}void* tfunc(void *arg){ // 线程的回调函数int i = (int)arg; // 强转为intif(i == 2){ // 第三个线程退出// exit(0); // 退出进程// return NULL; // 返回调用者--主线程pthread_exit(NULL); // 退出线程,参数为void*类型}printf("I'm %dth thread:pid = %d tid = %lu\n", i + 1, getpid(), pthread_self());return NULL;
}int main(int argc, char* argv[]){pthread_t tid;int i = 0, ret;// 创建多个线程for(i = 0; i < 5 ; i++){// 传i的值使用值传递,借助强转ret = pthread_create(&tid, NULL, tfunc, (void*)i);if(ret != 0){ // 若成功返回0,失败则返回errno,不会设置errnoprintf("pthread_create err");exit(1);}}printf("main thread:pid = %d id = %ld\n",getpid(), pthread_self());// sleep(1); // 保证主进程不会执行太快,导致线程未执行就结束// 在主线程调用相当于退出主线程,但不影响其他线程,进程继续执行pthread_exit(void*(0));
}
运行结果:
注:exit是退出当前进程; return是返回到函数的调用者;
pthread_exit是退出当前线程
总结:
11.7 pthread_join–线程回收(类似waitpid)
传出参数为指针的指针类型
实例:将结构体指针作为传入值传出并回收
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>void sys_err(const char* str){perror(str);exit(1);
}typedef struct thread_val{ // 设置一个结构体int val;char str[256];
}thread_val;void* tfn(void* arg){ // 设置线程回调函数thread_val* tval = malloc(sizeof(thread_val));tval->val = 100;strcpy(tval->str, "hello, world");return tval; // 返回结构体指针
}int main(int argc, char* argv[]){pthread_t tid;thread_val* retval;int ret = pthread_create(&tid, NULL, tfn, NULL);if(ret != 0){printf("pthread_create error: %s\n", strerror(ret));exit(1);}// 回收void* 类型, 使用void**类型作为传出参数接收ret = pthread_join(tid, &retval); // 回收线程if(ret != 0){printf("pthread_join error: %s\n", strerror(ret));exit(1);}printf("chlid thread:val = %d, str = %s\n", retval->val, retval->str);free(retval);; // 释放内存pthread_exit(NULL);
}
运行结果:
总结:
11.8 pthread_cancel函数–杀死线程
默认线程只有在到达取消点时才会响应取消请求。
实例
实例2:对比三种回收方式
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>void* tfn1(void* arg){ // return 返回printf("thread 1 running\n");return (void*)111;
}void* tfn2(void* arg){ // pthread_exit终止线程printf("thread 2 running\n");pthread_exit((void*)222);
}void *tfn3(void* arg){ // pthread_cancel杀死线程while(1){// 主动设置取消点,防止函数不会进入内核,导致取消线程不发生pthread_testcancel();}return (void*)333;
}int main(int argc, char* argv[]){pthread_t tid;void* tret = NULL;// 创建多个线程pthread_create(&tid, NULL, tfn1, NULL);pthread_join(tid, &tret);printf("thread 1 exit code = %d\n", (int)tret);pthread_create(&tid, NULL, tfn2, NULL);pthread_join(tid, &tret);printf("thread 2 exit code = %d\n", (int)tret);pthread_create(&tid, NULL, tfn3, NULL);sleep(3);pthread_cancel(tid); // 使用pthread_cancel杀死线程,无返回值pthread_join(tid, &tret); // 回收失败printf("thread 3 exit code = %d\n", (int)tret);pthread_exit((void*)0);
}
运行结果:
注:如果pthread_join回收的线程被pthread_cancel杀死, 则其传出参数的值会设置为PTHREAD_CANCELED == (void*)-1,因此这次线程3输出的值为-1
注2:pthread_cancel进入内核才能杀死线程,因此如果线程内一直未进入内核,则无法杀死该线程。可以设置pthread_testcancel(),判断是否出现pthread_cancel,保证杀死线程的触发。
总结:
11.9 pthread_detach–线程分离
实例:
运行结果:
fprintf 是 C 语言的标准格式化输出函数,可以向指定的文件流(如 stdout、stderr 或文件)写入格式化数据。
stderr(标准错误流)是默认的输出错误信息的流,与 stdout(标准输出流)不同,它通常不会被缓冲,能立即显示错误信息(即使程序崩溃或重定向 stdout)。
总结:
分离后的线程执行完回自动回收清理,不需要单独的pthread_join清理
11.10 线程控制原语与进程控制原语对比
11.11 线程属性
(1)线程属性结构体
(2)线程属性初始化
(3)设置分离–创建时设置线程属性
实例:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>void* tfn(void* arg){printf("thread:tid = %lu\n", pthread_self());
}int main(int argc, char* argv[]){pthread_attr_t attr; // 设置属性pthread_t tid;int ret;ret = pthread_attr_init(&attr); // 初始化属性if(ret != 0){fprintf(stderr, "pthread_attr_init error:%s\n", strerror(ret));exit(1);}ret = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); // 设置脱离if(ret != 0){fprintf(stderr, "pthread_attr_setdetachstate error:%s\n", strerror(ret));exit(1);}ret = pthread_create(&tid, &attr, tfn, NULL);if(ret != 0){fprintf(stderr, "pthread_create error:%s\n", strerror(ret));exit(1);}ret = pthread_attr_destroy(&attr); // 销毁属性if(ret != 0){fprintf(stderr, "pthread_attr_destroy error:%s\n", strerror(ret));exit(1);}sleep(1);printf("main thread: tid = %lu\n", pthread_self());// 使用pthread_join回收线程,判断线程是否已经成功分离ret = pthread_join(tid, NULL);if(ret != 0){fprintf(stderr, "pthread_join error:%s\n", strerror(ret));exit(1);}return 0;
}
运行结果:参数错误说明已经成功分离
总结:
11.12 线程注意事项
12 同步与互斥
12.1 线程同步
互斥量: 不具备强制性,建议锁
总结:
12.2 互斥锁的使用
(1)主要函数
(2)图示
(3)实例:主子线程分别完整大小写的hello,world
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
#include<time.h>pthread_mutex_t mutex; // 设置一把互斥锁(全局)void* tfunc(void *arg){ // 线程的回调函数srand(time(NULL));int ret;while(1){ret = pthread_mutex_lock(&mutex); // 加锁if(ret != 0){ // 若成功返回0,失败则返回errnofprintf(stderr,"pthread_mutex_lock error:%s\n", strerror(ret));exit(1);}printf("hello ");sleep(rand() % 3); // 模拟长时间操作共享资源,导致cpu易主printf("world\n"); // printf默认行缓冲,需要\n刷新缓冲区ret = pthread_mutex_unlock(&mutex); // 解锁if(ret != 0){ // 若成功返回0,失败则返回errnofprintf(stderr,"pthread_mutex_unlock error:%s\n", strerror(ret));exit(1);}sleep(rand() % 3);}return NULL;
}int main(int argc, char* argv[]){pthread_t tid;srand(time(NULL));int ret = pthread_mutex_init(&mutex, NULL); // 初始化锁if(ret != 0){ // 若成功返回0,失败则返回errnofprintf(stderr,"pthread_mutex_init error:%s\n", strerror(ret));exit(1);}// 创建线程ret = pthread_create(&tid, NULL, tfunc, NULL);if(ret != 0){ // 若成功返回0,失败则返回errnofprintf(stderr,"pthread_create error:%s\n", strerror(ret));exit(1);}while(1){ret = pthread_mutex_lock(&mutex); // 加锁if(ret != 0){ // 若成功返回0,失败则返回errnofprintf(stderr,"pthread_mutex_lock error:%s\n", strerror(ret));exit(1);}printf("HELLO ");sleep(rand() % 3);printf("WORLD\n"); // printf默认行缓冲,需要\n刷新缓冲区ret = pthread_mutex_unlock(&mutex); // 解锁if(ret != 0){ // 若成功返回0,失败则返回errnofprintf(stderr,"pthread_mutex_unlock error:%s\n", strerror(ret));exit(1);}sleep(rand() % 3);}pthread_join(tid, NULL);ret = pthread_mutex_destroy(&mutex); // 毁灭锁if(ret != 0){ // 若成功返回0,失败则返回errnofprintf(stderr,"pthread_mutex_destroy error:%s\n", strerror(ret));exit(1);}return 0;
}
运行结果:
对标准输出加锁
总结:
初始化有动态和静态初始化两种
12.3 死锁
图示:
12.4 读写锁
当写优先的情况下,如果读进程和写进程同时排队,无论当前是写进程还是读进程持有锁,持有锁的进程结束,都会是写进程先获得锁。(为了 防止写进程饥饿)
总结:
主要使用的函数:
与互斥锁类似
实例:写优先读写锁
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>int counter = 0; // 全局变量,作为共享资源
pthread_rwlock_t rwlock; // 读写锁void* tfn_w(void* arg){int t, i = (int)arg;int ret;while(1){ret = pthread_rwlock_wrlock(&rwlock); // 加写锁if(ret != 0){fprintf(stderr, "pthread_rwlock_wrlock error:%s\n", strerror(ret));exit(1);}t = counter;usleep(1000);printf("=========write %d : %lu: counter = %d,++counter = %d\n", i, pthread_self(), t, ++counter);ret = pthread_rwlock_unlock(&rwlock); // 解锁if(ret != 0){fprintf(stderr, "pthread_rwlock_unlock error:%s\n", strerror(ret));exit(1);}usleep(5000);}return NULL;
}void* tfn_r(void* arg){int i = (int)arg;int ret;printf("============\n");while(1){ret = pthread_rwlock_rdlock(&rwlock); // 加读锁if(ret != 0){fprintf(stderr, "pthread_rwlock_rdlock error:%s\n", strerror(ret));exit(1);}printf("-----------------read %d : %lu: counter = %d\n", i, pthread_self(), counter);ret = pthread_rwlock_unlock(&rwlock); // 解锁if(ret != 0){fprintf(stderr, "pthread_rwlock_unlock error:%s\n", strerror(ret));exit(1);}usleep(2000);}return NULL;
}int main(void){pthread_t tid[8]; // 创建8个线程int i, ret;ret = pthread_rwlock_init(&rwlock, NULL); // 初始化锁if(ret != 0){fprintf(stderr, "pthread_rwlock_init error:%s\n", strerror(ret));exit(1);}for(i = 0; i < 3; i++){ // 创建写线程ret = pthread_create(&tid[i], NULL, tfn_w, (void*)i);if(ret != 0){fprintf(stderr, "pthread_create error:%s\n", strerror(ret));exit(1);}}for(i = 3; i < 8; i++){ret = pthread_create(&tid[i], NULL, tfn_r, (void*)i);if(ret != 0){fprintf(stderr, "pthread_create error:%s\n", strerror(ret));exit(1);}}for(i = 0; i < 8; i++){ret = pthread_join(tid[i], NULL);if(ret != 0){fprintf(stderr, "pthread_join error:%s\n", strerror(ret));exit(1);}}ret = pthread_rwlock_destroy(&rwlock); // 破坏锁if(ret != 0){fprintf(stderr, "pthread_rwlock_destroy error:%s\n", strerror(ret));exit(1);}return 0;
}
运行结果:
12.5 条件变量
主要函数
12.6 条件变量------wait函数
图示:
该函数会使线程陷入阻塞,并解锁。等条件满足,再上锁,执行
该函数的前提是先上锁,再判断是否满足条件
12.7 生产者-消费者模型
(1)图示
(2)实例
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>void sys_err(const char* str, const int ret){fprintf(stderr, "%s error:%s\n", str, strerror(ret));exit(1);
}typedef struct msg{ // 使用一个链表的结点作为条件变量struct msg* next;int num;
}msg;// 静态初始化 一个条件变量和一个互斥锁
pthread_cond_t has_produce = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;// 设置一个指针,指向链表
msg* head = NULL;// 消费者
void* consumer(void* arg){msg* mp;while(1){pthread_mutex_lock(&mutex); // 加锁// 条件等待while(head == NULL){pthread_cond_wait(&has_produce, &mutex); // 阻塞并释放锁}// 解除阻塞mp = head;head = mp->next;pthread_mutex_unlock(&mutex);printf("consumer: %lu,-----consume:%d\n", pthread_self(), mp->num);free(mp);sleep(rand() % 3);}return NULL;
}// 生产者
void* producer(void* arg){msg* mp;while(1){mp = malloc(sizeof(msg));mp->num = rand() % 100;printf("producer: %lu,-------produce:%d\n", pthread_self(), mp->num);pthread_mutex_lock(&mutex); // 访问临界区,加锁mp->next = head;head = mp;pthread_mutex_unlock(&mutex); // 访问完立即解锁pthread_cond_signal(&has_produce); // 通知唤醒线程sleep(rand() % 3);}return NULL;
}int main(){pthread_t pid, cid; // 生产者和消费者的线程idint ret;// 创建生产者线程ret = pthread_create(&pid, NULL, producer, NULL);if(ret != 0)sys_err("pthread_create", ret);// 创建消费者线程ret = pthread_create(&cid, NULL, consumer, NULL);if(ret != 0)sys_err("pthread_create", ret);// 回收线程ret = pthread_join(pid, NULL);if(ret != 0)sys_err("pthread_join", ret);ret = pthread_join(cid, NULL);if(ret != 0)sys_err("pthread_join", ret);ret = pthread_mutex_destroy(&mutex); // 销毁锁if(ret != 0)sys_err("pthread_mutex_destroy", ret);pthread_cond_destroy(&has_produce);return 0;
}
运行结果:
总结:
12.8 信号量–PV操作
(1)主要函数
总结:
(2)图示
(3)实例:使用信号量实现生产者–消费者模型
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
#include<semaphore.h>sem_t product, empty; // 设置产品和空位的信号量
#define num 5 // 设置一个宏,保存空位的数量
int queue[num]; // 创建一个队列,为共享区void* producer(void* arg){int i = 0;while(1){sem_wait(&empty);// P操作,有空位时生产产品,无空位时阻塞queue[i] = rand() % 1000; // 操作共享区sem_post(&product); // V操作,生产出产品printf("producer------- produce: %d\n", queue[i]);i = (i + 1 ) % num;sleep(rand() % 2);}return NULL;
}void* consumer(void* arg){int i = 0;while(1){sem_wait(&product);printf("consumer------- consume: %d\n", queue[i]);sem_post(&empty); // V操作i = (i + 1 ) % num;sleep(rand() % 2);}return NULL;
}int main(void){pthread_t pid, cid; // 设置生产者和消费者线程号// 初始化信号量sem_init(&product, 1, 0); // 参2 = 1 用于进程间同步sem_init(&empty, 1, num);// 创建线程pthread_create(&pid, NULL, producer, NULL);pthread_create(&cid, NULL, consumer, NULL);// 回收线程pthread_join(pid, NULL);pthread_join(cid, NULL);// 销毁信号量sem_destroy(&product);sem_destroy(&empty);return 0;
}
运行结果:
这里没有对共享资源在操作时上锁。在多消费者或多生产者模型下,可能会有问题。因此需要对共享资源上互斥锁。