【Linux跬步积累】—— 环境变量、程序地址空间、进程地址空间、Linux2.6内核进程调度队列

🌏博客主页:PH_modest的博客主页
🚩当前专栏:Linux跬步积累
💌其他专栏:
🔴 每日一题
🟡 C++跬步积累
🟢 C语言跬步积累
🌈座右铭:广积粮,缓称王!

文章目录

  • 环境变量
    • 基本概念
    • 常见环境变量
    • 查看环境变量的方法
    • 测试PATH
    • 测试HOME
    • 测试SHELL
    • 和环境变量有关的命令
    • 环境变量的组织方式
    • 通过代码获取环境变量
    • 通过系统调用获取环境变量
    • 环境变量通常是具有全局属性的
  • 程序地址空间
  • 进程地址空间
  • Linux2.6内核进程调度队列
    • 一个CPU拥有一个runqueue
    • 优先级
    • 活动队列
    • 过期队列
    • active指针和expired指针


环境变量

基本概念

  • 环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数。
  • 如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。
  • 环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性。
  • 环境变量,不是一个,而是一堆,彼此其实没有关系
  • 环境变量,一般是系统内置的具有特殊用途的变量。定义变量的本质,其实就是开辟空间,在运行期间我们的程序也能开辟空间。

操作系统/bash是用C语言写的程序,它能在运行中开辟空间吗?

答:可以,系统的环境变量,本质就是系统自己开辟空间,给他名字和内容即可!

常见环境变量

  • PATH: 指定命令的搜索路径。
  • HOME: 指定用户的主工作目录(即用户登录到Linux系统中的默认所处目录)。
  • SHELL: 当前Shell,它的值通常是/bin/bash。

查看环境变量的方法

echo $NAME //NAME:你的环境变量名称
在这里插入图片描述

测试PATH

创建test.c文件

在这里插入图片描述

对比./test执行和直接test执行

在这里插入图片描述
直接执行test没有显示任何东西,而加了./之后就可以顺利执行代码,最终打印出hello world!

这是为什么呢?

因为我们执行一个可执行程序之前必须先找到它在哪里,./就是告诉系统我们要执行的程序就位于当前目录下。

在这里插入图片描述
那为什么ls不用加./也能直接执行呢?

因为ls的路径已经被配置到环境变量当中了,系统可以直接通过环境变量PATH来找到ls命令,查看环境变量PATH我们可以看到如下内容:
在这里插入图片描述
可以看到环境变量PATH当中有多条路径,这些路径由冒号隔开,当你使用ls命令的时候,系统就会查看环境变量PATH,然后默认从左到右依次在各个路径下查找。
而ls命令就处于PATH当中的某一路径下,所以就算ls命令不带路径执行,系统也是能够找到的。
在这里插入图片描述

那可不可以让我们自己的可执行程序也不用带路径就可以执行呢?

当然可以,有两种方法:

方法一:直接将可执行程序拷贝到环境变量PATH的某一路径下。

既然在未指定路径的情况下,系统会根据环境变量PATH当中的路径进行查找,那我们就可以将我们的可执行程序拷贝到PATH的某一路径下,此后我们的可执行程序不带路径系统也可以找到了。

[_HPH@iZbp1ezziqb3x7ubzpkn9wZ demo-8-15]$ sudo cp test /usr/bin

方法二: 将可执行程序所在的目录导入到环境变量PATH中。

将可执行程序所在的目录导入到环境变量PATH当中,这样一来,没有指定路径时,系统就会来到该目录下进行查找了。

[_HPH@iZbp1ezziqb3x7ubzpkn9wZ demo-8-15]$ export PATH=$PATH:/home/_HPH/dirforproc/ENV

测试HOME

任何一个用户在运行系统登录时都有自己的主工作目录(家目录),环境变量HOME当中即保存的改用户的主工作目录。

普通用户示例:
在这里插入图片描述
超级用户示例:
在这里插入图片描述

~和HOME的关系
在这里插入图片描述
由此可知,使用cd ~命令可以直接进入家目录

测试SHELL

