本次总结我们系统变成所用到的知识点

系统编程

进程概念

冯诺依曼体系结构

Linux知识点总结—2_基础IO

我们只需要了解五大硬件单元:

  1. 输入设备,例如:键盘
  2. 输出设备,例如:显示器
  3. 存储器,例如:内存
  4. 运算器,例如:算数逻辑单元,寄存器
  5. 控制器,例如:程序计数器、指令寄存器、指令译码器、时序产生器和操作控制器

这里我们需要知道的重点知识是存储器作为所有设备之间的数据缓冲地带,因此所有设备都是围绕着存储器进行数据处理的

操作系统

  • 概念:其本质是一种软件,通常是一个计算机系统的核心组件。它负责管理和协调计算机的各种硬件和软件资源,为应用程序和用户提供一个可靠、一致且方便的工作环境
  • 作用:让计算机的使用更加方便
  • 管理思想:为用户提供一个稳定,高效,可靠和方便的计算机环境
  • 系统调用接口:操作系统提供给用户用于访问内核功能的接口
  • 系统调用接口与库函数的关系:库函数中封装了系统调用接口

进程概念

  • 概念:进程就是运行中的程序。在操作系统中,进程是系统对程序运行过程中的动态描述—pcb,通过这些描述信息,系统可以实现对程序运行的管理及调度。在LInux中,进程就是正在运行程序的实例,是资源分配的基本单位。
  • 描述信息包括:进程ID,进程状态,执行路径,创建时间,所属关系,相关进程连接等信息。

在Linux中,我们只要输入命令top,即可查看所有正在运行进程的信息

Linux知识点总结—2_进程概念_02

进程状态

  • Linux下的进程状态可以分为以下六种状态:
  1. 运行态:表示进程正在执行或等待执行
  2. 可中断休眠态:进程正在等待某个条件满足而处于休眠状态,同时可以被接收到来自外部的中断信号而唤醒
  3. 不可中断休眠态:进程处于等待某些事件的状态,并且在等待期间不会被唤醒或中断
  4. 停止状态:表示进程的执行暂时停止,可能是受到了调试器的控制或接收到了停止信号
  5. 死亡态:进程已经终止或完成执行,不再具有任何可执行的代码,并且没有任何资源需要被释放或清理
  6. 僵尸态:表示进程已经终止,但其父进程尚未通过wait()系统调用获取到其终止状态。即程序退出,但是资源并没有被完全释放

这里我们主要需要了解,僵尸进程是如何产生的,以及它的危害,处理和避免方法都有什么

  • 产生:子进程先于父进程退出,为了保存自己退出的状态信息,因此资源不会被直接释放,需要父进程获取退出状态信息之后,再释放资源
  • 危害:会造成资源泄漏,进程表满载和进程状态信息混乱等情况
  • 处理:退出父进程,让子进程成为孤儿进程或者让父进程等待子进程退出
  • 避免方法:进程等待——创建子进程后,父进程会一直等待子进程退出,并获取子进程的状态信息用于释放资源。
  • 特殊进程
  • 孤儿进程
  • 产生:父进程先于子进程退出,子进程成为孤儿进程
  • 特性:
  1. 运行在后台
  2. 父进程变为1号进程
  3. 子进程一定不会成为僵尸进程
  • 守护/精灵进程
  • 一种特殊的孤儿进程,希望一个进程可以默默的运行在系统中,不受任何外界因素影响(终端,登录会话)
  • 作用:让系统知道一个进程当前如何被调度
  • 环境变量
  • 概念:操作系统或软件应用程序可访问和使用的动态值。它们是一种在计算机系统中存储和传递配置信息的机制。包含各种信息,如目录路径、系统参数、运行时选项等
  • 作用:便捷的对运行环境进行配置,可以进行进程之间的数据传递
  • 操作指令包括:
  • env,set(查看所有变量),export(声明环境变量),unset

env操作可以看到整个设备的环境变量

