2024年10月

0 引言

  • 我常以为 INFO 日志级别的 应用程序日志代码,不会被执行(比如,实验1中的
    printTestLog
    函数)。但今天线上的问题,证实了这个思路是错的。

1 验证实验

  • 版本信息
  • jdk : 1.8
  • 日志组件
  • slf4j.version
    : 1.7.25
  • log4j.version
    : 2.20.0
<!-- log [start] -->
<dependency>
	<groupId>org.slf4j</groupId>
	<artifactId>slf4j-api</artifactId>
	<version>${slf4j.version}</version>
</dependency>
<dependency>
	<groupId>org.apache.logging.log4j</groupId>
	<artifactId>log4j-api</artifactId>
	<version>${log4j.version}</version>
</dependency>
<dependency>
	<groupId>org.apache.logging.log4j</groupId>
	<artifactId>log4j-core</artifactId>
	<version>${log4j.version}</version>
</dependency>
<dependency>
	<groupId>org.apache.logging.log4j</groupId>
	<artifactId>log4j-slf4j-impl</artifactId>
	<version>${log4j.version}</version>
</dependency>
<dependency>
	<groupId>org.apache.logging.log4j</groupId>
	<artifactId>log4j-jul</artifactId>
	<!--<version>2.13.3</version>-->
	<version>${log4j.version}</version>
	<scope>compile</scope>
</dependency>
<!-- log [end] -->

实验1:日志框架打印应用程序日志代码的执行情况

日志配置策略: log4j2.properties

  • log4j2.properties
## 日志的等级(自定义配置项)
##property.log.level=ALL,TRACE,DEBUG,INFO,WARN,ERROR,FATAL,OFF
property.log.level=DEBUG

# ------------------- [1.1] 定义 RootLogger 等 全局性配置(不可随意修改) ------------------- #
## rootLogger, 根记录器,所有记录器的父辈
## 指定根日志的级别 | All < Trace < Debug < Info < Warn < Error < Fatal < OFF
rootLogger.level=${log.level}

... //略

应用程序代码: LogTest

  • LogTest
package test.java.lang;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class LogTest {
    public static String printTestLog(){
        return "HelloWorld";//关键代码行
    }

    public static void main(String[] args) {
        log.debug( "log:{}", printTestLog() );
    }
}

实验结果

  • log.level=INFO
关键代码行 : 被执行

日志输出结果: 空
  • log.level=DEBUG
关键代码行 : 被执行

日志输出结果: 
[20XX/10/16 16:01:28.585] [TID: N/A] [DEBUG] [main] [LogTest.java:12 main] log:HelloWorld

最终结论

  • 无论 应用程序日志代码 logger 使用何种日志级别打印日志,代码行中的程序均会被执行,只是最终输出时由日志框架根据配置logger所属class的日志级别决定是否输出(appender)

  • 解决方法1:应用程序中,如无必要,删除这类日志代码。

  • 解决方法2:
    log.isDebugEnabled(...)/isInfoEnabled(...)/isWarnEnabled(...)/isErrorEnabled(...)/...

    public static void main(String[] args) {
        if(log.isDebugEnabled()){//将会根据 用户所配置的日志级别(log.level),决定是否执行 IF 内的代码
            log.debug( "log:{}", printTestLog() );
        }
    }

X 参考文献

VMware Cloud Foundation(VCF)是一个由众多产品(vSphere、vSAN 以及 NSX 等)所构成的 SDDC 解决方案,这些环境中的不同组件的生命周期统一由 SDDC Manager 来进行管理,比如下载修补包、环境预检查、调度组件更新、监控运行报告等。比起传统解决方案来说,VCF 环境的生命周期管理要远远复杂的多,因为涉及到众多组件的产品互操作性以及依赖特定的升级顺序,如果没有 SDDC Manager 来统一进行编排,这对于管理来说会变得非常麻烦并且特别容易出错。

VCF 环境中具有管理工作负载域和 VI 工作负载域两种类型的工作负载域,不同工作负载域中具有一个或多个 vSphere/vSAN 集群,这些工作负载域中可能混合了不同物料清单(BOM)版本的组件,也可能使用了不同生命周期管理的方式,比如基于 Image(映像)或者 Baseline(基线)。如果环境不能连接互联网,可能需要配置代理服务器或者部署 Offline Bundle Transfer Utility (OBTU) 工具来下载或导入捆绑包;如果要进行异步补丁修补,早期可能需要使用 Async Patch Tool 工具来执行,但现在可以直接通过 SDDC Manager(5.2) 来完成这项工作。

本文以下内容参考 VMware 官方产品文档,有关更多细节和注意事项请访问
《VMware Cloud Foundation Lifecycle Management》

一、注意事项