我们在Linux操作系统中所敲的各种命令,实际上需要由命令行解释器进行解释,而在Linux当中有许多种命令行解释器(例如bash、sh),我们可以通过查看环境变量SHELL来知道自己当前所用的命令行解释器的种类。
在这里插入图片描述
而该命令行解释器实际上是系统当中的一条命令,当这个命令运行起来变成进程后就可以为我们进行命令行解释。
在这里插入图片描述

和环境变量有关的命令

1、echo:显示某个环境变量的值。
在这里插入图片描述

2、export:设置一个新的环境变量
在这里插入图片描述
3、env:显示所有的环境变量
在这里插入图片描述

环境变量名称表示内容
PATH命令的搜索路径
HOME用户的主工作目录
SHELL当前Shell
HOSTNAME主机名
TERM终端类型
HISTSIZE记录历史命令的条数
SSH_TTY当前终端文件
USER当前用户
MAIL邮箱
PWD当前所处路径
LANG编码格式
LOGNAME登录用户名

4、set:显示本地定义的shell变量和环境变量。
在这里插入图片描述
5、unset:清除环境变量
在这里插入图片描述

环境变量的组织方式

在系统中,环境变量的组织方式如下:
在这里插入图片描述

每个程序都会收到一张环境变量表,环境表是一个字符指针数组,每个指针指向一个以’\0’结尾的环境字符串,最后一个字符指针为空。

通过代码获取环境变量

main函数其实是有三个参数的,但是平时基本不用它们,所以一般情况都没写出来。

main(int argc, char *argv[ ],char *env[ ])

我们先聊一聊前面两个参数。
在这里插入图片描述

运行结果如下:
在这里插入图片描述
现在我们来说说main函数的前两个参数,main函数的第二个参数是一个字符指针数组,数组当中的第一个字符指针存储的是可执行程序的位置,其余字符指针存储的是所给的若干选项,最后一个字符指针为空,而main函数的第一个参数代表的就是字符指针数组当中的有效元素个数。
在这里插入图片描述
我们可以尝试编写一个简单的代码,再来感受一下:

#include <stdio.h>                                                                                                                         
#include <string.h>
int main(int argc, char *argv[], char* envp[])
{if(argc > 1){if(strcmp(argv[1], "-a") == 0){printf("you used -a option...\n");}else if(strcmp(argv[1], "-b") == 0){printf("you used -b option...\n");}else{printf("you used unrecognizable option...\n");}}else{printf("you did not use any option...\n");}return 0;
}

运行效果如下:
在这里插入图片描述

现在来聊一聊main的第三个参数

main函数的第三个参数接受的实际就是环境变量表,我们可以通过main函数的第三个参数来获取系统的环境变量。

例如如下代码:
在这里插入图片描述
运行结果就是各个环境变量的值:
在这里插入图片描述
除了使用main函数第三个参数来获取环境变量之外,还可以使用第三方变量environ来获取。
在这里插入图片描述
我们同样可以获取环境变量的值:
在这里插入图片描述
注意:

  • libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用时要用extern进行声明。
  • 本地变量不是环境变量,本地变量只在bash内部有效。

通过系统调用获取环境变量

我们还可以使用getenv函数来获取环境变量,getenv函数可以根据所给环境变量名,在环境变量表中进行搜索,并返回一个指向相应值的字符串指针。

例如,使用getenv函数获取环境变量PATH的值。
在这里插入图片描述
运行结果:
在这里插入图片描述

环境变量通常是具有全局属性的

  • 环境变量通常具有全局属性,可以被子进程继承下去。
    在这里插入图片描述
    直接查看,发现没有结果,说明该环境变量根本不存在。
    在这里插入图片描述
    但是当我们添加环境变量export MYENV="hello world"之后再次运行,发现有结果了!
    在这里插入图片描述

说明: 环境变量是可以被子进程继承下去的!

实验:

  • 如果只进行MYENV="hello world",不调用export导出,在用我们的程序查看,会有什么结果?
    在这里插入图片描述
    不会打印出任何东西,因为是普通变量。

程序地址空间

这张空间布局图大家应该都见过:
在这里插入图片描述

下面是部分讲解:

在这里插入图片描述

在Linux操作系统中,我们可以通过一下代码对该布局图进行验证:

在这里插入图片描述