Linux知识点总结—2_系统编程_03

  • 函数操作:
  • getenv(获取指定环境变量),setenv,putenv;extern char **environ;int main(int argc,char *argv,char *env[])
  • 程序地址空间
  • 本质:Linux中·,操作系统给每个进程使用mm_struct所描述的一个虚拟地址空间,进程中访问变量所使用的地址都是虚拟地址
  • 优点:给进程描述了一个连续的,完整的地址空间,进程可以随意使用地址,但是数据在物理内存中可以随意存储,实现数据在物理内存的离散式存储,提高了内存利用率
  • 虚拟地址与物理地址的关系
  1. 内存的管理方式:
  • 分段式:将地址空间分成多段,便于编译器对程序中的数据地址进行管理,段号+段内偏移量
  • 分页式:将地址空间与物理内存进行分块,分成大量的小的内存区域进行管理,充分利用内存碎片,提高内存利用率,通过页表进行内存访问控制
  • 段页式:先进行分段,在每个段内进行分页式管理

利用free -h指令,显示系统中的内存使用情况及内存管理方式

Linux知识点总结—2_系统编程_04

  1. 通过虚拟地址获取物理地址
  • 虚拟地址组成:段号+段内页号+业内偏移
  • 系统中的信息:段表+段内页表
  • 寻找方式:段号->段表->段内页表+段内页号->页表项+业内偏移 ->物理地址
  • 内存置换
  • 本质:当物理内存不够用时,系统会将内存中某些数据,置换到磁盘交换分区中,进行存储,空出来的内存,进行新数据的处理
  • 内存置换算法:LRU(最久未使用),LFU(最久最少未使用)等
  • 缺页中断:一旦一个数据被进行置换到了交换分区,这个数据在被访问时就会触发缺页中断,表示要访问的数据不在内存中。此时编译器会再次将数据加载到内存中

进程控制

创建

  • 接口:
  1. pid_t fork()
  • 父子进程代码共享,数据独有
  • 写实拷贝技术:子进程通过复制父进程的方式被创建,复制了虚拟地址空间,复制了页表,因此父进程所拥有的数据子进程也有,并在开始时映射的是同一块物理内存,但是进程应该具有独立性。因此当内存中数据改变时,给子进程重新开辟拷贝数据过去
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
    pid_t pid=fork();// 调用fork()函数创建一个新的进程

    // 检查fork()函数的返回值以判断当前代码运行在父进程还是子进程
    if (pid < 0) {// 子进程创建失败
        fprintf(stderr, "Fork failed\n");
        return 1;
    } else if (pid == 0) {//子进程运行
        printf("Child process\n");
        // 这里可以编写子进程需要执行的代码
    } else {//父进程运行
        printf("Parent process\n");
        // 这里可以编写父进程需要执行的代码
    }
    return 0;
}

运行截图如下:

Linux知识点总结—2_基础IO_05

  1. pid_t vfork()
  • 父子进程共有同一个虚拟地址空间
  • 父进程创建子进程后,需要阻塞,直到子进程退出或者程序替换后,才继续运行,避免调用栈混乱
  1. fork()和vfork()创建进程的区别:
    1.fork()创建子进程后,会复制父进程的地址空间、资源和文件描述符等,并在子进程中独立运行;vfork()创建子进程,子进程会在父进程的地址空间内运行,与父进程共享内存和资源。子进程会暂时挂起父进程的执行,直到子进程调用exec函数族或者调用_exit()函数退出
    2.fork()创建的子进程,资源与父进程是相互独立的;vfork()创建的子进程与父进程的资源是共享的,可能会影响到父进程的运行

终止

  • 方式
  1. 在main函数中return
  2. 使用库函数exist()在任意位置调用退出进程
  3. 使用系统调用接口_exist()在任意位置退出进程

等待

  • 作用:等待子进程退出,获取退出子进程的退出状态信息,避免子进程成为僵尸进程
  • 接口
  • pid_t wait(int *statu)
  • pid_t waitpid(pid_t pid, int *statu, int option)
  • 退出信息格式:
  1. 4字节中,低16位中高8位存储退出码
  2. 低8位中的高1位存储core dump标志
  3. 低7位存储异常退出信号值
int main() {
    pid_t childPid = fork(); // 创建子进程
    int status;
    if (childPid < 0) {
        // 创建子进程失败
        perror("Fork error");
        exit(1);
    } else if (childPid == 0) {
        // 子进程代码
        printf("This is the child process.\n");
        sleep(5); // 子进程休眠5秒钟
        exit(0);
    } else {
        // 父进程代码
        printf("This is the parent process.\n");
        printf("Waiting for the child process to complete...\n");
        waitpid(childPid, &status, 0); // 父进程等待子进程的结束
        printf("Child process completed.\n");
    }
    return 0;
}

