首页 文章 精选 留言 我的

精选列表

搜索[全家桶],共2072篇文章
优秀的个人博客,低调大师

使用MASA全家桶从零开始搭建IoT平台(二)设备注册

前言 我们不希望任何设备都可以接入我们的IoT平台,所以一个设备正常的接入流程是这样的, 1、上位机软件通过串口或其他方式读取设备的唯一标识码UUID。 2、上位机调用IoT后台接口,发送UUID和ProductID。 3、后台接口判断设备是否注册过,如果没有注册过,就根据ProductID并按照一定规律生成DeviceName和Password通过接口返回给上位机软件。 4、上位机软件通过串口将接口返回的数据写入设备。 一、设备注册流程 这里主要涉及四个概念 1、UUID(设备唯一ID,一般为设备主控板编号) 2、ProductID(设备所属产品ID,在IoT后台定义) 3、DeviceName(设备在IoT平台或MQTT的名称,该名称大多与产品相关) 4、Password(设备连接MQTT的密码) 二、MQTT注册 1.在EMQX中添加认证方式 选择Built-in Database方式,内置数据库进行密码认证 账号类型选择username,加密方式和加盐方式可以保持默认。 点击创建后可以在认证菜单中看到新建的认证方式,状态为:已连接。 我们点击用户管理->添加 可以手动创建用户 这里的场景我们是通过上位机调用IoT后端,IoT接口内部调用EMQX接口来实现自动创建用户的 2.创建Api Key 调用接口需要认证,这里我们使用Api key的方式,我们在系统设置->API密钥中创建一个API密钥 Secret Key 只有创建的时候才会显示明文,我们需要记录下API Key 和 Secret Key 3.调用接口创建用户 我们在浏览器打开EMQX 的RestAPI swagger http://localhost:18083/api-docs/index.html 我们可以通过这个接口来创建用户,这里的Authenticator ID 就是我们上面创建的内置数据库 Password Based的ID, 这个ID的获取通过下面的authentication方法获取 我们在认证中直接使用API Key 和 Secret Key,接口返回Id:password_based:built_in_database 调用authentication的Post接口,在 id字段输入:password_based:built_in_database,Request body中输入设备的user_id和password即可成功创建用户。 我们在Deshboard的界面中也可以看到刚刚创建的用户 三、测试设备连接 我们使用MQTTX来模拟客户端设备通过mqtt协议连接到EMQX,新建连接,填写地址、端口、和刚刚通过Api创建用户名密码。 点击连接、发现设备已经可以正常连接mqtt了。 在Dashboard中也可以看到当前连接的客户端ID等信息。 四、编写代码 在MASA.IoT.WebApi项目种添加DeviceController控制器并添加DeviceRegAsync方法用于设备注册, 设备如果没有注册过(UUID 数据库不存在),那么会根据ProductCode按照规律生成设备名称,名称以该产品供应商编号开头,后跟时间和序号。然后向EMQX添加设备,并同时存储到数据库中。 如果设备已经注册过,那么直接从数据库取出设备注册信息返回。 代码编写相对简单,不过多赘述。 //DeviceController namespace MASA.IoT.WebApi.Controllers { [Route("api/[controller]")] [ApiController] public class DeviceController : ControllerBase { private readonly IDeviceHandler _deviceHandler; public DeviceController(IDeviceHandler deviceHandler) { _deviceHandler = deviceHandler; } [HttpPost] public async Task<DeviceRegResponse> DeviceRegAsync(DeviceRegRequest request) { return await _deviceHandler.DeviceRegAsync(request); } } } //DeviceHandler using MASA.IoT.WebApi.Contract; using MASA.IoT.WebApi.IHandler; using MASA.IoT.WebApi.Models.Models; using Microsoft.EntityFrameworkCore; namespace MASA.IoT.WebApi.Handler { public class DeviceHandler : IDeviceHandler { private readonly MASAIoTContext _ioTDbContext; private readonly IMqttHandler _mqttHandler; public DeviceHandler(MASAIoTContext ioTDbContext, IMqttHandler mqttHandler) { _ioTDbContext = ioTDbContext; _mqttHandler = mqttHandler; } /// <summary> /// 注册设备 /// </summary> /// <param name="request"></param> /// <returns> /// 设备注册信息 /// </returns> public async Task<DeviceRegResponse> DeviceRegAsync(DeviceRegRequest request) { var productInfo = await _ioTDbContext.IoTProductInfo.FirstOrDefaultAsync(o => o.ProductCode == request.ProductCode); if (productInfo == null) { return new DeviceRegResponse { Succeed = false, ErrMsg = "ProductCode not found" }; } var deviceRegInfo = await GetDeviceRegInfoAsync(request); if (deviceRegInfo != null) //已经注册过 { return deviceRegInfo; } else //没有注册过 { var deviceName = await GenerateDeviceNameAsync(productInfo.SupplyNo, request.ProductCode, request.UUID); var password = Guid.NewGuid().ToString("N"); var addDeviceResponse = await _mqttHandler.DeviceRegAsync(deviceName, password); if (addDeviceResponse.user_id == deviceName) //注册成功 { deviceRegInfo = new DeviceRegResponse { DeviceName = deviceName, Password = password, Succeed = true, ErrMsg = string.Empty }; await _ioTDbContext.IoTDeviceInfo.AddAsync(new IoTDeviceInfo { Id = Guid.NewGuid(), DeviceName = deviceName, Password = password, ProductInfoId = productInfo.Id, }); await _ioTDbContext.SaveChangesAsync(); return deviceRegInfo; } return new DeviceRegResponse { Succeed = false, ErrMsg = addDeviceResponse.message }; } } /// <summary> /// 获取设备注册信息 /// </summary> /// <param name="request"></param> /// <returns> /// 设备已经注册返回设备注册信息,没有注册过返回null /// </returns> private async Task<DeviceRegResponse?> GetDeviceRegInfoAsync(DeviceRegRequest request) { var deviceware = await _ioTDbContext.IoTDevicewares.FirstOrDefaultAsync(o => o.ProductCode == request.ProductCode && o.UUID == request.UUID); if (deviceware == null) { return null; } else { var deviceInfo = await _ioTDbContext.IoTDeviceInfo.FirstAsync(o => o.DeviceName == deviceware.DeviceName); return new DeviceRegResponse { DeviceName = deviceInfo.DeviceName, Password = deviceInfo.Password, Succeed = true, ErrMsg = string.Empty }; } } /// <summary> /// 生成设备名称 /// </summary> /// <param name="supplyNo"></param> /// <param name="productCode"></param> /// <param name="uuid"></param> /// <returns> /// 设备Mqtt名称 /// </returns> private async Task<string> GenerateDeviceNameAsync(string supplyNo, string productCode, string uuid) { var lastDeviceware = await _ioTDbContext.IoTDevicewares.Where(o => o.ProductCode == productCode).OrderByDescending(o => o.CreationTime).FirstOrDefaultAsync(); var newDeviceware = new IoTDevicewares { Id = Guid.NewGuid(), UUID = uuid, ProductCode = productCode, CreationTime = DateTime.Now }; if (lastDeviceware != null && lastDeviceware.DeviceName.StartsWith(supplyNo + DateTime.Today.ToString("yyyyMMdd"))) { newDeviceware.DeviceName = (long.Parse(lastDeviceware.DeviceName) + 1).ToString(); } else { newDeviceware.DeviceName = supplyNo + DateTime.Today.ToString("yyyyMMdd") + "0001"; } await _ioTDbContext.IoTDevicewares.AddAsync(newDeviceware); await _ioTDbContext.SaveChangesAsync(); return newDeviceware.DeviceName; } } } 这里生成设备名称用了一个简单的算法 // MqttHandler using Flurl.Http; using MASA.IoT.WebApi.Contract.Mqtt; using MASA.IoT.WebApi.IHandler; using Microsoft.Extensions.Options; using System.Net; namespace MASA.IoT.WebApi.Handler { public class MqttHandler : IMqttHandler { private readonly AppSettings _appSettings; public MqttHandler(IOptions<AppSettings> settings) { _appSettings = settings.Value; } public async Task<AddDeviceResponse> DeviceRegAsync(string deviceName,string password) { var url = $"{_appSettings.MqttSetting.Url}/api/v5/authentication/password_based:built_in_database/users"; var response = await url.WithBasicAuth(_appSettings.MqttSetting.ApiKey, _appSettings.MqttSetting.SecretKey).AllowAnyHttpStatus().PostJsonAsync(new AddDeviceRequest { user_id = deviceName, password = password, } ); if (response.StatusCode is (int)HttpStatusCode.Created or (int)HttpStatusCode.BadRequest or (int)HttpStatusCode.NotFound) { return await response.GetJsonAsync<AddDeviceResponse>(); } else { throw new UserFriendlyException(await response.GetStringAsync()); } } } } 总结 以上就是本文要讲的内容,本文介绍了通过账号密码的方式通过接口在EMQX中创建用户,并连接EMQX的过程,EMQX支持的认账方式还有很多,例如JWT认证方式可以授权一次性密码认证,可以控制认证的有效期,我们在后面的章节具体应用中会进行说明。 完整代码在这里:https://github.com/sunday866/MASA.IoT-Training-Demos 如果你对我们的 MASA 感兴趣,无论是代码贡献、使用、提 Issue,欢迎联系我们 WeChat:MasaStackTechOps QQ:7424099

