unix进程控制及进程环境--自APUE

news2024/12/23 18:28:26

文章目录

  • 概述
    • 1、孤儿进程和僵尸进程
  • 进程终止
    • 进程的编译和启动
    • 进程终止的步骤
    • 进程8种终止方式
    • 进程退出函数1:exit
    • 进程退出函数2:_exit
    • 进程退出函数3:_Exit
    • 注册终止处理程序:atexit
  • 环境变量
    • 通过main函数传参
    • 全局的环境变量表:environ
    • 获取环境变量:getenv
    • 修改环境变量:putenv
    • 修改环境变量:setenv
  • 进程堆空间申请和释放
    • 申请指定大小的内存:malloc
    • 申请初始化的内存:calloc
    • 修改已申请内存大小:realloc
  • 子进程
    • 创建子进程:fork
    • 创建子进程:vfork
    • 探测子进程状态变化:wait
    • 探测特定一个子进程状态变化:waitpid
  • 执行另一个程序
    • 指定参数和环境变量:execve
    • 以列表方式传参:execl
    • 以向量方式传参:execv
    • 以列表方式传参并传递环境变量:execle
    • 特定执行顺序:execlp、execvp、execvpe

概述

1、孤儿进程和僵尸进程

  • 僵尸进程

一个进程退出后,内核会回收进程的资源,但是会留下一个僵尸进程的数据结构,保留了进程的ID、进程的状态等信息,这些信息被父进程通过wait函数获取后被释放,如果一个子进程退出后,父进程没有wait这些信息,那么这个子进程就变成了僵尸进程。

防止进程变成僵尸进程的几种方式:

  1. 父进程通过wait收集子进程的退出信息
  2. 子进程退出时,内核会向父进程发送SIGCHILD,父进程处理该信号的时候通过wait获取退出信息
  3. 让进程被进程1接管,进程1会wait每一个退出的子进程
  • 孤儿进程

父进程退出后,子进程还没有退出,那么子进程就会被进程1接管变成孤儿进程。因此子进程可以通过下面的命令来判断父进程是否退出了:

while(getppid() != 1) sleep(1);

进程终止

进程的编译和启动

进程的编译链接:裸机编程需要写链接脚本,但是linux下编程就不用了,因为每个进程的链接方式都是固定的,gcc会自动把事先准备好的引导代码链接到main函数的前面,这段代码每个程序都是一样的。

进程的加载:进程运行的时候,加载器会把进程加载到内存中,然后去执行。

进程编译的时候使用链接器,运行的时候使用加载器

argc和argv的传参:hell下执行进程的时候,这俩参数首先被shell解析,然后传递给加载器,最后通过main函数传递给进程,所以我们在main函数中能使用这两个参数。

进程终止的步骤

img

进程启动的时候,内核会通过exec打开一个启动例程,这个启动例程通过下面的函数执行目标的进程,如果目标进程在main函数中return 0的时候,就相当于直接执行了exit(0),所以return之后执行的步骤和exit一样;

exit(main(argc, argv));

进程终止的几种场景如下:

  • 如果功能函数调用return:那么返回main函数;
  • 如果功能函数调用_exit或_Exit:那么直接进入到内核
  • 如果功能函数调用exit:那么执行终止处理函数、清理IO、删除临时文件后进入内核
  • 如果main函数调用return:那么返回启动例程,然后启动例程会调用exit,进入exit处理流程后进入内核
  • 如果main函数调用_exit或_Exit:同功能函数
  • 如果main函数调用exit:同功能函数

进程8种终止方式

正常终止方式:

  1. 从main返回
  2. 调用exit
  3. 调用_exit或_Exit
  4. 最后一个线程 从其启动例程返回
  5. 最后一个线程调用thread_exit

异常终止:

  1. 调用abort
  2. 接到一个信号
  3. 最后一个线程对取消请求做出响应

进程退出函数1:exit

这是一种进程正常退出的库函数,通过man 3 exit可以查看,exit函数没有返回值,会返回status状态码给调用他的父进程。

进程调用exit时候,会执行以下步骤:

  1. 先调用终止处理程序。终止处理程序通过atexit函数注册,一个进程最多注册32个。调用的顺序和注册的顺序相反。终止处理程序没注册一次会被调用一次,尽管是相同的函数注册了多次
  2. 所有打开的标准IO流会被flush和close
  3. 通过tmpfile创建的临时文件会被删除