代码运行结果:

Linux知识点总结—2_进程控制_06

程序替换

  • 作用:
  1. 替换一个pcb正在调度管理的程序
  2. 让当前进程pcb调度运行另一个程序
  • exec函数簇
  • 系统调用接口:execve
  • 库函数:execl,execlp,execle,execv,execvp
  • l和v的区别:l参数——指定一个命令字符串,将整个命令作为一个单一的字符串进行执行;v参数——它需要一个以数组形式表示的命令,将命令及其参数作为数组的元素提供给exec函数。数组的第一个元素应该是命令本身,其后的元素可以是命令的参数
  • 有没有p的区别:是否会默认到PATH环境变量指定路径下查找程序(带p的表示使用系统环境变量,适合于替换系统指令)
  • 有没有e的区别:是否需要自定义环境变量

基础IO

库函数IO接口

  • 接口:fopen,fwrite,fread,fseek(跳转读取位置),fclose,ferror,feof(是否到达文件末尾)
  • 注意事项:
  1. fread返回值:当返回0时,存在歧义:出错/读取到文件末尾
  2. 文件打开方式:默认为文本方式打开文件,使用b进行二进程打开方式操作

系统调用IO接口

  • 接口:open,write,read,lseek(跳转文件读取位置),close,umask
  • 文件打开方式:O_RDONLY—只读,O_WRONLY—只写,O_RDWR—读写,O_CREAT—如果文件不存在,则创建,O_APPEND—文件末尾添加数据,O_TRUNC—若文件以只写方式打开,则情况内容;若以只读或读写方式打开,则打开失败
  • 注意事项:一旦open中用到了O_CREAT,则一定要设置文件的创建权限(umask)

文件描述符

  • 本质:一个进程中打开文件后,对文件使用file结构体进程描述,结构体变量的地址会被添加到files_struct结构体中的fd_arry数组成员中,并返回存放位置的下标作为文件描述符
  • 作用:1.允许文件和设备之间访问 2.用于进程间通信 3.用于管理系统中的资源 4.对系统和程序的运行情况进行合理分配

Linux知识点总结—2_进程间通信_07

文件描述符&文件流指针

  • 关系:FILE结构体中封装了文件描述符,文件流指针通常会与某个文件描述符关联起来,用于对该文件描述符所表示的文件进行读写操作
  • 特殊文件
  1. 标准输入:stdin,输出0
  2. 标准输出:stdout,输出1
  3. 标准错误:stderr,输出2

重定向

  • 作用:将原本要写入A文件的数据,在不改变所操作文件描述符的情况下,将数据写入到B文件中
  • 实现:文件描述符操作哪个文件,取决于fd_arry对应位置的文件描述符信息是哪个文件。因此重定向的实现就是改变文件描述符对应位置的文件信息来改变所操作文件
  • 接口:int dup2(int oldfd,int newfd)

动态库与静态库的生成与使用

  • 生成
  • 生成二进制指令:gcc -fPIC -c child.c -o child.o
  • 对所有二进制指令打包:
  1. 动态库:gcc --shared child.o -o libchild.so
  2. 静态库:ar -cr libchild.a child.o
  • 使用
  • 生成可执行程序时使用:cc main.c -o main -lchild
  • 出错情况:找不到库文件,连接器会默认指定路径下查找库文件,例如:/usr/lib64 & /usr/lib32
  • 解决方案:
  1. 将库文件放到指定路径下
  2. 设置环境变量LIBRARY_PATH,将库文件所在路径加入其中
  3. 使用gcc -L/lib指定库文件所在目录(适用于静态库的连接)
  • 运行可执行程序时使用(仅用于动态链接的动态库)
  • 出错情况:找不到库文件,连接器会默认指定路径下查找库文件
  • 解决方案
  1. 将库文件放到指定路径下
  • 设置环境变量LIBRARY_PATH,将库文件所在路径加入其中

进程间通信