VMware Cloud Foundation
环境的生命周期管理具有许多要求和
注意事项
,为了保证工作的顺利进行,请确保满足以下条件之后再进行后续操作。

  • 验证 ESXi 主机 TPM 模块为禁用状态。
  • 验证 ESXi 主机硬件是否与目标版本兼容。
  • 验证 VCF 组件没有过期或即将过期的密码。
  • 验证 VCF 组件没有过期或即将过期的证书。
  • 验证 VCF 组件具有最新基于配置文件的备份。
  • 验证 vSAN HCL 数据库以确保其为最新状态。
  • 获取更新目标版本的许可证(如从 4.5.x 升级时)。
  • 查看更新目标版本的发行说明了解升级相关的已知问题。
  • 解决更新目标版本的环境预检查中所有的失败检查结果。
  • 分配 vCenter Server 一个临时 IP 地址(如从 4.5.x 升级时)。
  • 在 vCenter Server 中,确保主机或 vSphere 集群上没有活动警报。
  • 在 SDDC Manager 中,确保系统没有正在运行任何错误或失败的任务。

二、更新流程

如果执行 VMware Cloud Foundation 更新工作流,需要遵循特定的
更新流程
。比如,要升级到
VMware Cloud Foundation
5.2.x,则管理域必须为
VMware Cloud Foundation
4.5 或更高版本,如果你的环境版本低于 4.5,则必须将管理域升级到 4.5 或更高版本后再升级到 5.2.x。在
SDDC Manager
升级到版本 5.2.x 之前,必须先升级管理工作负载域,然后再升级 VI 工作负载域;
SDDC Manager
版本为 5.2 或更高版本后,只要工作负载域中的所有组件都兼容,就可以在升级管理域之前或之后升级 VI 工作负载域。如果是升级管理域中的组件,需要先下载相关组件的捆绑包并执行环境的预检查,然后按以下顺序执行相关组件的更新:

  • SDDC Manager
  • VMware Aria Suite(若有)
    • VMware Aria Suite Lifecycle
    • VMware Aria Suite Products
  • NSX
    • NSX Global Manager(若有)
    • NSX Edge Cluster(若有)
    • NSX Manager
  • vSphere
    • vCenter Server
    • vSAN Witness(若有)
    • ESXi

注意,SDDC Manager 可以部署
VMware Aria Suite Lifecycle 组件,但是 VMware Aria Suite 解决方案相关产品的生命周期得通过 Aria Suite Lifecycle 来进行管理,需要先在 Aria Suite Lifecycle 中更新自己然后再更新其他 Aria 产品,比如 Aria Operations 或者 Aria Automation 等。
如果成功完成上面所有组件更新后,你还可以执行一些可选操作,比如更新集群中 VDS 分布式交换机以及 vSAN 磁盘的版本,执行 VCF 组件的最新配置备份等。如果 VCF 环境中还有其他解决方案,请访问
KB 89745
了解更多 VMware 产品的更新顺序。

三、配置联机库

导航到 SDDC Manager->管理->联机库,通过配置账号密码连接到 VMware 官方的在线仓库以获取安装和升级包。

输入 Broadcom 支持门户的账号和密码,点击“身份验证”。

已成功连接到 VMware 联机库。

四、下载更新包

导航到 SDDC Manager->生命周期管理->发行版本,查看当前 VCF 5.1 物料清单(BOM)版本。

计划将当前环境更新到 VCF 5.2 物料清单(BOM)版本。

导航到 SDDC Manager->生命周期管理->包管理,如果环境连接了互联网并配置了联机库,你将在这里看到所有可用的包,包含相关组件的安装包和修补/升级包。查看
KB 96099
了解更多有关 VMware Cloud Foundation 软件包的版本发布信息。

点击某一个包查看详细信息(注意这里包的大小单位显示有误)。

计划更新到 VCF 5.2 物理清单(BOM)版本,所以点击“立即下载”这些组件的修补/升级包,如下图所示。

点击筛选查看正在下载的包,将按顺序下载“调度下载”中的软件包。

点击“下载历史记录”查看所有已下载的软件包。

注意:
如果在列表中找不到 ESXi 的更新包,请在后面更新完 SDDC Manager 组件后再查看下载。

五、创建集群映像

由于 vSphere 集群的生命周期管理方式基于 Image,所以需要单独创建集群映像以用于 ESXi 主机的更新。导航到 SDDC Manager->生命周期管理->映像管理,需要在这里创建新的集群映像。

注意:
请在正式执行 ESXi 组件的更新之前再执行这一步,详见“
七、执行更新过程
”步骤中的“
4)更新 ESXi 主机
”小节。

点击“导入映像”,点击转到管理域 vCenter Server(
vSphere Client)
创建 vSphere Lifecycle Manager 映像,然后在创建映像期间定义 ESXi 版本,并选择添加供应商加载项、组件和固件等,最后将映像提取到
SDDC Manager
中。

进入 vSphere Lifecycle Manager 管理视图,如果之前在 SDDC Manager 中已经下载了 ESXi 更新包,则应该会自动将 ESXi 映像导入到 vLCM 映像库中;如果没有,请在“操作”中导入本地映像。

在数据中心级别右击新建集群,设置新集群的名称,选择使用映像管理集群,点击下一页。

选择映像的 ESXi 版本,若有供应商加载项可选择添加,点击下一页。

完成集群创建。

导航到新创建的集群->更新->主机->映像,你可以根据情况编辑集群的映像设备,比如添加供应商加载项、组件和固件等。

完成集群映像设置后回到 SDDC Manager,在“导入映像”中选择“选项 1”,选择工作负载域以及刚刚创建的新集群,设置集群映像的名称,然后点击“提取集群映像”。