#include <stdlib.h>
void exit(int status);
status:给父进程的返回码

进程退出函数2:_exit

_exit是一个系统调用,作用也是退出程序,但是不会去执行终止处理函数、清理IO、删除临时文件等步骤,直接进入到内核:

  1. 所有的文件描述符会被直接关闭
  2. 然后所有的子进程被进程1接管
  3. 给父进程传递SIGCHLD信号
#include <unistd.h>
void _exit(int status);
status:给父进程的返回码

进程退出函数3:_Exit

同_exit

#include <stdlib.h>
void _Exit(int status);

注册终止处理程序:atexit

#include <stdlib.h>
int atexit(void (*function)(void));
返回值:成功,返回0,失败返回非0数字。

示例代码:

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

static void test1(void)
{
        printf("test1\n");
}

static void test2(void)
{
        printf("test2\n");

}

int
main(int argc, char **argv)
{
        if (atexit(test1) != 0) {
                printf("atexist test1 failed.\n");
        }
        if (atexit(test2) != 0) {
                printf("atexist test2 failed.\n");
        }

        exit(1);
}

执行结果:

[root@localhost exit]# ./atexit 
test2
test1

环境变量

通过main函数传参

环境变量可以通过main函数直接传参进来,需要main函数按照以下格式定义。这种方式其实就是把全局环境变量表environ的地址传进来。

#include <stdio.h>
int
main(int argc, char **argv, char **envp)
{
        int i = 0;
        for (i = 0; i < argc; i++){
                printf("%s\n", argv[i]);
        }
        i = 0;
        while(envp[i]) {
                printf("%s\n", envp[i]);
                i++;
        }
        return 0;
}

全局的环境变量表:environ

进程打开之后会有一个默认的环境变量表,通过一个全局的指针数组可以访问到表里的内容:

#include <unistd.h>
extern char **environ;

获取环境变量表的示例代码如下,获取的结果和shell 命令env的结果基本一致:

#include <unistd.h>
#include <stdio.h>
extern char **environ;
int
main(int argc, char **argv)
{
        int i = 0;
        while(environ[i] != NULL) {
                printf("%d\t%s\n", i+1, environ[i]);
                i++;
        }
        return 0;
}

运行结果如下:

[root@localhost getenv]# vim environ.c ^C
[root@localhost getenv]# ./environ 
1       XDG_SESSION_ID=17
2       HOSTNAME=localhost.localdomain
3       RTE_INCLUDE=/usr/include/dpdk
4       TERM=xterm
5       SHELL=/bin/bash
6       HISTSIZE=1000

获取环境变量:getenv

环境变量都是key=value的格式,getenv在环境变量表中,查找key对应的value,返回指向value的指针(不会带上"key="),如果找不到返回NULL。

#include <stdlib.h>
char *getenv(const char *name);
name:环境变量的key;
成功,返回指向value指针,失败或者找不到返回NULL;

修改环境变量:putenv

作用如下:

  1. 如果环境变量不存在则添加环境变量
  2. 如果环境变量已经存在,那么把环境变量的值设置成最新的值。

注意事项:

  1. putenv了之后,string地址会添加到environ表中。
  2. putenv了之后,如果修改了string的内容,那么环境变量也会对应被修改。
  3. 环境变量的修改只会影响当前进程和子进程的环境变量表,对父进程无效
#include <stdlib.h>
int putenv(char *string);
string:必须是"key=value"的格式;
返回值:成功返回0,失败返回非0,并且置上errno;

代码示例:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
extern char **environ;
int
main(int argc, char **argv)
{
        char buf[256] = "name=xiaoming";
        int i = 0;
        if (0 != putenv(buf)) {
                perror("putenv failed.\n");
                return 0;
        }
        printf("%s=%s\n", "name", getenv("name"));
        strcpy(buf, "name=xiaowang");
        printf("%s=%s\n", "name", getenv("name"));
        while(environ[i]) {
                printf("%s\n", environ[i]);
                i++;
        }
        return 0;
}

输出结果:

name=xiaowang

修改环境变量:setenv

