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

深入Linux序列:进程的终止与等待

在这里插入图片描述

在之前的学习中,我们知道我们的进程在运行结束的时候,那么它并不会立即进入死亡状态,而是先进入僵尸状态,维持僵尸状态一段时间,那么此时在僵尸状态中的进程,那么它的内核数据已经移出内存被清理了,但是该进程在操作系统内对应的task_struct结构体不会被立即被回收清理,那么之所以不会被立即回收清理,就是因为我们需要知道获取到进程是最后是怎么退出的,而这个信息就被保存在我们该进程所对应的task_struct结构体当中,那我们需要获取到进程退出的信息。
那么接下来我们对于进程的终止无非概括起来就有这三个疑问:我们进程的终止的信息是什么,保存在哪里,以及我们要获取该进程的终止信息有什么用?那么接下来我便围绕这三个问题展开我们的下文,那么废话不多说,就让我们进入正文部分的讲解

进程的终止

进程就和我们人类一样,有着生命的开始与生命的结束,那么对于进程来说,那么我们进程一般是被用户创建来完成某项任务以及工作的,那么一旦我们该进程执行完他所有的内容,那么我们必须要知道它做的怎么样,不可能该进程处理执行完它所有的代码然后结束之后,我们就对它不管不问了,就好比于你在单位上你每天工作完都需要向单位上的领导或者上级汇报你今天干了什么事情,究竟是完成了领导交给我的任务还是没完成还是说我自己就摆烂不想做了,那么这些事情领导都有权知道然后再进行相应的决策。

那么现在我们知道我们为什么要获取进程终止的信息了,那么目的就是知道进程完成它的工作完成的怎么样,那么接下来的问题就是我们如何查询我们进程的终止的信息呢

那么我们知道我们一个进程的上下文就在我们进程代码的main函数当中,那么在大学的时候,我们平常在写c语言或者c++代码的时候,都是公式的引个头文件然后写一个main函数然后return 0,但是我们从来都没有思考过,我们在main函数定义的return 0的作用是什么,为什么要有return以及为return的为什么是0而不是1或者2等其他的数字

那么这个在main函数的return的内容,那么它就是我们程序的退出码,而退出码就反应了我们进程终止时的一个状态,那么我们进程结束时退出一般有三种场景,分别是:

  • 1: 执行完该进程所有的代码内容,并且结果正确
  • 2: 执行完该进程所有的代码内容,但结果不正确
  • 3: 进程在执行的途中出现异常,没有执行完所有的代码然后退出了

那么对于这三种情况,其实我们真正关心的只有第二和第三种情况,就好比于小学时你考试考了90分,考了全班第一名,那么你爸根本不会对你有所过问,更不会问你为什么考了90分,但是如果说你考了13分,并且还是全班倒数第一,那么你爸肯定就要责问你,为什么考了13分,是什么原因导致的,那么你得向你爸解释说到是因为状态不好还是说当天肚子不舒服导致的。

那么在刚才的例子中,你考了13分或者倒数第一,那么就类似于你的进程要么出现了第二种或者第三种情况,那么究竟是为什么出现这个原因,那么就需要退出码来进行解释,那么我们的退出码的每一个值都有具体的含义,那么退出码为0代表着进程执行完所有的代码内容,并且结果正确,而如果退出码为1或者2,那么说明进程出现了相应的问题

那么在我们的Linux下,也有相应的指令能够看到获取到最近一次结束的进程的退出码,那么该指令也就是echo $?

那么输入该指令接着下一行就能够显示反馈最近一次结束进程的退出码
在这里插入图片描述

那么这里我们一定要注意我刚才所说的那第一个以及第二场景中关于结果是否正确的问题,我觉得很多读者其实包括之前我在内,都会误以为这个结果是我们代码层面上的逻辑出现的错误,比如是我们要编写一个代码要实现两个数a和b相加,结果代码写成了a和b相乘,那么这是不是就意味着出现了我们上面所说的第二种场景,也就是代码执行完了,但是结果是错的呢,这里我们注意这里的结果不是指的是我们所谓代码的逻辑层面上的结果,因为操作系统它哪里能知道能检查我们写的代码想要完成什么,想要干什么,那么这里的结果是指的是语言或者说语法层面上的,比如我们指针是否出现了野指针是否有越界访问等,所以一定要注意理解