映像提取成功后,在“可用的映像”中可以看到这个新的集群映像。后续可将临时创建的集群从 vCenter Server 中删除。

六、更新前预检查

正式执行更新之前,需要先对当前环境运行预检查,确保已准备好进行更新。导航到 SDDC Manager->清单->工作负载域,选择要执行更新的工作负载域,比如管理工作负载域(vcf-mgmt01),转到“更新”并点击“运行预检查”。

预检查目标版本选项选择“常规升级就绪情况”或者目标 VCF 版本,默认检查整个工作负载域,也可根据情况选择指定组件,然后点击“运行预检查”。

预检查结果。如果有警告,可以暂时忽略而不影响更新;如果有错误,请一定要全部进行解决。由于当前测试环境是通过嵌套部署的,所以下面提示 vSAN 主机磁盘控制器错误,这个错误直接“静默”即可,不影响后续更新。

点击“静默预检查”。

七、执行更新过程

正式执行更新之前,请为所有组件创建虚拟机快照,NSX 组件无法创建虚拟机快照,基于文件的备份是 NSX 组件唯一受支持的方式。完成以上所有准备工作之后,下面正式执行更新过程。导航到 SDDC Manager->清单->工作负载域,选择要执行更新的工作负载域,转到“更新”并点击可用更新中的“计划升级”。

选择计划要升级的 VCF 版本,点击确认。

点击“查看包”。

以下是 VCF 组件的升级顺序。

1)更新 SDDC Manager

点击可用更新中的“立即更新”,开始 SDDC Manager 组件的更新工作流。

点击立即更新后,将跳转到一个更新页面,如下图所示。

点击“查看更新活动”了解具体更新内容。

展开更新选项卡查看更新任务状态。

完成更新。

重新登陆 SDDC Manager UI,查看当前版本。

2)更新 NSX Manager

点击可用更新中的“立即更新”,开始 NSX Manager 组件的更新工作流。

当前环境未部署 NSX Edge 集群,所以点击下一步。

默认一次性更新工作负载域中的所有主机集群,可以勾选“允许选择主机集群”自定义勾选更新的主机集群,然后点击下一步。

默认是并行升级,可勾选主机集群是否进行顺序更新,点击下一步。

检查更新选项,点击完成并开始更新过程。

点击任务视图可跟踪任务状态。

登陆 NSX Manager UI(VIP)查看升级状态。

成功更新。

查看当前版本。

3)更新 vCenter Server

点击可用更新中的“立即更新”,开始 vCenter Server 组件的更新工作流。

确认已完成 vCenter Server 配置备份。

点击任务视图可跟踪任务状态。

也可以在“正在进行的更新”中查看更新状态。

登陆 VAMI 管理后台跟踪更新状态。

成功更新。

查看当前版本。

4)更新 ESXi 主机

点击可用更新中的“配置更新”,开始 ESXi 主机组件的更新工作流。

查看使用集群映像更新 ESXi 主机的步骤,点击下一步。

选择要执行更新工作流的集群,点击下一步。

分配集群映像给指定集群,点击下一步。
注意:
请参阅“
五、创建集群映像
”了解集群映像创建过程。

配置自定义升级选项,点击下一步。

检查所有更新配置,点击完成,开始应用集群映像和兼容性检查。

点击查看兼容性检查结果,由于是嵌套虚拟化环境,所以会有很多错误和警告,如果是这些问题则可以直接忽略。

如果遇到 NVMe 设备兼容性问题,请登录 vCenter Server 在 Skyline 中将其“静默”即可。

确定一切就绪,点击预检查中的“调度更新”。

检查更新设置和更新选项,点击下一步。

选择“立即升级”并勾选确认兼容性检查结果,点击完成。

查看正在更新的任务状态。

点击任务视图可跟踪任务状态。

登录 vCenter Server 可查看任务执行情况。

成功更新。

查看当前版本。

现在,所有 VCF 组件都已完成更新,点击管理工作负载域查看摘要信息,确认版本已经升级到了 VCF 5.2。

查看 VCF 管理域更新历史记录。

查看 SDDC Manager 中所有发行版本。

八、后续可选操作

如果从 VCF
5.0 之前的
版本升级过来,则需要更新许可证密钥以支持 vSAN 8.x 和 vSphere 8.x。首先,将新的组件许可证密钥添加到 SDDC Manager,然后,您可以按工作负载域将许可证密钥应用于组件。

在 vCenter Server 中,将 vSphere Distributed Switch(VDS)交换机升级到最新版本以利用仅在更高版本中可用的功能。

在 vCenter Server 中,升级 vSAN 集群的 vSAN 磁盘格式来获得最佳状态以及最新磁盘格式提供的 vSAN 完整功能集。

完成所有组件更新并确保运行一切正常后,可删除组件虚拟机的快照,然后创建完整的备份以获取最新配置状态。

之前通过多级动态表单获取到多包裹,接下来就是再根据多包裹来判断可选择发货数量
要结合之前的多级表单动态添加包裹会更好理解

目前这个只是一种方法,我相信还有别的方法,可能会更简单

选择商品

  • 选择商品里面选择可选择发货数量,
    • 可选择的发货数量是总数量-每个包裹选择的当前商品的数量-已发货数量
  • 可选择发货数量为0时禁用
  • 默认展开时全选商品
  • 默认展开时可选择商品数默认展示