运行结果如下,与布局图所示是吻合的:

在这里插入图片描述

下面我们来看一段奇怪的代码:

在这里插入图片描述

代码当中用fork函数创建了一个子进程,其中让子进程先将全局变量g_val从100改为200后打印,而父进程先休眠3秒,然后再打印全局变量的值。

按道理来说子进程打印的全局变量的值为200,而父进程是在子进程将全局变量改后再打印的全局变量,那么也应该是200,但是结果却是100。

在这里插入图片描述

我们发现,父子进程,输出地址是一样的,但是变量内容不一样!能得出如下结论:

  • 变量内容不一样,所以父子进程输出的变量绝对不是同一个变量
  • 但地址值是一样的,说明,该地址绝对不是物理地址!
  • 在Linux系统下,这种地址叫做虚拟地址/线性地址
  • 我们在用C/C++语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,有OS统一管理

注意:虚拟地址物理地址之间的转化是由OS负责的。虚拟地址是给进程的——你的——用户的!

在这里插入图片描述

进程地址空间

我们之前将那张布局图成为进程地址空间实际上是不准确的,那张布局图实际上应该叫做进程地址空间,进程地址空间本质上是内存中的一种内核数据结构,在Linux中进程地址空间具体由结构体mm_struct实现。

进程地址空间就类似于一把尺子,尺子的刻度由0x00000000到0xffffffff,尺子按照刻度被划分为各个区域,例如代码区,堆区,栈区等。而在结构体mm_struct当中,便记录了各个边界刻度,例如代码区的开始刻度与结束刻度,如下图所示:

在这里插入图片描述

在结构体mm_struct当中,各个边界刻度之间的每一个刻度都代表一个虚拟地址,这些虚拟地址通过页表映射与物理内存建立联系。由于虚拟地址是由0x00000000到0xffffffff线性增长的,所以虚拟地址又叫做线性地址。

补充:

  1. 堆向上增长以及栈向下增长实际就是改变mm_struct当中堆和栈的边界刻度。
  2. 我们生成的可执行程序实际上也被分为了各个区域,例如初始化区,未初始化区等。当可执行程序运行起来的时候,操作系统将对应的数据加载到对应内存当中即可,大大提高了操作系统的工作效率。而进行可执行程序的“分区”操作的实际上是编译器,所以说代码的优化级别实际上是编译器说了算。

每个进程被创建时,其对应的进程控制块(task_struct)和进程地址空间(mm_struct)也会随之被创建。而操作系统可以通过进程的task_struct找到其mm_struct,因为task_struct当中有一个结构体指针存储的是mm_struct的地址。

例如,父进程有自己的task_struct和mm_struct,该父进程创建的子进程也有属于其自己的task_struct和mm_struct,父子进程的进程地址空间当中的各个虚拟地址分别通过页表映射到物理内存的某个位置,如下图:

在这里插入图片描述

说明: 上面的图就足以说明问题,同一个变量,地址相同,其实是虚拟地址相同,内容不同其实是被映射到了不同的物理地址!

而当子进程刚刚被创建的时候,子进程和父进程的数据和代码是共享的,即父子进程的代码和数据通过页表映射到物理内存的同一块空间。只有当父进程或子进程需要修改数据时,才将父进程的数据在内存当中拷贝一份,然后再进行修改。

例如,子进程需要将全局变量g_val改为200,那么此时就在内存的某处存储g_val的新值,并且改变子进程当中g_val的虚拟地址通过页表映射后得到的物理地址即可。

在这里插入图片描述

这种在需要进行数据修改时再进行拷贝的技术,叫做写时拷贝技术。

  1. 为什么数据要进行写实拷贝?

进程具有独立性。多进程运行,需要独享各种资源,多进程运行期间互不干扰,不能让子进程的修改影响到父进程。

  1. 为什么不在创建子进程的时候就进行数据的拷贝?

子进程不一定会使用父进程的所有数据,并且在子进程不对数据进行写入的情况下,没有必要对数据进行拷贝,我们应该按需分配,在需要修改数据的时候再进行分配(延时分配),这样可以高效的使用内存空间。

  1. 代码会不会进行写时拷贝?