为什么进程间不能直接通信?

  • 进程之间具有独立性,每个进程都有自己的虚拟地址空间,使用的都是自己的虚拟地址空间。此时,将一个数据的地址交给另一个进程,另一个进程无法访问,因此无法直接通信
  • 本质:系统提供一个公共的内存区域进行数据交换

进程间通信方式

  1. 管道
  • 本质:系统内核中的一块缓冲区,该缓冲区可以作为一种同步或异步的数据传输通道,使不同进程可以交换信息和共享数据。
  • 分类:匿名管道和命名管道
  • 匿名管道:管道没有标识符,因此只能用于具有亲缘关系的进程间通信(子进程复制父进程的方式,获取管道的操作句柄)
  • 命名管道:管道具有标识符,可用于同一主机上的任意进程间通信,标识符是一个可见于文件系统的管道文件(只是一个名字),多个进程通过打开同一个管道文件,进而访问同一个管道的缓冲区实现通信
  • 接口
  1. int pipe(int pipefd[2])
  2. int mkfifo(char *filename,mode_t mode)
  • 特性:
  1. 半双工通信:可以选择方向的单向通信,正向或者反向
  2. 生命周期随进程
  3. 自带同步与互斥
  • 管道中没有数据,则read会阻塞
  • 管道数据满了,则write会阻塞
  • 管道数据操作大小不超过PIPE_BUF-4096大小,保证操作原子性
  1. 管道提供字节流传输服务
  • 先进先出,不会覆盖
  • 基于链接
  • 所有读端被关闭,则write会触发异常
  • 所有写段被关闭,且read读完数据后,则返回0

下面我们来看一个简单的命名管道的用法

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>

#define BUFFER_SIZE 256

int main()
{
    int pipefd[2];//0表示读端,1表示写端
    char message[]="hello child process!!";
    char buffer[BUFFER_SIZE];//用于接收消息的缓冲区

    if(pipe(pipefd)==-1)//创建管道失败
    {
        perror("pipe error!!!");
        exit(EXIT_FAILURE);//利用退出状态码退出
    }
    
    pid_t pid=fork();//创建子进程
    if(pid<0)
    {
        perror("build child process error!!!");
        exit(EXIT_FAILURE);
    }

    if(pid==0)
    {
        close(pipefd[1]);//关闭读端,子进程只能读取数据
        read(pipefd[0],buffer,BUFFER_SIZE);//将管道内数据写入到buffer中

        printf("管道内数据为:%s\n",buffer);

        close(pipefd[0]);//关闭写端
    }
    else
    {
        close(pipefd[0]);//关闭写端,父进程写入数据
        write(pipefd[1],message,strlen(message)+1);

        printf("管道内写入数据为:%s\n",message);

        close(pipefd[1]);
    }
    return 0;
}

代码结果展示:

Linux知识点总结—2_进程控制_08

  1. 共享内存
  • 本质:操作系统开辟的一块新的物理内存,即多个进程可以将同一块共享内存映射到自己的虚拟地址空间,通过对虚拟地址空间的访问,实现数据共享
  • 接口:shmget,shmat,....,shmdt,shmctl
  • 特性:
  1. 生命周期随内核:被创建后,只有在系统重启后才会被删除/手动删除
  2. 最快的进程间通信方式:相较于其他方式,少了用户空间与内核之间进行的两次数据拷贝过程
  • 注意事项:多进程对共享内存进行访问是存在访问安全问题的,需要使用信号量来进行访问控制
  1. 消息队列
  • 本质:内核中的优先级队列,多进程通过访问同一个队列,以添加,取出节点的方式实现通信,由消息队列标识符标记
  • 接口:msgget,msgrcv,msgsnd,msgctl,struct msgbuf{long type,char buf[xxx]};
  • 特性
  1. 自带同步与互斥
  2. 生命周期随内核
  1. 信号量
  • 本质:内核中的计数器,用于控制多个线程对共享资源的访问
  • 操作:
  1. P操作:计数器<=0,则阻塞,否则计数器-1
  2. V操作:计算器+1,唤醒一个阻塞的进程
  • 作用:用于实现进程间的同步与互斥
  • 同步&互斥
  1. 同步:通过一些条件控制,让进程对资源的访问更合理
  • 实现:通过计数器对资源进行计数,计数>0表示有资源,可以访问;计数<0表示没有资源,进程要访问就会阻塞
  1. 互斥:通过同一时间的唯一访问机制,实现进程对资源访问的安全性
  • 实现:将计数器设置为1,表示资源只有1个,访问资源前进行P操作,访问完毕后进行V操作,实现互斥
  1. 信号
  • 本质:信号是一种软中断,用于通知或接收进程某个事件已经发生
  1. 套接字(网络通信)
  • 适用场景:不同主机之间的进程间通信
  • 特性
  1. 传输数据为字节级,传输数据可自定义,数据量小效率高
  2. 传输数据时间短,性能高
  3. 传输数据可以加密,数据安全性强