展示

image.png

思路

  • 只要把可选择商品把选择商品的数量解决来其他的都好说


    • 复选框的禁用
    • 商品可选择的数量
    • 商品最大值最小值的范围
  • 还有一个就是默认展示 这个只要根据element 的toggleRowSelection方法就可以默认全选选中了

页面代码

  <BaseDialog title="选择商品" v-model="goodsModel" @close="closegoodGroup">
    <div>
      <el-table
        ref="shopTable"
        :data="newTableData"
        style="width: 100%"
        @selection-change="handleSelectionChange"
        :row-key="getRowKey"
      >
        <el-table-column
          type="selection"
          :selectable="selected"
          :reserve-selection="true"
          width="55"
        ></el-table-column>
        <el-table-column label="SKUID" prop="outerSkuId"></el-table-column>
        <el-table-column label="商品名称" prop="skuName"></el-table-column>
        <el-table-column label="所属组合商品" prop="packageSkuName" width="200">
        </el-table-column>
        <el-table-column
          v-if="!subDeliver"
          label="商品规格"
          prop="specs"
        ></el-table-column>
        <el-table-column
          v-if="!subDeliver"
          label="总数量"
          prop="number"
        ></el-table-column>
        <el-table-column
          v-if="!subDeliver"
          label="已发货数量"
          prop="hadDeliveryNumber"
        ></el-table-column>
        <el-table-column
          v-if="!subDeliver"
          label="本次发货数量"
          prop=""
          width="200"
        >
          <template #default="{ row, $index }">
            <el-input-number
              :disabled="getMaxNumber(row) === 0 ? true : false"
              :max="getMaxNumber(row, $index)"
              :min="0"
              v-model="row.theSendGoods"
            ></el-input-number>
          </template>
        </el-table-column>
      </el-table>
    </div>
    <template #footer>
      <div class="dialog-footer">
        <el-button type="default" @click="closegoodGroup">取消</el-button>
        <el-button type="primary" @click="submitGood">确认</el-button>
      </div>
    </template>
  </BaseDialog>

逻辑代码