优秀的个人博客,低调大师

使用MASA全家桶从零开始搭建IoT平台(三)管理设备的连接状态

前言 获取一个设备的在线和离线状态,是一个很关键的功能。我们对设备下发的控制指令,设备处于在线状态才能及时给我们反馈。这里的在线和离线,我们可以简单的理解为设备与MQTT的连接状态。 分析 我们打电话的时候经常能听到:"您拨打的用户已关机“和”用户不在服务区或暂时无法接通“,这两种的区别是什么? 1、当用户开机时,会自动向最近的移动基站注册,基站标记该用户为"attach"(在线)状态。 2、当用户关机时,手机会发起datach流程,告知基站自己关机了,基站标记该用户为"detach"(离线)状态。这样再次拨打就可以节省寻呼资源,直接提示用户关机。 3、当用户忽然进入无网络的环境,或者手机故障,导致来不及发起datach流程,基站还认为用户"在线",当有人拨打用户号码时,基站测会对用户进行寻呼,但是超时得不到回应后,就会提示"不在服务区"或者"暂时无法接通" 的语音。 其实这个方案在IoT上也是可行的,我们可以让设备在线和离线的过程中向特定Topic发送状态消息,但是存在问题,我们需要一个单独的Broker去订阅这个Topic,但是这个单独的Broker很容易成为单点故障点。而且如果设备数量很大,这种意外离线的设备也很难及时发现,需要下发指令后等待设备响应超时才能发现。 方案1:遗嘱消息 MQTT 遗嘱消息可以在客户端意外断线时将“遗嘱”优雅地发送给第三方订阅者,以实现离线通知、设备状态更新等业务。其中意外断线指客户端断开前未向服务器发送 DISCONNECT 消息,比如: 因网络故障或网络波动,设备在保持连接周期内未能通讯,连接被服务端关闭 设备意外掉电 设备尝试进行不被允许的操作而被服务端关闭连接,例如订阅自身权限以外的主题等 遗嘱消息在 MQTT 客户端向服务器端 CONNECT 请求时设置,可选属性包括是否发送遗嘱消息 (Will Message)标志,和遗嘱消息主题 (Topic) 与内容(Payload) 以及 Properties。 值得一提的,遗嘱消息发布的时间可能会有延迟:通常意外断线时,服务器无法立即检测到断线行为,需要通过连接保活心跳机制并经过一定周期后才会触发;MQTT 5.0 提供的遗嘱延迟间隔(Will Delay Interval)属性也会影响发布时间。 演示遗嘱消息的使用 我们使用A、B两台电脑使用MQTT X来演示。 我们在A电脑的 MQTT X 中新建一个名为 Test 的连接,Host 修改为 修改为我们的MQTT地址(192.120.5.204),并输入账号密码,在 Advanced 部分选择 MQTT Version 为 5.0,并且将 Session Expiry Interval 设置为 10,确保会话不会在遗嘱消息发布前过期。 然后在 Lass Will and Testament 部分将 Last-Will Topic 设置为 offline,Last-Will Payload 设置为 I'm offline,Will Delay Interval (s) 设置为 5。 完成以上设置后,我们点击右上角的 Connect 按钮以建立连接。 我们在B电脑的MQTTX中新建一个连接Sub,mqtt地址同样指向我们的mqtt服务器(192.120.5.204) 并订阅offline主题 我们用任务管理器直接结束A电脑的MQTTX进程,这是连接会被直接断开,模拟了设备断电的场景,在5s之后,在B电脑的MQTTX订阅中收到了一条内容为 I‘m offline 的遗嘱消息。 实施流程 1、设备遗嘱消息内容设置为offline,该遗嘱主题与一个普通发送状态的主题设定成同一个 {设备名称}/status。例如 284202304230001/status 2、当设备连接时,向主题 {设备名称}/status 发送内容为 online 的Retained消息,其它客户端订阅主题 {设备名称}/status 的时候,将获取到 Retained 消息为 online。 保留消息(Retain ) MQTT 服务端收到 Retain 标志为 1 的 PUBLISH 报文时,会将该报文视为保留消息,除了被正常转发以外, 保留消息会被存储在服务端,每个主题下只能存在一份保留消息,因此如果已经存在相同主题的保留消息,则该保留消息被替换。 当客户端建立订阅时,如果服务端存在主题匹配的保留消息,则这些保留消息将被立即发送给该客户端。 借助保留消息,新的订阅者能够立即获取最近的状态,而不需要等待无法预期的时间,这在很多场景下是非常重要的。 EMQX 默认开启保留消息的能力和服务,可以在 etc/emqx.conf 中修改 mqtt.retain_available 为 false 来关闭保留消息的能力, 这样客户端将被禁止发送 Retain 标志为 1 的 PUBLISH 报文,否则,客户端将会收到原因码为 0x9A(不支持保留消息)的 DISCONNECT 报文。 保留消息的服务会存储和管理客户端发送的保留消息,并发送给相应的订阅者。 3、当客户端异常断开时,系统自动向主题 {设备名称}/status 发送内容为 offline 的消息,其它订阅了此主题的客户端会马上收到 offline 消息;如果遗嘱消息设置了 Will Retain,那么此时如果有新的订阅 A/status 主题的客户端上线,也将获取到内容为 offline 的遗嘱消息。 方案2:使用WebHook 方案1需要设备主动设置遗嘱消息才能实现,那么有没有更简单的方式,直接通过设备与Mqtt的连接事件来获取连接状态呢。 EMQX 设计了一套WebHook系统,可以通过这个自带的WebHook系统获取内部的事件并进行处理。WebHook的原理很简单,当设备与mqtt建立连接或者断开连接时,EMQX会把事件的信息通过我们的配置调用特定的URL上的接口,实现通知。 使用WebHook还可以有限避免单点故障。所以本项目会采用WebHook的方式来实现对设备在线和离线的管理。 开启WebHook 在数据集成 -> 数据桥接 中创建一个Webhook 名称设置为ConnectedEvent,URL 中填写我们的Webhook地址,也就是触发事件之后的调用接口地址,这里我们填: http://192.120.5.204:5000/api/Device/ConnectedEvent 请求方式为Post,其他内容保持默认不变 这里注意URL可以通过${field}的方式拼接,请求体也可以自己指定,如果留空会原样转发消息,我们这里请求体留空 设备在线和离线的事件转发的消息格式如下 { "username": "284202304230001", "timestamp": 1682652598840, "sockname": "172.17.0.5:1883", "receive_maximum": 32, "proto_ver": 5, "proto_name": "MQTT", "peername": "172.17.0.1:48524", "node": "emqx@172.17.0.5", "mountpoint": "undefined", "metadata": { "rule_id": "rule_3hsx" }, "keepalive": 60, "is_bridge": false, "expiry_interval": 10, "event": "client.connected", "connected_at": 1682652598840, "conn_props": { "User-Property": {}, "Session-Expiry-Interval": 10 }, "clientid": "mqttx_c4491df0", "clean_start": false } 我们点击 创建 ,并继续点击 创建规则 我们在创建规则中指定新的规则名称 rule_client_connected,并在SQL编辑器复制以下内容 SELECT * FROM "$events/client_connected", "$events/client_disconnected" 在右侧的事件中,我们可以看到所有可用的事件,我们选择了连接和断开两个事件,在这两个事件触发时会通过Webhook调用我们配置的接口,这样我们就能获取到设备的在线、离线状态了。 我们点击 创建按钮 完成规则的创建 我们可以看见我们创建好的规则 点击规则ID,还可以看到统计数据 在FLows中还可以看到整个工作流程 演示Webhook 我们使用MQTTX模拟一次设备连接和断开动作,可以在规则统计界面看到我们的操作已经被记录。 编写代码 我们这里采用方案2。 我们需要实现之前配置的ConnectedEvent接口 /// <summary> /// 连接事件请求 /// </summary> public class ConnectedEventRequest { /// <summary> /// 设备名称 /// </summary> public string Username { get; set; } /// <summary> /// 时间戳 /// </summary> public long Timestamp { get; set; } /// <summary> /// 事件(连接/断开) /// </summary> public string Event { get; set; } /// <summary> /// 连接时间(断开事件中为0) /// </summary> public long Connected_at { get; set; } /// <summary> /// Client ID /// </summary> public string Clientid { get; set; } } /// <summary> /// 更新设备在线状态 /// </summary> /// <param name="deviceName"></param> /// <param name="onlineStatus"></param> /// <returns></returns> public async Task UpdateDeviceOnlineStatusAsync(string deviceName, OnLineStates onlineStatus) { var device = await _ioTDbContext.IoTDeviceInfo.Include(o => o.IoTDeviceExtend).AsNoTracking() .FirstOrDefaultAsync(o => o.DeviceName == deviceName); if (device == null) { return; } else { if (device.IoTDeviceExtend == null) //扩展表为空 { device.IoTDeviceExtend = new IoTDeviceExtend { DeviceInfoId = device.Id, OnLineStates = (int)onlineStatus, }; _ioTDbContext.Attach(device.IoTDeviceExtend); _ioTDbContext.Entry(device.IoTDeviceExtend).State = EntityState.Added; _ioTDbContext.Entry(device.IoTDeviceExtend).Property(o => o.OnLineStates).IsModified = true; await _ioTDbContext.SaveChangesAsync(); } if (device.IoTDeviceExtend.OnLineStates != (int)onlineStatus) //在线状态不一致 { device.IoTDeviceExtend.OnLineStates = (int)onlineStatus; _ioTDbContext.Attach(device.IoTDeviceExtend); //防止更新其他字段 _ioTDbContext.Entry(device.IoTDeviceExtend).State = EntityState.Unchanged; _ioTDbContext.Entry(device.IoTDeviceExtend).Property(o => o.OnLineStates).IsModified = true; await _ioTDbContext.SaveChangesAsync(); } } } 我们根据Event中的内容来判断是 连接(client.connected)/断开(client.disconnected) 的事件 /// <summary> /// 连接、断开事件 /// </summary> /// <param name="request"></param> /// <returns></returns> [HttpPost] public async Task ConnectedEventAsync([FromBody] ConnectedEventRequest request) { var onlineStatus = request.Event switch { "client.connected" => OnLineStates.OnLine, _ => OnLineStates.OffLine }; await _deviceHandler.UpdateDeviceOnlineStatusAsync(request.Username, onlineStatus); } #总结 以上就是本文要讲的内容,我们可以通过MQTTX来测试我们的代码有效性。 该方案还存在部分缺点,例如: 1、每次设备上下线会导致频繁的请求接口,在大量设备接入的场景中需要考虑接口性能。 2、由于网络等问题,Web调用顺序可能不能完全保证,也许离线会比在线事件更早处理,从而导致状态不一致。我们后面会尝试用其他方案来替代WebHook,尝试解决上述问题,在此之前我们都会继续使用WebHook进行功能演示。 完整代码在这里:https://github.com/sunday866/MASA.IoT-Training-Demos 如果你对我们的 MASA 感兴趣,无论是代码贡献、使用、提 Issue,欢迎联系我们 WeChat:MasaStackTechOps QQ:7424099

