登录
注册
开源
企业版
高校版
搜索
帮助中心
使用条款
关于我们
开源
企业版
高校版
私有云
模力方舟
AI 队友
登录
注册
代码拉取完成,页面将自动刷新
捐赠
捐赠前请先登录
取消
前往登录
扫描微信二维码支付
取消
支付完成
支付提示
将跳转至支付宝完成支付
确定
取消
Watch
不关注
关注所有动态
仅关注版本发行动态
关注但不提醒动态
18
Star
27
Fork
12
InspireFunction
/
CadLabelBar
代码
Issues
3
Pull Requests
0
Wiki
统计
流水线
服务
质量分析
Jenkins for Gitee
腾讯云托管
腾讯云 Serverless
悬镜安全
阿里云 SAE
Codeblitz
SBOM
我知道了,不再自动展开
更新失败,请稍后重试!
移除标识
内容风险标识
本任务被
标识为内容中包含有代码安全 Bug 、隐私泄露等敏感信息,仓库外成员不可访问
关于卸载的一些猜想
待办的
#IAI0ZZ
liuqihong
拥有者
创建于
2024-08-06 07:26
# Loadx的架构 在[惊惊博客](https://www.cnblogs.com/JJBox/p/13833350.html#_lab2_6_2)上面写了: 其实现在卸载是成功的,但是关闭cad程序的时候弹出报错: `System.ArgumentException:“无法跨 AppDomain 传递 GCHandle。”` 下面有[小专题](https://gitee.com/inspirefunction/CadLabelBar/issues/IAI0ZZ#note_30817538_link)讨论它 下面的全部工作降低其他错误的出现可能. NetLoad命令加载Loadx,Loadx利用自动接口加载所有用户插件. 形成三层架构:CAD,Loadx(加载层),Plugin(用户层). Loadx是不能卸载的,如果有耦合的就靠它进行信息转发,重点就是Loadx内部有两层. ## 对接用户层 以下工作需要修改用户层代码,这样使得用户层不是爱怎么写就怎么写.并且每一步都需要验证,有点难受. 1,改名 用户层不能让cad检查到接口和命令特性, 这意味着我们需要把用户层**接口**和**命令特性**都改名... 因为cad直接用命令netload会反射命令特性和执行自动接口,它是否存在利用反射自动巡查机制?(见下面的动态编译你就会产生怀疑了) 因此能避免则避免,需要全部改名. 2,动态编译 利用自动接口动态编译256个颜色命令能够直接被cad识别命令,因此是编译在主程序域的. 并且发生时机是在netload之后,这说明了是动态巡查的,存在巡查命令时机. a,cad会在找不到命令时候自动反射主程序域内部. 能够通过实现程序域事件验证吗? b,cad能够直接监控动态编译事件来获取动态编译区的代码? 没发现有动态编译前后事件,更为相信cad是通过第一点来实现的. 那么需要动态编译到新的程序域,然后跨域事件进行命令(改名)调用. 卸载前也要跑去这里清空静态变量和Dispose和卸载域.(其实Loadx是跑去每个程序域,应该能够自动完成这一步) loadx触发--用户层的自动接口实现动态编译,编译到新域--新域的命令(改名)需要被收集到loadx命令栈... 3,写析构和Dispose接口 Loadx层卸载程序域前会遍历用户层全部类型, 有Dispose就执行,否则设置为null. 因此用户层要写好这些释放函数, 尤其是static的释放. 尤其是class上面有cad的点集/各种c++集合的,要清空他们. 本意就是卸载此dll程序域前执行某些功能,令它安全退出,尚且不知道程序域卸载事件,是主程序域执行还是写入用户层中. 4,停止标记 用户层只能用线程池,并制作一个全局停止标记给Loadx层卸载时候用. 因为这是线程安全退出的方式,所以必须要用此方式,不要依赖程序域卸载自动关闭线程这种不安全退出,避免文件损坏 全部线程退出之后,把标记改成Stop,并且会await,一直等待全部线程死亡. 事件停止: ```c# var doneEvent = new ManualResetEvent(false); ThreadPool.QueueUserWorkItem(state =>{ while (!doneEvent.WaitOne(0)) // 非阻塞等待 { /*执行任务逻辑*/ } }); doneEvent.Set();// 当需要停止线程时 ``` task停止: ```c# using System; using System.Threading; using System.Threading.Tasks; class Program { static async Task Main(string[] args) { // 创建取消令牌源 var cts = new CancellationTokenSource(); CancellationToken token = cts.Token; // 启动线程或任务 Task task = Task.Run(() => DoWork(token), token); // 假设线程运行了3秒,然后发出取消 await Task.Delay(3000); cts.Cancel(); // 等待任务完成或抛出异常 try { await task; } catch (OperationCanceledException) { Console.WriteLine("任务已取消"); } } static void DoWork(CancellationToken token) { for (int i = 0; i < 100; i++) { // 定期检查取消请求,然后抛异常(这个不太环保,可以循环检测 token.IsCancellationRequested) token.ThrowIfCancellationRequested(); // 模拟工作 Console.WriteLine($"执行工作 {i + 1}"); Thread.Sleep(100); // 模拟工作延迟 } // 标记为停止,实际中这里可以是清理工作 Console.WriteLine("工作已停止"); } } ``` 当前程序域卸载事件,执行把标记全部停止: ```c# AppDomain.CurrentDomain.DomainUnload += OnDomainUnload; 异常:常用于线程和AOP拦截 try { /* 执行一些操作 */ } catch (AppDomainUnloadedException) { /* 程序域正在被卸载 */ } ``` 如果某个程序域无法释放,那么可以通过监控来查看资源的问题,监控程序域的状态变化AppDomain.MonitoringIsEnabled 属性来确定是否启用了监控,然后通过 AppDomain.MonitoringTotalAllocatedMemorySize 来获取当前域的内存使用情况. [相关接口](https://www.cnblogs.com/PurpleTide/archive/2011/01/07/1929566.html) [msdn:应用程序域资源监控](https://learn.microsoft.com/zh-cn/dotnet/standard/garbage-collection/app-domain-resource-monitoring) 5,弱事件 弱事件标记,停止执行事件.. acap全局事件怎么处理?全部转为弱事件吗? https://www.cnblogs.com/MuNet/p/6687823.html 6,yield和async/await.卸载的影响? 7,winform/WPF线程呢? 通过子类化拦截吗?直接调用Dispose? 那么线程有没有影响? 他们的资源dll呢?例如图标这些的卸载. 8,跨域的数据,事务,点集合处理?共享内存传输? 跨程序域事务 创建Entity并通过事务提交给数据库,我还可以继续操控它ent.Dispose(),其实Dispose内部处理了double free,你并没有真正释放它. 而提交事务是写了一个副本给数据库(不同的数据库,实现不同的序列化),不是穿透到数据库内部执行的. 不过能进去ent说明本地内存并没有释放,一旦再使用ent是报错的.(发生时机是另一个事务读取时候) a,这可能是因为cad采取指针所有权转移来代替序列化. b,也可能是提交事务时候,把事务id写入entity,它本身也是序列化提交到数据库的. 无论cad如何实现上面两点,你都不能把ent通过List(外部声明等)偷到事务外使用. 事务外使用要用id,如果想再次处理需要再开新事务. 因为没有断开和数据库的链接的机制,所以要视为每次事务都是一次数据库链接. 如果cad是采取转移指针所有权,那么跨程序域提交事务,这可能强关联了程序域内存,从而导致卸载失败. 因此需要包装一个传输功能MarshalByRefObject,通过跨域方式序列化给Loadx主程序域,再去创建事务提交,避免其他程序域不能释放. 跨程序域的时候,类型名相同,那么is type它们也是不相同的.通过Type.AssemblyQualifiedName 完全限定名称,来进行判断为同一个. ## 对接CAD ### 加载层实现命令栈. 遍历获取用户层自动接口(改名后)和命令特性(改名后),执行自动接口函数(改名的)以及缓存命令. class cmd { 命令名, 命令函数指针, // 程序集(强引用), 程序集名, // 程序域(强引用), dll路径 } var cmdMap = map<命令名,cmd> 卸载程序集记得来这里移除. 全部强引用的单独一个map,然后卸载时候也要移除. 通过"未知命令事件"进行跨域调用命令. ### 创建dll程序域 为每个用户dll各自创建程序域. map<dllFile,AppDomain> 执行的时候没找到,就通过跨域事件搜索依赖图,要在还没有才加载,加载时候又放入新程序域,并且调用.(这一步是异步的,放入到消息队列的任务中,在结束事件后再搜索依赖图,不然加载时候搜不到岂不是嵌套调用事件了) (注意:不进行链式加载) #### 依赖图 任何dll都可能已经卸载,如果设计成用完就卸载,会面临一个功能上面反反复复加载卸载. 多个相互引用的dll会出现菱形依赖,此时无法卸载,需要确定依赖关系.因此它是有向图,需要广度/深度优先去确定依赖关系. 找到目标的叶子节点开始卸载程序域,直到目标...如果叶子也产生菱形依赖呢? 需要把整个菱形都移除.(弱引用的概念) 幻影依赖:继承引用/菱形引用. 例如:J引用IFox,此时A引用了J,就能使用IFox.一旦J升级了IFox,那么A也要升级. js那边包管理是利用pnpm安装包(代替npm),它是利用了文件系统把依赖图全部拍扁到一个文件夹,再通过快捷方式到包文件夹,由此解决了幻影依赖问题. 快捷方式:硬链接(Unix-like系统)或软链接(Windows系统). 包管理器在一个项目上面扁平化包,实现了共享储存,剔除了同版本冲突.再通过package.json知道当前项目用什么包版本.再提取全部子项目package.json维护一份总的映射表,内部子项目就可以引用不同版本旧包和新包. ### 卸载dll程序域 1,停止标记:跨域事件,线程池,单线程.停止所有事件?卸载事件?acap事件? 2,遍历全局静态变量执行Dispose. 3,按照dll依赖图进行卸载程序域. 4,通过子类化阻塞cad主线程. 5,卸载命令栈map缓存的跨域信息. 6,JIT特化,函数影响内联,使得卸载失败? 参考微软的[如何在 .NET Core 中使用和调试程序集可卸载性](https://www.bookstack.cn/read/dotnet/18b062d94c6a58fa.md) 7,async/await和return yield,会不会状态机阻碍卸载?停止线程应该都停止了吧. 8,内存泄露FFI/COM/数据库事务提交/加载单例 9,菱形依赖 10,COM问题: “不能通过GCHandle通过AppDomains" https://cloud.tencent.com/developer/ask/sof/111002318 11,FFI问题: a,发现之前动态查找acad.exe的接口,然后转为委托指针并且static作为缓存了, 这个行为可能导致卸载失败,要写弱引用吗?怎么写呢? b,Dllimport会不会引起耦合导致卸载失败呢? 链式加载用LoadLibraryEx, https://blog.csdn.net/xiaoyao961/article/details/135096688 c,Point3dCollection这种和c++通讯的,写了GC.KeepAlive是否存在影响?它和GCHandle是存在关系的,要跨域处理COM问题而不是内存共享. 12,加载单例: 动态加载dll两次导致事件被加载两次,然后执行时候会弹两次打印. 先-=再+=其实不对的,因为这两次就是不同程序域. a,如果卸载是成功的,先卸载再加载就好. b,可以通过管道通讯获取共享内存,共享内存上面对于所有事件更改为弱事件.acap+=都要自己在Loadx层实现一次. #### 已经做过了 1,c#unload的可能: https://m.jb51.net/article/221129.htm 2,这里面提及了,先构造程序域,参数是开启影相复制,再进行loadbyte和调用,不然卸载不掉. https://blog.csdn.net/xiansenLee/article/details/114831641 3,编译后无法删除dll是因为vs.exe会进行mmap, 要进行程序集影相复制domain.SetShadowCopyFiles() https://developer.aliyun.com/article/331951 实际上也是不行的,这样才成功:需要复制到其他路径后删除原本,让vs读取不到. (完)
# Loadx的架构 在[惊惊博客](https://www.cnblogs.com/JJBox/p/13833350.html#_lab2_6_2)上面写了: 其实现在卸载是成功的,但是关闭cad程序的时候弹出报错: `System.ArgumentException:“无法跨 AppDomain 传递 GCHandle。”` 下面有[小专题](https://gitee.com/inspirefunction/CadLabelBar/issues/IAI0ZZ#note_30817538_link)讨论它 下面的全部工作降低其他错误的出现可能. NetLoad命令加载Loadx,Loadx利用自动接口加载所有用户插件. 形成三层架构:CAD,Loadx(加载层),Plugin(用户层). Loadx是不能卸载的,如果有耦合的就靠它进行信息转发,重点就是Loadx内部有两层. ## 对接用户层 以下工作需要修改用户层代码,这样使得用户层不是爱怎么写就怎么写.并且每一步都需要验证,有点难受. 1,改名 用户层不能让cad检查到接口和命令特性, 这意味着我们需要把用户层**接口**和**命令特性**都改名... 因为cad直接用命令netload会反射命令特性和执行自动接口,它是否存在利用反射自动巡查机制?(见下面的动态编译你就会产生怀疑了) 因此能避免则避免,需要全部改名. 2,动态编译 利用自动接口动态编译256个颜色命令能够直接被cad识别命令,因此是编译在主程序域的. 并且发生时机是在netload之后,这说明了是动态巡查的,存在巡查命令时机. a,cad会在找不到命令时候自动反射主程序域内部. 能够通过实现程序域事件验证吗? b,cad能够直接监控动态编译事件来获取动态编译区的代码? 没发现有动态编译前后事件,更为相信cad是通过第一点来实现的. 那么需要动态编译到新的程序域,然后跨域事件进行命令(改名)调用. 卸载前也要跑去这里清空静态变量和Dispose和卸载域.(其实Loadx是跑去每个程序域,应该能够自动完成这一步) loadx触发--用户层的自动接口实现动态编译,编译到新域--新域的命令(改名)需要被收集到loadx命令栈... 3,写析构和Dispose接口 Loadx层卸载程序域前会遍历用户层全部类型, 有Dispose就执行,否则设置为null. 因此用户层要写好这些释放函数, 尤其是static的释放. 尤其是class上面有cad的点集/各种c++集合的,要清空他们. 本意就是卸载此dll程序域前执行某些功能,令它安全退出,尚且不知道程序域卸载事件,是主程序域执行还是写入用户层中. 4,停止标记 用户层只能用线程池,并制作一个全局停止标记给Loadx层卸载时候用. 因为这是线程安全退出的方式,所以必须要用此方式,不要依赖程序域卸载自动关闭线程这种不安全退出,避免文件损坏 全部线程退出之后,把标记改成Stop,并且会await,一直等待全部线程死亡. 事件停止: ```c# var doneEvent = new ManualResetEvent(false); ThreadPool.QueueUserWorkItem(state =>{ while (!doneEvent.WaitOne(0)) // 非阻塞等待 { /*执行任务逻辑*/ } }); doneEvent.Set();// 当需要停止线程时 ``` task停止: ```c# using System; using System.Threading; using System.Threading.Tasks; class Program { static async Task Main(string[] args) { // 创建取消令牌源 var cts = new CancellationTokenSource(); CancellationToken token = cts.Token; // 启动线程或任务 Task task = Task.Run(() => DoWork(token), token); // 假设线程运行了3秒,然后发出取消 await Task.Delay(3000); cts.Cancel(); // 等待任务完成或抛出异常 try { await task; } catch (OperationCanceledException) { Console.WriteLine("任务已取消"); } } static void DoWork(CancellationToken token) { for (int i = 0; i < 100; i++) { // 定期检查取消请求,然后抛异常(这个不太环保,可以循环检测 token.IsCancellationRequested) token.ThrowIfCancellationRequested(); // 模拟工作 Console.WriteLine($"执行工作 {i + 1}"); Thread.Sleep(100); // 模拟工作延迟 } // 标记为停止,实际中这里可以是清理工作 Console.WriteLine("工作已停止"); } } ``` 当前程序域卸载事件,执行把标记全部停止: ```c# AppDomain.CurrentDomain.DomainUnload += OnDomainUnload; 异常:常用于线程和AOP拦截 try { /* 执行一些操作 */ } catch (AppDomainUnloadedException) { /* 程序域正在被卸载 */ } ``` 如果某个程序域无法释放,那么可以通过监控来查看资源的问题,监控程序域的状态变化AppDomain.MonitoringIsEnabled 属性来确定是否启用了监控,然后通过 AppDomain.MonitoringTotalAllocatedMemorySize 来获取当前域的内存使用情况. [相关接口](https://www.cnblogs.com/PurpleTide/archive/2011/01/07/1929566.html) [msdn:应用程序域资源监控](https://learn.microsoft.com/zh-cn/dotnet/standard/garbage-collection/app-domain-resource-monitoring) 5,弱事件 弱事件标记,停止执行事件.. acap全局事件怎么处理?全部转为弱事件吗? https://www.cnblogs.com/MuNet/p/6687823.html 6,yield和async/await.卸载的影响? 7,winform/WPF线程呢? 通过子类化拦截吗?直接调用Dispose? 那么线程有没有影响? 他们的资源dll呢?例如图标这些的卸载. 8,跨域的数据,事务,点集合处理?共享内存传输? 跨程序域事务 创建Entity并通过事务提交给数据库,我还可以继续操控它ent.Dispose(),其实Dispose内部处理了double free,你并没有真正释放它. 而提交事务是写了一个副本给数据库(不同的数据库,实现不同的序列化),不是穿透到数据库内部执行的. 不过能进去ent说明本地内存并没有释放,一旦再使用ent是报错的.(发生时机是另一个事务读取时候) a,这可能是因为cad采取指针所有权转移来代替序列化. b,也可能是提交事务时候,把事务id写入entity,它本身也是序列化提交到数据库的. 无论cad如何实现上面两点,你都不能把ent通过List(外部声明等)偷到事务外使用. 事务外使用要用id,如果想再次处理需要再开新事务. 因为没有断开和数据库的链接的机制,所以要视为每次事务都是一次数据库链接. 如果cad是采取转移指针所有权,那么跨程序域提交事务,这可能强关联了程序域内存,从而导致卸载失败. 因此需要包装一个传输功能MarshalByRefObject,通过跨域方式序列化给Loadx主程序域,再去创建事务提交,避免其他程序域不能释放. 跨程序域的时候,类型名相同,那么is type它们也是不相同的.通过Type.AssemblyQualifiedName 完全限定名称,来进行判断为同一个. ## 对接CAD ### 加载层实现命令栈. 遍历获取用户层自动接口(改名后)和命令特性(改名后),执行自动接口函数(改名的)以及缓存命令. class cmd { 命令名, 命令函数指针, // 程序集(强引用), 程序集名, // 程序域(强引用), dll路径 } var cmdMap = map<命令名,cmd> 卸载程序集记得来这里移除. 全部强引用的单独一个map,然后卸载时候也要移除. 通过"未知命令事件"进行跨域调用命令. ### 创建dll程序域 为每个用户dll各自创建程序域. map<dllFile,AppDomain> 执行的时候没找到,就通过跨域事件搜索依赖图,要在还没有才加载,加载时候又放入新程序域,并且调用.(这一步是异步的,放入到消息队列的任务中,在结束事件后再搜索依赖图,不然加载时候搜不到岂不是嵌套调用事件了) (注意:不进行链式加载) #### 依赖图 任何dll都可能已经卸载,如果设计成用完就卸载,会面临一个功能上面反反复复加载卸载. 多个相互引用的dll会出现菱形依赖,此时无法卸载,需要确定依赖关系.因此它是有向图,需要广度/深度优先去确定依赖关系. 找到目标的叶子节点开始卸载程序域,直到目标...如果叶子也产生菱形依赖呢? 需要把整个菱形都移除.(弱引用的概念) 幻影依赖:继承引用/菱形引用. 例如:J引用IFox,此时A引用了J,就能使用IFox.一旦J升级了IFox,那么A也要升级. js那边包管理是利用pnpm安装包(代替npm),它是利用了文件系统把依赖图全部拍扁到一个文件夹,再通过快捷方式到包文件夹,由此解决了幻影依赖问题. 快捷方式:硬链接(Unix-like系统)或软链接(Windows系统). 包管理器在一个项目上面扁平化包,实现了共享储存,剔除了同版本冲突.再通过package.json知道当前项目用什么包版本.再提取全部子项目package.json维护一份总的映射表,内部子项目就可以引用不同版本旧包和新包. ### 卸载dll程序域 1,停止标记:跨域事件,线程池,单线程.停止所有事件?卸载事件?acap事件? 2,遍历全局静态变量执行Dispose. 3,按照dll依赖图进行卸载程序域. 4,通过子类化阻塞cad主线程. 5,卸载命令栈map缓存的跨域信息. 6,JIT特化,函数影响内联,使得卸载失败? 参考微软的[如何在 .NET Core 中使用和调试程序集可卸载性](https://www.bookstack.cn/read/dotnet/18b062d94c6a58fa.md) 7,async/await和return yield,会不会状态机阻碍卸载?停止线程应该都停止了吧. 8,内存泄露FFI/COM/数据库事务提交/加载单例 9,菱形依赖 10,COM问题: “不能通过GCHandle通过AppDomains" https://cloud.tencent.com/developer/ask/sof/111002318 11,FFI问题: a,发现之前动态查找acad.exe的接口,然后转为委托指针并且static作为缓存了, 这个行为可能导致卸载失败,要写弱引用吗?怎么写呢? b,Dllimport会不会引起耦合导致卸载失败呢? 链式加载用LoadLibraryEx, https://blog.csdn.net/xiaoyao961/article/details/135096688 c,Point3dCollection这种和c++通讯的,写了GC.KeepAlive是否存在影响?它和GCHandle是存在关系的,要跨域处理COM问题而不是内存共享. 12,加载单例: 动态加载dll两次导致事件被加载两次,然后执行时候会弹两次打印. 先-=再+=其实不对的,因为这两次就是不同程序域. a,如果卸载是成功的,先卸载再加载就好. b,可以通过管道通讯获取共享内存,共享内存上面对于所有事件更改为弱事件.acap+=都要自己在Loadx层实现一次. #### 已经做过了 1,c#unload的可能: https://m.jb51.net/article/221129.htm 2,这里面提及了,先构造程序域,参数是开启影相复制,再进行loadbyte和调用,不然卸载不掉. https://blog.csdn.net/xiansenLee/article/details/114831641 3,编译后无法删除dll是因为vs.exe会进行mmap, 要进行程序集影相复制domain.SetShadowCopyFiles() https://developer.aliyun.com/article/331951 实际上也是不行的,这样才成功:需要复制到其他路径后删除原本,让vs读取不到. (完)
评论 (
10
)
登录
后才可以发表评论
状态
待办的
待办的
进行中
已完成
已关闭
负责人
未设置
标签
未设置
标签管理
里程碑
未关联里程碑
未关联里程碑
Pull Requests
未关联
未关联
关联的 Pull Requests 被合并后可能会关闭此 issue
分支
未关联
分支 (4)
标签 (2)
dev
main
loadx
winform
v2.0.0
v1.0.0
开始日期   -   截止日期
-
置顶选项
不置顶
置顶等级:高
置顶等级:中
置顶等级:低
优先级
不指定
严重
主要
次要
不重要
参与者(1)
C#
1
https://gitee.com/inspirefunction/CadLabelBar.git
git@gitee.com:inspirefunction/CadLabelBar.git
inspirefunction
CadLabelBar
CadLabelBar
点此查找更多帮助
搜索帮助
Git 命令在线学习
如何在 Gitee 导入 GitHub 仓库
Git 仓库基础操作
企业版和社区版功能对比
SSH 公钥设置
如何处理代码冲突
仓库体积过大,如何减小?
如何找回被删除的仓库数据
Gitee 产品配额说明
GitHub仓库快速导入Gitee及同步更新
什么是 Release(发行版)
将 PHP 项目自动发布到 packagist.org
评论
仓库举报
回到顶部
登录提示
该操作需登录 Gitee 帐号,请先登录后再操作。
立即登录
没有帐号,去注册