export default {
 name: '',
 props: {
   subDeliver: {
     type: Boolean,
     default: null,
   },
   goodsModel: {
     type: Boolean,
     default: null,
   },
   newGoodList: {
     type: Array,
     default: () => {
       return []
     },
   },
 },

 data() {
   return {
     // goodsModel: false, // 选择商品弹框
     pitchData: [],
     flagNum: false,
     newTableData: [],
     tableData: [],
   }
 },

 watch: {
   tableData: {
     deep: true,
     immediate: true,
     handler(newVal, oldVal) {
       console.log('newVal: ', newVal)
       this.newTableData = JSON.parse(JSON.stringify(newVal))
       this.newTableData.forEach((v) => {
         this.$nextTick(() => {
           v.selectFlagState = false
           v.theSendGoods = this.setSelectNum(v)
           this.$refs.shopTable.toggleRowSelection(v, true)
         })
       })
     },
   },
 },

 methods: {
   selected(row, index) {
     if (this.subDeliver) {
       return true
     } else {
       return this.setSelectNum(row) === 0 ? false : true
     }
   },
   getMaxNumber(row) {
     return this.setSelectNum(row)
   },
   // 返回唯一值
   getRowKey(row) {
     return row.id
   },
   closegoodGroup() {
     // this.goodsModel = false
     this.$emit('closeGood', false)
   },
   submitGood() {
     // this.goodsModel = false

     if (this.pitchData.length > 0) {
       if (this.subDeliver) {
         if (this.flagSelect) {
           this.$emit('subPitchData', this.pitchData)
         } else {
           this.$message.warning('请选择子商品')
           this.$emit('subPitchData', [])
         }
       } else {
         let selectEditData = []
         JSON.parse(JSON.stringify(this.newTableData)).map((i) => {
           i.isCommonProduct = i.packageSkuName ? true : false
           i.isCommonProduct
             ? (i.newSkuId = i.skuId + i.outerSkuId)
             : (i.newSkuId = i.skuId)
           this.pitchData.map((v) => {
             v.isCommonProduct
               ? (v.newSkuId = v.skuId + v.outerSkuId)
               : (v.newSkuId = v.skuId)
             if (v.selectFlagState && v.newSkuId === i.newSkuId) {
               selectEditData.push({
                 orderItemId: i.id,
                 skuId: i.skuId,
                 skuName: i.skuName,
                 storeOuCode: i.storeOuCode,
                 theSendGoods: i.theSendGoods,
                 number: i.theSendGoods,
                 totalNumber: i.number,
                 outerSkuId: v.outerSkuId,
                 packageSkuName: v.packageSkuName,
               })
             }
           })
         })

         try {
           let isHasProduct = selectEditData.every((item) => item.number === 0)
           if (isHasProduct) {
             throw new Error('请选择商品')
           } else {
           }
           selectEditData = selectEditData.filter((item) => item.number !== 0)
         } catch (error) {
           this.$message.error(error.message)
         }

         this.$emit('pitchData', selectEditData)
       }
     } else {
       this.$message({
         type: 'warning', // success error warning
         message: '请选择商品',
         duration: 2000,
       })
     }
     this.flagSelect = false
   },
   handleSelectionChange(data) {
     this.pitchData = []
     this.pitchData = JSON.parse(JSON.stringify(data)).map((v) => {
       return {
         orderItemId: v.id,
         skuId: v.skuId,
         skuName: v.skuName,
         storeOuCode: v.storeOuCode,
         number: this.subDeliver ? '0' : v.number,
         selectFlagState: true,
         outerSkuId: v.outerSkuId,
         packageSkuName: v.packageSkuName,
         isCommonProduct: v.packageSkuName ? true : false,
       }
     })

     this.flagSelect = this.pitchData.length > 0 ? true : false
   },
   //默认展示最发货数量 及
   setSelectNum(row) {
     // 当前包裹没有商品可选择的商品数量
     let selectCanNumber = 0

     // 有多个包裹那么此商品累计的数量
     let selectNumber = 0

     // 当前包裹有商品可选择的商品数量
     let selectFlagNumber = 0

     // 判断当前包裹是否有商品
     let flagNum = false

     this.newGoodList.forEach((item) => {
       console.log('item: ', item)
       //如果该项目有多个 packageList,则进一步遍历每个包裹
       if (item.packageList.length > 0) {
         item.packageList.forEach((v) => {
           // 对于每个包裹,判断里面是否有商品,如果有 packageSkuList 商品
           // 如果包裹里面有商品
           if (v.packageSkuList.length > 0) {
             // 遍历每一个商品
             v.packageSkuList.forEach((value) => {
               // 如果包裹里面的商品与可选择的表格商品中每一个商品保持一致
               if (value.skuId == row.skuId) {
                 selectNumber += value.number
                 // 累计其他包裹中该商品的数量
                 // 可选择的商品数量 等于总的商品数量减去已发货数量减去其他包裹的该商品的数量
                 selectFlagNumber =
                   parseInt(row.number) -
                   parseInt(row.hadDeliveryNumber) -
                   parseInt(selectNumber)

                 // 做一步校验如果总数量等于已发货数量,则可选择的数量为0
                 if (
                   selectNumber >=
                   parseInt(row.number) - parseInt(row.hadDeliveryNumber)
                 ) {
                   selectFlagNumber = 0
                 }
                 flagNum = true
               } else {
                 // 其他包裹中该商品不包含当前表格中的商品

                 // 可选择的商品等总数量的商品减去已发货的商品
                 selectFlagNumber =
                   parseInt(row.number) - parseInt(row.hadDeliveryNumber)
                 // 做一步校验如果总数量等于已发货数量,则可选择的数量为0
                 console.log('selectNumber: ', selectNumber)
                 console.log('row.number: ', row.number)
                 console.log('row.hadDeliveryNumber: ', row.hadDeliveryNumber)

                 if (
                   selectNumber >=
                   parseInt(row.number) - parseInt(row.hadDeliveryNumber)
                 ) {
                   selectFlagNumber = 0
                 } else {
                   // 其他包裹中该商品不包含当前表格中的商品 且总数量减去发货数量不等于0 那么得需要再减去其他包裹已选择的发货数量
                   selectFlagNumber =
                     parseInt(row.number) -
                     parseInt(row.hadDeliveryNumber) -
                     parseInt(selectNumber)
                 }
                 flagNum = true
               }
             })
           } else {
             // 如果包裹里面没有商品 那么可以选择商品数量等于总数量减去已发货的数量
             selectCanNumber =
               parseInt(row.number) - parseInt(row.hadDeliveryNumber)
           }
         })
       } else {
         // 如果当前包裹的数量只有一个  那么可以选择的商品等于总数量减去已发货的商品数量
         selectCanNumber =
           parseInt(row.number) - parseInt(row.hadDeliveryNumber)
       }
     })

     if (flagNum) {
       // 如果包裹里面有商品 那么可选择的商品数量就是 selectFlagNumber
       return selectFlagNumber
     } else {
       // 如果包裹里面没有商品 那么可选择的商品数量就是 selectCanNumber
       return selectCanNumber
     }
   },
 },
}

一:背景

1. 讲故事

上篇聊到了
C#程序编译成Native代码
的宏观过程,有粉丝朋友提了一个问题,能不能在 dotnet publish 发布的过程中对
AOT编译器
拦截进行源码级调试,这是一个好问题,也是深度研究的必经之路,这篇我们就来分享下吧。

二:托管和非托管调试器

1. 调试器介绍

相信大家现在都知道
AOT Compiler (ilc.exe)
是用 C# 代码写的,也就表明它是一个托管程序,对托管程序的调试有两种调试器:

  • Visual Studio 托管调试器

调试 C# 代码它是当仁不让,缺点在于对非托管部分的查看缺少了手段。

  • WinDbg 非托管调试器

调试 C/C++ 是一把利器,但用它调试托管的C#代码,虽然可以用,但在变量显示各方面不是很直观。

截个图如下,总之各有利弊吧:

2. 测试代码

为了方便演示,先上一段测试代码,非常简单。


    internal class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello, World!");
            Console.ReadLine();
        }
    }