#include <stdlib.h>
int setenv(const char *name, const char *value, int overwrite);
作用:把name=value的环境变量加入到环境变量表中;
overwrite:当overwrite为0,如果环境变量已经存在,那么不会覆盖掉原来的值,如果环境变量不存在,则添加环境变量;当overwrite为非0,如果环境变量已经存在,那么覆盖掉原来的值,如果环境变量不存在,则添加环境变量;
返回值:成功,返回0,失败返回-1,并且置上errno;

删除环境变量

进程堆空间申请和释放

申请指定大小的内存:malloc

malloc用于申请指定大小的内存,返回指针指向申请的内存。如果size的值为0,返回值是NULL或者是一个特定值得指针,这个指针可以作为free函数的参数被释放而不会报错。

#include <stdlib.h>
void *malloc(size_t size);
size:申请内存以字节为单位的大小;
返回指向新申请内存的指针

申请初始化的内存:calloc

calloc用于申请一段指定大小、指定数目的内存,该内存会被初始化成0,如果大小和数目为0,返回值是NULL或者是一个特定值得指针,这个指针可以作为free函数的参数被释放而不会报错。

#include <stdlib.h>
void *calloc(size_t nmemb, size_t size);
nmemb:内存数目;
size:内存大小;
返回新申请内存的指针

修改已申请内存大小:realloc

realloc用于修改已申请内存的大小,ptr指向修改前的内存,size设置修改后的内存大小,可以改大也可以改小,大体分为以下几个场景:

  1. 如果改小:那么修改前的内存的起始地址到size大小的范围内的数据不会被修改,返回指向修改前的内存指针
  2. 如果改大,将在原来堆地址继续往高地址空间扩展,有两种可能,1)一是空间连续且足够,那么返回原来的空间地址,注意新增加的内存不会被初始化;2)二是连续的空间不够,那么寻找新的地址空间,并将原来的数据转移到新的空间中,原来的内存会被自动释放掉,返回新的空间地址

ptr指针和size之间的组合关系大体分为以下几种情况:

  1. ptr等于NULL:realloc等效于malloc()
  2. ptr不等于NULL:ptr必须是通过malloc(), calloc(), realloc()分配过的
  3. size等于0:realloc等效于free()
#include <stdlib.h>
void *realloc(void *ptr, size_t size);
ptr:NULL或者指向修改前的内存;
size:修改后的内存大小;
返回值:修改成功返回新的内存地址,修改失败返回NULL,此时原来的ptr还可以继续用,数据也不会被修改;

子进程

创建子进程:fork

fork()系统调用用于创建一个子进程,子进程和父进程之间的关系为:

  • 子进程除了代码段,数据、堆、栈都复制了一份副本,父子进程对数据的访问互相不影响
  • 子进程复制父进程文件描述符,但是指向同一个文件表项,共享文件偏移量
  • 父子进程返回值不同
  • 父子进程ID不同
  • 子进程不继承父进程的内存锁和记录锁
  • 子进程不继承父进程的定时器
  • 子进程的signal会被清空
  • 父进程退出,子进程未退出,子进程被init进程收养
  • 子进程退出,其信息未被父进程通过wait函数收集,子进程会变成僵死进程
#include <unistd.h>
pid_t fork(void);
返回值:父进程返回子进程的进程ID,子进程返回0;如果创建失败,父进程返回-1,并且置上errno,没有子进程被创建;

创建子进程:vfork

vfork也是创建一个子进程,和fork之间的区别在于:

  1. 子进程和父进程之间共享内存数据,包括代码段、数据段、堆、栈等,创建子进程的效率比fork高
  2. 子进程先执行,父进程会卡主,直到子进程调用了exit或execve(不能是调用return),父进程继续运行

应用场景:很多时候创建子进程只是为了执行exec,这种场景下,没必要对父进程所有的数据都进行复制,用vfork效率会更高。

#include <sys/types.h>
#include <unistd.h>
pid_t vfork(void);
返回值:父进程返回子进程的进程ID,子进程返回0;如果创建失败,父进程返回-1,并且置上errno,没有子进程被创建;

探测子进程状态变化:wait

wait是一种系统调用,用于父进程探测子进程的状态变化。。子进程退出的时候,内核还保留数据结构保存退出状态,当父进程调用wait,如果此时已经有子进程退出,那么立即返回,如果没有,父进程会阻塞在wait调用上,直到至少一个子进程退出,然后系统会把子进程的资源彻底释放。

