linux系统编程

参考视频:黑马linux系统编程

文章目录

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,子进程返回0
    pid_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…

文件描述符存储在内核空间pcb中,会在fork时继承,但父子间的文件描述符关闭不互相影响

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); // 设置阻塞,等同于wait
        printf("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++); // 格式化输入buf
        write(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; // 保存子进程id
    int 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.0s
    cur.it_interval.tv_sec = 5;
    cur.it_interval.tv_usec = 0;
    // 设置第一次信号发送时间为2.0s
    cur.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');
        else
            putchar('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;
        // 初始化act
        act.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=755

    close(STDIN_FILENO); // 关闭标准输入-0
    fd = 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中

文件描述符存储在pcb中,同一进程的线程共享pcb,因此关闭其中一个fd,会影响其他线程的pcb

(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,失败则返回errno
        printf("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;  // 强转为int
    printf("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,不会设置errno
            printf("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;  // 强转为int
    if(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,不会设置errno
            printf("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,失败则返回errno
            fprintf(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,失败则返回errno
            fprintf(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,失败则返回errno
        fprintf(stderr,"pthread_mutex_init error:%s\n", strerror(ret));
        exit(1);
    }

    // 创建线程
    ret = pthread_create(&tid, NULL, tfunc, NULL);
    if(ret != 0){ // 若成功返回0,失败则返回errno
        fprintf(stderr,"pthread_create error:%s\n", strerror(ret));
        exit(1);
    }
    
    while(1){
        ret = pthread_mutex_lock(&mutex); // 加锁
        if(ret != 0){ // 若成功返回0,失败则返回errno
            fprintf(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,失败则返回errno
            fprintf(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,失败则返回errno
        fprintf(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; // 生产者和消费者的线程id
    int 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;
}

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值