有了代码之后,接下来一起观赏下如何通过 Visual Studio 和 Windbg 实现各自的拦截。

三:调试器拦截实战

1. WinDbg 拦截

作为 Windows平台上王者般存在的非托管调试器,用它来劫持
ilc.exe
非常方便,在注册表的
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\ilc.exe
下配置个 Deubgger 键值即可,截图如下:

接下来使用
dotnet publish
发布程序,稍等片刻之后会看到 windbg 立即启动拦截了 ilc.exe,然后
ctrl+o
打开我们需要下断点的 cs 文件,比如核心的
Compilation
方法,下完断点之后直接 g 执行,截图如下:

从卦中可以看到
Compilation.ctor
果然命中,而且用 dv 也能看到各个局部变量的内存地址,是不是挺有意思的。

总的来说这种方式使用起来简单粗暴,但用 windbg 这种非托管调试器调试C# 总有点
名不正言不顺
,更好的方式应该还是用 visual studio 这种专业级的家宝,不是吗?

2. Visual Studio 拦截

上一篇文章跟大家说过执行
dotnet publish
调用的ilc.exe 是来自于目录
.nuget\packages\runtime.win-x64.microsoft.dotnet.ilcompiler\8.0.8\tools
下的,截图如下:

为了能够用上托管调试器,这里我们把 ilc.sln 项目手工编译出一个 ilc.exe 来替换这里的 ilc.exe 即可,截图如下:

为了能够让 VS 附加到 ilc.exe 进程上,ilc 提供了一个
--waitfordebugger
参数,参考如下:


PS D:\csharpapplication\21.20240910\src\Example\Example_21_1> ilc -h
Description:
  .NET Native IL Compiler

Usage:
  ilc <input-file-path>... [options]

Arguments:
  <input-file-path>  Input file(s)

Options:
  -?, -h, --help                  Show help and usage information
  ...
  --waitfordebugger               Pause to give opportunity to attach debugger

这个参数的作用就是通过
Console.ReadLine
让程序暂停,好让你用 VS 去 Attach ,源码中是这么写的:


        public Program(ILCompilerRootCommand command)
        {
            _command = command;

            if (Get(command.WaitForDebugger))
            {
                Console.WriteLine("Waiting for debugger to attach. Press ENTER to continue");
                Console.ReadLine();
            }
        }

但在我手工编译的 ilc.exe 上用
Console.ReadLine
貌似拦不住,所以这里稍微改一下,参考如下:


        public Program(ILCompilerRootCommand command)
        {
            _command = command;

            while (!Debugger.IsAttached)
            {
                Console.WriteLine("Waiting for debugger to attach. Press ENTER to continue");
                //Console.ReadLine();
                Thread.Sleep(1000);
            }
        }

接下来重新编译项目,将生成目录
runtime\artifacts\bin\coreclr\windows.x64.Debug\ilc\net8.0
下的所有文件复制到nugut目录
.nuget\packages\runtime.win-x64.microsoft.dotnet.ilcompiler\8.0.8\tools
下,截图如下:

一切都准备好之后,接下来使用
dotnet publish
重新发布程序,从 cmd 输出中可以看到正在等待 attach 附加。


PS D:\csharpapplication\21.20240910\src\Example\Example_21_1> dotnet publish  -r win-x64 -c Debug -o D:\testdump
  正在确定要还原的项目…
  所有项目均是最新的,无法还原。
  Example_21_1 -> D:\csharpapplication\21.20240910\src\Example\Example_21_1\bin\Debug\net8.0\win-x64\Example_21_1.d
  ll
  Generating native code
  Waiting for debugger to attach. Press ENTER to continue
  Waiting for debugger to attach. Press ENTER to continue
  Waiting for debugger to attach. Press ENTER to continue
  Waiting for debugger to attach. Press ENTER to continue
  Waiting for debugger to attach. Press ENTER to continue

在VS菜单上 Debug -> Attach to Process 到我们的 ilc.exe 进程,可以看到果然就命中了,大家看看这调试体验是不是高了很多,截图如下:

体验过这种方式的朋友我相信又有一些新的问题,那就是重复调试的时候太麻烦了,能不能直接以
启动程序
的方式来调试?这就是接下来我们要聊的。

3. VS 对ilc的启动调试

看过上篇的朋友知道,每一次AOT编译之前在 native 目录下都会有一个 xxx.ilc.rsp ,这个文件是 AOT Compiler 的 input 来源,截图如下:

所以完全可以将它作为 ilc.sln 项目的启动参数,接下来我们将
@D:\csharpapplication\21.20240910\src\Example\Example_21_1\obj\Debug\net8.0\win-x64\native\Example_21_1.ilc.rsp
放到 ILCompiler 项目的 command line 中,截图如下:

配置好之后,接下来把 Example_21_1.ilc.rsp 中的
Example_21_1.dll,Example_21_1.obj,Example_21_1.def
三块都改成全路径,参考如下:


D:\csharpapplication\21.20240910\src\Example\Example_21_1\obj\Debug\net8.0\win-x64\Example_21_1.dll
-o:D:\csharpapplication\21.20240910\src\Example\Example_21_1\obj\Debug\net8.0\win-x64\native\Example_21_1.obj
-r:C:\Users\Administrator\.nuget\packages\microsoft.netcore.app.runtime.win-x64\8.0.8\runtimes\win-x64\lib\net8.0\WindowsBase.dll
-r:C:\Users\Administrator\.nuget\packages\runtime.win-x64.microsoft.dotnet.ilcompiler\8.0.8\sdk\System.Private.CoreLib.dll
...
--targetarch:x64
--dehydrate
-g
--exportsfile:D:\csharpapplication\21.20240910\src\Example\Example_21_1\obj\Debug\net8.0\win-x64\native\Example_21_1.def
...

直接 F5 启动 ILCompiler 项目,可以看到轻轻松松的成功调试,这种方式就很好的解决了反复调试的问题,截图如下:

三:总结

以劫持的方式对 AOT Compiler 自身进行源码级调试,这本身就是一个很有意思的话题,不断的介入Compiler编译的的各个阶段,相信能给大家深度学习AOT提供了一些不寻常的手段。
图片名称

前言

在工业自动化和机器视觉领域,对实时性、可靠性和效率的要求越来越高。为了满足这些需求,我们开发了一款专为工业自动化运动控制和机器视觉流程开发设计的 C# 并发流程控制框架。

该框架不仅适用于各种工业自动化场景,还能在单线程环境下实现每秒百万次以上的调度频率,从而从容应对涉及成千上万输入输出点数的复杂任务。

并发流程控制框架

本框架提供一种全新的并发流程控制框架,它借鉴了Golang语言中的高效并发模式,并在此基础上进行了必要的功能扩展。框架不仅能够支持自定义的单/多线程调度机制,还允许在主UI线程中进行调度,从而简化了逻辑与用户界面之间的交互。

另外,该框架还集成了高精度定时器、可配置的调度优先级、逻辑停止与暂停等功能,让我们能够更加灵活地管理和控制复杂的自动化流程。

框架优势

  • 相较于传统模型:相对于传统的多线程模型、状态机模型以及类PLC模型,本框架具有更加紧凑清晰的逻辑结构,显著提升了开发效率,并简化了后续的维护与升级过程。
  • 受Go语言启发:框架的设计借鉴了 Go 语言中的高效并发模式,并在此基础上进行了必要的功能扩展,以适应工业自动化领域的具体需求。
  • 灵活的调度机制:支持自定义单线程或多线程调度,同时也可在主 UI 线程中进行调度,便于逻辑与用户界面的交互,增强了用户体验。
  • 丰富的内置功能:内置高精度定时器、可配置的调度优先级、逻辑停止与逻辑暂停功能,确保任务执行的准确性和可控性。
  • 树形多任务调度:采用树形结构管理多任务调度,提高了逻辑的可靠性和系统的整体稳定性。
  • 卓越的性能表现:在单线程环境下,每秒可实现超过一百万次的调度频率,能够从容应对成千上万输入输出点数的复杂场景。
  • 广泛的实践验证:该框架已在多个实际项目中成功应用,证明了其稳定性和可靠性。

框架示例

代码中定义了一系列不同的任务执行模式,展示如何通过不同的调度策略来管理并发任务。

  • 全局变量

static shared_strand strand:全局共享的调度器,用于保证线程安全。

  • 日志记录函数

Log(string msg):记录带有时间戳的日志信息到控制台。

  • 工作任务函数

Worker(string name, int time = 1000):模拟一个简单的任务,该任务会在指定的毫秒数后打印一条消息。

  • 主函数

MainWorker():异步主任务函数,依次调用前面定义的各种任务模式。

Main(string[] args):程序入口点,初始化工作服务、共享调度器,并启动主任务。