#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
返回值: 成功返回子进程的ID号,失败返回-1,置上errno;

探测特定一个子进程状态变化:waitpid

#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
pid:
	pid < -1: 那么等待进程组ID等于pid绝对值的进程组中的进程;
	pid = -1: 等待所有的子进程;
	pid = 0: 等待进程组ID等于父进程组ID的进程组中的进程;
	pid > 0: 等待进程ID等于pid的子进程
options:
	WNOHANG: 正常waitpid的时候父进程会hang住,用这个参数,如果没有子进程退出,立即返回0;
	...
status:输出型参数,如果status非NULL,那么会返回子进程的状态,通过一些宏可以获得状态;
	WIFEXITED(*status): 子进程正常退出,返回true;
	WEXITSTATUS(*status): 如果子进程正常退出的话,返回返回码;
	WIFSIGNALED(*status): 子进程被信号中断退出,返回true;
	WTERMSIG(*status): 中断子进程的信号ID;
	WCOREDUMP(*status): 子进程coredump了,返回true,同时WIFSIGNALED也返回true;
	...
返回值: 成功返回子进程的ID号,失败返回-1,置上errno;如果子进程ID不存在,或者存在但是非该进程的子进程,返回-1,并且置上errno.

执行另一个程序

exec()函数族用于执行一个新的程序代替当前程序。函数包括execl、execv、 execle、execve、execlp、execvp、execvp等,底层都是使用execve系统调用。这几个函数命名方式有一定规律,v表示向量,就是用二维指针来传递参数,l表示list,就是用多个指针传递参数,e表示传递环境变量,p表示寻找可执行文件的顺序和shell一样。

指定参数和环境变量:execve

execve系统调用用于执行filename指向的可执行程序或者shell脚本,脚本可以被执行有两个条件:

  1. 脚本具有可执行权限
  2. 脚本开头必须指定解释器,比如:#!/bin/bash,否则会报 Exec format error

执行execve之后,当前程序的代码段、数据段、bss段、栈等信息会被filename指向的程序覆盖,因此没有返回值同时被执行程序的进程ID和当前程序的ID相同。

#include <unistd.h>
int execve(const char *filename, char *const argv[], char *const envp[]);
filename: 被执行的程序;
argv: 传递给被执行程序的参数,以NULL结尾;
envp: 传递给被执行程序的环境变量(key=value的格式),以NULL结尾;
如果main函数的定义为:int main(int argc, char *argv[], char *envp[]);那么可以argv和envp就指向execve中的argv和envp;
返回值: 成功不返回,失败返回-1,并且置上errno。

以列表方式传参:execl

execl函数使用列表方式传参,最后一个参数之后以NULL结尾,就是每个参数使用一个指针。不用传递环境变量,默认使用当前程序的环境变量。

#include <unistd.h>
int execl(const char *path, const char *arg, ...);
path: 被执行程序;
arg: 指向单个参数的指针;
返回值:同execve;

代码示例:

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

int
main(int argc, char **argv)
{
        if (argc != 2){
                printf("usage: %s filename\n", argv[0]);
                return 0;
        }
        char a[] = "hello";
        char b[] = "world";
        execl(argv[1], a, b, NULL);
        return 0;
}

以向量方式传参:execv

execv函数使用向量方式传参,默认使用当前程序的环境变量。

#include <unistd.h>
int execv(const char *path, char *const argv[]);
path: 被执行程序;
argv: 指向参数的二维指针;
返回值:同execve;

以列表方式传参并传递环境变量:execle

#include <unistd.h>
int execle(const char *path, const char *arg, ..., char * const envp[]);
path: 被执行程序;
arg: 指向单个参数的指针;
envp: 指向环境变量的指针;
返回值:同execve;

代码示例

#include <unistd.h>
#include <stdio.h>
int
main(int argc, char **argv)
{
        if (argc != 2){
                printf("usage: %s filename\n", argv[0]);
                return 0;
        }
        char a[] = "hello";
        char b[] = "world";
        char *c[] = {"name=xiaoming", "age=18", NULL};
        int ret;
        ret = execle(argv[1], a, b, NULL, c);
        if (ret == -1) {
                perror("execle: ");
        }
        return 0;
}

特定执行顺序:execlp、execvp、execvpe