90%的情况下是不会的,但并不能代表代码不能进行写时拷贝,例如在进行进程替换的时候,则需要进行代码的写时拷贝。

  1. 什么是地址空间?

进程地址空间,每一个进程,都会存在一个进程地址空间,32[0,4GB];
进程地址空间是数据结构,具体到进程中,就是特定数据结构的对象!

  1. 为什么要有进程地址空间?
  • 有了进程地址空间后,就不会有任何系统级别的越界和野指针等得发访问的问题存在了(可以进行拦截)。例如进程1不会错误的访问到进程2的物理地址空间,因为你对某一地址空间进行操作之前需要先通过页表映射到物理内存,而页表只会映射属于你自己的物理内存。总的来说,虚拟地址和页表的配合使用,本质功能就是包含内存。
  • 有了进程地址空间后,每个进程都认为时相同的空间范围,包括进程地址空间的构成和内部区域的划分顺序等都是相同的,这样一来我们在编写程序的时候只需要关注虚拟地址,无需关注数据在物理内存当中实际的存储位置。
  • 有了进程地址空间后,每个进程都会认为自己在独占内存,这样能更好的完成进程的独立性以及合理使用内存空间(当实际需要使用内存空间的时候再在内存进行开辟),并能将进程调度与内存管理进行解耦与分离。
  1. 对于创建进程的现阶段理解:

一个进程的创建实际上伴随着其进程控制块(task_struct)、进程地址空间(mm_struct)以及页表的创建。

  1. malloc/new 申请内存,申请的内存,你会直接使用吗?本质是在哪里申请的?

申请的内存,我们不一定直接使用。申请内存,本质是在进程的虚拟地址空间内申请的。操作系统需要为效率和资源的使用率负责,这样可以保证内存的使用率,不会空转;可以提升new或者malloc的速度。

Linux2.6内核进程调度队列

在这里插入图片描述

一个CPU拥有一个runqueue

如果有多个CPU就要考虑进程个数的父子均衡问题。

优先级

queue下标说明:

  • 普通优先级:100~139。
  • 实时优先级:0~99。

我们进程的都是普通的优先级,前面说到nice值的取值范围是 -20 ~ 19,共40个级别,一次对用queue当中普通优先级的下标100 ~ 139。

注意: 实时优先级对应实时进程,实时进程是指先将一个进程执行完毕再执行下一个进程,现在基本不存在这种机器了,所以对于queue当中下标为 0 ~ 99的元素我们不关心。

活动队列

时间片还没有结束的所有进程都按照优先级放在活动队列当中,其中nr_active代表总共有多少个运行状态的进程,而queue[140]数组当中的一个元素就是一个进程队列,相同优先级的进程按照FIFO规则进行排队调度。

调度过程如下:

  1. 从0下标开始遍历queue[140]。
  2. 找到第一个非空队列,该队列必定为优先级最高的队列。
  3. 拿到选中队列的第一个进程,开始运行,调度完成。
  4. 接着拿到选中队列的第二个进程进行调度,直到选中进程队列当中的所有进程都被调度。
  5. 继续向后遍历queue[140],寻找下一个非空队列。

bitmap[5]:queue数组当中一个有140个元素,即140个优先级,一共140个进程队列,为了提高查找非空队列的效率,就可以用5 × 32个比特位表示队列是否为空,这样一来便可以大大提高查找效率。

总结: 在系统当中查找一个最合适调度的进程的时间复杂度是一个常数,不会随着进程增多而导致时间成本增加,我们称之为进程调度的O(1)算法。

过期队列

  • 过期队列和活动队列的结构相同。
  • 过期队列上放置的进程都是时间片耗尽的进程。
  • 当活动队列上的进程被处理完毕之后,对过期队列的进程进行时间片重新计算。

active指针和expired指针

  • active指针永远指向活动队列。
  • expired指针永远指向过期队列。

由于活动队列上时间片未到期的进程会越来越少,而过期队列上的进程数量越来越多(新创建的进程都会被放到过期队列上),那么总会出现活动队列上的全部进程的时间片都到期的情况,这时将active指针和expired指针的内容交换,就相当于让过期队列变成活动队列,活动队列变成过期队列,就相当于又具有了一批新的活动进程,如此循环进行即可。

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

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