进程信号

概念

  1. 本质:软件中断,通知进程某件事情已经发生
  2. 作用:打断进程当前操作,去处理信号所代表的事件

种类

  1. 查看全部信号:kill -l,总共有62种信号
  2. 分类:1.非可靠信号:1~31 2.可靠信号:34~64

Linux知识点总结—2_基础IO_09

生命周期

  1. 产生:
  1. 硬件方面:ctrl+c/z
  2. 软件方面:kill命令,kill(),raise(),alarm,sigqueue()
  1. 注册
  1. 本质:在进程pcb中的pending信号集合中标记信号,并且在sigqueue链表中添加信号的信息节点,能够让进程知道自己收到了某个信号
  2. 区别:
  1. 可靠信号:每次注册不管是否已经注册,都会添加一个信息节点
  2. 非可靠信息:如果位图已经置1,表示已经注册,则什么都不做
  1. 注销
  1. 本质:将进程pcb中的sigqueue链表中的信号信息节点删除掉,若没有相同节点,则重置信号对应的pending位图
  2. 目的:先注销,再处理,抹除信号存在的痕迹,避免在多执行流的进程中,信号事件被重复处理
  1. 处理
  1. 本质:调用信号时间对应的处理函数,完成某个功能
  2. 方式
  1. 默认处理方式:使用系统自定义好的各个信号处理方式进行信号处理
  2. 忽略处理方式:收到信号后,处理方式就是不处理
  3. 自定义处理方式:使用用户自己定义的函数,替换掉系统默认的处理函数,收到信号后使用自定义的函数进行事件处理
  1. 接口函数:signal()
  2. 自定义处理方式的信号捕捉流程:1.程序运行因为系统调用/中断/异常切换到内核态运行;2.内核功能完成后,返回用户态之前,判断有没有信号待处理;3.有,且默认/忽略,则直接在内核态完成;若自定义,则切换到用户态,运行自定义函数,完成后切换到内核态

我给出信号捕捉流程的图解,以便大家理解起来更方便:

Linux知识点总结—2_进程概念_10

  1. 阻塞
  1. 本质:在pcb的block信号集合中,标记信号,收到对应信号,暂时不处理这个信号
  2. 接口:sigprocmask,sigempty,sigaddset,sigfillset,sigdelset,sigismember
  1. 特殊信号
  1. 在所有信号中,有两个信号无法被阻塞,无法被自定义处理:SIGKILL和SIGSTOP信号
  2. kill杀死进程的本质:给进程发送一个终止信号,终止信号的默认处理方式就是退出程序运行
  3. 一个进程杀不死的原因:1.信号被阻塞;2.信号被自定义处理了;3.进程处于停止状态;4.进程是一个僵尸进程

信号的使用

  1. SIGCHILD:子进程退出时,给父进程发送的信号
  2. SIGPIPE:管道所有读端关闭后,或者socket连接断开后,继续write/send触发的异常

函数的重入

  1. 概念:指一个函数可以被同时或多次进入,而每个调用之间不会相互干扰或产生冲突。简单来说,就是一个函数可以在正在执行的过程中被中断,然后通过另一个调用再次进入,而不会造成错误或数据混乱,那么该函数就是可重入的。
  2. 可重入:函数重入后,并不会产生预期外的结果
  3. 不可重入:函数重入后,可能会导致预期外的处理结果
  4. 判断函数是否可重入:一个函数中是否对一个全局数据进行不受保护的非原子操作