execlp、execvp、execvpe函数的功能分别和execl、execv、execve相同,不同点在于:

  1. 参考shell寻找可执行文件的逻辑
  2. 如果不是绝对路径,先从PATH环境变量中找,找不到就从当前目录下寻找。
  3. 如果PATH路径下程序找到了,但是没有可执行权限,那么继续从下一级目录下寻找
  4. 如果被执行的程序是shell脚本,但是没有解释器,比如:#!/bin/bash,那么会默认使用/bin/sh来解释

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/164407.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

uni-app跨端自定义指令实现按钮权限

前言 初看这个标题可能很迷&#xff0c;uni-app明明不支持自定义指令&#xff0c;这文章是在搞笑吗&#xff0c;本文对于uni-app自定义指令实现按钮权限的方式也有可能是多余&#xff0c;但为了给业务部门更友好的开发体验&#xff0c;还是做了一些可能没意义的操作&#xff0…

回顾2022,展望 2023

个人相关&#xff1a; PMP 因为疫情多次延期的PMP终于搞定&#xff0c;光环的PMP就是妥妥。基本只要认真做题和思考都会过。但是考试不仅仅是考试&#xff0c;有时候更多的是对项目发展和项目管理的思考&#xff1a;风险&#xff0c;里程碑&#xff0c;相关方&#xff0c;敏捷&…

红日内网渗透靶场2

目录 环境搭建&#xff1a; Web渗透&#xff1a; weblogic漏洞利用 java反序列化漏洞利用、哥斯拉获取shell 上线msf msf派生shell到cs 内网信息收集 mimikatz获取用户密码 cs横向移动 PTT攻击&#xff08;票据传递&#xff09; 方法2&#xff1a;通过msf利用永恒之蓝…

测试之分类【测试对象、是否查看代码、开发】

文章目录1. 按测试对象分类2. 按照是否查看代码划分3. 按照开发阶段划分1. 按测试对象分类 可靠性测试容错性测试安装卸载测试内存泄露测试弱网测试 &#xff08;1&#xff09;可靠性测试 可靠性 正常运行时间 / (正常运行时间 非正常运行时间) * 100% &#xff08;最高 10…

Servlet的实战用法(表白墙前后端)

作者&#xff1a;~小明学编程 文章专栏&#xff1a;JavaEE 格言&#xff1a;热爱编程的&#xff0c;终将被编程所厚爱。 目录 服务器版本的表白墙 创建项目 约定前后端交互接口 获取全部留言 发表新的留言 服务端代码 创建Message类 创建DBUtil类 创建MessageServlet…

双指针合集