那么我们除了我们的退出码,其实我们还有错误码的存在,那么所谓的错误码就是我们调用我们的库函数出现失败或者异常,那么此时我们调用库函数失败的错误信息会保存到我们的全局变量中errno当中,那么如果我们还是正常执行完我们所有的代码,那么我们的退出码是是0,但是执行完所有的代码不代表我们的代码没出现任何错误,如果有比如有库函数的调用失败发生,那么我们可以通过错误码在我们的代码中定义一个检查逻辑,看是否出现库函数的调用出错,而我们的errno只记录最新一次库函数调用出错的错误码,
我们可以接着借组perror函数,能够将我们的错误码对应的含义转换为字符串,perror函数会返回一个字符类型的指针,那么这里注意我们的错误码不是我们的退出码,我们检查该进程的退出状态的时候,关心的是退出码而不是所谓的错误码

那么了解了我们的退出码等概念,那么我们就可以在代码中定义相应的逻辑比如if else的逻辑判断我们代码如果出现了这样的错误的结果,那么我们会进行怎么样的处理,然后return一个非0的退出码,那么这个非0的退出码的含义则是由我们用户自己来定义的

#include <stdio.h>int main() {FILE *file = fopen("example.txt", "r");if (file == NULL) {printf("File not found.\n");return 1;  // 文件不存在,返回非零值}// 如果文件存在,进行其他操作// ...fclose(file);printf("File found and processed.\n");return 0;  // 成功完成所有操作,返回0
}

那么进程会专门记录进程的退出码以及异常的情况,那么在我们进程所对应的task_struct结构体中会专门有两个字段分别是exit_code以及exit_signal来记录这两个内容,所以这就是为什么我们进程在进入僵尸状态之后我们该进程对应的task_struct结构体不会被立即回收清理掉的原因,因为得有进程来接收这两个字段的内容来获取该进程的退出情况。

进程的等待

那么前面详细的介绍我们进程的终止对应的退出码就为了给我们进程的等待做铺垫。

那么我们知道我们父进程在代码层面上可以调用我们的系统调用接口也就是fork函数来创建一个子进程,那么我们知道我们父进程创建子进程的目的就是为了让子进程来帮组父进程来完成某项特定的任务或者工作,就好比我们自己在写代码的时候,我们代码所有的逻辑以及各种模块我们不会都全部定义在我们的main函数当中,而是会自己定义一些函数来完成这个主函数特定的功能与模块,在将处理后的结果返回给我们的主函数即可,这样的好处就是功能模块化使得逻辑清晰便于管理。

同理我们的父进程创建子进程的思路和我们上面的函数的例子异曲同工,就是父进程让子进程帮自己办点事,那么我们可以通过一个循环来创建一批子进程,然后根据fork函数的返回值让子进程有着自己的执行流,然后我们可以用过一个函数来封装子进程的执行内容,那么这就是我们一个刚才的模块化思想的简单应用
在这里插入图片描述
在这里插入图片描述

而在此过程中父进程要知道子进程完成的怎么样,那么则是需要获取子进程的退出码等内容

那么我们就需要我们的系统调用接口,这里有两个函数wait函数与waitpid函数,来获取我们子进程的退出码等内容。

那么wait函数的功能是waitpid函数的子集,所以我们一般使用waitpid函数要多一些,那么我们的wait函数的参数是一个int类型的指针,而这个参数的作用我在后文介绍waitpid的时候会进行讲解,而wait的返回值则有3种,分别是要么大于0,或者等于0以及小于0.

那么大于0则代表我们等待的子进程结束,那么我们这个返回值就是子进程的PID而小于0则是等待出错,当我们等待的不是该父进程对应的子进程或者该父进程没有创建子进程,那么就会返回一个小于0的返回值,而返回值为0,说明我们该父进程等待的子进程还没有运行结束,而waitpid的返回值也是和wait函数是同样的内容
在这里插入图片描述

在这里插入图片描述


那么我们知道当我们的父进程调用了fork函数创建了子进程,那么此时我们的父进程与子进程会有着不同的执行流,那么当我们父进程执行到我们的wait函数语句的时候,那么此时我们父进程不会执行wait函数之后的语句了,而是一直卡在wait函数这里等待我们子进程的结束,那么此时我们父进程的状态会从原来的R状态修改为S状态也就是阻塞等待状态。