volatile关键字和synchronized关键字

  1. volatile关键字
  • 作用:
  1. 用于修饰变量,保持变量的内存可见性
  2. cpu每次访问数据时,都需要重新到内存中取出数据,防止编译器过度优化,禁止指令重排序
  1. synchronized关键字
  • 作用:
  1. 用于修饰变量,保持变量的内存可见性
  2. cpu每次访问数据时,都需要重新到内存中取出数据,防止编译器过度优化,禁止指令重排序
  3. 保证操作的原子性

多线程

线程概念

  1. 本质:线程就是进程中的一条执行流,一个进程中可以存在一个或者多个线程,在Linux下线程的实现实现是通过pcb实现的,因此Linux下的线程也被称作轻量级进程
  2. 独有&共享
  1. 独有:标识符,上下文数据,栈,寄存器,信号屏蔽字,程序计数器等
  2. 共享:虚拟地址空间,信号处理方式,IO信息,工作路径,全局变量,静态变量等
  1. 进程&线程的区别
  1. 概念方面:进程是系统进程资源分配的基本单元;线程是cpu调度的基本单元
  2. 使用方面:多进程使用更加稳定,健壮;多线程通信更加灵活;创建与销毁成本低;同一进程间,切换调度成本更低
  3. 从调度开销方面:进程的创建,调度和销毁的开销都远远大于线程
  4. 从CPU利用率方面:进程CPU利用率低,上下文切换开销大;线程的CPU的利用率较高,上下文的切换速度快

线程控制

  1. 用户态线程&轻量级进程
  1. 概念:Linux操作系统并没有提供线程的操作接口,所使用的接口都是库函数。创建一个线程,本质上是创建一个pcb,然后在用户态进行了一层封装来模拟线程
  2. 分类:
  1. 用户态线程:又称轻量级线程,是操作系统中线程实现的方式。在用户态线程中,线程的创建、管理和调度完全由应用程序自身或其所使用的库来处理,而不依赖于操作系统的内核支持。
  2. 轻量级进程:是操作系统中的概念,又被称为线程。它是在同一个进程内创建的执行单元,与传统进程不同的是,轻量级进程共享相同的内存空间和系统资源,但拥有独立的堆栈和执行上下文。
  1. 创建
  1. 接口:pthread_create
  1. 终止
  1. 接口:在线程入口函数中return,不是主线程main函数中的return
  1. pthread_cancel
  2. pthread_exit
  1. 等待
  1. 概念:等待一个指定线程退出,获取返回结果,释放资源
  2. 注意事项:每个线程创建出来后,有个属性--分离属性,默认是joinable状态,表示线程退出后,资源不会被自动释放,需要被其他线程等待
  3. 接口:pthread_join
  1. 分离
  1. 概念:将线程的分离属性设置为detach,表示线程退出后,将自动释放所有资源,因此被分离出来的线程是不需要被等待的
  2. 接口:pthread_detach

下面我们来实现一个简单的线程:

#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>