87合并两个有序的数组 import java.util.*; public class Solution {public void merge(int A[], int m, int B[], int n) { int i m-1;int j n-1;for(int k nm-1;k>0;k--){if(j<0) A[k] A[i--];else if(i<0) A[k] B[j--];else if(A[i]>B[j]) A[k] A[i--]…

六道数据结构算法题详解

目录 1.力扣350题. 两个数组的交集 II 2.力扣121题. 买卖股票的最佳时机 3.力扣566题. 重塑矩阵 4.力扣118题. 杨辉三角 5.牛客BM13 判断一个链表是否为回文结构 6.牛客BM14 链表的奇偶重排 1.力扣350题. 两个数组的交集 II 题目&#xff1a;给你两个整数数组 nums1 和 n…

2022年终总结---权衡好工作和生活

2022总结 【校园】2022年6月研究生顺利毕业&#xff0c;让下文的一切才变的有机会。感谢师弟送学长毕业&#xff0c;感谢在最后时刻各位舍友帮忙送材料&#xff0c;怀念最后一个月一起打球的时光。 【工作】2022年6月入职阿里&#xff0c;成为打工人。在这个大的平台&#xf…

Goland项目使用gomod配置

Goland 项目创建 goland2020.3 及以上 IDE&#xff0c;默认创建的 go 项目 就是使用 gomod 管理&#xff01; goland2020.3 及以下的 IDE&#xff0c;创建项目时需要选择 带小括号 vgo 的才是 gomod 管理模式 下图为使用 goland2021.3 版本创建使用 gomod 管理的 go 项目&…

14种可用于时间序列预测的损失函数

在处理时间序列预测问任务时&#xff0c;损失函数的选择非常重要&#xff0c;因为它会驱动算法的学习过程。以往的工作提出了不同的损失函数&#xff0c;以解决数据存在偏差、需要长期预测、存在多重共线性特征等问题。本文工作总结了常用的的 14 个损失函数并对它们的优缺点进…

线段树(Segment tree)

线段树 线段树是一种二叉树形数据结构,用以储存区间或线段,并且允许快速查询结构内包含某一点的所有区间。 视频讲解 线段树主要实现两个方法:「求区间和」&「修改区间」,且时间复杂度均为 O(logn)。 nums = [1, 2, 3, 4, 5] 对应的线段树如下所示: 使用数组表示线段…

【阶段三】Python机器学习33篇:机器学习项目实战:医学病症关联规则分析

本篇的思维导图: 医学病症关联规则分析 项目背景 本项目演示一个医学领域的有趣应用——病症关联规则分析,同时利用apyori库和mlxtend库来编写代码,从数据分析的角度去研究病症背后的关联规则。假设有一种医学理论认为,五脏和一些病症之间存在关联关系,见下表。例…

4.线性神经网络

4.线性神经网络 目录 线性回归 线性回归的基本元素 线性模型损失函数解析解随机梯度下降 矢量化加速正态分布与平方损失 优化方法 梯度下降选择学习率小批量随机梯度下降选择批量大小总结 线性回归的从零开始实现 生成数据集读取数据集初始化模型参数定义模型定义损失函数定义…

磨金石教育摄影技能干货分享|优秀摄影作品欣赏——世界掠影

这世上很多地方都在发生着有趣的事&#xff0c;很多地方不同的人与物&#xff0c;都在不同的时间和环境下展现着不同的状态。让我们跟随摄影师的镜头去欣赏这些精彩的画面吧。1 悬而未定想象一下这张照片是什么角度拍的&#xff1f;是不是看上去很像俯拍&#xff1f;无论怎么看…

【Python】groupby操作后不把列作为索引单独提出

这是一个困了我几天的问题。 一开始的搜索方向错了&#xff0c;按照groupby的key column搜索&#xff0c;没有搜到。 最近悟出的一个技巧是&#xff0c;没有头绪时看看数据类型和数据内容。如果思路是没问题的情况下。 问题描述 date_date.groupby([XXXX]) poi_date.groupby(…

可视化工具,Java 应用性能分析、调优

JVisualVM 简介 VisualVM 是Netbeans的profile子项目&#xff0c;已在JDK6.0 update 7 中自带&#xff0c;能够监控线程&#xff0c;内存情况&#xff0c;查看方法的CPU时间和内存中的对 象&#xff0c;已被GC的对象&#xff0c;反向查看分配的堆栈(如100个String对象分别由哪…

动态规划 —— 最长上升子序列全解

题目链接&#xff1a;300. 最长递增子序列 - 力扣&#xff08;LeetCode&#xff09; 朴素做法 设元素数组为arr&#xff0c;定义一维数组dp&#xff0c;dp[i]表示以i位置结尾的子序列中&#xff0c;最长的上升子序列的长度 这些子序列可以被划分成哪些子集合呢&#xff1f; …

ArcGIS10.8保姆式安装教程,超详细;附安装包

安装前请关闭杀毒软件&#xff0c;系统防火墙&#xff0c;断开网络连接 参考链接&#xff1a;请点击 下载链接&#xff1a; 通过百度网盘分享的文件&#xff1a;ArcGIS10.8zip 链接:https://pan.baidu.com/s/1023fbyQpt6r6U6wtgBuReg 提取码:820w 复制这段内容打开「百度网盘A…

设计模式——解释器模式

解释器模式一、基本思想二、应用场景三、结构图四、代码五、优缺点优点缺点一、基本思想 给分析对象定义一个语言&#xff0c;并定义该语言的文法表示&#xff0c;再设计一个解析器来解释语言中的句子。 二、应用场景 当对象间存在一对多关系&#xff0c;一个对象的状态发生…

ESP32蓝牙+EC11旋转编码器实现对电脑音量控制

ESP32蓝牙EC11旋转编码器实现对电脑音量控制✨本项目基于Arduino开发框架下功能实现。 &#x1f6e0;蓝牙设备添加和连接 ⚡需要有带蓝牙硬件支持的电脑才能实现连接并控制&#xff0c;当然手机也可以连接但是不能实现对手机音量控制&#xff0c; &#x1f33f;以Win10系统电脑…