那么在我们之前的学习中,我们知道我们的S状态是因为我们该进程需要与我们的io设备交互,此时要等待我们相应的io设备就绪有响应才能进行之后的代码,比如我们在代码中调用了我们的printf函数需要向显示器做写入或者调用了scanf函数要获取用户键盘上的输入,但是在这里我们知道调用wait函数,那么如果等待的子进程还在运行,那么此时我们的父进程也会陷入等待状态,而进程对应的task_struct结构体中也有对应的一个指针,指向该进程对应的等待队列,那么此时该父进程就会被放到等待队列当中去


而对于waitPID来说,那么它有三个参数:

  1. pid

    :指定要等待的子进程的PID。

    • 可以是特定的子进程PID,也可以是特殊值如-1(等待任何子进程)。
  2. status

    :一个指向int类型的指针,用于存储子进程的退出状态。

    • 这个参数是一个输出型参数,waitpid()会将子进程的退出状态保存在这里。
  3. options

    :等待选项,可以是0或其他标志的组合。

    • 如果设置为0,则表示采用阻塞式等待,直到指定的子进程终止。

输出型参数也就是int类型的指针,那么它则是带出我们子进程的退出码以及异常等信息,而之前我们说过我们进程的结构体中专门有两个字段来记录该进程终止时的退出码以及异常状况,那么此时我们的系统调用函数wait就可以获取到这两个字段然后通过位运算将其保存到传入的int类型的参数中,那么我们知道我们一个int类型的数有32个二进制位,那么此时我们这个数据的最低的8位则是用来表示进程的异常,而接着的后8位则是用来表示进程的退出码,那么同样我们可以通过我们的位运算来获取得到对应的内容,但是我们Linux下专门定义了对应的宏可以用来专门来将其转换得到我们的子进程的退出码,那么这个宏就是:

WEXITSTATUS(status):
如果子进程正常终止,则返回其退出码。


而对于option选项的话,如果option是0的话,那么意味着我们采取的是阻塞式等待我们的子进程的退出信息,那么一旦我们该子进程还在运行,那么我们就陷入等待,

那么这里我的option也可以采取的是非阻塞式等待,那么就需要我们设置为WONOHANG,那么WONOHANG是Linux控制waitpid函数行为的一个宏定义,而这个选项就意味着我们都waitpid是采取非阻塞方式来等待
那么我还是举一个例子来理解我们阻塞式等待与非阻塞式等待的一个区别,就好比你现在打电话查询你的考试信息,那么阻塞式等待就是你在电话的那一头一直不挂着电话,然后自己什么也不干,就一直等待电话的另一头给你考试的信息,而非阻塞式等待的话,则是你先打一通电话,来查询是否有结果,如果没有结果,那么就将电话给挂掉,然后自己在玩会儿手机敢自己的事情,然后过了一段时间,再打电话进行查询,重复这样的过程知道最终获取到信息。