优秀的个人博客,低调大师

Day9 鸿蒙,Ability全家桶(二)一步步实现AbilitySlice间导航

昨天更新完DevEco Studio后,新建项目必须填入node.js的路径,本想着官网直接下载更新,无奈速度实在太慢了!(文章最后附录在mac上安装node.js的步骤) 我们今天接着介绍来介绍一下Page Ability的AbilitySlice间导航 首先创建工程会默认创建一个Ability和一个AbilitySlice,这里要验证实现AbilitySlice间导航,则需要新增一个AbilitySlice。见下图 (1)创建AbilitySlice的布局XML文件,在resources/base/layout路径下创建如ability_second.xml,具体内容可参考ability_main.xml; (2)创建AbilitySlice的java文件,如在java/包名/slice路径下创建如SecondAbilitySlice.java,并将第一步创建的XML布局文件添加到Slice中去: [@Override](https://my.oschina.net/u/1162528) public void onStart(Intent intent) { super.onStart(intent); super.setUIContent(ResourceTable.Layout_ability_second); } (3)这个时候其实SecondAbilitySlice还未添加到MainAbility,需在MainAbility中添加入口: super.setMainRoute(MainAbilitySlice.class.getName()); //添加SecondAbilitySlice的入口 addActionRoute("action.second", SecondAbilitySlice.class.getName()); (4)步骤(3)中的定义的“action.second”还需要添加到config.json中进行配置,这个时候基本的AbilitySlice已经添加完毕: "actions": [ "action.system.home", "action.second" ] (5)同一Page内导航,可以通过以下方式实现: present(new SecondAbilitySlice(),new Intent()); //这里的0则是会通过onResult(int requestCode, Intent resultIntent)回调,检查requestCode presentForResult(new SecondAbilitySlice(),new Intent(),0); 【小知识点】 在mac上安装node.js的步骤 (1)安装brew: /bin/zsh -c "$(curl -fsSL https://gitee.com/cunkai/HomebrewCN/raw/master/Homebrew.sh)" (2)验证brew是否安装: brew -v (3)安装node.js: brew link node brew uninstall node brew install node (4)验证node.js是否安装: 下发命令npm -v、node -v,能正确显示版本号即表示node安装成功,如果是通过homebrew安装的,下发命令brew list会显示node 本文由GZH程序员小小叶发布!

优秀的个人博客,低调大师

每日一博 | 进程和线程基础知识全家桶,30 张图一套带走

前言 先来看看一则小故事 我们写好的一行行代码,为了让其工作起来,我们还得把它送进城(进程)里,那既然进了城里,那肯定不能胡作非为了。 城里人有城里人的规矩,城中有个专门管辖你们的城管(操作系统),人家让你休息就休息,让你工作就工作,毕竟摊位(CPU)就一个,每个人都要占这个摊位来工作,城里要工作的人多着去了。 所以城管为了公平起见,它使用一种策略(调度)方式,给每个人一个固定的工作时间(时间片),时间到了就会通知你去休息而换另外一个人上场工作。 另外,在休息时候你也不能偷懒,要记住工作到哪了,不然下次到你工作了,你忘记工作到哪了,那还怎么继续? 有的人,可能还进入了县城(线程)工作,这里相对轻松一些,在休息的时候,要记住的东西相对较少,而且还能共享城里的资源。 “哎哟,难道本文内容是进程和线程?” 可以,聪明的你猜出来了,也不枉费我瞎编乱造的故事了。 进程和线程对于写代码的我们,真的天天见、日日见了,但见的多不代表你就熟悉它们,比如简单问你一句,你知道它们的工作原理和区别吗? 不知道没关系,今天就要跟大家讨论操作系统的进程和线程。 提纲 正文 进程 我们编写的代码只是一个存储在硬盘的静态文件,通过编译后就会生成二进制可执行文件,当我们运行这个可执行文件后,它会被装载到内存中,接着 CPU 会执行程序中的每一条指令,那么这个运行中的程序,就被称为「进程」。 现在我们考虑有一个会读取硬盘文件数据的程序被执行了,那么当运行到读取文件的指令时,就会去从硬盘读取数据,但是硬盘的读写速度是非常慢的,那么在这个时候,如果 CPU 傻傻的等硬盘返回数据的话,那 CPU 的利用率是非常低的。 做个类比,你去煮开水时,你会傻傻的等水壶烧开吗?很明显,小孩也不会傻等。我们可以在水壶烧开之前去做其他事情。当水壶烧开了,我们自然就会听到“嘀嘀嘀”的声音,于是再把烧开的水倒入到水杯里就好了。 所以,当进程要从硬盘读取数据时,CPU 不需要阻塞等待数据的返回,而是去执行另外的进程。当硬盘数据返回时,CPU 会收到个中断,于是 CPU 再继续运行这个进程。 进程 1 与进程 2 切换 这种多个程序、交替执行的思想,就有 CPU 管理多个进程的初步想法。 对于一个支持多进程的系统,CPU 会从一个进程快速切换至另一个进程,其间每个进程各运行几十或几百个毫秒。 虽然单核的 CPU 在某一个瞬间,只能运行一个进程。但在 1 秒钟期间,它可能会运行多个进程,这样就产生并行的错觉,实际上这是并发。 并发和并行有什么区别? 一图胜千言。 并发与并行 进程与程序的关系的类比 到了晚饭时间,一对小情侣肚子都咕咕叫了,于是男生见机行事,就想给女生做晚饭,所以他就在网上找了辣子鸡的菜谱,接着买了一些鸡肉、辣椒、香料等材料,然后边看边学边做这道菜。 突然,女生说她想喝可乐,那么男生只好把做菜的事情暂停一下,并在手机菜谱标记做到哪一个步骤,把状态信息记录了下来。 然后男生听从女生的指令,跑去下楼买了一瓶冰可乐后,又回到厨房继续做菜。 这体现了,CPU 可以从一个进程(做菜)切换到另外一个进程(买可乐),在切换前必须要记录当前进程中运行的状态信息,以备下次切换回来的时候可以恢复执行。 所以,可以发现进程有着「运行 - 暂停 - 运行」的活动规律。 进程的状态 在上面,我们知道了进程有着「运行 - 暂停 - 运行」的活动规律。一般说来,一个进程并不是自始至终连续不停地运行的,它与并发执行中的其他进程的执行是相互制约的。 它有时处于运行状态,有时又由于某种原因而暂停运行处于等待状态,当使它暂停的原因消失后,它又进入准备运行状态。 所以,在一个进程的活动期间至少具备三种基本状态,即运行状态、就绪状态、阻塞状态。 进程的三种基本状态 上图中各个状态的意义: 运行状态(Runing):该时刻进程占用 CPU; 就绪状态(Ready):可运行,但因为其他进程正在运行而暂停停止; 阻塞状态(Blocked):该进程正在等待某一事件发生(如等待输入/输出操作的完成)而暂时停止运行,这时,即使给它CPU控制权,它也无法运行; 当然,进程另外两个基本状态: 创建状态(new):进程正在被创建时的状态; 结束状态(Exit):进程正在从系统中消失时的状态; 于是,一个完整的进程状态的变迁如下图: 进程五种状态的变迁 再来详细说明一下进程的状态变迁: NULL -> 创建状态:一个新进程被创建时的第一个状态; 创建状态 -> 就绪状态:当进程被创建完成并初始化后,一切就绪准备运行时,变为就绪状态,这个过程是很快的; 就绪态 -> 运行状态:处于就绪状态的进程被操作系统的进程调度器选中后,就分配给 CPU 正式运行该进程; 运行状态 -> 结束状态:当进程已经运行完成或出错时,会被操作系统作结束状态处理; 运行状态 -> 就绪状态:处于运行状态的进程在运行过程中,由于分配给它的运行时间片用完,操作系统会把该进程变为就绪态,接着从就绪态选中另外一个进程运行; 运行状态 -> 阻塞状态:当进程请求某个事件且必须等待时,例如请求 I/O 事件; 阻塞状态 -> 就绪状态:当进程要等待的事件完成时,它从阻塞状态变到就绪状态; 另外,还有一个状态叫挂起状态,它表示进程没有占有内存空间。这跟阻塞状态是不一样,阻塞状态是等待某个事件的返回。 由于虚拟内存管理原因,进程的所使用的空间可能并没有映射到物理内存,而是在硬盘上,这时进程就会出现挂起状态。 虚拟内存管理-换入换出 挂起状态可以分为两种: 阻塞挂起状态:进程在外存(硬盘)并等待某个事件的出现; 就绪挂起状态:进程在外存(硬盘),但只要进入内存,即刻立刻运行; 这两种挂起状态加上前面的五种状态,就变成了七种状态变迁(留给我的颜色不多了),见如下图: 七种状态变迁 进程的控制结构 在操作系统中,是用进程控制块(process control block,PCB)数据结构来描述进程的。 那 PCB 是什么呢?打开知乎搜索你就会发现这个东西并不是那么简单。 知乎搜 PCB 的提示 打住打住,我们是个正经的人,怎么会去看那些问题呢?是吧,回来回来。 PCB 是进程存在的唯一标识,这意味着一个进程的存在,必然会有一个 PCB,如果进程消失了,那么 PCB 也会随之消失。 PCB 具体包含什么信息呢? 进程描述信息: 进程标识符:标识各个进程,每个进程都有一个并且唯一的标识符; 用户标识符:进程归属的用户,用户标识符主要为共享和保护服务; 进程控制和管理信息: 进程当前状态,如 new、ready、running、waiting 或 blocked 等; 进程优先级:进程抢占 CPU 时的优先级; 资源分配清单: 有关内存地址空间或虚拟地址空间的信息,所打开文件的列表和所使用的 I/O 设备信息。 CPU 相关信息: CPU 中各个寄存器的值,当进程被切换时,CPU 的状态信息都会被保存在相应的 PCB 中,以便进程重新执行时,能从断点处继续执行。 可见,PCB 包含信息还是比较多的。 每个 PCB 是如何组织的呢? 通常是通过链表的方式进行组织,把具有相同状态的进程链在一起,组成各种队列。比如: 将所有处于就绪状态的进程链在一起,称为就绪队列; 把所有因等待某事件而处于等待状态的进程链在一起就组成各种阻塞队列; 另外,对于运行队列在单核 CPU 系统中则只有一个运行指针了,因为单核 CPU 在某个时间,只能运行一个程序。 那么,就绪队列和阻塞队列链表的组织形式如下图: 就绪队列和阻塞队列 除了链接的组织方式,还有索引方式,它的工作原理:将同一状态的进程组织在一个索引表中,索引表项指向相应的 PCB,不同状态对应不同的索引表。 一般会选择链表,因为可能面临进程创建,销毁等调度导致进程状态发生变化,所以链表能够更加灵活的插入和删除。 进程的控制 我们熟知了进程的状态变迁和进程的数据结构 PCB 后,再来看看进程的创建、终止、阻塞、唤醒的过程,这些过程也就是进程的控制。 01 创建进程 操作系统允许一个进程创建另一个进程,而且允许子进程继承父进程所拥有的资源,当子进程被终止时,其在父进程处继承的资源应当还给父进程。同时,终止父进程时同时也会终止其所有的子进程。 创建进程的过程如下: 为新进程分配一个唯一的进程标识号,并申请一个空白的 PCB,PCB 是有限的,若申请失败则创建失败; 为进程分配资源,此处如果资源不足,进程就会进入等待状态,以等待资源; 初始化 PCB; 如果进程的调度队列能够接纳新进程,那就将进程插入到就绪队列,等待被调度运行; 02 终止进程 进程可以有 3 种终止方式:正常结束、异常结束以及外界干预(信号 kill 掉)。 终止进程的过程如下: 查找需要终止的进程的 PCB; 如果处于执行状态,则立即终止该进程的执行,然后将 CPU 资源分配给其他进程; 如果其还有子进程,则应将其所有子进程终止; 将该进程所拥有的全部资源都归还给父进程或操作系统; 将其从 PCB 所在队列中删除; 03 阻塞进程 当进程需要等待某一事件完成时,它可以调用阻塞语句把自己阻塞等待。而一旦被阻塞等待,它只能由另一个进程唤醒。 阻塞进程的过程如下: 找到将要被阻塞进程标识号对应的 PCB; 如果该进程为运行状态,则保护其现场,将其状态转为阻塞状态,停止运行; 将该 PCB 插入的阻塞队列中去; 04 唤醒进程 进程由「运行」转变为「阻塞」状态是由于进程必须等待某一事件的完成,所以处于阻塞状态的进程是绝对不可能叫醒自己的。 如果某进程正在等待 I/O 事件,需由别的进程发消息给它,则只有当该进程所期待的事件出现时,才由发现者进程用唤醒语句叫醒它。 唤醒进程的过程如下: 在该事件的阻塞队列中找到相应进程的 PCB; 将其从阻塞队列中移出,并置其状态为就绪状态; 把该 PCB 插入到就绪队列中,等待调度程序调度; 进程的阻塞和唤醒是一对功能相反的语句,如果某个进程调用了阻塞语句,则必有一个与之对应的唤醒语句。 进程的上下文切换 各个进程之间是共享 CPU 资源的,在不同的时候进程之间需要切换,让不同的进程可以在 CPU 执行,那么这个一个进程切换到另一个进程运行,称为进程的上下文切换。 在详细说进程上下文切换前,我们先来看看 CPU 上下文切换 大多数操作系统都是多任务,通常支持大于 CPU 数量的任务同时运行。实际上,这些任务并不是同时运行的,只是因为系统在很短的时间内,让各个任务分别在 CPU 运行,于是就造成同时运行的错误。 任务是交给 CPU 运行的,那么在每个任务运行前,CPU 需要知道任务从哪里加载,又从哪里开始运行。 所以,操作系统需要事先帮 CPU 设置好 CPU 寄存器和程序计数器。 CPU 寄存器是 CPU 内部一个容量小,但是速度极快的内存(缓存)。我举个例子,寄存器像是你的口袋,内存像你的书包,硬盘则是你家里的柜子,如果你的东西存放到口袋,那肯定是比你从书包或家里柜子取出来要快的多。 再来,程序计数器则是用来存储 CPU 正在执行的指令位置、或者即将执行的下一条指令位置。 所以说,CPU 寄存器和程序计数是 CPU 在运行任何任务前,所必须依赖的环境,这些环境就叫做 CPU 上下文。 既然知道了什么是 CPU 上下文,那理解 CPU 上下文切换就不难了。 CPU 上下文切换就是先把前一个任务的 CPU 上下文(CPU 寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。 系统内核会存储保持下来的上下文信息,当此任务再次被分配给 CPU 运行时,CPU 会重新加载这些上下文,这样就能保证任务原来的状态不受影响,让任务看起来还是连续运行。 上面说到所谓的「任务」,主要包含进程、线程和中断。所以,可以根据任务的不同,把 CPU 上下文切换分成:进程上下文切换、线程上下文切换和中断上下文切换。 进程的上下文切换到底是切换什么呢? 进程是由内核管理和调度的,所以进程的切换只能发生在内核态。 所以,进程的上下文切换不仅包含了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的资源。 通常,会把交换的信息保存在进程的 PCB,当要运行另外一个进程的时候,我们需要从这个进程的 PCB 取出上下文,然后恢复到 CPU 中,这使得这个进程可以继续执行,如下图所示: 进程上下文切换 大家需要注意,进程的上下文开销是很关键的,我们希望它的开销越小越好,这样可以使得进程可以把更多时间花费在执行程序上,而不是耗费在上下文切换。 发生进程上下文切换有哪些场景? 为了保证所有进程可以得到公平调度,CPU 时间被划分为一段段的时间片,这些时间片再被轮流分配给各个进程。这样,当某个进程的时间片耗尽了,就会被系统挂起,切换到其它正在等待 CPU 的进程运行; 进程在系统资源不足(比如内存不足)时,要等到资源满足后才可以运行,这个时候进程也会被挂起,并由系统调度其他进程运行; 当进程通过睡眠函数 sleep 这样的方法将自己主动挂起时,自然也会重新调度; 当有优先级更高的进程运行时,为了保证高优先级进程的运行,当前进程会被挂起,由高优先级进程来运行; 发生硬件中断时,CPU 上的进程会被中断挂起,转而执行内核中的中断服务程序; 以上,就是发生进程上下文切换的常见场景了。 线程 在早期的操作系统中都是以进程作为独立运行的基本单位,直到后面,计算机科学家们又提出了更小的能独立运行的基本单位,也就是线程。 为什么使用线程? 我们举个例子,假设你要编写一个视频播放器软件,那么该软件功能的核心模块有三个: 从视频文件当中读取数据; 对读取的数据进行解压缩; 把解压缩后的视频数据播放出来; 对于单进程的实现方式,我想大家都会是以下这个方式: 单进程实现方式 对于单进程的这种方式,存在以下问题: 播放出来的画面和声音会不连贯,因为当 CPU 能力不够强的时候,Read 的时候可能进程就等在这了,这样就会导致等半天才进行数据解压和播放; 各个函数之间不是并发执行,影响资源的使用效率; 那改进成多进程的方式: 多进程实现方式 对于多进程的这种方式,依然会存在问题: 进程之间如何通信,共享数据? 维护进程的系统开销较大,如创建进程时,分配资源、建立 PCB;终止进程时,回收资源、撤销 PCB;进程切换时,保存当前进程的状态信息; 那到底如何解决呢?需要有一种新的实体,满足以下特性: 实体之间可以并发运行; 实体之间共享相同的地址空间; 这个新的实体,就是线程( Thread ),线程之间可以并发运行且共享相同的地址空间。 什么是线程? 线程是进程当中的一条执行流程。 同一个进程内多个线程之间可以共享代码段、数据段、打开的文件等资源,但每个线程都有独立一套的寄存器和栈,这样可以确保线程的控制流是相对独立的。 多线程 线程的优缺点? 线程的优点: 一个进程中可以同时存在多个线程; 各个线程之间可以并发执行; 各个线程之间可以共享地址空间和文件等资源; 线程的缺点: 当进程中的一个线程奔溃时,会导致其所属进程的所有线程奔溃。 举个例子,对于游戏的用户设计,则不应该使用多线程的方式,否则一个用户挂了,会影响其他同个进程的线程。 线程与进程的比较 线程与进程的比较如下: 进程是资源(包括内存、打开的文件等)分配的单位,线程是 CPU 调度的单位; 进程拥有一个完整的资源平台,而线程只独享必不可少的资源,如寄存器和栈; 线程同样具有就绪、阻塞、执行三种基本状态,同样具有状态之间的转换关系; 线程能减少并发执行的时间和空间开销; 对于,线程相比进程能减少开销,体现在: 线程的创建时间比进程快,因为进程在创建的过程中,还需要资源管理信息,比如内存管理信息、文件管理信息,而线程在创建的过程中,不会涉及这些资源管理信息,而是共享它们; 线程的终止时间比进程快,因为线程释放的资源相比进程少很多; 同一个进程内的线程切换比进程切换快,因为线程具有相同的地址空间(虚拟内存共享),这意味着同一个进程的线程都具有同一个页表,那么在切换的时候不需要切换页表。而对于进程之间的切换,切换的时候要把页表给切换掉,而页表的切换过程开销是比较大的; 由于同一进程的各线程间共享内存和文件资源,那么在线程之间数据传递的时候,就不需要经过内核了,这就使得线程之间的数据交互效率更高了; 所以,线程比进程不管是时间效率,还是空间效率都要高。 线程的上下文切换 在前面我们知道了,线程与进程最大的区别在于:线程是调度的基本单位,而进程则是资源拥有的基本单位。 所以,所谓操作系统的任务调度,实际上的调度对象是线程,而进程只是给线程提供了虚拟内存、全局变量等资源。 对于线程和进程,我们可以这么理解: 当进程只有一个线程时,可以认为进程就等于线程; 当进程拥有多个线程时,这些线程会共享相同的虚拟内存和全局变量等资源,这些资源在上下文切换时是不需要修改的; 另外,线程也有自己的私有数据,比如栈和寄存器等,这些在上下文切换时也是需要保存的。 线程上下文切换的是什么? 这还得看线程是不是属于同一个进程: 当两个线程不是属于同一个进程,则切换的过程就跟进程上下文切换一样; 当两个线程是属于同一个进程,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据; 所以,线程的上下文切换相比进程,开销要小很多。 线程的实现 主要有三种线程的实现方式: 用户线程(User Thread):在用户空间实现的线程,不是由内核管理的线程,是由用户态的线程库来完成线程的管理; 内核线程(Kernel Thread):在内核中实现的线程,是由内核管理的线程; 轻量级进程(LightWeight Process):在内核中来支持用户线程; 那么,这还需要考虑一个问题,用户线程和内核线程的对应关系。 首先,第一种关系是多对一的关系,也就是多个用户线程对应同一个内核线程: 多对一 第二种是一对一的关系,也就是一个用户线程对应一个内核线程: 一对一 第三种是多对多的关系,也就是多个用户线程对应到多个内核线程: 多对多 用户线程如何理解?存在什么优势和缺陷? 用户线程是基于用户态的线程管理库来实现的,那么线程控制块(Thread Control Block, TCB) 也是在库里面来实现的,对于操作系统而言是看不到这个 TCB 的,它只能看到整个进程的 PCB。 所以,用户线程的整个线程管理和调度,操作系统是不直接参与的,而是由用户级线程库函数来完成线程的管理,包括线程的创建、终止、同步和调度等。 用户级线程的模型,也就类似前面提到的多对一的关系,即多个用户线程对应同一个内核线程,如下图所示: 用户级线程模型 用户线程的优点: 每个进程都需要有它私有的线程控制块(TCB)列表,用来跟踪记录它各个线程状态信息(PC、栈指针、寄存器),TCB 由用户级线程库函数来维护,可用于不支持线程技术的操作系统; 用户线程的切换也是由线程库函数来完成的,无需用户态与内核态的切换,所以速度特别快; 用户线程的缺点: 由于操作系统不参与线程的调度,如果一个线程发起了系统调用而阻塞,那进程所包含的用户线程都不能执行了。 当一个线程开始运行后,除非它主动地交出 CPU 的使用权,否则它所在的进程当中的其他线程无法运行,因为用户态的线程没法打断当前运行中的线程,它没有这个特权,只有操作系统才有,但是用户线程不是由操作系统管理的。 由于时间片分配给进程,故与其他进程比,在多线程执行时,每个线程得到的时间片较少,执行会比较慢; 以上,就是用户线程的优缺点了。 那内核线程如何理解?存在什么优势和缺陷? 内核线程是由操作系统管理的,线程对应的 TCB 自然是放在操作系统里的,这样线程的创建、终止和管理都是由操作系统负责。 内核线程的模型,也就类似前面提到的一对一的关系,即一个用户线程对应一个内核线程,如下图所示: 内核线程模型 内核线程的优点: 在一个进程当中,如果某个内核线程发起系统调用而被阻塞,并不会影响其他内核线程的运行; 分配给线程,多线程的进程获得更多的 CPU 运行时间; 内核线程的缺点: 在支持内核线程的操作系统中,由内核来维护进程和线程的上下问信息,如 PCB 和 TCB; 线程的创建、终止和切换都是通过系统调用的方式来进行,因此对于系统来说,系统开销比较大; 以上,就是内核线的优缺点了。 最后的轻量级进程如何理解? 轻量级进程(Light-weight process,LWP)是内核支持的用户线程,一个进程可有一个或多个 LWP,每个 LWP 是跟内核线程一对一映射的,也就是 LWP 都是由一个内核线程支持。 另外,LWP 只能由内核管理并像普通进程一样被调度,Linux 内核是支持 LWP 的典型例子。 在大多数系统中,LWP与普通进程的区别也在于它只有一个最小的执行上下文和调度程序所需的统计信息。一般来说,一个进程代表程序的一个实例,而 LWP 代表程序的执行线程,因为一个执行线程不像进程那样需要那么多状态信息,所以 LWP 也不带有这样的信息。 在 LWP 之上也是可以使用用户线程的,那么 LWP 与用户线程的对应关系就有三种: 1 : 1,即一个 LWP 对应 一个用户线程; N : 1,即一个 LWP 对应多个用户线程; N : N,即多个 LMP 对应多个用户线程; 接下来针对上面这三种对应关系说明它们优缺点。先下图的 LWP 模型: LWP 模型 1 : 1 模式 一个线程对应到一个 LWP 再对应到一个内核线程,如上图的进程 4,属于此模型。 优点:实现并行,当一个 LWP 阻塞,不会影响其他 LWP; 缺点:每一个用户线程,就产生一个内核线程,创建线程的开销较大。 N : 1 模式 多个用户线程对应一个 LWP 再对应一个内核线程,如上图的进程 2,线程管理是在用户空间完成的,此模式中用户的线程对操作系统不可见。 优点:用户线程要开几个都没问题,且上下文切换发生用户空间,切换的效率较高; 缺点:一个用户线程如果阻塞了,则整个进程都将会阻塞,另外在多核 CPU 中,是没办法充分利用 CPU 的。 M : N 模式 根据前面的两个模型混搭一起,就形成 M:N 模型,该模型提供了两级控制,首先多个用户线程对应到多个 LWP,LWP 再一一对应到内核线程,如上图的进程 3。 优点:综合了前两种优点,大部分的线程上下文发生在用户空间,且多个线程又可以充分利用多核 CPU 的资源。 组合模式 如上图的进程 5,此进程结合 1:1 模型和 M:N 模型。开发人员可以针对不同的应用特点调节内核线程的数目来达到物理并行性和逻辑并行性的最佳方案。 调度 进程都希望自己能够占用 CPU 进行工作,那么这涉及到前面说过的进程上下文切换。 一旦操作系统把进程切换到运行状态,也就意味着该进程占用着 CPU 在执行,但是当操作系统把进程切换到其他状态时,那就不能在 CPU 中执行了,于是操作系统会选择下一个要运行的进程。 选择一个进程运行这一功能是在操作系统中完成的,通常称为调度程序(scheduler)。 那到底什么时候调度进程,或以什么原则来调度进程呢? 调度时机 在进程的生命周期中,当进程从一个运行状态到另外一状态变化的时候,其实会触发一次调度。 比如,以下状态的变化都会触发操作系统的调度: 从就绪态 -> 运行态:当进程被创建时,会进入到就绪队列,操作系统会从就绪队列选择一个进程运行; 从运行态 -> 阻塞态:当进程发生 I/O 事件而阻塞时,操作系统必须另外一个进程运行; 从运行态 -> 结束态:当进程退出结束后,操作系统得从就绪队列选择另外一个进程运行; 因为,这些状态变化的时候,操作系统需要考虑是否要让新的进程给 CPU 运行,或者是否让当前进程从 CPU 上退出来而换另一个进程运行。 另外,如果硬件时钟提供某个频率的周期性中断,那么可以根据如何处理时钟中断,把调度算法分为两类: 非抢占式调度算法挑选一个进程,然后让该进程运行直到被阻塞,或者直到该进程退出,才会调用另外一个进程,也就是说不会理时钟中断这个事情。 抢占式调度算法挑选一个进程,然后让该进程只运行某段时间,如果在该时段结束时,该进程仍然在运行时,则会把它挂起,接着调度程序从就绪队列挑选另外一个进程。这种抢占式调度处理,需要在时间间隔的末端发生时钟中断,以便把 CPU 控制返回给调度程序进行调度,也就是常说的时间片机制。 调度原则 原则一:如果运行的程序,发生了 I/O 事件的请求,那 CPU 使用率必然会很低,因为此时进程在阻塞等待硬盘的数据返回。这样的过程,势必会造成 CPU 突然的空闲。所以,为了提高 CPU 利用率,在这种发送 I/O 事件致使 CPU 空闲的情况下,调度程序需要从就绪队列中选择一个进程来运行。 原则二:有的程序执行某个任务花费的时间会比较长,如果这个程序一直占用着 CPU,会造成系统吞吐量(CPU 在单位时间内完成的进程数量)的降低。所以,要提高系统的吞吐率,调度程序要权衡长任务和短任务进程的运行完成数量。 原则三:从进程开始到结束的过程中,实际上是包含两个时间,分别是进程运行时间和进程等待时间,这两个时间总和就称为周转时间。进程的周转时间越小越好,如果进程的等待时间很长而运行时间很短,那周转时间就很长,这不是我们所期望的,调度程序应该避免这种情况发生。 原则四:处于就绪队列的进程,也不能等太久,当然希望这个等待的时间越短越好,这样可以使得进程更快的在 CPU 中执行。所以,就绪队列中进程的等待时间也是调度程序所需要考虑的原则。 原则五:对于鼠标、键盘这种交互式比较强的应用,我们当然希望它的响应时间越快越好,否则就会影响用户体验了。所以,对于交互式比较强的应用,响应时间也是调度程序需要考虑的原则。 五种调度原则 针对上面的五种调度原则,总结成如下: CPU 利用率:调度程序应确保 CPU 是始终匆忙的状态,这可提高 CPU 的利用率; 系统吞吐量:吞吐量表示的是单位时间内 CPU 完成进程的数量,长作业的进程会占用较长的 CPU 资源,因此会降低吞吐量,相反,短作业的进程会提升系统吞吐量; 周转时间:周转时间是进程运行和阻塞时间总和,一个进程的周转时间越小越好; 等待时间:这个等待时间不是阻塞状态的时间,而是进程处于就绪队列的时间,等待的时间越长,用户越不满意; 响应时间:用户提交请求到系统第一次产生响应所花费的时间,在交互式系统中,响应时间是衡量调度算法好坏的主要标准。 说白了,这么多调度原则,目的就是要使得进程要「快」。 调度算法 不同的调度算法适用的场景也是不同的。 接下来,说说在单核 CPU 系统中常见的调度算法。 01 先来先服务调度算法 最简单的一个调度算法,就是非抢占式的先来先服务(First Come First Severd, FCFS)算法了。 FCFS 调度算法 顾名思义,先来后到,每次从就绪队列选择最先进入队列的进程,然后一直运行,直到进程退出或被阻塞,才会继续从队列中选择第一个进程接着运行。 这似乎很公平,但是当一个长作业先运行了,那么后面的短作业等待的时间就会很长,不利于短作业。 FCFS 对长作业有利,适用于 CPU 繁忙型作业的系统,而不适用于 I/O 繁忙型作业的系统。 02 最短作业优先调度算法 最短作业优先(Shortest Job First, SJF)调度算法同样也是顾名思义,它会优先选择运行时间最短的进程来运行,这有助于提高系统的吞吐量。 SJF 调度算法 这显然对长作业不利,很容易造成一种极端现象。 比如,一个长作业在就绪队列等待运行,而这个就绪队列有非常多的短作业,那么就会使得长作业不断的往后推,周转时间变长,致使长作业长期不会被运行。 03 高响应比优先调度算法 前面的「先来先服务调度算法」和「最短作业优先调度算法」都没有很好的权衡短作业和长作业。 那么,高响应比优先 (Highest Response Ratio Next, HRRN)调度算法主要是权衡了短作业和长作业。 每次进行进程调度时,先计算「响应比优先级」,然后把「响应比优先级」最高的进程投入运行,「响应比优先级」的计算公式: 从上面的公式,可以发现: 如果两个进程的「等待时间」相同时,「要求的服务时间」越短,「响应比」就越高,这样短作业的进程容易被选中运行; 如果两个进程「要求的服务时间」相同时,「等待时间」越长,「响应比」就越高,这就兼顾到了长作业进程,因为进程的响应比可以随时间等待的增加而提高,当其等待时间足够长时,其响应比便可以升到很高,从而获得运行的机会; 04 时间片轮转调度算法 最古老、最简单、最公平且使用最广的算法就是时间片轮转(Round Robin, RR)调度算法。。 RR 调度算法 每个进程被分配一个时间段,称为时间片(Quantum),即允许该进程在该时间段中运行。 如果时间片用完,进程还在运行,那么将会把此进程从 CPU 释放出来,并把 CPU 分配另外一个进程; 如果该进程在时间片结束前阻塞或结束,则 CPU 立即进行切换; 另外,时间片的长度就是一个很关键的点: 如果时间片设得太短会导致过多的进程上下文切换,降低了 CPU 效率; 如果设得太长又可能引起对短作业进程的响应时间变长。将 通常时间片设为 20ms~50ms 通常是一个比较合理的折中值。 05 最高优先级调度算法 前面的「时间片轮转算法」做了个假设,即让所有的进程同等重要,也不偏袒谁,大家的运行时间都一样。 但是,对于多用户计算机系统就有不同的看法了,它们希望调度是有优先级的,即希望调度程序能从就绪队列中选择最高优先级的进程进行运行,这称为最高优先级(Highest Priority First,HPF)调度算法。 进程的优先级可以分为,静态优先级或动态优先级: 静态优先级:创建进程时候,就已经确定了优先级了,然后整个运行时间优先级都不会变化; 动态优先级:根据进程的动态变化调整优先级,比如如果进程运行时间增加,则降低其优先级,如果进程等待时间(就绪队列的等待时间)增加,则升高其优先级,也就是随着时间的推移增加等待进程的优先级。 该算法也有两种处理优先级高的方法,非抢占式和抢占式: 非抢占式:当就绪队列中出现优先级高的进程,运行完当前进程,再选择优先级高的进程。 抢占式:当就绪队列中出现优先级高的进程,当前进程挂起,调度优先级高的进程运行。 但是依然有缺点,可能会导致低优先级的进程永远不会运行。 06 多级反馈队列调度算法 多级反馈队列(Multilevel Feedback Queue)调度算法是「时间片轮转算法」和「最高优先级算法」的综合和发展。 顾名思义: 「多级」表示有多个队列,每个队列优先级从高到低,同时优先级越高时间片越短。 「反馈」表示如果有新的进程加入优先级高的队列时,立刻停止当前正在运行的进程,转而去运行优先级高的队列; 多级反馈队列 来看看,它是如何工作的: 设置了多个队列,赋予每个队列不同的优先级,每个队列优先级从高到低,同时优先级越高时间片越短; 新的进程会被放入到第一级队列的末尾,按先来先服务的原则排队等待被调度,如果在第一级队列规定的时间片没运行完成,则将其转入到第二级队列的末尾,以此类推,直至完成; 当较高优先级的队列为空,才调度较低优先级的队列中的进程运行。如果进程运行时,有新进程进入较高优先级的队列,则停止当前运行的进程并将其移入到原队列末尾,接着让较高优先级的进程运行; 可以发现,对于短作业可能可以在第一级队列很快被处理完。对于长作业,如果在第一级队列处理不完,可以移入下次队列等待被执行,虽然等待的时间变长了,但是运行时间也会更长了,所以该算法很好的兼顾了长短作业,同时有较好的响应时间。 看的迷迷糊糊?那我拿去银行办业务的例子,把上面的调度算法串起来,你还不懂,你锤我! 办理业务的客户相当于进程,银行窗口工作人员相当于 CPU。 现在,假设这个银行只有一个窗口(单核 CPU ),那么工作人员一次只能处理一个业务。 银行办业务 那么最简单的处理方式,就是先来的先处理,后面来的就乖乖排队,这就是先来先服务(FCFS)调度算法。但是万一先来的这位老哥是来贷款的,这一谈就好几个小时,一直占用着窗口,这样后面的人只能干等,或许后面的人只是想简单的取个钱,几分钟就能搞定,却因为前面老哥办长业务而要等几个小时,你说气不气人? 先来先服务 有客户抱怨了,那我们就要改进,我们干脆优先给那些几分钟就能搞定的人办理业务,这就是短作业优先(SJF)调度算法。听起来不错,但是依然还是有个极端情况,万一办理短业务的人非常的多,这会导致长业务的人一直得不到服务,万一这个长业务是个大客户,那不就捡了芝麻丢了西瓜 最短作业优先 那就公平起见,现在窗口工作人员规定,每个人我只处理 10 分钟。如果 10 分钟之内处理完,就马上换下一个人。如果没处理完,依然换下一个人,但是客户自己得记住办理到哪个步骤了。这个也就是时间片轮转(RR)调度算法。但是如果时间片设置过短,那么就会造成大量的上下文切换,增大了系统开销。如果时间片过长,相当于退化成退化成 FCFS 算法了。 时间片轮转 既然公平也可能存在问题,那银行就对客户分等级,分为普通客户、VIP 客户、SVIP 客户。只要高优先级的客户一来,就第一时间处理这个客户,这就是最高优先级(HPF)调度算法。但依然也会有极端的问题,万一当天来的全是高级客户,那普通客户不是没有被服务的机会,不把普通客户当人是吗?那我们把优先级改成动态的,如果客户办理业务时间增加,则降低其优先级,如果客户等待时间增加,则升高其优先级。 最高优先级(静态) 那有没有兼顾到公平和效率的方式呢?这里介绍一种算法,考虑的还算充分的,多级反馈队列(MFQ)调度算法,它是时间片轮转算法和优先级算法的综合和发展。它的工作方式: 多级反馈队列 银行设置了多个排队(就绪)队列,每个队列都有不同的优先级,各个队列优先级从高到低,同时每个队列执行时间片的长度也不同,优先级越高的时间片越短。 新客户(进程)来了,先进入第一级队列的末尾,按先来先服务原则排队等待被叫号(运行)。如果时间片用完客户的业务还没办理完成,则让客户进入到下一级队列的末尾,以此类推,直至客户业务办理完成。 当第一级队列没人排队时,就会叫号二级队列的客户。如果客户办理业务过程中,有新的客户加入到较高优先级的队列,那么此时办理中的客户需要停止办理,回到原队列的末尾等待再次叫号,因为要把窗口让给刚进入较高优先级队列的客户。 可以发现,对于要办理短业务的客户来说,可以很快的轮到并解决。对于要办理长业务的客户,一下子解决不了,就可以放到下一个队列,虽然等待的时间稍微变长了,但是轮到自己的办理时间也变长了,也可以接受,不会造成极端的现象,可以说是综合上面几种算法的优点。 好文推荐 真棒!20 张图揭开内存管理的迷雾,瞬间豁然开朗 唠叨唠叨 其实,关于进程和线程的部分,小林周末就已经写好了。 但是,写到调度算法的时候,我就懵逼了,在想用什么方式能更通俗易懂的表达这些晦涩难懂的算法,这一小结花了我非常多时间。唉,菜就是菜,小林我也不找借口了。。。 另外,最近小林创了技术交流群,里面的人说话又好听,各个都是人才,有兴趣的读者,可以扫一扫小林的私人微信二维码,备注「加群」即可。 扫一扫,进群 如果大家在阅读过程中,发现了不理解或有错误的地方,欢迎跟在底部留言,你们的每一条留言,小林都会回复。 小林是专为大家图解的工具人,Goodbye,我们下次见! 本文分享自微信公众号 - 小林coding(CodingLin)。如有侵权,请联系 support@oschina.cn 删除。本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

优秀的个人博客,低调大师

Xinference v1.7.0 重磅发布:Qwen3 全家桶 + 图生视频黑科技,7 大更新

Xorbits Inference(Xinference)是一个 性能强大且功能全面的 分布式 推理框架。可用于大语言模型(LLM),语音识别模型,多模态模型等各种模型的推理。通过 Xorbits Inference,你可以轻松地 一键部署你自己的模型或内置的前沿开源模型 - https://github.com/xorbitsai/inference。无论你是研究者,开发者,或是数据科学家,都可以通过 Xorbits Inference 与最前沿的 AI 模型,发掘更多可能。 Xinference 的功能和亮点有: 🌟 模型推理,轻而易举:大语言模型,语音识别模型,多模态模型的部署流程被大大简化。一个命令即可完成模型的部署工作。 ⚡️ 前沿模型,应有尽有:框架内置众多中英文的前沿大语言模型,包括 baichuan,chatglm2 等,一键即可体验!内置模型列表还在快速更新中! 🖥 异构硬件,快如闪电:通过 ggml,同时使用你的 GPU 与 CPU 进行推理,降低延迟,提高吞吐! ⚙️ 接口调用,灵活多样:提供多种使用模型的接口,包括 OpenAI 兼容的 RESTful API(包括 Function Calling),RPC,命令行,web UI 等等。方便模型的管理与交互。 🌐 集群计算,分布协同:支持分布式部署,通过内置的资源调度器,让不同大小的模型按需调度到不同机器,充分使用集群资源。 🔌 开放生态,无缝对接:与流行的三方库无缝对接,包括 LangChain, LlamaIndex, Dify,以及 Chatbox。 🚀 Xinference v1.7.0 更新日志 ✅ 本次亮点 🧠 Qwen3 模型新增 Embedding 与 Reranker 支持 🔍 Embedding 支持多引擎切换机制新增 vLLM 后端支持,统一调用方式,灵活适配不同部署需求。 🎞️ 首尾帧生成视频能力上线图生视频模块支持传入首帧与尾帧,自动补全过程动画。 🌐 社区版更新 📦 安装方式 pip 安装:pip install 'xinference==1.7.0' Docker 使用:拉取最新版镜像,或在容器中使用 pip 更新 🆕 新模型支持 CogView4(图像生成模型) Deepseek-R1-0528 混合量化版 Qwen3 Embedding Qwen3-Reranker MiniCPM4 系列 SeACoParaformer(语音识别) ✨ 新特性 图像生成:新增 CogView4 模型支持 Embedding 模型支持指定运行引擎(如 vLLM) Qwen3 Embedding 模型支持 Qwen3 Reranker 模型支持 图生视频新增 首帧-尾帧生成 能力 FunASR 系列语音模型支持 verbose_json 输出格式 Embedding 模型新增 model_engine 参数(UI 支持) transcripts API 支持透传 kwargs Web UI 支持日语与韩语界面语言 运行中模型页面加载时自动切换至活动标签页 UI 支持 image / video 模型筛选(基于 model_ability) 🛠 功能增强 支持 PCM 音频响应格式(response_format=pcm) 🐞 Bug 修复 修复 Qwen 与 Spring AI function call 配合使用时报错问题 修复 sglang 潜在卡死问题 修复移动端语言切换异常问题(UI) 修复命令行参数 bug 修复依赖安装问题 📚 文档更新 CosyVoice 文档更新 模型列表文档更新 链接整理与修复 🏢 企业版更新 🧠 支持 vLLM v1 引擎自动选择与分布式部署默认启用 v1 引擎,自动适配多机多卡,显著提升模型推理吞吐与稳定性。 🖥️ UI 显示优化:异构集群信息更清晰可见 我们感谢每一位参与的社区伙伴对 Xinference 的帮助和支持,也欢迎更多使用者和开发者参与体验和使用 Xinference。 欢迎您在 https://github.com/xorbitsai/inference 给我们一个 星标,这样你就可以在 GitHub 上及时收到每个新版本的通知。

优秀的个人博客,低调大师

DeepSeek 微调秘籍揭秘,一键解锁升级版全家桶,AI 玩家必备神器!

DeepSeek V3/R1 火爆全网,基于原始模型的解决方案和 API 服务已随处可见,陷入低价和免费内卷。 如何站在巨人肩膀上,通过后训练(post-training)结合专业领域数据,低成本打造高质量私有模型,提升业务竞争力与价值? 已收获近 4 万 GitHub Star 的 Colossal-AI,发布开源大模型后训练工具箱,包含: DeepSeek V3/R1 满血 671B LoRA 低成本 SFT 微调 完整的强化学习工具链 PPO、GRPO、DPO、SimPO 等 无缝适配 DeepSeek 系列蒸馏模型在内的 HuggingFace 开源模型 兼容支持英伟达 GPU、华为昇腾 NPU 等多种硬件 支持混合精度训练、gradient checkpoint 等训练加速降低成本 灵活的训练配置接口,支持自定义奖励函数、损失函数等 提供灵活的并行策略配置接口,包括数据并行、模型并行、专家并行、ZeRO 和 Offload 等,以适应不同硬件规模 开源地址:github.com/hpcaitech/ColossalAI 低成本监督微调满血版 DeepSeek V3/R1 671B DeepSeek V3/R1 满血版参数高达 6710 亿,如何低成本进行低成本微调呢? 仅需以下几个步骤,即可快速完成。 数据集准备 该脚本接收 JSONL(JSON Lines)格式的文件作为输入数据集,例如 lora_sft_data.jsonl 数据集的每一行应为一个聊天对话列表。例如: [{"role": "user", "content": "你好,最近怎么样?"}, {"role": "assistant", "content": "我很好。今天有什么可以帮你的吗?"}] [{"role": "user", "content": "火烧赤壁 曹操为何不拨打119求救?"}, {"role": "assistant", "content": "因为在三国时期,还没有电话和现代的消防系统,所以曹操无法拨打119求救。"}] 该数据格式,兼容 Huggingface chat template,支持自定义 system prompt,因此可灵活按需配置。 模型权重准备 为保证更好的微调效果,使用 BF16 权重进行微调。 如果已下载了 FP8 的 DeepSeek V3/R1 权重,可以使用 DeepSeek 官方脚本 fp8_cast_bf16.py 通过 GPU 将权重转换为 BF16。 对于使用国产华为昇腾算力,可以下载 fp8_cast_bf16.py 脚本转换权重。 使用方法 在准备好数据集和模型权重后,可使用 Colossal-AI 提供的一键启动脚本 lora_finetune.py 该脚本与常见 SFT 脚本类似,且完全兼容 HuggingFace PEFT,启动命令: colossalai run --hostfile path-to-host-file --nproc_per_node 8 lora_finetune.py --pretrained path-to-DeepSeek-R1-bf16 --dataset path-to-dataset.jsonl --plugin moe --lr 2e-5 --max_length 256 -g --ep 8 --pp 3 --batch_size 24 --lora_rank 8 --lora_alpha 16 --num_epochs 2 --warmup_steps 8 --tensorboard_dir logs --save_dir DeepSeek-R1-bf16-lora 有关每个参数的更多详细信息,可以运行 python lora_finetune.py --help 查看。该脚本可通过 tensorboard 记录学习率、loss、grad norm 信息,方便对训练进行监控。 使用 LoRA 优化硬件资源消耗 通过使用 LoRA 等优化,示例命令已将 SFT DeepSeek V3/R1 671B 最低硬件要求降低近 10 倍,可使用 32 个 Ascend 910B NPU 64GB(使用 ep=8,pp=4)或 24 个 H100/H800 GPU(使用 ep=8,pp=3)。如果你通过 --zero_cpu_offload 启用 CPU offload,硬件要求可以进一步降低,但会损失一定的训练速度。 如下图验证,在 SFT DeepSeek V3/R1 671B 时,Loss 可以顺利降低。 对于资金充裕的开发团队,也可以使用上述脚本,将并行度高效扩展至数百及数千卡,快速完成 DeepSeek V3/R1 671B 全参微调或并行加速。 对于预算有限,又想借助强化学习构建自己的类 DeepSeek R1 模型,Colossal-AI 也提供了解决方案,并利用小模型对算法进行了验证。 通过强化学习微调蒸馏版 DeepSeek Colossal-AI 团队验证并实现了 DeepSeek 论文中的 GRPO 算法及 verifiable reward,使用 Qwen2.5-3B-Base 模型进行了实验。其中,奖励的设计如下: 奖励 = 0,如果格式是错误的; 奖励 = 1, 如果格式是正确的但是结果是错误的; 奖励 = 10,如果格式与结果都是正确的; Colossal-AI 团队以 Qwen2.5-3B-Base 模型为例,提供了用于验证 GRPO 的对话模板及设定 Qwen_Qwen2.5-3B.json,通过配置以下 bash 文件,即可一键启动:train_grpo.sh 同时,在 GRPO 章节,Colossal-AI 团队还提供了验证过程中的部分发现及各种参数的详细描述,可供参考。 代码中设计了可灵活配置奖励函数的模板,因此,用户可根据自己的具体情况设计自己的奖励函数体系。 由下图可以看到,即使是 3B 的模型,平均奖励与模型回复长度随着时间逐步增长。 随着训练的进行,我们可以看到一些有意思的例子。例如随着训练迭代,模型开始了自我纠正: Colossal-AI:最佳后训练工具箱 Colossal-AI 在深耕大模型预训练降本增效的基础上,致力于进一步成为开发者开箱即用的最佳后训练工具,帮助用户基于开源模型,低成本快速构建私有模型。 开源地址:github.com/hpcaitech/ColossalAI

优秀的个人博客,低调大师

【8.23更新--技术干货全家桶】大数据计算技术共享计划 — MaxCompute技术公开课第二季

2018年5月-6月 MaxCompute 开启大数据计算技术共享计划技术公开课第一季,有超过1500名用户以及大数据爱好者参与到直播学习中来。7月,我们又开启第二季直播,5次大数据技术直播,有近6000名用户、大数据专家、技术牛人、大数据爱好者参与其中。 在前两季的直播中,我们看到了开发者们对大数据的热情和需求,第三季正在火热策划和准备中,请大家摆好姿势,保持期待! 究竟第二季技术共享中有哪些产品和技术干货?错过了第二季直播分享,哪里可以看回放?下资料?来这里,全都有。(往下看吧) 第二季技术干货 1、MaxCompute客户端 - odpscmd操作使用—7月24日 曲宁 阿里巴巴计算平台 产品专家摘要:介绍如何借助客户端命令行工具使用 MaxCompute 服务的基础功能。 分享内容文字稿 直播视频回看 分享资料下载 2、MaxComput

资源下载

更多资源
Oracle

Oracle

Oracle Database,又名Oracle RDBMS,或简称Oracle。是甲骨文公司的一款关系数据库管理系统。它是在数据库领域一直处于领先地位的产品。可以说Oracle数据库系统是目前世界上流行的关系数据库管理系统,系统可移植性好、使用方便、功能强,适用于各类大、中、小、微机环境。它是一种高效率、可靠性好的、适应高吞吐量的数据库方案。

Apache Tomcat

Apache Tomcat

Tomcat是Apache 软件基金会(Apache Software Foundation)的Jakarta 项目中的一个核心项目,由Apache、Sun 和其他一些公司及个人共同开发而成。因为Tomcat 技术先进、性能稳定,而且免费,因而深受Java 爱好者的喜爱并得到了部分软件开发商的认可,成为目前比较流行的Web 应用服务器。

Eclipse

Eclipse

Eclipse 是一个开放源代码的、基于Java的可扩展开发平台。就其本身而言,它只是一个框架和一组服务,用于通过插件组件构建开发环境。幸运的是,Eclipse 附带了一个标准的插件集,包括Java开发工具(Java Development Kit,JDK)。

JDK

JDK

JDK是 Java 语言的软件开发工具包,主要用于移动设备、嵌入式设备上的java应用程序。JDK是整个java开发的核心,它包含了JAVA的运行环境(JVM+Java系统类库)和JAVA工具。