void* thread_function()
{
    printf("成功进程创建线程的逻辑代码!!!\n");
    pthread_exit(NULL);//退出线程
}
int main()
{
    pthread_t pthread;//定义一个线程变量
    int result=pthread_create(&pthread,NULL,thread_function,NULL);//参数为线程变量地址,线程属性,空为默认属性,线程函数,线程函数参数

    if(result!=0)
    {
        perror("线程创建失败!!!");
        exit(EXIT_FAILURE);
    }

    result = pthread_join(pthread, NULL);//等待特定线程退出,空表示不关心返回值
    if(result!=0)
    {
        perror("等待线程退出失败!!!");
        exit(EXIT_FAILURE);
    }

    return 0;

代码的运行结果为:

Linux知识点总结—2_进程间通信_11

线程安全

  1. 概念:在多线程环境下,一个代码段(函数、方法等)能够正确地处理多个线程同时访问共享资源的情况,保证数据的一致性和正确性。具体来说,线程安全的代码在并发环境中能够正确地协调多个线程的访问,避免出现竞态条件等问题。

竞态条件是指当多个线程同时访问和修改共享资源时,由于执行的时序不确定,可能导致程序出现不可预测的错误或结果。

  1. 实现
  1. 概念:通过条件控制,保证资源获取合理,例如锁机制,信号量机制等
  2. 实现
  1. 条件变量:提供一个可以使线程阻塞,以及唤醒阻塞线程的接口用户在进行资源访问控制时,可以在无法获取资源时阻塞线程;可以访问时,唤醒线程
  2. 信号量:提供了一个计数器,对资源进行计数,通过计数来判断当前是否还有资源可以获取,可以则返回,不可以则阻塞,并且提供唤醒线程的接口
  1. 互斥
  1. 概念:同一时间,保证临界资源的唯一访问
  2. 实现:通过互斥锁来实现
  1. 本质:一个0/1计数器,用于标记当前资源状态,或者标记当前是否可以加锁的状态
  2. 流程:在访问临界资源之前加锁,访问完毕后解锁
  3. 接口:包括pthread_mutex_t——结构体类型,表示互斥锁对象;pthread_mutex_init——初始化互斥锁;pthread_mutex_lock/pthread_mutex_trylock——对互斥锁进行加锁操作;pthread_mutex_unlock——对互斥锁进行解锁操作;pthread_mutex_destroy——销毁互斥锁对象
  1. 死锁
  1. 概念:多个线程因为对锁资源的争抢不当,导致程序流程卡死的状态。简单来说就是一组相互进竞争资源的线程因为相互等待,导致“永久”阻塞的现象
  2. 产生的必要条件
  1. 互斥:一个锁资源只能被一个线程占用
  2. 不可剥夺:不能强行抢占进程中已被占有的锁资源
  3. 请求与剥夺:当一个进程在等待分配时已经占有其他锁资源,其继续占有已分配得到的锁资源
  4. 循环等待:线程t1等待线程t2占有的锁资源,线程t2等待线程t1占有的锁资源
  1. 预防死锁
  1. 本质:只需要破坏死锁产生的必要条件之一即可
  2. 方法:使用非阻塞加锁,若请求一个锁,请求不到,则把已经拥有的其他锁资源释放掉,重新请求;保证多个线程对多个锁的加解锁顺序一致;一次性获取空间中的全部资源
  1. 避免死锁
  1. 资源分配拒绝:当进程或者线程在调度资源时,系统检测到该资源一旦被使用就会导致死锁的情况产生,此时系统将拒绝为其分配资源。
  2. 进程启动拒绝:当进程启动时,需要给进程分配资源,此时系统检测出给该进程分配资源会导致死锁,则该进程启动失败。
  3. 银行家算法(资源分配拒绝):将资源进行分表管理,在银行家算法中,系统会维护有关资源的信息,包括可用资源的总数和已分配给各个进程的资源数量。每个进程在请求资源时,系统会检查该请求是否会导致系统进入不安全的状态。如果请求满足安全条件,系统会分配资源给该进程;否则,进程会被阻塞,直到资源可用或安全条件满足为止
  1. 死锁解除
  1. 撤销进程法
  2. 资源剥夺法

线程应用

  1. 生产者消费者模型
  1. 概念:一种典型的设计模式,专门针对某种典型场景设计的解决方案
  2. 适用场景:有大量请求产生并进行处理的场景
  3. 模型结构:将请求的产生放在一个生产线程中进行,将生产的请求数据放到线程安全缓冲区队列中;将数据的处理工作放在消费者线程中,从线程安全缓冲区中取出数据进行处理
  4. 优点:
  1. 解耦合
  2. 支持忙闲不均
  3. 支持并发:多个生产者同时产生数据,多个消费者同时处理数据
  1. 具体实现:创建两种不同角色的线程,各自负责自己的工作;创建一个线程安全的任务队列

生产者消费者模型的C代码:

#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>

#define BUFFER_SIZE 5//缓冲区的最大物品数量

int buffer[BUFFER_SIZE];//数据存储区
int count=0;

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;//互斥锁
pthread_cond_t full_cond = PTHREAD_COND_INITIALIZER;//缓冲区满条件
pthread_cond_t empty_cond = PTHREAD_COND_INITIALIZER;//缓冲区空条件

void* producer()//生产者线程函数
{
    int item=0;//生产者已生产的物品数量
    while(1)
    {
        pthread_mutex_lock(&mutex);//加锁变为原子操作

        while(count==BUFFER_SIZE)//若缓冲区已满,则需要等待消费者处理数据
        {
            pthread_cond_wait(&full_cond,&mutex);
        }

        buffer[count]=item;//生产一个数据
        printf("生产物品数量为:%d\n",item);
        count++;

        pthread_cond_signal(&empty_cond);//等待唤醒消费者
        pthread_mutex_unlock(&mutex);

        item++;
    }
}

void* consumer()
{
    while(1)
    {
        pthread_mutex_lock(&mutex);//加锁操作

        while(count==0)//缓冲区中为空,则需要等待生产者产生数据
        {
            pthread_cond_wait(&empty_cond,&mutex);
        }

        int item=buffer[count-1];
        printf("消费者消费的数据:%d\n",item);
        count--;

        pthread_cond_signal(&full_cond);//唤起生产者
        pthread_mutex_unlock(&mutex);//解锁操作
    }
}
int main()
{
    pthread_t producer_thread,consumer_thread;//定义线程变量

    pthread_create(&producer_thread,NULL,producer,NULL);//创建生产者消费者线程模型
    pthread_create(&consumer_thread,NULL,consumer,NULL);

    pthread_join(producer_thread,NULL);//等待线程退出
    pthread_join(consumer_thread,NULL);
    return 0;
}

代码执行结果为:

Linux知识点总结—2_基础IO_12

  1. 读者写者模型
  1. 概念:一种典型的设计模式,专门针对某种典型场景设计的解决方案
  2. 适用场景:针对一种读共享,写互斥的场景,即多个线程可以一起读,但是在写的时候,其他线程既不能读,也不能写
  3. 底层依赖——读写锁
  1. 支持读共享,写互斥,默认为读优先(可设置)
  2. pthread_r wlock_t
  3. 读写锁依赖于自旋锁实现的(自旋锁:不释放cpu资源,循环进行条件判断的锁,优点是更加实时;缺点是消耗资源较多,更适合于明确知道加锁后并不需要等待太长时间的场景)
  1. 线程池
  1. 适用场景:针对高并发场景的处理模式
  2. 本质:线程池就是半个生产者与消费者模型
  3. 概念:线程池本身就是一个消费者
  4. 具体实现:提前创建好只当数量的线程,以及一个安全的任务队列,这些线程不断从任务队列中取出任务进行处理
  5. 对于线程池的最大线程数量设置
  1. 对于CPU密集型任务:CPU核心数+1
  2. 对于IO型任务:2*CPU核心数
  3. 一般公式:线程数=CPU核心数+(1+线程等待时间/先后曾运行总时间)
  1. 线程安全的单例模式
  1. 概念:一种典型的设计模式,专门针对某种典型场景设计的解决方案
  2. 适用场景:一个类只能实例化一个对象的场景,一个资源只能在内存中存在一份的场景
  3. 具体实现类型
  1. 饿汉模式
  1. 实现思想:采用以空间换时间的思想,在程序初始化阶段,将对象直接进行实例化
  2. 具体实现代码
#include<stdio.h>

typedef struct//定义一个数据类
{
    int data;
}Singleton;

Singleton instance={.data=0};//创建一个全局的数据类,并初始化为0

Singleton* getInstance()
{
    return &instance;//返回单例实例化的地址
}
int main()
{
    Singleton* Singleton=getInstance();//获取实例化指针
    printf("单例模式下的实例化数据:%d\n",Singleton->data);
    return 0;
}

代码运行结果:

Linux知识点总结—2_系统编程_13

  1. 懒汉模式
  1. 实现思想:采用延迟加载的思想,类的对象在需要使用的时候再去实例化
  2. 具体实现代码
#include<stdio.h>
#include<stdlib.h>

typedef struct 
{
    int data;
}Singleton;

Singleton* getInstance()//懒汉单例模式
{
    static Singleton* instance=NULL;//局部静态变量,用于保存实例化地址
    if(instance==NULL)
    {
        instance=(Singleton*)malloc(sizeof(Singleton));//分配内存空间,创建新的实例化对象
        instance->data=1;//初始化
    }
    return instance;
}

int main()
{
    Singleton* Singleton=getInstance();
    
    printf("懒汉模式实例化对象数据:%d\n",Singleton->data);
    return 0;
}

代码运行结果:

Linux知识点总结—2_进程控制_14