相关文章

什么?!新版 Node.js V22.5 自带 SQLite 模块啦

前言 2024年7月&#xff0c;Node.js V22.5.0 版本发布&#xff0c;自带了 SQLite 模块&#xff0c;意味着开发者可以直接在程序中使用 SQLite 数据库&#xff0c;而无需引入第三方库&#x1f44d;。 话不多说&#xff0c;感觉来体验一波✈。 安装/升级 我现在用的是21.4.0版…

Trm理论 3(ELMo)

LSTM模型 如图&#xff0c;LSTM模型是rnn模型的改良版&#xff0c;通过ft来选择性的保留上一次得到的信息 ELMo模型&#xff08;双向LSTM&#xff09; ELMo模型是对word2vec的改良&#xff0c;改良了word2vec的二义性 对比上下两图&#xff0c;可以发现&#xff0c;WE对预测…

【qt】qss使用

1.按钮设置颜色 ui->pushButton->setStyleSheet("QPushButton { color : red;}");也可以通过rgb来设置 ff表示红色拉满&#xff0c;gb为0当然是红色 这只是针对pushbutton对象的控件设置的&#xff0c;如果我想设置所有的按钮空间都是一个颜色 这是通过设置界…

【无标题】【Datawhale X 李宏毅苹果书 AI夏令营】批量归一化

1、批量归一化的作用 批量归一化&#xff08;Batch Normalization&#xff0c;BN&#xff09;的把误差曲面变得平滑&#xff0c;使训练能够得到快速收敛&#xff1b; 训练过程的优化&#xff1a;使用自适应学习率等比较进阶的优化训练方法&#xff1b; 训练对象的优化&#xf…

Linux 服务器下非root用户安装CUDA完整流程(多次踩雷经验总结)

参考博客&#xff1a; linux下安装cuda和cudnn&#xff08;非root权限&#xff09;_cuda下载安装 远程服务器 linux-CSDN博客 Linux下非root用户安装CUDA_linux下cuda-toolkit-archive-CSDN博客 非root用户安装cuda10.1&#xff0c;以及CUDA不同版本间切换_非root用户.run文…

android kotlin基础复习—if when

1、新建kt并运行 新建文件kt 运行文件kt 2、kotlin语句 if when的使用 var x 5val y 9if (x in 1..8) {println("x 在区间内")} 说明&#xff1a; var&#xff1a;定义变量 val定义常量。 代码中会看到那个<&#xff0c;也就是说包括1&#xff0c;8。 3、输…

glsl着色器学习(二)

书接上文&#xff0c;第一篇文章已经将顶点着色器和片段着色器的内容编写好了&#xff0c;这篇文章就创建着色器并编译 创建顶点着色器对象 const vertexShader gl.createShader(gl.VERTEEX_SHADER); gl.shaderSource(vertexShader,vsGLSL); gl.compileShader(vertexShader …

J.U.C Review - 阻塞队列原理/源码分析

文章目录 阻塞队列的由来BlockingQueue的操作方法BlockingQueue的实现类ArrayBlockingQueueLinkedBlockingQueueDelayQueuePriorityBlockingQueueSynchronousQueue 阻塞队列原理深入分析1. 构造器和监视器初始化2. put操作的实现3. take操作的实现4. 注意事项小结 线程池中的阻…

qmt量化交易策略小白学习笔记第57期【qmt编程之期权数据--获取指定期权品种的详细信息--内置Python】

qmt编程之获取期权数据 qmt更加详细的教程方法&#xff0c;会持续慢慢梳理。 也可找寻博主的历史文章&#xff0c;搜索关键词查看解决方案 &#xff01; 获取指定期权品种的详细信息 该函数能帮助用户获取指定期权品种的详细信息&#xff0c;如期权代码、市场、涨跌停价、期…

c++返回一个pair类型

前言 Under the new standard we can list initialize the return value. 代码测试 #include<iostream> #include<string> #include<vector>std::pair<std::string, int> process(std::vector<std::string>& v) {if (!v.empty()){return …

窖藏之秘:白酒在窖藏过程中经历了哪些变化?

