当前位置: 首页 > news >正文

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编译四步骤

在这里插入图片描述

注:

  1. 编译阶段会将.i文件转换为汇编语言的文件
  2. 链接阶段会将生成的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 来指定终极目标

例:
在这里插入图片描述

  1. 先编译.o 再 链接的目的是为了在修改某源码时,不会重新编译其他源码,而是直接将其他源码的.o 文件和修改后的文件编译生成的.o文件链接即可,降低了编译运行整个文件的速度(编译阶段最耗时)。
  2. makefile会根据规则中目标和依赖的修改时间来判断依赖是否修改,进而判断是否需要修改目标文件。
  3. 可用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;
}

运行结果:
在这里插入图片描述

  1. 在父进程fork时,会创建出子进程,父进程中的pid变量保存子进程的pid,而创建出的子进程中pid变量保存0。父进程可以打印代码中所有打印的,而子进程只能打印fork以后打印的。
  2. 在打印时父进程需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;
}

运行结果:
在这里插入图片描述

对标准输出加锁

总结:
c8eeb21edbf23385f.png)

初始化有动态和静态初始化两种

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;
}

运行结果:
在这里插入图片描述

这里没有对共享资源在操作时上锁。在多消费者或多生产者模型下,可能会有问题。因此需要对共享资源上互斥锁。

http://www.dtcms.com/a/320347.html

相关文章:

  • 使用winsw把SpringBoot项目注册成window服务
  • javaweb开发之会话_过滤器_监听器
  • 【感知机】感知机(perceptron)学习算法的收敛性
  • 【Unity3D实例-功能-镜头】第三人称视觉-镜头优化
  • 基于深度学习的污水新冠RNA测序数据分析系统
  • Linux机器可直接使用的自动化编译文件
  • AGV_ads通讯exe的创建
  • Java日志技术:从基础到实战
  • 蒙文OCR识别技术难点实现及应用场景剖析
  • Transformer:Attention is all you need
  • HCIP | BGP综合实验报告册
  • PMP项目管理:理解PMP、PMP学什么 / 适合谁学 / Project Management Professional / 项目管理专业人士
  • uat是什么
  • Day32--动态规划--509. 斐波那契数,70. 爬楼梯,746. 使用最小花费爬楼梯
  • 华为服务器如何部署Mindie镜像
  • 俄文识别技术,高精度识别,支持多场景多平台
  • 天猫商品评论API技术指南
  • 如何在NVIDIA H100 GPU上用Ollama以最高性能运行大语言模型
  • 2025数字马力一面面经(社)
  • 【2025最新版】火狐浏览器(官方版)安装-附教程
  • Ubuntu 22 下脚本登录MFA堡垒机
  • 一个自动定位并查询天气的工具(c语言)
  • 八股文智力题
  • 目标检测数据集 - 高架视角道路车辆检测数据集下载「包含VOC、COCO、YOLO三种格式」
  • 为什么会有反射
  • js中的设计模式
  • UnivNet论文分析(20210615)
  • Flutter报错...Unsupported class file major version 65
  • 接口测试-mock测试
  • sigfillset 函数详解