所以我们的刚才例子中的第二种方式就是我们的非阻塞+轮询的方式,那么我们的父进程此时的状态不会被设置为S状态,而是可以继续执行wait之后的代码,然后时不时的在调用waitpid来进行查询,那么这里就是通过我们的循环来实现,那么这就是我们的非阻塞+轮询的方式来等待,相比于之前啥都不干,那么非阻塞等待意味着父进程能够执行自己之后的代码,显然这种方式要更高效一点

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>int main() {pid_t pid = fork();if (pid == -1) {perror("fork");exit(EXIT_FAILURE);} else if (pid == 0) {// 子进程printf("Child process is running...\n");sleep(5);  // 模拟子进程执行一些任务printf("Child process is exiting with status 42\n");exit(42);} else {// 父进程int status;while (1) {pid_t child_pid = waitpid(pid, &status, WNOHANG);if (child_pid == -1) {perror("waitpid");exit(EXIT_FAILURE);//EXIT_FAILURE是一个用于表示程序异常终止的宏}if (child_pid == 0) {// 子进程尚未终止,父进程可以继续执行其他任务printf("Child process is still running. Polling...\n");sleep(1);  // 等待一段时间再继续轮询} else {// 子进程已终止if(WIFSIGNALED(status)==0)printf("Child process exited with status %d\n", WEXITSTATUS(status));break;  // 退出循环}}}return 0;
}

结语

那么这就是本篇文章的全部内容,主要讲述了我们的进程的终止与等待,那么我的下一篇文章将介绍进程的替换的内容,那么我会持续更新,希望你能够多多关注与支持,那么如果本篇文章对你有所帮助的话,那么还请多多三连加关注支持一下哦,你的支持就是我最大的动力!
在这里插入图片描述

相关文章:

深入Linux序列:进程的终止与等待

在之前的学习中&#xff0c;我们知道我们的进程在运行结束的时候&#xff0c;那么它并不会立即进入死亡状态&#xff0c;而是先进入僵尸状态&#xff0c;维持僵尸状态一段时间&#xff0c;那么此时在僵尸状态中的进程&#xff0c;那么它的内核数据已经移出内存被清理了&#xf…...

蓝桥杯之日期问题2

文章目录 需求11.1 代码 2.需求22.1代码 需求1 2020 年春节期间&#xff0c;有一个特殊的日期引起了大家的注意&#xff1a;2020 年 2 月 2 日。因为如果将这个日期按 “yyyymmdd” 的格式写成一个 8 位数是 20200202&#xff0c;恰好是一个回文数。我们称这样的日期是回文日期…...

【STL】7.STL常用算法(1)

STL常用算法&#xff08;1&#xff09; 前言简介一.遍历算法1.for_each2.transform 二.查找算法1.find2.find_if3.adjacent_find4.binary_search5.count6.cout_if 三.排序算法1.sort2.random_shuffle3.merge4.reverse 总结 前言 stl系列主要讲述有关stl的文章&#xff0c;使用S…...

uniapp 本地数据库多端适配实例(根据运行环境自动选择适配器)

项目有个需求&#xff0c;需要生成app和小程序&#xff0c;app支持离线数据库&#xff0c;如果当前没有网络提醒用户开启离线模式&#xff0c;所以就随便搞了下&#xff0c;具体的思路就是&#xff1a; 一个接口和多个实现类&#xff08;类似后端的模板设计模式&#xff09;&am…...

百度觉醒,李彦宏渴望光荣

文 | 大力财经 作者 | 魏力 2025年刚刚开年&#xff0c;被一家名为DeepSeek的初创公司强势改写。在量化交易出身的创始人梁文锋的带领下&#xff0c;这支团队以不到ChatGPT 6%的训练成本&#xff0c;成功推出了性能可与OpenAI媲美的开源大模型。 此成果一经问世&#xff0c;…...

【算法工程】大模型局限性新发现之解决能连github但无法clone项目的问题

最近&#xff0c;linux服务器遇到一个奇怪的问题&#xff0c;能ping通github&#xff0c;但是无法clone git项目&#xff0c;尝试了各种大模型&#xff0c;都提到代理啥的问题&#xff0c;发现没有一个能解决问题。 后来尝试设置 http.sslVerify 为 false&#xff0c;才解决问题…...

SOME/IP-SD -- 协议英文原文讲解3

前言 SOME/IP协议越来越多的用于汽车电子行业中&#xff0c;关于协议详细完全的中文资料却没有&#xff0c;所以我将结合工作经验并对照英文原版协议做一系列的文章。基本分三大块&#xff1a; 1. SOME/IP协议讲解 2. SOME/IP-SD协议讲解 3. python/C举例调试讲解 5.1.2.4…...

软件测试八股文,软件测试常见面试合集【附答案】

PS&#xff1a;加上参考答案有几十万字&#xff0c;答案就没有全部放上来了&#xff0c;高清打印版本超过400多页&#xff0c;评论区留言直接获取 1、你的测试职业发展是什么? 2、你认为测试人员需要具备哪些素质 3、你为什么能够做测试这一行 4、测试的目的是什么? 5、…...

数据结构秘籍(一)线性数据结构

1.数组 数组&#xff08;Array&#xff09;是一种很常见的数据结构。它由相同类型的元素&#xff08;element&#xff09;组成&#xff0c;并且是使用一块连续的内存来存储。 我们直接可以利用元素的索引&#xff08;index&#xff09;计算出该元素对应的存储地址。 数组的特…...

TFChat:腾讯大模型知识引擎(DeepSeek R1)+飞书机器人实现AI智能助手

效果 TFChat项目地址 https://github.com/fish2018/TFChat 腾讯大模型知识引擎用的是DeepSeek R1&#xff0c;项目为sanic和redis实现&#xff0c;利用httpx异步处理流式响应&#xff0c;同时使用buffer来避免频繁调用飞书接口更新卡片的网络耗时。为了进一步减少网络IO消耗&…...

使用消息队列怎样防止消息重复?

大家好&#xff0c;我是君哥。 使用消息队列时&#xff0c;我们经常会遇到一个可能对业务产生影响的问题&#xff0c;消息重复。在订单、扣款、对账等对幂等有要求的场景&#xff0c;消息重复的问题必须解决。 那怎样应对重复消息呢&#xff1f;今天来聊一聊这个话题。 1.三…...

MySQL安装多版本与版本切换

起因 今天在将一个项目部署到本地&#xff0c;想着是先找到一个功能差不多的开源项目&#xff0c;再在这基础之上进行改动&#xff0c;找到的这个项目使用的MySQL版本是MySQL5.7&#xff0c;应该是比较古早的项目了&#xff0c;但是我现在装的是8.4版本的&#xff0c;所以涉及…...

Docker02 - 深入理解Docker

深入理解Docker 文章目录 深入理解Docker一&#xff1a;Docker镜像原理1&#xff1a;镜像加载原理1.1&#xff1a;unionFS1.2&#xff1a;加载原理 2&#xff1a;分层理解 二&#xff1a;容器数据卷详解1&#xff1a;什么是容器数据卷2&#xff1a;使用数据卷3&#xff1a;具名…...

检查SSH安全配置-sshd服务端未认证连接最大并发量配置

介绍 MaxStartups参数指到SSH守护进程的未经身份验证的最大并发连接数。 逻辑依据 为防止系统因大量待处理的身份验证连接尝试而出现拒绝服务的情况&#xff0c;请使用 MaxStartups 的速率限制功能来保护 sshd 登录的可用性&#xff0c;并防止守护进程不堪重负。 检查方法 …...

HarmonyOS Design 介绍

HarmonyOS Design 介绍 文章目录 HarmonyOS Design 介绍一、HarmonyOS Design 是什么&#xff1f;1. 设计系统&#xff08;Design System&#xff09;2. UI 框架的支持3. 设计工具和资源4. 开发指南5. 与其他设计系统的对比总结 二、HarmonyOS Design 特点 | 应用场景1. Harmon…...

C++中的多重继承

在 C 中&#xff0c;多重继承是一种允许一个类同时继承多个基类的特性。这意味着派生类可以继承多个基类的属 性和方法。 多重继承增加了语言的灵活性&#xff0c;但同时也引入了额外的复杂性&#xff0c;特别是当多个基类具有相同 的成员时。 基本概念 在多重继承中&#xff…...

Java基础第14天-坦克大战【1】

Java绘图坐标体系 像素 计算机在屏幕上显示的内容都是由屏幕上的每一个像素组成的。如&#xff0c;计算机显示器的分辨率是800x600&#xff0c;表示计算机屏幕上的每一行由800个点组成&#xff0c;共有600行&#xff0c;整个计算机屏幕共有480000个像素。像素是一个密度单位。…...

Java线程池入门04

1. 提交任务的两种方式 executorsubmit 2. executor executor位于Executor接口中 public interface Executor {void executor(Runnable command); }executor提交的是无返回值的任务 下面是一个具体的例子 package LearnThreadPool; import java.util.concurrent.ExecutorSe…...

【论文笔记-ECCV 2024】AnyControl:使用文本到图像生成的多功能控件创建您的艺术作品

AnyControl&#xff1a;使用文本到图像生成的多功能控件创建您的艺术作品 图1 AnyControl的多控制图像合成。该研究的模型支持多个控制信号的自由组合&#xff0c;并生成与每个输入对齐的和谐结果。输入到模型中的输入控制信号以组合图像显示&#xff0c;以实现更好的可视化。 …...

计算机毕业设计 ——jspssm519Springboot 的幼儿园管理系统

作者&#xff1a;程序媛9688 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等。 &#x1f31f;文末获取源码数据库&#x1f31f; 感兴趣的可以先收藏起来&#xff0c;还有大家在毕设选题&#xff08;免费咨询指导选题&#xff09;&#xf…...

KubeSphere 容器平台高可用:环境搭建与可视化操作指南

Linux_k8s篇 欢迎来到Linux的世界&#xff0c;看笔记好好学多敲多打&#xff0c;每个人都是大神&#xff01; 题目&#xff1a;KubeSphere 容器平台高可用&#xff1a;环境搭建与可视化操作指南 版本号: 1.0,0 作者: 老王要学习 日期: 2025.06.05 适用环境: Ubuntu22 文档说…...

深入浅出Asp.Net Core MVC应用开发系列-AspNetCore中的日志记录

ASP.NET Core 是一个跨平台的开源框架&#xff0c;用于在 Windows、macOS 或 Linux 上生成基于云的新式 Web 应用。 ASP.NET Core 中的日志记录 .NET 通过 ILogger API 支持高性能结构化日志记录&#xff0c;以帮助监视应用程序行为和诊断问题。 可以通过配置不同的记录提供程…...

三维GIS开发cesium智慧地铁教程(5)Cesium相机控制

一、环境搭建 <script src"../cesium1.99/Build/Cesium/Cesium.js"></script> <link rel"stylesheet" href"../cesium1.99/Build/Cesium/Widgets/widgets.css"> 关键配置点&#xff1a; 路径验证&#xff1a;确保相对路径.…...

Cilium动手实验室: 精通之旅---20.Isovalent Enterprise for Cilium: Zero Trust Visibility

Cilium动手实验室: 精通之旅---20.Isovalent Enterprise for Cilium: Zero Trust Visibility 1. 实验室环境1.1 实验室环境1.2 小测试 2. The Endor System2.1 部署应用2.2 检查现有策略 3. Cilium 策略实体3.1 创建 allow-all 网络策略3.2 在 Hubble CLI 中验证网络策略源3.3 …...

C++使用 new 来创建动态数组

问题&#xff1a; 不能使用变量定义数组大小 原因&#xff1a; 这是因为数组在内存中是连续存储的&#xff0c;编译器需要在编译阶段就确定数组的大小&#xff0c;以便正确地分配内存空间。如果允许使用变量来定义数组的大小&#xff0c;那么编译器就无法在编译时确定数组的大…...

保姆级教程:在无网络无显卡的Windows电脑的vscode本地部署deepseek

文章目录 1 前言2 部署流程2.1 准备工作2.2 Ollama2.2.1 使用有网络的电脑下载Ollama2.2.2 安装Ollama&#xff08;有网络的电脑&#xff09;2.2.3 安装Ollama&#xff08;无网络的电脑&#xff09;2.2.4 安装验证2.2.5 修改大模型安装位置2.2.6 下载Deepseek模型 2.3 将deepse…...

PostgreSQL——环境搭建

一、Linux # 安装 PostgreSQL 15 仓库 sudo dnf install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-$(rpm -E %{rhel})-x86_64/pgdg-redhat-repo-latest.noarch.rpm# 安装之前先确认是否已经存在PostgreSQL rpm -qa | grep postgres# 如果存在&#xff0…...

Modbus RTU与Modbus TCP详解指南

目录 1. Modbus协议基础 1.1 什么是Modbus? 1.2 Modbus协议历史 1.3 Modbus协议族 1.4 Modbus通信模型 🎭 主从架构 🔄 请求响应模式 2. Modbus RTU详解 2.1 RTU是什么? 2.2 RTU物理层 🔌 连接方式 ⚡ 通信参数 2.3 RTU数据帧格式 📦 帧结构详解 🔍…...

嵌入式学习之系统编程(九)OSI模型、TCP/IP模型、UDP协议网络相关编程(6.3)

目录 一、网络编程--OSI模型 二、网络编程--TCP/IP模型 三、网络接口 四、UDP网络相关编程及主要函数 ​编辑​编辑 UDP的特征 socke函数 bind函数 recvfrom函数&#xff08;接收函数&#xff09; sendto函数&#xff08;发送函数&#xff09; 五、网络编程之 UDP 用…...

CSS 工具对比:UnoCSS vs Tailwind CSS,谁是你的菜?

在现代前端开发中&#xff0c;Utility-First (功能优先) CSS 框架已经成为主流。其中&#xff0c;Tailwind CSS 无疑是市场的领导者和标杆。然而&#xff0c;一个名为 UnoCSS 的新星正以其惊人的性能和极致的灵活性迅速崛起。 这篇文章将深入探讨这两款工具的核心理念、技术差…...