在中华五千年的文明史中&#xff0c;白酒一直扮演着举足轻重的角色。它不仅是文人墨客笔下的灵感源泉&#xff0c;更是亲朋好友间传递情感的桥梁。在众多白酒品牌中&#xff0c;豪迈白酒&#xff08;HOMANLISM&#xff09;以其不同的酿造工艺和窖藏技艺&#xff0c;成为了酒中翘…

【前端面试】设计循环双端队列javascript

题目 https://leetcode.cn/problems/design-circular-deque/description/ 存储循环队列的向量空间是循环的&#xff0c;用通俗的话来讲&#xff0c;就是我们在做next或者prev操作时&#xff0c;不会发生溢出 取模、或者直接判断是否为0/size返回一个值。 数组实现 用函数来…

Python文件自动分类

假如这样的步骤全部手动做下来耗时是6秒&#xff0c;在文件数量不多的情况下&#xff0c;比如10个文件&#xff0c;总共耗时一分钟其实是能够接受的。 但当文件数量特别多时&#xff0c;或者这个操作特别频繁每天都要做十几二十次时&#xff0c;手动操作就会变得耗时又繁琐…

【Agent】Agent Q: Advanced Reasoning and Learning for Autonomous AI Agents

1、问题背景 传统的训练Agent方法是在静态数据集上进行监督预训练&#xff0c;这种方式对于要求Agent能够自主的在动态环境中可进行复杂决策的能力存在不足。例如&#xff0c;要求Agent在web导航等动态设置中执行复杂决策。 现有的方式是用高质量数据进行微调来增强Agent在动…

SpringBoot3.x+MyBatisPlus+druid多数据源配置

1 引言 本章主要介绍SpringBoot3.x多数据源配置&#xff0c;以及在此基础上配置分页拦截&#xff0c;自动填充功等功能&#xff0c;源码链接在文章最后。下面列出几个重要文件进行介绍。 2 项目结构 整体项目结构如下&#xff0c;主要介绍配置文件和配置类。 3 主要代码 …

Android Telephony总结

1、Telephony 业务介绍 Android telephony涉及较多模块 1.1、STK业务介绍 1.1.1、STK域选 1.1.2、是否支持STK Telephon STK-CSDN博客 1.1.3、STK应用的安装卸载 1.2、SS补充业务 1.3、通话业务 1.3.1、紧急号码 ECC 号码总结_ecc号码-CSDN博客 1.4、SMS 1.4.1 短信发送方式…

Datawhale X 李宏毅苹果书 AI夏令营-深度学习入门task3:实践方法论

在应用机器学习算法时&#xff0c;实践方法论能够帮助我们更好地训练模型。 1.模型偏差 模型偏差可能会影响模型训练。举个例子&#xff0c;假设模型过于简单&#xff0c;即使找到的最好的函数也不能满足需求。这种情况就是想要在大海里面捞针&#xff08;一个损失低的函数&am…

数学建模强化宝典(9)遗传算法

前言 遗传算法&#xff08;Genetic Algorithm, GA&#xff09;是一种模拟达尔文生物进化论的自然选择和遗传学机理的生物进化过程的计算模型&#xff0c;它通过模拟自然进化过程来搜索最优解。遗传算法最早由美国的John Holland于20世纪70年代提出&#xff0c;并逐渐成为解决复…

Spring6学习笔记2:容器IoC

文章目录 3 容器&#xff1a;IoC3.1 IoC容器3.1.2 依赖注入3.1.3 IoC容器在Spring的实现 3.2 基于XML管理Bean3.2.1 搭建子模块spring6-ioc-xml3.2.2 实验一&#xff1a;获取bean①方式一&#xff1a;根据id获取②方式二&#xff1a;根据类型获取③方式三&#xff1a;根据id和类…

探索英文字体设计的奥秘,解读风格与实用技巧

英文字体设计是一门融合了艺术与技术的学科。字体不仅仅是文本的视觉表现&#xff0c;更是传递情感、信息和品牌个性的媒介。从印刷时代到数字时代&#xff0c;英文字体的设计和应用发生了巨大的变化&#xff0c;而现代字体设计师则肩负着为视觉传达赋予新生命的使命。本文将深…