usingSystem;usingSystem.Collections.Generic;usingSystem.Linq;usingSystem.Text;usingSystem.Threading.Tasks;usingGo;namespaceWorkerFlow
{
classProgram
{
staticshared_strand strand;static void Log(stringmsg)
{
Console.WriteLine($
"{DateTime.Now.ToString("HH:mm:ss.fff")} {msg}");
}
static async Task Worker(string name, int time = 1000)
{
awaitgenerator.sleep(time);
Log(name);
}
//1 A、B、C依次串行//A->B->C static asyncTask Worker1()
{
await Worker("A");await Worker("B");await Worker("C");
}
//2 A、B、C全部并行,且依赖同一个strand(隐含参数,所有依赖同一个strand的任务都是线程安全的)//A//B//C static asyncTask Worker2()
{
generator.children children
= newgenerator.children();
children.go(()
=> Worker("A"));
children.go(()
=> Worker("B"));
children.go(()
=> Worker("C"));awaitchildren.wait_all();
}
//3 A执行完后,B、C再并行//-->B//|//A->//|//-->C static asyncTask Worker3()
{
await Worker("A");
generator.children children
= newgenerator.children();
children.go(()
=> Worker("B"));
children.go(()
=> Worker("C"));awaitchildren.wait_all();
}
//4 B、C都并行执行完后,再执行A//B--//|//-->A//|//C-- static asyncTask Worker4()
{
generator.children children
= newgenerator.children();
children.go(()
=> Worker("B"));
children.go(()
=> Worker("C"));awaitchildren.wait_all();await Worker("A");
}
//5 B、C任意一个执行完后,再执行A//B--//|//>-->A//|//C-- static asyncTask Worker5()
{
generator.children children
= newgenerator.children();var B = children.tgo(() => Worker("B", 1000));var C = children.tgo(() => Worker("C", 2000));var task = awaitchildren.wait_any();if (task ==B)
{
Log(
"B成功");
}
else{
Log(
"C成功");
}
await Worker("A");
}
//6 等待一个特定任务 static asyncTask Worker6()
{
generator.children children
= newgenerator.children();var A = children.tgo(() => Worker("A"));var B = children.tgo(() => Worker("B"));awaitchildren.wait(A);
}
//7 超时等待一个特定任务,然后中止所有任务 static asyncTask Worker7()
{
generator.children children
= newgenerator.children();var A = children.tgo(() => Worker("A", 1000));var B = children.tgo(() => Worker("B", 2000));if (await children.timed_wait(1500, A))
{
Log(
"成功");
}
else{
Log(
"超时");
}
awaitchildren.stop();
}
//8 超时等待一组任务,然后中止所有任务 static asyncTask Worker8()
{
generator.children children
= newgenerator.children();
children.go(()
=> Worker("A", 1000));
children.go(()
=> Worker("B", 2000));var tasks = await children.timed_wait_all(1500);awaitchildren.stop();
Log($
"成功{tasks.Count}个");
}
//9 超时等待一组任务,然后中止所有任务,且在中止任务中就地善后处理 static asyncTask Worker9()
{
generator.children children
= newgenerator.children();
children.go(()
=> Worker("A", 1000));
children.go(
async delegate()
{
try{await Worker("B", 2000);
}
catch(generator.stop_exception)
{
Log(
"B被中止");await generator.sleep(500);throw;
}
catch(System.Exception)
{
}
});
var task = await children.timed_wait_all(1500);awaitchildren.stop();
Log($
"成功{task.Count}个");
}
//10 嵌套任务 static asyncTask Worker10()
{
generator.children children
= newgenerator.children();
children.go(
async delegate()
{
generator.children children1
= newgenerator.children();
children1.go(()
=> Worker("A"));
children1.go(()
=> Worker("B"));awaitchildren1.wait_all();
});
children.go(
async delegate()
{
generator.children children1
= newgenerator.children();
children1.go(()
=> Worker("C"));
children1.go(()
=> Worker("D"));awaitchildren1.wait_all();
});
awaitchildren.wait_all();
}
//11 嵌套中止 static asyncTask Worker11()
{
generator.children children
= newgenerator.children();
children.go(()
=> Worker("A", 1000));
children.go(
async delegate()
{
try{
generator.children children1
= newgenerator.children();
children1.go(
async delegate()
{
try{await Worker("B", 2000);
}
catch(generator.stop_exception)
{
Log(
"B被中止1");await generator.sleep(500);throw;
}
catch(System.Exception)
{
}
});
awaitchildren1.wait_all();
}
catch(generator.stop_exception)
{
Log(
"B被中止2");throw;
}
catch(System.Exception)
{
}
});
await generator.sleep(1500);awaitchildren.stop();
}
//12 并行执行且等待一组耗时算法 static asyncTask Worker12()
{
wait_group wg
= newwait_group();for (int i = 0; i < 2; i++)
{
wg.add();
int idx =i;var _ = Task.Run(delegate()
{
try{
Log($
"执行算法{idx}");
}
finally{
wg.done();
}
});
}
awaitwg.wait();
Log(
"执行算法完成");
}
//13 串行执行耗时算法,耗时算法必需放在线程池中执行,否则依赖同一个strand的调度将不能及时 static asyncTask Worker13()
{
for (int i = 0; i < 2; i++)
{
await generator.send_task(() => Log($"执行算法{i}"));
}
}
static asyncTask MainWorker()
{
awaitWorker1();awaitWorker2();awaitWorker3();awaitWorker4();awaitWorker5();awaitWorker6();awaitWorker7();awaitWorker8();awaitWorker9();awaitWorker10();awaitWorker11();awaitWorker12();awaitWorker13();
}
static void Main(string[] args)
{
work_service work
= newwork_service();
strand
= newwork_strand(work);
generator.go(strand, MainWorker);
work.run();
Console.ReadKey();
}
}
}

框架地址

总结

值得一提的是,该框架特别设计用于工业自动化运动控制以及机器视觉流程开发领域,其独特的树形多任务调度机制极大提高了逻辑的可靠性,同时单线程环境下的每秒调度次数可达一百万次以上,足以应对涉及成千上万输入输出点数的应用场景。经过多个项目的实际验证,证明了其稳定性和可靠性,为工业自动化提供了强有力的支持。

通过本文的介绍,希望能为工业自动化领域的开发者提供一个高效、可靠且易于使用的工具。借助这一工具,大家在构建复杂的控制系统时,能够更加轻松地应对并发处理的挑战。也期待您在评论区留言交流,分享您的宝贵经验和建议。

最后

如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!你的支持是我继续分享知识的动力。如果有任何疑问或需要进一步的帮助,欢迎随时留言。

也可以加入微信公众号
[DotNet技术匠]
社区,与其他热爱技术的同行一起交流心得,共同成长!
优秀是一种习惯,欢迎大家留言学习!