首页> 搜索结果页
"sprintf如何搭建" 检索
共 10 条结果
如何用Link Develop开发一个智能厕所
概述 由于阿里云悬殊的男女比例,以及厕所占地面积的限制,各位男同胞每天都要在特殊味道的包围下等待有限的坑位。我们在想能不能利用物联网技术做一个低成本的解决方案,让男同胞们不需要亲自去现场的情况下也能得知坑位的空余情况(像停车场一样),基于我们IoT事业部的一站式开发平台——Link Develop,做一个IoT的智能厕所。 技术架构 前期准备 硬件 NodeMCU x9,PIR传感器 x9,1800mAh LiPo电池 x9,防水盒子 x9,usb mini 连接线 x1 软件 Arduino IDE 平台 阿里云账号,Link Develop平台权限开通 具体实施 建立设备模型 设备模型可以理解为一个类,或者一个模板。它规定了某种设备下的共有属性,并且允许你批量实例化设备。 进入Link Develop平台,跟其他云产品一样,需要先完成基本的入驻信息填写。 接下来我们会进入到一个项目空页面,我们需要在页面顶部点击“新建设备模型”。打开设备模型弹窗,输入名称以及定义类别,这里我们选择“自定义类型”即可。然后进入到设备模型的详情页之后,添加标准功能——输入红外——选择红外检测状态。我们就完成了一个可以上报红外检测数据的设备模型了。这个模型已经满足我们的需求。 然后我们需要实例化我们的厕所监控设备,在设备模型详情页里点击“测试设备”tab。然后依次创建9个不同设备名称(deviceName)的测试设备。每个设备建立完成后都有一个ProductKey,DeviceName和DeviceSecret,我们简称为“三元组”,接下来我们还会用到这个。 做完这些步骤之后,设备模型的建立与实例化就OK了。接下来我们进入Arduino代码编写。 硬件开发 PIR只有三个口,分别是VCC,GND和输出口。接线方法如下(NodeMCU的D7相当于arduino里的Pin 13) 然后接上usb线。打开arduino IDE。复制以下代码: #include <ESP8266WiFi.h> #include <PubSubClient.h> #include <ArduinoJson.h> #define SENSOR_PIN 13 /* 连接您的WIFI SSID和密码,这个9个设备可以一致 */ #define WIFI_SSID "yourssid" #define WIFI_PASSWD "yourwifipassword" /* 产品的三元组信息,根据9个测试设备的三元组,每个设备都烧录不同的*/ #define PRODUCT_KEY "yourproductKey" #define DEVICE_NAME "yourdeviceName" #define DEVICE_SECRET "yourdeviceSecret" /* LD线上环境域名和端口号,不需要改 */ #define MQTT_SERVER PRODUCT_KEY".iot-as-mqtt.cn-shanghai.aliyuncs.com" #define MQTT_PORT 1883 #define MQTT_USRNAME DEVICE_NAME "&" PRODUCT_KEY // TODO: MQTT连接的签名信息,哈希加密请以"clientIdtestdeviceName"+设备名称+"productKey"+设备模型标识+“timestamp123456789”前往http://tool.oschina.net/encrypt?type=2进行加密 // HMACSHA1_SRC clientIdtestdeviceNamehuman04productKeya1rezUVs103timestamp123456789 #define CLIENT_ID "test|securemode=3,timestamp=123456789,signmethod=hmacsha1|" #define MQTT_PASSWD "e0748281f8db36e12cac478801318f95ae821ba7" #define ALINK_BODY_FORMAT "{\"id\":\"123\",\"version\":\"1.0\",\"method\":\"%s\",\"params\":%s}" #define ALINK_TOPIC_PROP_POST "/sys/" PRODUCT_KEY "/" DEVICE_NAME "/thing/event/property/post" #define ALINK_TOPIC_PROP_POSTRSP "/sys/" PRODUCT_KEY "/" DEVICE_NAME "/thing/event/property/post_reply" #define ALINK_TOPIC_PROP_SET "/sys/" PRODUCT_KEY "/" DEVICE_NAME "/thing/service/property/set" #define ALINK_METHOD_PROP_POST "thing.event.property.post" unsigned long lastMs = 0; WiFiClient espClient; PubSubClient client(espClient); void callback(char *topic, byte *payload, unsigned int length) { Serial.print("Message arrived ["); Serial.print(topic); Serial.print("] "); payload[length] = '\0'; Serial.println((char *)payload); if (strstr(topic, ALINK_TOPIC_PROP_SET)) { StaticJsonBuffer<100> jsonBuffer; JsonObject& root = jsonBuffer.parseObject(payload); if (!root.success()) { Serial.println("parseObject() failed"); return ; } } } void wifiInit() { WiFi.mode(WIFI_STA); WiFi.begin(WIFI_SSID, WIFI_PASSWD); while (WiFi.status() != WL_CONNECTED) { delay(1000); Serial.println("WiFi not Connect"); } Serial.println("Connected to AP"); Serial.println("IP address: "); Serial.println(WiFi.localIP()); client.setServer(MQTT_SERVER, MQTT_PORT); /* 连接WiFi之后,连接MQTT服务器 */ client.setCallback(callback); } void mqttCheckConnect() { while (!client.connected()) { Serial.println("Connecting to MQTT Server ..."); if (client.connect(CLIENT_ID, MQTT_USRNAME, MQTT_PASSWD)) { Serial.println("MQTT Connected!"); // client.subscribe(ALINK_TOPIC_PROP_POSTRSP); client.subscribe(ALINK_TOPIC_PROP_SET); Serial.println("subscribe done"); } else { Serial.print("MQTT Connect err:"); Serial.println(client.state()); delay(5000); } } } void mqttIntervalPost() { char param[32]; char jsonBuf[128]; sprintf(param, "{\"MotionAlarmState\":%d}", digitalRead(13)); sprintf(jsonBuf, ALINK_BODY_FORMAT, ALINK_METHOD_PROP_POST, param); Serial.println(jsonBuf); client.publish(ALINK_TOPIC_PROP_POST, jsonBuf); } void setup() { pinMode(SENSOR_PIN, INPUT); /* initialize serial for debugging */ Serial.begin(115200); Serial.println("Demo Start"); wifiInit(); } // the loop function runs over and over again forever void loop() { if (millis() - lastMs >= 5000) { lastMs = millis(); mqttCheckConnect(); /* 上报 */ mqttIntervalPost(); } client.loop(); if (digitalRead(SENSOR_PIN) == HIGH){ Serial.println("Motion detected!"); delay(2000); } else { Serial.println("Motion absent!"); delay(2000); } } 这里有几段代码需要解释一下: 引用库 #include <ESP8266WiFi.h> #include <PubSubClient.h> #include <ArduinoJson.h> 如果之前没有使用过arduino,这些库不是arduino自带的,需要去import一下。以ArduinoJson为例,具体操作方法见下图: 修改WiFi #define WIFI_SSID "yourssid" #define WIFI_PASSWD "yourwifipassword" 这里要输入你家的wifi SSID(即wifi名称)和密码。如下图红框内wifi,SSID即“TOM”(注意大小写),密码是“123456” 修改三元组 /* 产品的三元组信息,根据9个测试设备的三元组,每个设备都烧录不同的*/ #define PRODUCT_KEY "yourproductKey" #define DEVICE_NAME "yourdeviceName" #define DEVICE_SECRET "yourdeviceSecret" 这里就是之前说的三元组,把不同设备的对应三元组填入即可。 修改HashMAC密钥 // TODO: MQTT连接的签名信息,哈希加密请以"clientIdtestdeviceName"+设备名称+"productKey"+设备模型标识+“timestamp123456789”前往http://tool.oschina.net/encrypt?type=2进行加密 // HMACSHA1_SRC clientIdtestdeviceNamehuman04productKeya1rezUVs103timestamp123456789 #define CLIENT_ID "test|securemode=3,timestamp=123456789,signmethod=hmacsha1|" #define MQTT_PASSWD "e0748281f8db36e12cac478801318f95ae821ba7" Link Develop平台为了保证安全性,设备上传数据的时候需要验证身份。因此需要为每个设备配置不同的Hmac值。方法是先组合一段字段:"clientIdtestdeviceName"+设备名称+"productKey"+设备模型标识+“timestamp123456789”例如刚才截图的三元组,字段是clientIdtestdeviceNamehuman01productKeya1rezUVs103timestamp123456789写完这个字段之后,前往这里进行加密。选择HmacSHA1方法,以上面的字段为明文,deviceSecret为密钥。见下图: 可以得到如下的密文:e30657f36e7ded9a2781bba97b0dee79e8fa31b6然后替换掉MQTT_PASSWD原来的字段即可。 完成代码编译之后,选择好对应的开发板和烧录线即可进行上传。通电之后可以查看串口以及Link Develop上的测试设备列表看看有否成功上线。 应用开发 点击左侧栏的Web应用开发,选择“可视化应用”并输入名称。然后就进入可视化开发Web应用的工作台了。 然后我们需要文字组件用来表示标题等。需要指示灯用来表示当前坑位有没有人的状态。 接下来以坑位1为例举例如何将组件绑定设备。点击右侧面板的“数据”,选择关联设备。这里会显示出所有已经创建的测试设备。选择对应坑位的传感器,之后选择展示的数据(布尔型的红外监测状态)。这样就完成了绑定数据了。 可以通过模拟上线看看是否成功关联。另外有一些个性化的样式也可以自己配置。 接下来就是发挥创造力的时候了。可以上传各种背景图,加各种小样式,做成如图的看板。最后点击右上角预览即可。 这样就完成了一个看板了!不用忍气吞声,直接查看厕所有没有空余坑位!钉钉推送的教程后续会跟进同步~ 部署 这是实体装机的盒子。为了更好的用户体验,我们在盒子上加了一个字条“只检测有人,没有摄像头”,以免用户产生隐私方面的担忧。 部署的时候用了树莓派+显示屏。 造福90%飞天园区一号楼四楼员工的智能厕所解决方案,完成! 欢迎使用Link Develop Link Develop(物联网开发平台)是阿里云针对物联网领域提供的端到端一站式开发服务平台,可覆盖各个物联网行业应用场景,主要解决物联网开发领域开发链路长、技术栈复杂、协同成本高、方案移植困难的问题,提供了从硬件开发、服务编排、Web应用开发到移动APP开发全链路的开发流程、框架/引擎和调试工具,并可将成熟的开发产出物对接阿里云云市场进行售卖,为开发者实现商业收益。这篇文章的两个作者,一位是学法律出身的运营同学,一位是学设计出身的UED同学,两位都不是专业的工程师,却能在短短一天之内基于Link Develop完成整个智能厕所的解决方案搭建。相信大家认识到了Link Develop平台的开发能力之后,会有更多更新奇的idea出现,并且借助Link Develop快速实现物联网应用开发!欢迎访问:https://linkdevelop.aliyun.com
文章
物联网 · 数据安全/隐私保护
2018-09-27
十分钟上线-函数计算玩转wordpress
前言 这篇文章适合所有的php开发新手、老鸟以及想准备学习开发php的程序猿。本文以部署一个wordpress在函数计算环境中为例,向您讲解如何使用阿里云函数计算快速构建或移植基于php框架开发的web,通过本文,您将会了解以下内容: 案例概览 传统服务器架构 VS Serverless架构 Serverless架构详解 函数计算运行php框架原理 案例开发配置步骤 FC Web 设置自定义域名 案例概览 在本教程中,我们讲解如何利用函数计算一步一步来构建web的server端,该案例是把一个wordpress部署到函数计算,本文旨在展示函数计算做web backend 能力,具体表现为以下几点: 完善的php系统迁移到FC的成本不高 FC打通了专有网络vpc功能,用户的函数可以配置访问专有网络的云资源,比如本案例中mysql, nas fun工具自动化部署函数计算及相关资源的能力 案例体验入口: 体验地址: http://1986114430573743.cn-hangzhou.fc.aliyuncs.com/2016-08-15/proxy/share/wp-func/ 账号:wp-test 密码:wp-pwd 传统服务器架构 VS Serverless架构 正常来说,用户开发server端服务,常常面临开发效率,运维成本高,机器资源弹性伸缩等痛点,而使用Serverless架构可以很好的解决上述问题。下面是传统架构和Serverless架构的对比: 阿里云函数计算是一个事件驱动的全托管计算服务。通过函数计算,您无需管理服务器等基础设施,只需编写代码并上传。函数计算会为您准备好计算资源,以弹性、可靠的方式运行您的代码,并提供日志查询,性能监控,报警等功能。借助于函数计算,您可以快速构建任何类型的应用和服务,无需管理和运维。 Serverless架构详解 从上面的示例图中,整体架构十分简单明了, 用FC替代了web服务器,但是换来的是免运维,弹性扩容,按需付费等一系列优点 函数计算运行php框架原理 传统服务器php运行原理 原理示意图 A simple nginx conf 从上面原理示意图我们可以看出,web服务器根据conf 中 location将php脚本交给php-fpm去解析,然后将解析后的结果返回给client端 FC驱动php工程原理 函数计算的执行环境相当于传统web服务的Apache/Nginx 用户函数相当于实现Apache/Nginx的conf中location 用户将web网站部署在nas,然后挂载nas到函数的执行环境, 比如下面代码中/mnt/www目录 对于wordpress入口函数代码就是这么简单, 建议您先了解下php runtime php 入口函数 php 执行环境 <?php use RingCentral\Psr7\Response; function startsWith($haystack, $needle) { $length = strlen($needle); return (substr($haystack, 0, $length) === $needle); } function handler($request, $context): Response{ $uri = $request->getAttribute("requestURI"); $uriArr = explode("?", $uri); // default php / or /wp-admin/ if (preg_match('#/$#', $uriArr[0]) && !(strpos($uri, '.php'))) { $uriArr[0] .= "index.php"; $uri = implode($uriArr); if (startsWith($uri, "/2016-08-15/proxy/share/wp-func/wp-admin/")) { // wordpress admin entrypoint $request = $request->withAttribute("requestURI", $uri); } } $proxy = $GLOBALS['fcPhpCgiProxy']; $root_dir = '/mnt/www'; //php script if (preg_match('#\.php.*#', $uri)) { $format = '%s.%s.fc.aliyuncs.com'; $host = sprintf($format, $context['accountId'], $context['region']); // maybe user define domain $resp = $proxy->requestPhpCgi($request, $root_dir, "index.php", ['SERVER_NAME' => $host, 'SERVER_PORT' => '80', 'HTTP_HOST' => $host], ['debug_show_cgi_params' => false, 'readWriteTimeout' => 15000] ); return $resp; } else { // static files, js, css, jpg ... $filename = $root_dir . explode("?", $uri)[0]; $handle = fopen($filename, "r"); $contents = fread($handle, filesize($filename)); fclose($handle); $headers = [ 'Content-Type' => $proxy->getMimeType($filename), 'Cache-Control' => "max-age=8640000", 'Accept-Ranges' => 'bytes', ]; return new Response(200, $headers, $contents); } } 其中函数计算为用户提供了一个$GLOBALS['fcPhpCgiProxy']对象用来和php-fpm进行交互,对php工程中的php文件进行解析,该对象提供了两个重要的接口: requestPhpCgi requestPhpCgi($request, $docRoot, $phpFile = "index.php", $fastCgiParams = [], $options = []): Response - `$request`: 跟`php http invoke`入口的参数一致 - `$docRoot`: web 工程的根目录 - `$phpFile`: 用于拼接cgi参数中的SCRIPT_FILENAME的默认参数 - `$fastCgiParams`: 函数计算内部尽量根据$request给您构造default cgi params, 但是如果您不是想要的,可以使用$fastCgiParams覆盖一些参数 (reference: [cgi](https://en.wikipedia.org/wiki/Common_Gateway_Interface)) - `$options`: array类型, debug_show_cgi_params 设为true,会打印每次请求php解析时候的cgi参数;readWriteTimeout 设置解析的时间 案例开发配置步骤 准备工作 由于函数运行时的IP是不固定的,您需要设置RDS允许所有IP访问。但是这样会有风险,不建议这样做。在本教程中,我们将创建一个rds mysql 数据库,并将它置于一个专有网络VPC环境内,函数计算支持VPC功能,用户可以通过授权的方式安全地访问VPC中的资源(同时包含本示例中的NAS)。 1. 创建RDS mysql数据库, 配置vpc, 具体参考通过vpc访问rds实例 2. 创建NAS 挂接点,配置vpc(注意:这里跟rds采用相同的vpc), 具体参考函数计算nas使用示例 3. 可选操作,在准备函数的region创建日志,用于函数的调试, 具体参考函数计算配置日志服务 创建函数 1. 创建service(假设是share), 配置准备vpc config, nas config 和日志服务,比如案例体验的service配置如下图: 2. 下载wordpress, 然后将wordpress工程移到上述配置的nas中,www里面表示wordpress的工程 |-- index.py |-- www index.py代码: # -*- coding: utf-8 -*- import logging import os file = "/mnt/www/2016-08-15/proxy/share/wp-func" def mkdir(path): folder = os.path.exists(path) if not folder: os.makedirs(path) def lsDir(): os.system("ls -ll /mnt/www/2016-08-15/proxy/share/wp-func/") def handler(event, context): mkdir(file) os.system("cp -r /code/www/* /mnt/www/2016-08-15/proxy/share/wp-func/") print(lsDir()) return 'ok' 基于上述代码创一个函数move-wp-nas, 执行函数,将wordpress工程包移动到nas的/mnt/www/2016-08-15/proxy/share/wp-func目录。 Q1: 为什么创建/2016-08-15/proxy/share/wp-func这么奇怪的目录?A:因为http trigger, 函数访问的格式为下面的url: http://${account_id}.${region}.fc.aliyuncs.com/2016-08-15/proxy/$(seevice_name}/{function_name}/,为了保证从一个页面跳转到另外一个页面的时候,能自动带上/2016-08-15/proxy/$(seevice_name}/{function_name}/,我们需要建立这样目录和设置cgi相关参数达到php框架内部自动跳转正确的问题。 Q2: 可不可以不用/2016-08-15/proxy/share/wp-func这么奇怪的目录?A:可以,等函数计算自定义域名功能上线,可以解决这个问题,具体操作后续会在此文中更新。 3. 创建入口函数wp-func(对应上面步骤中的"/mnt/www/2016-08-15/proxy/share/wp-func"), 给函数设置http trigger,类型为anonymous, 类型都选上。 <?php use RingCentral\Psr7\Response; function startsWith($haystack, $needle) { $length = strlen($needle); return (substr($haystack, 0, $length) === $needle); } function handler($request, $context): Response{ $uri = $request->getAttribute("requestURI"); $uriArr = explode("?", $uri); // default php / or /wp-admin/ if (preg_match('#/$#', $uriArr[0]) && !(strpos($uri, '.php'))) { $uriArr[0] .= "index.php"; $uri = implode($uriArr); if (startsWith($uri, "/2016-08-15/proxy/share/wp-func/wp-admin/")) { // wordpress admin entrypoint $request = $request->withAttribute("requestURI", $uri); } } $proxy = $GLOBALS['fcPhpCgiProxy']; $root_dir = '/mnt/www'; //php script if (preg_match('#\.php.*#', $uri)) { $format = '%s.%s.fc.aliyuncs.com'; $host = sprintf($format, $context['accountId'], $context['region']); // maybe user define domain $resp = $proxy->requestPhpCgi($request, $root_dir, "index.php", ['SERVER_NAME' => $host, 'SERVER_PORT' => '80', 'HTTP_HOST' => $host], ['debug_show_cgi_params' => false, 'readWriteTimeout' => 15000] ); return $resp; } else { // static files, js, css, jpg ... $filename = $root_dir . explode("?", $uri)[0]; $handle = fopen($filename, "r"); $contents = fread($handle, filesize($filename)); fclose($handle); $headers = [ 'Content-Type' => $proxy->getMimeType($filename), 'Cache-Control' => "max-age=8640000", 'Accept-Ranges' => 'bytes', ]; return new Response(200, $headers, $contents); } } 4. 直接通过url访问首页,第一次首先安装wordpress, 安装过程中配置之前准备好的数据库、管理员等相关信息, 安装成功后,就可以成功访问首页,登录后台管理wordpress网站了。 http://${account_id}.${region}.fc.aliyuncs.com/2016-08-15/proxy/$(seevice_name}/{function_name}/ for example: http://1986114430573743.cn-hangzhou.fc.aliyuncs.com/2016-08-15/proxy/share/wp-func/ FC Web 设置自定义域名 未完待更新... 总结 函数计算有如下优势: 无需采购和管理服务器等基础设施 专注业务逻辑的开发 提供日志查询、性能监控、报警等功能快速排查故障 以事件驱动的方式触发应用响应用户请求 毫秒级别弹性伸缩,快速实现底层扩容以应对峰值压力 按需付费。只需为实际使用的计算资源付费,适合有明显波峰波谷的用户访问场景 除了上面所列的优势,FC 可以做为web backend,只需要编写一个函数实现传统web服务器中的conf中的逻辑,就可以将一个完整的web工程迁移到FC,从而从传统的web网站运维,监控等繁琐的事务中解放出来。 最后欢迎大家通过扫码加入我们用户群中,搭建过程中有问题或者有其他问题可以在群里提出来。 函数计算官网客户群(11721331)。
文章
Web App开发 · 应用服务中间件 · Serverless · PHP · 文件存储
2018-09-19
十分钟上线-函数计算玩转 WordPress
最新使用 Fun 工具简洁版操作: https://yq.aliyun.com/articles/721594 @Deprecated 前言 这篇文章适合所有的PHP开发新手、老鸟以及想准备学习开发 PHP 的程序猿。众所周知,PHP 是 Web 编程最流行的编程语言,如果有人告诉你,有 Serverless 的 PHP WEB 开发新模式,你是不是会感到好奇和兴奋?在介绍 Serverless Web 开发新模式之前,我们先了解下将 PHP Web Serverless 化的好处: 无需采购和管理服务器等基础设施 弹性伸缩,动态扩容 免运维, 极大降低人力成本 按需付费,财务成本低 本文以部署 WordPress 工程在函数计算环境中为例,向您讲解如何使用阿里云函数计算快速构建或移植基于 PHP 框架开发的 Web ,通过本文,您将会了解以下内容: 案例概览 传统服务器架构 VS Serverless架构 Serverless架构详解 函数计算运行PHP框架原理 案例开发配置步骤 FC Web 设置自定义域名 案例概览 在本教程中,我们讲解如何利用函数计算一步一步来构建 Web 的 Server 端,该案例是把一个 WordPress 部署到函数计算,本文旨在展示函数计算做 Web Backend 能力,具体表现为以下几点: 完善的 PHP 系统迁移到 FC 的成本不高 FC 打通了专有网络 VPC 功能,用户的函数可以配置访问专有网络的云资源,比如本案例中 MYSQL, NAS 案例体验入口: 体验地址: http://1986114430573743.cn-hangzhou.fc.aliyuncs.com/2016-08-15/proxy/share/wp-func/ 账号:wp-test 密码:wp-pwd 自定义域名 WordPress : http://fcdemo.mofangdegisn.cn/ 账号:wp-test 密码:wp-pwd 传统服务器架构 VS Serverless架构 正常来说,用户开发 Server 端服务,常常面临开发效率,运维成本高,机器资源弹性伸缩等痛点,而使用 Serverless 架构可以很好的解决上述问题。下面是传统架构和 Serverless 架构的对比: 阿里云函数计算是一个事件驱动的全托管计算服务。通过函数计算,您无需管理服务器等基础设施,只需编写代码并上传。函数计算会为您准备好计算资源,以弹性、可靠的方式运行您的代码,并提供日志查询,性能监控,报警等功能。借助于函数计算,您可以快速构建任何类型的应用和服务,无需管理和运维。 Serverless 架构详解 从上面的示例图中,整体架构十分简单明了, 用 FC 替代了 Web 服务器,但是换来的是免运维,弹性扩容,按需付费等一系列优点 函数计算运行 PHP 框架原理 传统服务器 PHP 运行原理 原理示意图 A simple nginx conf 从上面原理示意图我们可以看出,Web 服务器根据conf 中 location将 PHP 脚本交给 php-fpm 去解析,然后将解析后的结果返回给 client 端 FC 驱动 PHP 工程原理 函数计算的执行环境相当于传统 web 服务的 Apache/Nginx 用户函数相当于实现 Apache/Nginx 的 conf 中 location 用户将 Web 网站部署在 NAS,然后挂载 NAS 到函数的执行环境, 比如下面代码中 /mnt/www 目录 对于 WordPress 入口函数代码就是这么简单, 建议您先了解下 PHP Runtime PHP 入口函数 PHP 执行环境 <?php use RingCentral\Psr7\Response; function startsWith($haystack, $needle) { $length = strlen($needle); return (substr($haystack, 0, $length) === $needle); } function handler($request, $context): Response{ $uri = $request->getAttribute("requestURI"); $uriArr = explode("?", $uri); // default php / or /wp-admin/ if (preg_match('#/$#', $uriArr[0]) && !(strpos($uri, '.php'))) { $uriArr[0] .= "index.php"; $uri = implode($uriArr); if (startsWith($uri, "/2016-08-15/proxy/share/wp-func/wp-admin/")) { // wordpress admin entrypoint $request = $request->withAttribute("requestURI", $uri); } } $proxy = $GLOBALS['fcPhpCgiProxy']; $root_dir = '/mnt/www'; //php script if (preg_match('#\.php.*#', $uri)) { $format = '%s.%s.fc.aliyuncs.com'; $host = sprintf($format, $context['accountId'], $context['region']); // maybe user define domain $resp = $proxy->requestPhpCgi($request, $root_dir, "index.php", ['SERVER_NAME' => $host, 'SERVER_PORT' => '80', 'HTTP_HOST' => $host], ['debug_show_cgi_params' => false, 'readWriteTimeout' => 15000] ); return $resp; } else { // static files, js, css, jpg ... $filename = $root_dir . explode("?", $uri)[0]; $filename = rawurldecode($filename); $handle = fopen($filename, "r"); $contents = fread($handle, filesize($filename)); fclose($handle); $headers = [ 'Content-Type' => $proxy->getMimeType($filename), 'Cache-Control' => "max-age=8640000", 'Accept-Ranges' => 'bytes', ]; return new Response(200, $headers, $contents); } } 其中函数计算为用户提供了一个 $GLOBALS['fcPhpCgiProxy'] 对象用来和 php-fpm 进行交互,对 PHP 工程中的 php 文件进行解析,该对象提供了两个重要的接口: requestPhpCgi requestPhpCgi($request, $docRoot, $phpFile = "index.php", $fastCgiParams = [], $options = []) $request: 跟 php http invoke 入口的参数一致 $docRoot: Web 工程的根目录 $phpFile: 用于拼接 cgi 参数中的 SCRIPT_FILENAME 的默认参数 $fastCgiParams: 函数计算内部尽量根据$request给您构造 default cgi params, 但是如果您不是想要的,可以使用$fastCgiParams覆盖一些参数 (reference: cgi) $options: array类型,可选参数, debug_show_cgi_params 设为 true ,会打印每次请求 php 解析时候的 cgi 参数, 默认为 false ;readWriteTimeout 设置解析的时间, 默认为 5 秒 案例开发配置步骤 本文编写的时候由于工具链不够完善, 后续章节的操作都是基于控制台,如果您喜欢命令行,具体操作可以参考: https://yq.aliyun.com/articles/721594, 使用 Fun 工具可以大大提高配置部署效率。 准备工作 由于函数运行时的 IP 是不固定的,您需要设置 RDS 允许所有 IP 访问。但是这样会有风险,不建议这样做。在本教程中,我们将创建一个 RDS MYSQL 数据库,并将它置于一个专有网络 VPC 环境内,函数计算支持 VPC 功能,用户可以通过授权的方式安全地访问 VPC 中的资源(同时包含本示例中的 NAS )。 1. 创建 RDS MYSQL 数据库, 配置 VPC , 具体参考通过 VPC 访问 RDS 实例 2. 创建 NAS 挂接点,配置 VPC (注意:这里跟 RDS 采用相同的 VPC), 具体参考函数计算nas使用示例 3. 可选操作,在准备函数的 region 创建日志,用于函数的调试, 具体参考函数计算配置日志服务 创建函数 1. 创建 Service (假设是 share ), 配置准备 vpc config , nas config 和日志服务,比如案例体验的Service配置如下图: 2. 下载 WordPress, 然后将 WordPress 工程移到上述配置的 NAS 中, www 表示 WordPress 的工程的根目录 |-- index.py |-- www index.py代码: # -*- coding: utf-8 -*- import logging import os file = "/mnt/www/2016-08-15/proxy/share/wp-func" def mkdir(path): folder = os.path.exists(path) if not folder: os.makedirs(path) def lsDir(): os.system("ls -ll /mnt/www/2016-08-15/proxy/share/wp-func/") def handler(event, context): mkdir(file) os.system("cp -r /code/www/* /mnt/www/2016-08-15/proxy/share/wp-func/") print(lsDir()) return 'ok' 基于上述代码创一个函数 move-wp-nas , 执行函数,将 WordPress 工程包移动到 NAS 的/mnt/www/2016-08-15/proxy/share/wp-func 目录。 Q1: 为什么创建 /2016-08-15/proxy/share/wp-func 这么奇怪的目录?A:因为http trigger, 函数访问的格式为下面的url: http://${account_id}.${region}.fc.aliyuncs.com/2016-08-15/proxy/$(seevice_name}/{function_name}/,为了保证从一个页面跳转到另外一个页面的时候,能自动带上/2016-08-15/proxy/$(seevice_name}/{function_name}/,我们需要建立这样目录和设置 cgi 相关参数达到 PHP 框架内部自动跳转正确的目的。 Q2: 可不可以不用/2016-08-15/proxy/share/wp-func这么奇怪的目录?A:可以,FC Web 设置自定义域名 3. 创建入口函数 wp-func (对应上面步骤中的 /mnt/www/2016-08-15/proxy/share/wp-func ), 给函数设置 http trigger ,类型为 anonymous , 类型都选上。 <?php use RingCentral\Psr7\Response; function startsWith($haystack, $needle) { $length = strlen($needle); return (substr($haystack, 0, $length) === $needle); } function handler($request, $context): Response{ $uri = $request->getAttribute("requestURI"); $uriArr = explode("?", $uri); // default php / or /wp-admin/ if (preg_match('#/$#', $uriArr[0]) && !(strpos($uri, '.php'))) { $uriArr[0] .= "index.php"; $uri = implode($uriArr); if (startsWith($uri, "/2016-08-15/proxy/share/wp-func/wp-admin/")) { // wordpress admin entrypoint $request = $request->withAttribute("requestURI", $uri); } } $proxy = $GLOBALS['fcPhpCgiProxy']; $root_dir = '/mnt/www'; //php script if (preg_match('#\.php.*#', $uri)) { $format = '%s.%s.fc.aliyuncs.com'; $host = sprintf($format, $context['accountId'], $context['region']); // maybe user define domain $resp = $proxy->requestPhpCgi($request, $root_dir, "index.php", ['SERVER_NAME' => $host, 'SERVER_PORT' => '80', 'HTTP_HOST' => $host], ['debug_show_cgi_params' => false, 'readWriteTimeout' => 15000] ); return $resp; } else { // static files, js, css, jpg ... $filename = $root_dir . explode("?", $uri)[0]; $filename = rawurldecode($filename); $handle = fopen($filename, "r"); $contents = fread($handle, filesize($filename)); fclose($handle); $headers = [ 'Content-Type' => $proxy->getMimeType($filename), 'Cache-Control' => "max-age=8640000", 'Accept-Ranges' => 'bytes', ]; return new Response(200, $headers, $contents); } } 4. 直接通过 url 访问首页,第一次访问会提示您安装 WordPress, 安装过程中配置之前准备好的数据库、管理员等相关信息, 安装成功后,就可以成功访问首页,登录后台管理 WordPress 网站了。 http://${account_id}.${region}.fc.aliyuncs.com/2016-08-15/proxy/$(seevice_name}/{function_name}/ for example: http://1986114430573743.cn-hangzhou.fc.aliyuncs.com/2016-08-15/proxy/share/wp-func/ FC Web 设置自定义域名 下载 WordPress, 然后将 WordPress 工程移到上述配置的 NAS 中, www 表示 WordPress 的工程的根目录 |-- index.py |-- www index.py代码: # -*- coding: utf-8 -*- import logging import os file = "/mnt/www/" def mkdir(path): folder = os.path.exists(path) if not folder: os.makedirs(path) def lsDir(): os.system("ls -ll /mnt/www/") def handler(event, context): mkdir(file) os.system("cp -r /code/www/* /mnt/www/") print(lsDir()) return 'ok' 基于上述代码创一个函数 move-wp-nas , 执行函数,将 WordPress 工程包移动到 NAS 的/mnt/www/ 目录。 同时入口函数做下小改动,改成如下代码: <?php use RingCentral\Psr7\Response; function endsWith($haystack, $needle) { $length = strlen($needle); return $length === 0 || (substr($haystack, -$length) === $needle); } function handler($request, $context): Response{ $uri = $request->getAttribute("requestURI"); $uriArr = explode("?", $uri); // default php / or /wp-admin/ if (preg_match('#/$#', $uriArr[0]) && !(strpos($uri, '.php'))) { $uriArr[0] .= "index.php"; $uri = implode($uriArr); } $proxy = $GLOBALS['fcPhpCgiProxy']; $root_dir = '/mnt/www'; //php script if (preg_match('#\.php.*#', $uri)) { $host = "fcdemo.mofangdegisn.cn"; // your define domain $resp = $proxy->requestPhpCgi($request, $root_dir, "index.php", ['HTTP_HOST' => $host, 'SERVER_NAME' => $host, 'SERVER_PORT' => '80'], ['debug_show_cgi_params' => false, 'readWriteTimeout' => 60000] ); return $resp; } else { // static files, js, css, jpg ... $filename = $root_dir . explode("?", $uri)[0]; $filename = rawurldecode($filename); $handle = fopen($filename, "r"); $contents = fread($handle, filesize($filename)); fclose($handle); $headers = [ 'Content-Type' => $proxy->getMimeType($filename), 'Cache-Control' => "max-age=8640000", 'Accept-Ranges' => 'bytes', ]; return new Response(200, $headers, $contents); } } 给函数入口配置自定义域名(操作过程请参考:绑定自定义域名示例), 具体配置假设如下: 注意: 绑定自定义域名之后,不用使用控制台来进行调试,就只能使用浏览器来触发函数,日志服务来进行调试。 总结 函数计算有如下优势: 无需采购和管理服务器等基础设施 专注业务逻辑的开发 提供日志查询、性能监控、报警等功能快速排查故障 以事件驱动的方式触发应用响应用户请求 毫秒级别弹性伸缩,快速实现底层扩容以应对峰值压力 按需付费。只需为实际使用的计算资源付费,适合有明显波峰波谷的用户访问场景 除了上面所列的优势,FC 可以做为 Web Backend,只需要编写一个函数实现传统 Web 服务器中的 conf 中的逻辑,就可以将一个完整的 Web 工程迁移到 FC ,从而从传统的 Web 网站运维,监控等繁琐的事务中解放出来。 最后欢迎大家通过扫码加入我们用户群中,搭建过程中有问题或者有其他问题可以在群里提出来。 函数计算官网客户群(11721331)。
文章
运维 · Serverless · PHP · 文件存储 · 关系型数据库 · 监控 · RDS · 前端开发 · 弹性计算 · 应用服务中间件
2018-09-17
Linux下的实时流媒体编程
一、流媒体简介 随着Internet的日益普及,在网络上传输的数据已经不再局限于文字和图形,而是逐渐向声音和视频等多媒体格式过渡。目前在网络上传输音频/视频(Audio/Video,简称A/V)等多媒体文件时,基本上只有下载和流式传输两种选择。通常说来,A/V文件占据的存储空间都比较大,在带宽受限的网络环境中下载可能要耗费数分钟甚至数小时,所以这种处理方法的延迟很大。如果换用流式传输的话,声音、影像、动画等多媒体文件将由专门的流媒体服务器负责向用户连续、实时地发送,这样用户可以不必等到整个文件全部下载完毕,而只需要经过几秒钟的启动延时就可以了,当这些多媒体数据在客户机上播放时,文件的剩余部分将继续从流媒体服务器下载。 流(Streaming)是近年在Internet上出现的新概念,其定义非常广泛,主要是指通过网络传输多媒体数据的技术总称。流媒体包含广义和狭义两种内涵:广义上的流媒体指的是使音频和视频形成稳定和连续的传输流和回放流的一系列技术、方法和协议的总称,即流媒体技术;狭义上的流媒体是相对于传统的下载-回放方式而言的,指的是一种从Internet上获取音频和视频等多媒体数据的新方法,它能够支持多媒体数据流的实时传输和实时播放。通过运用流媒体技术,服务器能够向客户机发送稳定和连续的多媒体数据流,客户机在接收数据的同时以一个稳定的速率回放,而不用等数据全部下载完之后再进行回放。 由于受网络带宽、计算机处理能力和协议规范等方面的限制,要想从Internet上下载大量的音频和视频数据,无论从下载时间和存储空间上来讲都是不太现实的,而流媒体技术的出现则很好地解决了这一难题。目前实现流媒体传输主要有两种方法:顺序流(progressive streaming)传输和实时流(realtime streaming)传输,它们分别适合于不同的应用场合。 顺序流传输 顺序流传输采用顺序下载的方式进行传输,在下载的同时用户可以在线回放多媒体数据,但给定时刻只能观看已经下载的部分,不能跳到尚未下载的部分,也不能在传输期间根据网络状况对下载速度进行调整。由于标准的HTTP服务器就可以发送这种形式的流媒体,而不需要其他特殊协议的支持,因此也常常被称作HTTP流式传输。顺序流式传输比较适合于高质量的多媒体片段,如片头、片尾或者广告等。 实时流传输 实时流式传输保证媒体信号带宽能够与当前网络状况相匹配,从而使得流媒体数据总是被实时地传送,因此特别适合于现场事件。实时流传输支持随机访问,即用户可以通过快进或者后退操作来观看前面或者后面的内容。从理论上讲,实时流媒体一经播放就不会停顿,但事实上仍有可能发生周期性的暂停现象,尤其是在网络状况恶化时更是如此。与顺序流传输不同的是,实时流传输需要用到特定的流媒体服务器,而且还需要特定网络协议的支持。 回页首 二、流媒体协议 实时传输协议(Real-time Transport Protocol,PRT)是在Internet上处理多媒体数据流的一种网络协议,利用它能够在一对一(unicast,单播)或者一对多(multicast,多播)的网络环境中实现传流媒体数据的实时传输。RTP通常使用UDP来进行多媒体数据的传输,但如果需要的话可以使用TCP或者ATM等其它协议,整个RTP协议由两个密切相关的部分组成:RTP数据协议和RTP控制协议。实时流协议(Real Time Streaming Protocol,RTSP)最早由Real Networks和Netscape公司共同提出,它位于RTP和RTCP之上,其目的是希望通过IP网络有效地传输多媒体数据。 2.1 RTP数据协议 RTP数据协议负责对流媒体数据进行封包并实现媒体流的实时传输,每一个RTP数据报都由头部(Header)和负载(Payload)两个部分组成,其中头部前12个字节的含义是固定的,而负载则可以是音频或者视频数据。RTP数据报的头部格式如图1所示: 图1 RTP头部格式 其中比较重要的几个域及其意义如下:  CSRC记数(CC)  表示CSRC标识的数目。CSRC标识紧跟在RTP固定头部之后,用来表示RTP数据报的来源,RTP协议允许在同一个会话中存在多个数据源,它们可以通过RTP混合器合并为一个数据源。例如,可以产生一个CSRC列表来表示一个电话会议,该会议通过一个RTP混合器将所有讲话者的语音数据组合为一个RTP数据源。 负载类型(PT)  标明RTP负载的格式,包括所采用的编码算法、采样频率、承载通道等。例如,类型2表明该RTP数据包中承载的是用ITU G.721算法编码的语音数据,采样频率为8000Hz,并且采用单声道。 序列号  用来为接收方提供探测数据丢失的方法,但如何处理丢失的数据则是应用程序自己的事情,RTP协议本身并不负责数据的重传。 时间戳  记录了负载中第一个字节的采样时间,接收方能够时间戳能够确定数据的到达是否受到了延迟抖动的影响,但具体如何来补偿延迟抖动则是应用程序自己的事情。 从RTP数据报的格式不难看出,它包含了传输媒体的类型、格式、序列号、时间戳以及是否有附加数据等信息,这些都为实时的流媒体传输提供了相应的基础。RTP协议的目的是提供实时数据(如交互式的音频和视频)的端到端传输服务,因此在RTP中没有连接的概念,它可以建立在底层的面向连接或面向非连接的传输协议之上;RTP也不依赖于特别的网络地址格式,而仅仅只需要底层传输协议支持组帧(Framing)和分段(Segmentation)就足够了;另外RTP本身还不提供任何可靠性机制,这些都要由传输协议或者应用程序自己来保证。在典型的应用场合下,RTP一般是在传输协议之上作为应用程序的一部分加以实现的,如图2所示: 图2 RTP与各种网络协议的关系 2.2 RTCP控制协议 RTCP控制协议需要与RTP数据协议一起配合使用,当应用程序启动一个RTP会话时将同时占用两个端口,分别供RTP和RTCP使用。RTP本身并不能为按序传输数据包提供可靠的保证,也不提供流量控制和拥塞控制,这些都由RTCP来负责完成。通常RTCP会采用与RTP相同的分发机制,向会话中的所有成员周期性地发送控制信息,应用程序通过接收这些数据,从中获取会话参与者的相关资料,以及网络状况、分组丢失概率等反馈信息,从而能够对服务质量进行控制或者对网络状况进行诊断。 RTCP协议的功能是通过不同的RTCP数据报来实现的,主要有如下几种类型:  SR  发送端报告,所谓发送端是指发出RTP数据报的应用程序或者终端,发送端同时也可以是接收端。 RR  接收端报告,所谓接收端是指仅接收但不发送RTP数据报的应用程序或者终端。 SDES  源描述,主要功能是作为会话成员有关标识信息的载体,如用户名、邮件地址、电话号码等,此外还具有向会话成员传达会话控制信息的功能。 BYE  通知离开,主要功能是指示某一个或者几个源不再有效,即通知会话中的其他成员自己将退出会话。 APP  由应用程序自己定义,解决了RTCP的扩展性问题,并且为协议的实现者提供了很大的灵活性。 RTCP数据报携带有服务质量监控的必要信息,能够对服务质量进行动态的调整,并能够对网络拥塞进行有效的控制。由于RTCP数据报采用的是多播方式,因此会话中的所有成员都可以通过RTCP数据报返回的控制信息,来了解其他参与者的当前情况。 在一个典型的应用场合下,发送媒体流的应用程序将周期性地产生发送端报告SR,该RTCP数据报含有不同媒体流间的同步信息,以及已经发送的数据报和字节的计数,接收端根据这些信息可以估计出实际的数据传输速率。另一方面,接收端会向所有已知的发送端发送接收端报告RR,该RTCP数据报含有已接收数据报的最大序列号、丢失的数据报数目、延时抖动和时间戳等重要信息,发送端应用根据这些信息可以估计出往返时延,并且可以根据数据报丢失概率和时延抖动情况动态调整发送速率,以改善网络拥塞状况,或者根据网络状况平滑地调整应用程序的服务质量。 2.3 RTSP实时流协议 作为一个应用层协议,RTSP提供了一个可供扩展的框架,它的意义在于使得实时流媒体数据的受控和点播变得可能。总的说来,RTSP是一个流媒体表示协议,主要用来控制具有实时特性的数据发送,但它本身并不传输数据,而是必须依赖于下层传输协议所提供的某些服务。RTSP可以对流媒体提供诸如播放、暂停、快进等操作,它负责定义具体的控制消息、操作方法、状态码等,此外还描述了与RTP间的交互操作。 RTSP在制定时较多地参考了HTTP/1.1协议,甚至许多描述与HTTP/1.1完全相同。RTSP之所以特意使用与HTTP/1.1类似的语法和操作,在很大程度上是为了兼容现有的Web基础结构,正因如此,HTTP/1.1的扩展机制大都可以直接引入到RTSP中。 由RTSP控制的媒体流集合可以用表示描述(Presentation Description)来定义,所谓表示是指流媒体服务器提供给客户机的一个或者多个媒体流的集合,而表示描述则包含了一个表示中各个媒体流的相关信息,如数据编码/解码算法、网络地址、媒体流的内容等。 虽然RTSP服务器同样也使用标识符来区别每一流连接会话(Session),但RTSP连接并没有被绑定到传输层连接(如TCP等),也就是说在整个RTSP连接期间,RTSP用户可打开或者关闭多个对RTSP服务器的可靠传输连接以发出RTSP 请求。此外,RTSP连接也可以基于面向无连接的传输协议(如UDP等)。 RTSP协议目前支持以下操作:  检索媒体  允许用户通过HTTP或者其它方法向媒体服务器提交一个表示描述。如表示是组播的,则表示描述就包含用于该媒体流的组播地址和端口号;如果表示是单播的,为了安全在表示描述中应该只提供目的地址。 邀请加入  媒体服务器可以被邀请参加正在进行的会议,或者在表示中回放媒体,或者在表示中录制全部媒体或其子集,非常适合于分布式教学。 添加媒体  通知用户新加入的可利用媒体流,这对现场讲座来讲显得尤其有用。与HTTP/1.1类似,RTSP请求也可以交由代理、通道或者缓存来进行处理。 回页首 三、流媒体编程 RTP是目前解决流媒体实时传输问题的最好办法,如果需要在Linux平台上进行实时流媒体编程,可以考虑使用一些开放源代码的RTP库,如LIBRTP、JRTPLIB等。JRTPLIB是一个面向对象的RTP库,它完全遵循RFC 1889设计,在很多场合下是一个非常不错的选择,下面就以JRTPLIB为例,讲述如何在Linux平台上运用RTP协议进行实时流媒体编程。 3.1 环境搭建 JRTPLIB是一个用C++语言实现的RTP库,目前已经可以运行在Windows、Linux、FreeBSD、Solaris、Unix和VxWorks等多种操作系统上。要为Linux 系统安装JRTPLIB,首先从JRTPLIB的网站(http://lumumba.luc.ac.be/jori/jrtplib/jrtplib.html)下载最新的源码包,此处使用的是jrtplib-2.7b.tar.bz2。假设下载后的源码包保存在/usr/local/src目录下,执行下面的命令可以对其进行解压缩: [root@linuxgam src]# bzip2 -dc jrtplib-2.7b.tar.bz2 | tar xvf - 接下去需要对JRTPLIB进行配置和编译: [root@linuxgam src]# cd jrtplib-2.7 [root@linuxgam jrtplib-2.7b]# ./configure [root@linuxgam jrtplib-2.7b]# make 最后再执行如下命令就可以完成JRTPLIB的安装: [root@linuxgam jrtplib-2.7b]# make install 3.2 初始化 在使用JRTPLIB进行实时流媒体数据传输之前,首先应该生成RTPSession类的一个实例来表示此次RTP会话,然后调用Create()方法来对其进行初始化操作。RTPSession类的Create()方法只有一个参数,用来指明此次RTP会话所采用的端口号。清单1给出了一个最简单的初始化框架,它只是完成了RTP会话的初始化工作,还不具备任何实际的功能。 代码清单1:initial.cpp#include "rtpsession.h" int main(void) { RTPSession sess; sess.Create(5000); return 0; } 如果RTP会话创建过程失败,Create()方法将会返回一个负数,通过它虽然可以很容易地判断出函数调用究竟是成功的还是失败的,但却很难明白出错的原因到底什么。JRTPLIB采用了统一的错误处理机制,它提供的所有函数如果返回负数就表明出现了某种形式的错误,而具体的出错信息则可以通过调用RTPGetErrorString()函数得到。RTPGetErrorString()函数将错误代码作为参数传入,然后返回该错误代码所对应的错误信息。清单2给出了一个更加完整的初始化框架,它可以对RTP会话初始化过程中所产生的错误进行更好的处理: 代码清单2:framework.cpp#include <stdio.h> #include "rtpsession.h" int main(void) { RTPSession sess; int status; char* msg; sess.Create(6000); msg = RTPGetErrorString(status); printf("Error String: %s\\n", msg); return 0; } 设置恰当的时戳单元,是RTP会话初始化过程所要进行的另外一项重要工作,这是通过调用RTPSession类的SetTimestampUnit()方法来实现的,该方法同样也只有一个参数,表示的是以秒为单元的时戳单元。例如,当使用RTP会话传输8000Hz采样的音频数据时,由于时戳每秒钟将递增8000,所以时戳单元相应地应该被设置成1/8000: sess.SetTimestampUnit(1.0/8000.0); 3.3 数据发送 当RTP会话成功建立起来之后,接下去就可以开始进行流媒体数据的实时传输了。首先需要设置好数据发送的目标地址,RTP协议允许同一会话存在多个目标地址,这可以通过调用RTPSession类的AddDestination()、DeleteDestination()和ClearDestinations()方法来完成。例如,下面的语句表示的是让RTP会话将数据发送到本地主机的6000端口: unsigned long addr = ntohl(inet_addr("127.0.0.1")); sess.AddDestination(addr, 6000); 目标地址全部指定之后,接着就可以调用RTPSession类的SendPacket()方法,向所有的目标地址发送流媒体数据。SendPacket()是RTPSession类提供的一个重载函数,它具有下列多种形式: int SendPacket(void *data,int len) int SendPacket(void *data,int len,unsigned char pt,bool mark,unsigned long timestampinc) int SendPacket(void *data,int len,unsigned short hdrextID,void *hdrextdata, int numhdrextwords) int SendPacket(void *data,int len,unsigned char pt,bool mark,unsigned long timestampinc, unsigned short hdrextID,void *hdrextdata,int numhdrextwords) SendPacket()最典型的用法是类似于下面的语句,其中第一个参数是要被发送的数据,而第二个参数则指明将要发送数据的长度,再往后依次是RTP负载类型、标识和时戳增量。 sess.SendPacket(buffer, 5, 0, false, 10); 对于同一个RTP会话来讲,负载类型、标识和时戳增量通常来讲都是相同的,JRTPLIB允许将它们设置为会话的默认参数,这是通过调用RTPSession类的SetDefaultPayloadType()、SetDefaultMark()和SetDefaultTimeStampIncrement()方法来完成的。为RTP会话设置这些默认参数的好处是可以简化数据的发送,例如,如果为RTP会话设置了默认参数: sess.SetDefaultPayloadType(0); sess.SetDefaultMark(false); sess.SetDefaultTimeStampIncrement(10); 之后在进行数据发送时只需指明要发送的数据及其长度就可以了: sess.SendPacket(buffer, 5); 3.4 数据接收 对于流媒体数据的接收端,首先需要调用RTPSession类的PollData()方法来接收发送过来的RTP或者RTCP数据报。由于同一个RTP会话中允许有多个参与者(源),你既可以通过调用RTPSession类的GotoFirstSource()和GotoNextSource()方法来遍历所有的源,也可以通过调用RTPSession类的GotoFirstSourceWithData()和GotoNextSourceWithData()方法来遍历那些携带有数据的源。在从RTP会话中检测出有效的数据源之后,接下去就可以调用RTPSession类的GetNextPacket()方法从中抽取RTP数据报,当接收到的RTP数据报处理完之后,一定要记得及时释放。下面的代码示范了该如何对接收到的RTP数据报进行处理: if (sess.GotoFirstSourceWithData()) { do { RTPPacket *pack; pack = sess.GetNextPacket(); // 处理接收到的数据 delete pack; } while (sess.GotoNextSourceWithData()); } JRTPLIB为RTP数据报定义了三种接收模式,其中每种接收模式都具体规定了哪些到达的RTP数据报将会被接受,而哪些到达的RTP数据报将会被拒绝。通过调用RTPSession类的SetReceiveMode()方法可以设置下列这些接收模式: RECEIVEMODE_ALL  缺省的接收模式,所有到达的RTP数据报都将被接受; RECEIVEMODE_IGNORESOME  除了某些特定的发送者之外,所有到达的RTP数据报都将被接受,而被拒绝的发送者列表可以通过调用AddToIgnoreList()、DeleteFromIgnoreList()和ClearIgnoreList()方法来进行设置; RECEIVEMODE_ACCEPTSOME  除了某些特定的发送者之外,所有到达的RTP数据报都将被拒绝,而被接受的发送者列表可以通过调用AddToAcceptList ()、DeleteFromAcceptList和ClearAcceptList ()方法来进行设置。 3.5 控制信息 JRTPLIB是一个高度封装后的RTP库,程序员在使用它时很多时候并不用关心RTCP数据报是如何被发送和接收的,因为这些都可以由JRTPLIB自己来完成。只要PollData()或者SendPacket()方法被成功调用,JRTPLIB就能够自动对到达的RTCP数据报进行处理,并且还会在需要的时候发送RTCP数据报,从而能够确保整个RTP会话过程的正确性。 而另一方面,通过调用RTPSession类提供的SetLocalName()、SetLocalEMail()、SetLocalLocation()、SetLocalPhone()、SetLocalTool()和SetLocalNote()方法,JRTPLIB又允许程序员对RTP会话的控制信息进行设置。所有这些方法在调用时都带有两个参数,其中第一个参数是一个char型的指针,指向将要被设置的数据;而第二个参数则是一个int型的数值,表明该数据中的前面多少个字符将会被使用。例如下面的语句可以被用来设置控制信息中的电子邮件地址: sess.SetLocalEMail("xiaowp@linuxgam.com",19); 在RTP会话过程中,不是所有的控制信息都需要被发送,通过调用RTPSession类提供的EnableSendName()、EnableSendEMail()、EnableSendLocation()、EnableSendPhone()、EnableSendTool()和EnableSendNote()方法,可以为当前RTP会话选择将被发送的控制信息。 3.6 实际应用 最后通过一个简单的流媒体发送-接收实例,介绍如何利用JRTPLIB来进行实时流媒体的编程。清单3给出了数据发送端的完整代码,它负责向用户指定的IP地址和端口,不断地发送RTP数据包: 代码清单3:sender.cpp#include <stdio.h> #include <string.h> #include "rtpsession.h" // 错误处理函数 void checkerror(int err) { if (err < 0) { char* errstr = RTPGetErrorString(err); printf("Error:%s\\n", errstr); exit(-1); } } int main(int argc, char** argv) { RTPSession sess; unsigned long destip; int destport; int portbase = 6000; int status, index; char buffer[128]; if (argc != 3) { printf("Usage: ./sender destip destport\\n"); return -1; } // 获得接收端的IP地址和端口号 destip = inet_addr(argv[1]); if (destip == INADDR_NONE) { printf("Bad IP address specified.\\n"); return -1; } destip = ntohl(destip); destport = atoi(argv[2]); // 创建RTP会话 status = sess.Create(portbase); checkerror(status); // 指定RTP数据接收端 status = sess.AddDestination(destip, destport); checkerror(status); // 设置RTP会话默认参数 sess.SetDefaultPayloadType(0); sess.SetDefaultMark(false); sess.SetDefaultTimeStampIncrement(10); // 发送流媒体数据 index = 1; do { sprintf(buffer, "%d: RTP packet", index ++); sess.SendPacket(buffer, strlen(buffer)); printf("Send packet !\\n"); } while(1); return 0; } 清单4则给出了数据接收端的完整代码,它负责从指定的端口不断地读取RTP数据包: 代码清单4:receiver.cpp#include <stdio.h> #include "rtpsession.h" #include "rtppacket.h" // 错误处理函数 void checkerror(int err) { if (err < 0) { char* errstr = RTPGetErrorString(err); printf("Error:%s\\n", errstr); exit(-1); } } int main(int argc, char** argv) { RTPSession sess; int localport; int status; if (argc != 2) { printf("Usage: ./sender localport\\n"); return -1; } // 获得用户指定的端口号 localport = atoi(argv[1]); // 创建RTP会话 status = sess.Create(localport); checkerror(status); do { // 接受RTP数据 status = sess.PollData(); // 检索RTP数据源 if (sess.GotoFirstSourceWithData()) { do { RTPPacket* packet; // 获取RTP数据报 while ((packet = sess.GetNextPacket()) != NULL) { printf("Got packet !\\n"); // 删除RTP数据报 delete packet; } } while (sess.GotoNextSourceWithData()); } } while(1); return 0; } 本文源码 下载 回页首 四、小结 随着多媒体数据在Internet上所承担的作用变得越来越重要,需要实时传输音频和视频等多媒体数据的场合也将变得越来越多,如IP电话、视频点播、在线会议等。RTP是用来在Internet上进行实时流媒体传输的一种协议,目前已经被广泛地应用在各种场合,JRTPLIB是一个面向对象的RTP封装库,利用它可以很方便地完成Linux平台上的实时流媒体编程。
文章
网络协议 · 算法 · Unix · Linux · 流计算 · 程序员 · C++ · 网络性能优化 · 缓存 · Windows
2017-10-09
流媒体相关知识介绍 及其 RTP 应用
一、流媒体简介 随着Internet的日益普及,在网络上传输的数据已经不再局限于文字和图形,而是逐渐向声音和视频等多媒体格式过渡。目前在网络上传输音频/视频(Audio/Video,简称A/V)等多媒体文件时,基本上只有下载和流式传输两种选择。通常说来,A/V文件占据的存储空间都比较大,在带宽受限的网络环境中下载可能要耗费数分钟甚至数小时,所以这种处理方法的延迟很大。如果换用流式传输的话,声音、影像、动画等多媒体文件将由专门的流媒体服务器负责向用户连续、实时地发送,这样用户可以不必等到整个文件全部下载完毕,而只需要经过几秒钟的启动延时就可以了,当这些多媒体数据在客户机上播放时,文件的剩余部分将继续从流媒体服务器下载。 流(Streaming)是近年在Internet上出现的新概念,其定义非常广泛,主要是指通过网络传输多媒体数据的技术总称。流媒体包含广义和狭义两种内涵:广义上的流媒体指的是使音频和视频形成稳定和连续的传输流和回放流的一系列技术、方法和协议的总称,即流媒体技术;狭义上的流媒体是相对于传统的下载-回放方式而言的,指的是一种从Internet上获取音频和视频等多媒体数据的新方法,它能够支持多媒体数据流的实时传输和实时播放。通过运用流媒体技术,服务器能够向客户机发送稳定和连续的多媒体数据流,客户机在接收数据的同时以一个稳定的速率回放,而不用等数据全部下载完之后再进行回放。 由于受网络带宽、计算机处理能力和协议规范等方面的限制,要想从Internet上下载大量的音频和视频数据,无论从下载时间和存储空间上来讲都是不太现实的,而流媒体技术的出现则很好地解决了这一难题。目前实现流媒体传输主要有两种方法:顺序流(progressive streaming)传输和实时流(realtime streaming)传输,它们分别适合于不同的应用场合。 顺序流传输 顺序流传输采用顺序下载的方式进行传输,在下载的同时用户可以在线回放多媒体数据,但给定时刻只能观看已经下载的部分,不能跳到尚未下载的部分,也不能在传输期间根据网络状况对下载速度进行调整。由于标准的HTTP服务器就可以发送这种形式的流媒体,而不需要其他特殊协议的支持,因此也常常被称作HTTP流式传输。顺序流式传输比较适合于高质量的多媒体片段,如片头、片尾或者广告等。 实时流传输 实时流式传输保证媒体信号带宽能够与当前网络状况相匹配,从而使得流媒体数据总是被实时地传送,因此特别适合于现场事件。实时流传输支持随机访问,即用户可以通过快进或者后退操作来观看前面或者后面的内容。从理论上讲,实时流媒体一经播放就不会停顿,但事实上仍有可能发生周期性的暂停现象,尤其是在网络状况恶化时更是如此。与顺序流传输不同的是,实时流传输需要用到特定的流媒体服务器,而且还需要特定网络协议的支持。   回页首 二、流媒体协议 实时传输协议(Real-time Transport Protocol,PRT)是在Internet上处理多媒体数据流的一种网络协议,利用它能够在一对一(unicast,单播)或者一对多(multicast,多播)的网络环境中实现传流媒体数据的实时传输。RTP通常使用UDP来进行多媒体数据的传输,但如果需要的话可以使用TCP或者ATM等其它协议,整个RTP协议由两个密切相关的部分组成:RTP数据协议和RTP控制协议。实时流协议(Real Time Streaming Protocol,RTSP)最早由Real Networks和Netscape公司共同提出,它位于RTP和RTCP之上,其目的是希望通过IP网络有效地传输多媒体数据。 2.1 RTP数据协议 RTP数据协议负责对流媒体数据进行封包并实现媒体流的实时传输,每一个RTP数据报都由头部(Header)和负载(Payload)两个部分组成,其中头部前12个字节的含义是固定的,而负载则可以是音频或者视频数据。RTP数据报的头部格式如图1所示: 图1 RTP头部格式 其中比较重要的几个域及其意义如下: CSRC记数(CC)  表示CSRC标识的数目。CSRC标识紧跟在RTP固定头部之后,用来表示RTP数据报的来源,RTP协议允许在同一个会话中存在多个数据源,它们可以通过RTP混合器合并为一个数据源。例如,可以产生一个CSRC列表来表示一个电话会议,该会议通过一个RTP混合器将所有讲话者的语音数据组合为一个RTP数据源。 负载类型(PT)  标明RTP负载的格式,包括所采用的编码算法、采样频率、承载通道等。例如,类型2表明该RTP数据包中承载的是用ITU G.721算法编码的语音数据,采样频率为8000Hz,并且采用单声道。 序列号  用来为接收方提供探测数据丢失的方法,但如何处理丢失的数据则是应用程序自己的事情,RTP协议本身并不负责数据的重传。 时间戳  记录了负载中第一个字节的采样时间,接收方能够时间戳能够确定数据的到达是否受到了延迟抖动的影响,但具体如何来补偿延迟抖动则是应用程序自己的事情。   从RTP数据报的格式不难看出,它包含了传输媒体的类型、格式、序列号、时间戳以及是否有附加数据等信息,这些都为实时的流媒体传输提供了相应的基础。RTP协议的目的是提供实时数据(如交互式的音频和视频)的端到端传输服务,因此在RTP中没有连接的概念,它可以建立在底层的面向连接或面向非连接的传输协议之上;RTP也不依赖于特别的网络地址格式,而仅仅只需要底层传输协议支持组帧(Framing)和分段(Segmentation)就足够了;另外RTP本身还不提供任何可靠性机制,这些都要由传输协议或者应用程序自己来保证。在典型的应用场合下,RTP一般是在传输协议之上作为应用程序的一部分加以实现的,如图2所示: 图2 RTP与各种网络协议的关系 2.2 RTCP控制协议 RTCP控制协议需要与RTP数据协议一起配合使用,当应用程序启动一个RTP会话时将同时占用两个端口,分别供RTP和RTCP使用。RTP本身并不能为按序传输数据包提供可靠的保证,也不提供流量控制和拥塞控制,这些都由RTCP来负责完成。通常RTCP会采用与RTP相同的分发机制,向会话中的所有成员周期性地发送控制信息,应用程序通过接收这些数据,从中获取会话参与者的相关资料,以及网络状况、分组丢失概率等反馈信息,从而能够对服务质量进行控制或者对网络状况进行诊断。 RTCP协议的功能是通过不同的RTCP数据报来实现的,主要有如下几种类型: SR  发送端报告,所谓发送端是指发出RTP数据报的应用程序或者终端,发送端同时也可以是接收端。 RR  接收端报告,所谓接收端是指仅接收但不发送RTP数据报的应用程序或者终端。 SDES  源描述,主要功能是作为会话成员有关标识信息的载体,如用户名、邮件地址、电话号码等,此外还具有向会话成员传达会话控制信息的功能。 BYE  通知离开,主要功能是指示某一个或者几个源不再有效,即通知会话中的其他成员自己将退出会话。 APP  由应用程序自己定义,解决了RTCP的扩展性问题,并且为协议的实现者提供了很大的灵活性。   RTCP数据报携带有服务质量监控的必要信息,能够对服务质量进行动态的调整,并能够对网络拥塞进行有效的控制。由于RTCP数据报采用的是多播方式,因此会话中的所有成员都可以通过RTCP数据报返回的控制信息,来了解其他参与者的当前情况。 在一个典型的应用场合下,发送媒体流的应用程序将周期性地产生发送端报告SR,该RTCP数据报含有不同媒体流间的同步信息,以及已经发送的数据报和字节的计数,接收端根据这些信息可以估计出实际的数据传输速率。另一方面,接收端会向所有已知的发送端发送接收端报告RR,该RTCP数据报含有已接收数据报的最大序列号、丢失的数据报数目、延时抖动和时间戳等重要信息,发送端应用根据这些信息可以估计出往返时延,并且可以根据数据报丢失概率和时延抖动情况动态调整发送速率,以改善网络拥塞状况,或者根据网络状况平滑地调整应用程序的服务质量。 2.3 RTSP实时流协议 作为一个应用层协议,RTSP提供了一个可供扩展的框架,它的意义在于使得实时流媒体数据的受控和点播变得可能。总的说来,RTSP是一个流媒体表示协议,主要用来控制具有实时特性的数据发送,但它本身并不传输数据,而是必须依赖于下层传输协议所提供的某些服务。RTSP可以对流媒体提供诸如播放、暂停、快进等操作,它负责定义具体的控制消息、操作方法、状态码等,此外还描述了与RTP间的交互操作。 RTSP在制定时较多地参考了HTTP/1.1协议,甚至许多描述与HTTP/1.1完全相同。RTSP之所以特意使用与HTTP/1.1类似的语法和操作,在很大程度上是为了兼容现有的Web基础结构,正因如此,HTTP/1.1的扩展机制大都可以直接引入到RTSP中。 由RTSP控制的媒体流集合可以用表示描述(Presentation Description)来定义,所谓表示是指流媒体服务器提供给客户机的一个或者多个媒体流的集合,而表示描述则包含了一个表示中各个媒体流的相关信息,如数据编码/解码算法、网络地址、媒体流的内容等。 虽然RTSP服务器同样也使用标识符来区别每一流连接会话(Session),但RTSP连接并没有被绑定到传输层连接(如TCP等),也就是说在整个RTSP连接期间,RTSP用户可打开或者关闭多个对RTSP服务器的可靠传输连接以发出RTSP 请求。此外,RTSP连接也可以基于面向无连接的传输协议(如UDP等)。 RTSP协议目前支持以下操作: 检索媒体  允许用户通过HTTP或者其它方法向媒体服务器提交一个表示描述。如表示是组播的,则表示描述就包含用于该媒体流的组播地址和端口号;如果表示是单播的,为了安全在表示描述中应该只提供目的地址。 邀请加入  媒体服务器可以被邀请参加正在进行的会议,或者在表示中回放媒体,或者在表示中录制全部媒体或其子集,非常适合于分布式教学。 添加媒体  通知用户新加入的可利用媒体流,这对现场讲座来讲显得尤其有用。与HTTP/1.1类似,RTSP请求也可以交由代理、通道或者缓存来进行处理。     回页首 三、流媒体编程 RTP是目前解决流媒体实时传输问题的最好办法,如果需要在Linux平台上进行实时流媒体编程,可以考虑使用一些开放源代码的RTP库,如LIBRTP、JRTPLIB等。JRTPLIB是一个面向对象的RTP库,它完全遵循RFC 1889设计,在很多场合下是一个非常不错的选择,下面就以JRTPLIB为例,讲述如何在Linux平台上运用RTP协议进行实时流媒体编程。 3.1 环境搭建 JRTPLIB是一个用C++语言实现的RTP库,目前已经可以运行在Windows、Linux、FreeBSD、Solaris、Unix和VxWorks等多种操作系统上。要为Linux 系统安装JRTPLIB,首先从JRTPLIB的网站(http://lumumba.luc.ac.be/jori/jrtplib/jrtplib.html)下载最新的源码包,此处使用的是jrtplib-2.7b.tar.bz2。假设下载后的源码包保存在/usr/local/src目录下,执行下面的命令可以对其进行解压缩: [root@linuxgam src]# bzip2 -dc jrtplib-2.7b.tar.bz2 | tar xvf -   接下去需要对JRTPLIB进行配置和编译: [root@linuxgam src]# cd jrtplib-2.7 [root@linuxgam jrtplib-2.7b]# ./configure [root@linuxgam jrtplib-2.7b]# make   最后再执行如下命令就可以完成JRTPLIB的安装: [root@linuxgam jrtplib-2.7b]# make install   3.2 初始化 在使用JRTPLIB进行实时流媒体数据传输之前,首先应该生成RTPSession类的一个实例来表示此次RTP会话,然后调用Create()方法来对其进行初始化操作。RTPSession类的Create()方法只有一个参数,用来指明此次RTP会话所采用的端口号。清单1给出了一个最简单的初始化框架,它只是完成了RTP会话的初始化工作,还不具备任何实际的功能。 代码清单1:initial.cpp #include "rtpsession.h" int main(void) { RTPSession sess; sess.Create(5000); return 0; }   如果RTP会话创建过程失败,Create()方法将会返回一个负数,通过它虽然可以很容易地判断出函数调用究竟是成功的还是失败的,但却很难明白出错的原因到底什么。JRTPLIB采用了统一的错误处理机制,它提供的所有函数如果返回负数就表明出现了某种形式的错误,而具体的出错信息则可以通过调用RTPGetErrorString()函数得到。RTPGetErrorString()函数将错误代码作为参数传入,然后返回该错误代码所对应的错误信息。清单2给出了一个更加完整的初始化框架,它可以对RTP会话初始化过程中所产生的错误进行更好的处理: 代码清单2:framework.cpp #include <stdio.h> #include "rtpsession.h" int main(void) { RTPSession sess; int status; char* msg; sess.Create(6000); msg = RTPGetErrorString(status); printf("Error String: %s\\n", msg); return 0; }   设置恰当的时戳单元,是RTP会话初始化过程所要进行的另外一项重要工作,这是通过调用RTPSession类的SetTimestampUnit()方法来实现的,该方法同样也只有一个参数,表示的是以秒为单元的时戳单元。例如,当使用RTP会话传输8000Hz采样的音频数据时,由于时戳每秒钟将递增8000,所以时戳单元相应地应该被设置成1/8000: sess.SetTimestampUnit(1.0/8000.0);   3.3 数据发送 当RTP会话成功建立起来之后,接下去就可以开始进行流媒体数据的实时传输了。首先需要设置好数据发送的目标地址,RTP协议允许同一会话存在多个目标地址,这可以通过调用RTPSession类的AddDestination()、DeleteDestination()和ClearDestinations()方法来完成。例如,下面的语句表示的是让RTP会话将数据发送到本地主机的6000端口: unsigned long addr = ntohl(inet_addr("127.0.0.1")); sess.AddDestination(addr, 6000);   目标地址全部指定之后,接着就可以调用RTPSession类的SendPacket()方法,向所有的目标地址发送流媒体数据。SendPacket()是RTPSession类提供的一个重载函数,它具有下列多种形式: int SendPacket(void *data,int len) int SendPacket(void *data,int len,unsigned char pt,bool mark,unsigned long timestampinc) int SendPacket(void *data,int len,unsigned short hdrextID,void *hdrextdata,int numhdrextwords) int SendPacket(void *data,int len,unsigned char pt,bool mark,unsigned long timestampinc, unsigned short hdrextID,void *hdrextdata,int numhdrextwords)   SendPacket()最典型的用法是类似于下面的语句,其中第一个参数是要被发送的数据,而第二个参数则指明将要发送数据的长度,再往后依次是RTP负载类型、标识和时戳增量。 sess.SendPacket(buffer, 5, 0, false, 10);   对于同一个RTP会话来讲,负载类型、标识和时戳增量通常来讲都是相同的,JRTPLIB允许将它们设置为会话的默认参数,这是通过调用RTPSession类的SetDefaultPayloadType()、SetDefaultMark()和SetDefaultTimeStampIncrement()方法来完成的。为RTP会话设置这些默认参数的好处是可以简化数据的发送,例如,如果为RTP会话设置了默认参数: sess.SetDefaultPayloadType(0); sess.SetDefaultMark(false); sess.SetDefaultTimeStampIncrement(10);   之后在进行数据发送时只需指明要发送的数据及其长度就可以了: sess.SendPacket(buffer, 5);   3.4 数据接收 对于流媒体数据的接收端,首先需要调用RTPSession类的PollData()方法来接收发送过来的RTP或者RTCP数据报。由于同一个RTP会话中允许有多个参与者(源),你既可以通过调用RTPSession类的GotoFirstSource()和GotoNextSource()方法来遍历所有的源,也可以通过调用RTPSession类的GotoFirstSourceWithData()和GotoNextSourceWithData()方法来遍历那些携带有数据的源。在从RTP会话中检测出有效的数据源之后,接下去就可以调用RTPSession类的GetNextPacket()方法从中抽取RTP数据报,当接收到的RTP数据报处理完之后,一定要记得及时释放。下面的代码示范了该如何对接收到的RTP数据报进行处理: if (sess.GotoFirstSourceWithData()) { do { RTPPacket *pack; pack = sess.GetNextPacket(); // 处理接收到的数据 delete pack; } while (sess.GotoNextSourceWithData()); }   JRTPLIB为RTP数据报定义了三种接收模式,其中每种接收模式都具体规定了哪些到达的RTP数据报将会被接受,而哪些到达的RTP数据报将会被拒绝。通过调用RTPSession类的SetReceiveMode()方法可以设置下列这些接收模式: RECEIVEMODE_ALL  缺省的接收模式,所有到达的RTP数据报都将被接受; RECEIVEMODE_IGNORESOME  除了某些特定的发送者之外,所有到达的RTP数据报都将被接受,而被拒绝的发送者列表可以通过调用AddToIgnoreList()、DeleteFromIgnoreList()和ClearIgnoreList()方法来进行设置; RECEIVEMODE_ACCEPTSOME  除了某些特定的发送者之外,所有到达的RTP数据报都将被拒绝,而被接受的发送者列表可以通过调用AddToAcceptList ()、DeleteFromAcceptList和ClearAcceptList ()方法来进行设置。 3.5 控制信息 JRTPLIB是一个高度封装后的RTP库,程序员在使用它时很多时候并不用关心RTCP数据报是如何被发送和接收的,因为这些都可以由JRTPLIB自己来完成。只要PollData()或者SendPacket()方法被成功调用,JRTPLIB就能够自动对到达的RTCP数据报进行处理,并且还会在需要的时候发送RTCP数据报,从而能够确保整个RTP会话过程的正确性。 而另一方面,通过调用RTPSession类提供的SetLocalName()、SetLocalEMail()、SetLocalLocation()、SetLocalPhone()、SetLocalTool()和SetLocalNote()方法,JRTPLIB又允许程序员对RTP会话的控制信息进行设置。所有这些方法在调用时都带有两个参数,其中第一个参数是一个char型的指针,指向将要被设置的数据;而第二个参数则是一个int型的数值,表明该数据中的前面多少个字符将会被使用。例如下面的语句可以被用来设置控制信息中的电子邮件地址: sess.SetLocalEMail("xiaowp@linuxgam.com",19);   在RTP会话过程中,不是所有的控制信息都需要被发送,通过调用RTPSession类提供的EnableSendName()、EnableSendEMail()、EnableSendLocation()、EnableSendPhone()、EnableSendTool()和EnableSendNote()方法,可以为当前RTP会话选择将被发送的控制信息。 3.6 实际应用 最后通过一个简单的流媒体发送-接收实例,介绍如何利用JRTPLIB来进行实时流媒体的编程。清单3给出了数据发送端的完整代码,它负责向用户指定的IP地址和端口,不断地发送RTP数据包: 代码清单3:sender.cpp #include <stdio.h> #include <string.h> #include "rtpsession.h" // 错误处理函数 void checkerror(int err) { if (err < 0) { char* errstr = RTPGetErrorString(err); printf("Error:%s\\n", errstr); exit(-1); } } int main(int argc, char** argv) { RTPSession sess; unsigned long destip; int destport; int portbase = 6000; int status, index; char buffer[128]; if (argc != 3) { printf("Usage: ./sender destip destport\\n"); return -1; } // 获得接收端的IP地址和端口号 destip = inet_addr(argv[1]); if (destip == INADDR_NONE) { printf("Bad IP address specified.\\n"); return -1; } destip = ntohl(destip); destport = atoi(argv[2]); // 创建RTP会话 status = sess.Create(portbase); checkerror(status); // 指定RTP数据接收端 status = sess.AddDestination(destip, destport); checkerror(status); // 设置RTP会话默认参数 sess.SetDefaultPayloadType(0); sess.SetDefaultMark(false); sess.SetDefaultTimeStampIncrement(10); // 发送流媒体数据 index = 1; do { sprintf(buffer, "%d: RTP packet", index ++); sess.SendPacket(buffer, strlen(buffer)); printf("Send packet !\\n"); } while(1); return 0; }   清单4则给出了数据接收端的完整代码,它负责从指定的端口不断地读取RTP数据包: 代码清单4:receiver.cpp #include <stdio.h> #include "rtpsession.h" #include "rtppacket.h" // 错误处理函数 void checkerror(int err) { if (err < 0) { char* errstr = RTPGetErrorString(err); printf("Error:%s\\n", errstr); exit(-1); } } int main(int argc, char** argv) { RTPSession sess; int localport; int status; if (argc != 2) { printf("Usage: ./sender localport\\n"); return -1; } // 获得用户指定的端口号 localport = atoi(argv[1]); // 创建RTP会话 status = sess.Create(localport); checkerror(status); do { // 接受RTP数据 status = sess.PollData(); // 检索RTP数据源 if (sess.GotoFirstSourceWithData()) { do { RTPPacket* packet; // 获取RTP数据报 while ((packet = sess.GetNextPacket()) != NULL) { printf("Got packet !\\n"); // 删除RTP数据报 delete packet; } } while (sess.GotoNextSourceWithData()); } } while(1); return 0; }   本文源码 下载   回页首 四、小结 随着多媒体数据在Internet上所承担的作用变得越来越重要,需要实时传输音频和视频等多媒体数据的场合也将变得越来越多,如IP电话、视频点播、在线会议等。RTP是用来在Internet上进行实时流媒体传输的一种协议,目前已经被广泛地应用在各种场合,JRTPLIB是一个面向对象的RTP封装库,利用它可以很方便地完成Linux平台上的实时流媒体编程。   回页首 参考资料 1. 在JRTPLIB的网站http://lumumba.luc.ac.be/jori/jrtplib/jrtplib.html上,可以下载到JRTPLIB最新的源码包,并且还能找到一些与RTP相关的资源。 2. 顾淑珍等编著,宽带增值服务开发实例,北京:机械工业出版社,2002 3. 黄永峰等编著,IP网络多媒体通信技术,北京:人民邮电出版社,2003
文章
网络协议 · 算法 · Unix · Linux · 流计算 · 程序员 · C++ · 网络性能优化 · 缓存 · Windows
2014-02-27
[ZigBee] 9、ZigBee之AD剖析——AD采集CC2530温度串口显示
    1、ADC 简介   ADC 支持多达14 位的模拟数字转换,具有多达12 位有效数字位。它包括一个模拟多路转换器,具有多达8 个各自可配置的通道;以及一个参考电压发生器。转换结果通过DMA 写入存储器。还具有若干运行模式。   ADC 的主要特性如下: ● 可选的抽取率,这也设置了分辨率(7 到12 位)● 8 个独立的输入通道,可接受单端或差分信号● 参考电压可选为内部单端、外部单端、外部差分或AVDD5● 产生中断请求● 转换结束时的DMA 触发● 温度传感器输入● 电池测量功能   2、ADC 操作   本节描述了ADC 的一般安装和操作,并描述了CPU 存取的ADC 控制和状态寄存器的使用。   2.1、ADC 输入   The signals on the Port 0 pins can be used as ADC inputs. In the following, these port pins are referred to as the AIN0–AIN7 pins. The input pins AIN0–AIN7 are connected to the ADC.   可以把输入配置为单端或差分输入。在选择差分输入的情况下,差分输入包括输入对AIN0-1、AIN2-3、AIN4-5 和AIN6-7。电压不能为负或者大于VDD。这些输入对之间的区别书他们采用不同的模式进行转换。   除了输入引脚AIN0-AIN7,片上温度传感器的输出也可以选择作为ADC 的输入,用于温度测量。为此寄存器TR0.ADCTM 和ATEST.ATESTCTRL 必须分别按2.10 节和寄存器描述所述设置。   还可以输入一个对应AVDD5/3 的电压作为一个ADC 输入。这个输入允许诸如需要在应用中实现一个电池监测器的功能。注意在这种情况下参考电压不能取决于电源电压,比如AVDD5 电压不能用作一个参考电压。   单端电压输入AIN0 到AIN7 以通道号码0 到7 表示。通道号码8 到11 表示差分输入,由AIN0–AIN1、AIN2–AIN3、AIN4–AIN5 和AIN6–AIN7 组成。通道号码12 到15 表示G N D(12)温度传感器(14),和AVDD5/3(15)。这些值在ADCCON2.SCH 和ADCCON3.SCH 域中使用。   2.2、ADC 转换序列(暂时难理解)   ADC将执行一系列的转换,并把结果移动到存储器(通过DMA),不需要任何CPU 干预。   转换序列可以被APCFG 寄存器影响,八位模拟输入来自I/O 引脚,不必经过编程变为模拟输入。如果一个通道正常情况下应是序列的一部分,但是相应的模拟输入在APCFG 中禁用,那么通道将被跳过。当使用差分输入,处于差分对的两个引脚都必须在APCFG 寄存器中设置为模拟输入引脚。   The ADCCON2.SCH(用于定义转换序列) register bits are used to define an ADC conversion sequence from the ADC inputs.   If ADCCON2.SCH is set to a value less than 8, the conversion sequence contains a conversion from each channel from 0 up to and including the channel number programmed in ADCCON2.SCH.(当设置该寄存器值小于8时,转换序列为从通道0到SCH定义的值,包括该值)   When ADCCON2.SCH is set to a value between 8 and 12, the sequence consists of differential inputs, starting at channel 8 and ending at the programmed channel.(在8~12之间,为缠粉输入,通道从8到定义的值)   For ADCCON2.SCH greater than or equal to 12, the sequence consists of the selected channel only.(如果大于12,则定义哪个就是哪个)   PS:the channel define in the last of 2.1   2.3、Single ADC Conversion(单个ADC转换,2.2是设置一个ADC序列进行转换)   In addition to this sequence of conversions, the ADC can be programmed to perform a single conversion from any channel(ADC能够配置从任何一个channel开执行一次单通道转换). Such a conversion is triggered by writing to the ADCCON3 register. (转换开始的条件->)The conversion starts immediately unless a conversion sequence is already ongoing, in which case the single conversion is performed as soon as that sequence is finished.   2.4、ADC Operating Modes   本节描述:operating modes and initialization of conversions.   The ADC has three control registers: ADCCON1, ADCCON2, and ADCCON3. These registers are used to configure the ADC and to report status. ADCCON1.EOC 位是一个状态位,当一个转换结束时,设置为高电平;当读取ADCH 时,它就被清除。 ADCCON1.ST 位用于启动一个转换序列。当这个位设置为高电平,ADCCON1.STSEL 是11,且当前没有转换正在运行时,就启动一个序列。当这个序列转换完成,这个位就被自动清除。 ADCCON1.STSEL 位选择哪个事件将启动一个新的转换序列。该选项可以选择为外部引脚P2.0 上升沿或外部引脚事件,之前序列的结束事件,定时器1 的通道0 比较事件或ADCCON1.ST 是1。 ADCCON2 寄存器控制转换序列是如何执行的。 ADCCON2.SREF 用于选择参考电压。参考电压只能在没有转换运行的时候修改。 ADCCON2.SDIV 位选择抽取率(并因此也设置了分辨率和完成一个转换所需的时间,或样本率)。抽取率只能在没有转换运行的时候修改。 转换序列的最后一个通道由ADCCON2.SCH 位选择,如上所述。 ADCCON3 寄存器控制单个转换的通道号码、参考电压和抽取率。单个转换在寄存器ADCCON3 写入后将立即发生,或如果一个转换序列正在进行,该序列结束之后立即发生。该寄存器位的编码和ADCCON2 是完全一样的。   2.5、ADC 转换结果   数字转换结果以2 的补码形式表示。对于单端配置,结果总是为正。这是因为结果是输入信号和地面之间的差值,它总是一个正符号数(Vconv=Vinp-Vinn,其中Vinn=0V)。当输入幅度等于所选的电压参考VREF时,达到最大值。   对于差分配置,两个引脚对之间的差分被转换,这个差分可以是负符号数。对于抽取率是512的一个数字转换结果的12 位MSB,当模拟输入Vconv 等于VREF 时,数字转换结果是2047。当模拟输入等于 -VREF 时,数字转换结果是-2048。   当ADCCON1.EOC 设置为1 时,数字转换结果是可以获得的,且结果放在ADCH 和ADCL 中。注意转换结果总是驻留在ADCH 和ADCL 寄存器组合的MSB 段中。   当读取ADCCON2.SCH 位时,它们将指示转换在哪个通道上进行。ADCL 和ADCH 中的结果一般适用于之前的转换。如果转换序列已经结束, ADCCON2.SCH 的值大于最后一个通道号码,但是如果最后写入ADCCON2.SCH 的通道号码是12 或更大,将读回同一个值。   2.6、ADC 参考电压   模拟数字转换的正参考电压可选择为一个内部生成的电压,AVDD5 引脚,适用于AIN7 输入引脚的外部电压,或适用于AIN6-AIN7 输入引脚的差分电压。   转换结果的准确性取决于参考电压的稳定性和噪音属性。希望的电压有偏差会导致ADC 增益误差,与希望电压和实际电压的比例成正比。参考电压的噪音必须低于ADC 的量化噪音,以确保达到规定的SNR。   2.7、ADC 转换时间   ADC 只能运行在32 MHz XOSC 上,用户不能整除系统时钟。实际ADC 采样的4 MHz 的频率由固定的内部划分器产生。执行一个转换所需的时间取决于所选的抽取率。总的来说,转换时间由以下公式给定: Tconv = (抽取率+ 16) x 0.25 μs。   2.8、ADC 中断   当通过写ADCCON3 触发的一个单个转换完成时,ADC 将产生一个中断。当完成一个序列转换时,不产生一个中断。   2.9、ADC DMA 触发(和ADC中断有种互补的感觉~)   每完成一个序列转换,ADC 将产生一个DMA 触发。当完成一个单个转换,不产生DMA 触发。   There is one DMA trigger for each of the eight channels defined by the first eight possible settings for ADCCON2.SCH。当通道中一个新的样本准备转换,DMA 触发是活动的。The DMA triggers are named ADC_CHsd in Following Table, where s is single-ended channel and d is differential channel。   In addition, one DMA trigger, ADC_CHALL, is active when new data is ready from any of the channels in the ADC conversion sequence.   3、工程解析 从下面main函数可以看出,整个流程是先初始化串口收发(这个和上一节介绍的串口收发一模一样,请参考上一节);接着是初始化ADC将片上片上温度传感器的输出选择作为ADC 的输入用于温度测量;在while大循环内则是连续读取64次温度数据并求平均(代码中求平均方法有点怪),最后通过串口将采集的片内温度传感器数据输出。 1 void main(void) 2 { 3 char i; 4 float AvgTemp; 5 char strTemp[6]; 6 7 InitUART(); //初始化串口 8 InitSensor(); //初始化 ADC 9 10 while(1) 11 { 12 AvgTemp = GetTemperature(); 13 14 for (i=0; i<63; i++) 15 { 16 AvgTemp += GetTemperature(); 17 AvgTemp = AvgTemp/2; //每次累加后除 2 18 } 19 20 memset(strTemp, 0, 6); 21 sprintf(strTemp,"%.02f", AvgTemp);//将浮点数转成字符串 22 UartSendString(strTemp, 5); //通过串口发给电脑显示芯片温度 23 DelayMS(1000); //延时 24 } 25 }   其中initSensor()是对ADC进行初始化: 第3行#define DISABLE_ALL_INTERRUPTS() (IEN0 = IEN1 = IEN2 = 0x00)是关闭所有中断; 第4行为设置系统主时钟为32M,上一节中main函数最前面做的工作; 1 void InitSensor(void) 2 { 3 DISABLE_ALL_INTERRUPTS(); //关闭所有中断 4 InitClock(); //设置系统主时钟为 32M 5 TR0=0x01; //设置为1来连接温度传感器到SOC_ADC 6 ATEST=0x01; //使能温度传感 7 } 第5行TR0=1为设置温度传感器连接到SOC_ADC: 第6行ATEST=1为使能温度传感器:   其中GetTemperature()函数用来获取温度传感器AD的值: 1 /**************************************************************************** 2 * 名 称: GetTemperature() 3 * 功 能: 获取温度传感器 AD 值 4 * 入口参数: 无 5 * 出口参数: 通过计算返回实际的温度值 6 ****************************************************************************/ 7 float GetTemperature(void) 8 { 9 uint value; 10 11 ADCCON3 = (0x3E); //选择1.25V为参考电压;14位分辨率;对片内温度传感器采样 12 ADCCON1 |= 0x30; //选择ADC的启动模式为手动 13 ADCCON1 |= 0x40; //启动AD转化 14 while(!(ADCCON1 & 0x80)); //等待 AD 转换完成 15 value = ADCL >> 4; //ADCL 寄存器低 2 位无效,由于他只有12位有效,ADCL寄存器低4位无效。网络上很多代码这里都是右移两位,那是不对的 16 value |= (((uint)ADCH) << 4); 17 18 return (value-1367.5)/4.5-5; //根据 AD 值,计算出实际的温度,芯片手册有错,温度系数应该是4.5 /℃ 19 //进行温度校正,这里减去5℃(不同芯片根据具体情况校正) 20 } 其中ADCCON3设置为0x3E,即选择内联参考电压,512采样率(12位有效位数),对片内温度传感器单通道采样! 其中 ADCCON1 |= 0x30 即Start select. Selects the event that starts a new conversion sequence(ADC启动模式为手动) 其中 ADCCON1 |= 0x40 即Start a conversion sequence if ADCCON1.STSEL = 11 and no sequence is running(启动ADC转换) 其中 while(!(ADCCON1 & 0x80)) 即等待一次转换完成 第15、16行:是获得ADC采样的12位有效数据的值保存在value中 15 value = ADCL >> 4; //ADCL 寄存器低 2 位无效,由于他只有12位有效,ADCL寄存器低4位无效。网络上很多代码这里都是右移两位,那是不对的 16 value |= (((uint)ADCH) << 4); 但是value值只是ADC值,并不是温度值,需要转换,代码18、19行就是完成转换:(至于怎么算的我猜测应该有个公式对应!) 18 return (value-1367.5)/4.5-5; //根据 AD 值,计算出实际的温度,芯片手册有错,温度系数应该是4.5 /℃ 19 //进行温度校正,这里减去5℃(不同芯片根据具体情况校正)   4、实验现象 将程序烧入CC2530,用USB连接开发板与PC,可以用串口助手观察zigbee发来的温度数据,当用手触摸芯片时温度会有明显变化:     Zigbee系列文章: [ZigBee] 1、 ZigBee简介 [ZigBee] 2、 ZigBee开发环境搭建 [ZigBee] 3、ZigBee基础实验——GPIO输出控制实验-控制Led亮灭 [ZigBee] 4、ZigBee基础实验——中断 [ZigBee] 5、ZigBee基础实验——图文与代码详解定时器1(16位定时器)(长文) [ZigBee] 6、ZigBee基础实验——定时器3和定时器4(8 位定时器) [ZigBee] 7、ZigBee之UART剖析(ONLY串口发送) [ZigBee] 8、ZigBee之UART剖析·二(串口收发) 本文转自beautifulzzzz博客园博客,原文链接:http://www.cnblogs.com/zjutlitao/p/5677624.html,如需转载请自行联系原作者
文章
传感器 · 编解码 · 物联网 · 芯片
2018-01-20
Davinci DM6446开发攻略——u-boot-1.3.4移植(1)
UBOOT的版本更新速度比较快,截止今天,稳定正式的版本是u-boot-2009.11-rc2,而TI最新的EVM开发包里的UBOOT是1.2.0版本,国内很多公司还一直使用u-boot-1.1.4和u-boot-1.1.6。其实,我们也没必要追风跟上最新版本,程序跑稳定才是最重要的。当然,有兴趣研究研究也不错,毕竟最新版本增加很多实用的功能。在移植之前,我们简单介绍u-boot这些版本架构的变化。从u-boot-1.3.0到u-boot-1.3.2基本上架构是一样的,而从u-boot-1.3.3到u-boot-1.3.4,架构相对u-boot-1.3.2变化比较大。从u-boot-2008.10开始,nand flash驱动变化非常大,u-boot-2009.03增加强大的lzma压缩解压功能,fs支持yaffs2,u-boot-2009.06  nand flash变化更大。到u-boot-2009.11.1增加DM6467 DM365的支持。 关于u-boot-1.3.4的移植,本人的博客也介绍在三星s3c2440上移植过,我们在这里主要针对davinci 平台。由于UBOOT功能很多,要全部把移植的东西立刻写出来,对本人还是有难度,所以中间会先发布有关montavista linux-2.6.18的移植,如何把DSP程序先跑起来,等等。由于本人的主要工作是开发产品,卖卖DM6446核心板、DM6437核心板,及相关开发板,智能视频监控IVR,推推DSP方案,所以博客更新速度比较慢,其实写博客的目的,有很大的部分就是想和全国各地朋友交流技术。同时这里要感谢51CTO的小松管理员,把本人的开发攻略改为推荐博文。回到移植正题,我们一步一步把UBOOT跑起来,把内核也跑起来。鉴于学习的目的,本人这里不提供patch。   第一步:解压和简化UBOOT   从ftp.denx.de下载u-boot-1.3.4.tar.bz2或u-boot-1.3.4-rc2.tar.bz2,然后解压到你的工作目录,很多人解压完后,就马上进入正题,修改makefile什么的,本人觉得不用那么急。首先删除和平台不相关的文件和文件夹,目的让UBOOT更简化,好理解,减少虚拟机的存储空间,便于备份(每次有进展的修改后,备份和修改记录很重要,这是良好习惯): 在顶层目录:把文件avr32_config.mk,blackfin_config.mk,i386_config.mk,m68k_config.mk,microblaze_config.mk,mips_config.mk,nios2_config.mk,nios_config.mk,ppc_config.mk,sh_config.mk,sparc_config.mk删除;文件夹lib_avr32,lib_blackfin,lib_i386,lib_m68k,lib_microblaze,lib_mips,lib_nios,lib_nios2,lib_ppc,lib_sh,lib_sparc,nand_spl,onenand_ipl,其他就不要删了。 在board目录下:只保留davinci文件夹,其他平台板子全部干掉!男人就要狠一点。而davinci也只保留TI 自己的dv-evm文件夹,这也是我们要修改的平台,schmoogie、sffsdr、sonata是其他公司基于davinci上的板子,你可以删掉,也可以参考。当然,还是在board\davinci目录下,你可以COPY dv-evm并改成你公司的板子的名字,然后在顶层修改makefile支持你公司的板子,下一步再说。 在cpu的目录:只保留arm926ejs,其他CPU全部干掉。进入arm926ejs目录,同时把at91sam9、omap、versatile文件夹删除,保留davinci和其他文件。 在include目录:把文件夹asm-avr32、asm-blackfin、asm-i386、asm-m68k、asm-microblaze、asm-mips、asm-nios、asm-nios2、asm-ppc、asm-sh、asm-sparc删除掉。进入configs目录,只保留davinci_dvevm.h,其他*.h文件全部删除调! 做完以上的工作后,UBOOT相当简洁,其实还有一些文件和文件可以再删,不过已经没必要,我们删除的对象是其他不相关的平台。备份一下这个源版本,便于日后自己修改的UBOOT和这个源版本比较。   第二步:链接交叉编译环境   如果你已经看过本人有关《DAVINCI DM6446开发攻略——环境搭建篇》,按里边描述的方法,对交叉编译环境进行搭建,那么下面编译工作就好进行了。 修改顶层makefile: 在144行:把CROSS_COMPILE = arm-linux-改为CROSS_COMPILE = arm_v5t_le-   在282行:把ALL += $(obj)u-boot.srec $(obj)u-boot.bin $(obj)System.map $(U_BOOT_NAND) $(U_BOOT_ONENAND),改为ALL += $(obj)u-boot.srec $(obj)u-boot.bin $(obj)System.map $(U_BOOT_NAND) $(U_BOOT_ONENAND) u-boot.img,就是后面添加u-boot.img   在308行:./tools/mkimage -A $(ARCH) -T firmware -C none \后面,添加和注销以下代码:     -a 0x$(shell grep "T _start" $(TOPDIR)/System.map | awk '{ printf "%s", $$1 }') \     -e 0x$(shell grep "T _start" $(TOPDIR)/System.map | awk '{ printf "%s", $$1 }') \     -n 'u-boot image' -d $< $@ #       -a $(TEXT_BASE) -e 0 \ #       -n $(shell sed -n -e 's/.*U_BOOT_VERSION//p' $(VERSION_FILE) | \ #          sed -e 's/"[  ]*$$/ for $(BOARD) board"/') \ #       -d $< $@ (注意要加tab键) 这里这样做的目的,生成的u-boot.img可以被上篇介绍的UBL给BOOT起来,而u-boot.bin可以被TI提供的uart_load.exe 和uartapp.bin 软件方式(soft boot)启动起来,便于生产和测试。   在源makefile文件2416行:就是davinci_dvevm_config :    unconfig     @$(MKCONFIG) $(@:_config=) arm arm926ejs dv-evm davinci davinci 根据这一行,你可以参考TI 这个做法定义自己的板子,添加自己板子的config,比如加入: davinci_dm6446_config : unconfig     @$(MKCONFIG) $(@:_config=) arm arm926ejs dm6446 davinci davinci 然后在board\davinci目录下,使用 mkdir dm6446 cp –f dv-evm/* dm6446/ 同时进入include/configs/目录,使用cp –f davinci_dvevm.h davinci_dm6446.h 注:其实直接在TI  dv-evm上移植也可以,没必要定义自己的板子和配置。这里只不过给大家举个例子。 编译工作: $make distclean $make davinci_dm6446_config $make 看是否编译全部通过,是否生成u-boot.bin和u-boot.img等文件,同时检查你的交叉编译环境是否建立好,没问题继续往下进行。   第三步:移植板子驱动和配置 1、  修改davinci_dm6446.h 首先说说本人板子的信息:DDR2——256M-Byte,NAND——128M-Byte——2K Page; 通用网口PHY芯片,没有NOR FLASH和ATA。 /*=======*/ /* Board */ /*=======*/ #define DV_EVM #define CFG_USE_NAND(支持NAND) #define CFG_NAND_LARGEPAGE (支持2K Page的NAND) //#define CFG_NAND_SMALLPAGE(表示支持512 字节 Page) //#define CFG_USE_NOR(表示支持NOR FLASH)   。。。。。。。 /*===================*/ /* SoC Configuration */ /*===================*/ #define CONFIG_ARM926EJS                   /* arm926ejs CPU core */ #define CONFIG_SYS_CLK_FREQ     297000000     /* Arm Clock frequency */ #define CFG_TIMERBASE          0x01c21400    /* use timer 0 */ #define CFG_HZ_CLOCK            27000000       /* Timer Input clock freq */ #define CFG_HZ                   1000 #define CONFIG_SOC_DM644X   (SOC为DM644X)   /*====================================================*/ /* EEPROM definitions for Atmel 24C256BN SEEPROM chip */ /* on Sonata/DV_EVM board. No EEPROM on schmoogie.    */ /*====================================================*/ //#define CFG_I2C_EEPROM_ADDR_LEN            2 //#define CFG_I2C_EEPROM_ADDR              0x50 //#define CFG_EEPROM_PAGE_WRITE_BITS      6 //#define CFG_EEPROM_PAGE_WRITE_DELAY_MS  20 (如果你的板子没有I2C接口的EEPROM,把上面的代码注释掉)   /*=============*/ /* Memory Info */ /*=============*/ #define CFG_MALLOC_LEN              (0x10000 + 128*1024)  /* malloc() len */ #define CFG_GBL_DATA_SIZE    128         /* reserved for initial data */ #define CFG_MEMTEST_START  0x80000000    /* memtest start address */ #define CFG_MEMTEST_END            0x81000000    /* 16MB RAM test */ #define CONFIG_NR_DRAM_BANKS 1            /* we have 1 bank of DRAM */ #define CONFIG_STACKSIZE     (256*1024)     /* regular stack */ #define PHYS_SDRAM_1           0x80000000    /* DDR Start */ #define PHYS_SDRAM_1_SIZE 0x10000000    /* DDR size 256MB */ #define DDR_8BANKS                      /* 8-bank DDR2 (256MB) */ 有关DDR Memory这里不需要修改,因为本人的板子是256M的。除非你的板子是128M才改为:SIZE     0x08000000 DDR_4BANKS。   /*====================*/ /* Serial Driver info */ /*====================*/ 串口驱动不用改。   /*===================*/ /* I2C Configuration */ /*===================*/ I2C 驱动可以不用改。也可以注释掉,如果你不想在UBOOT操作任何I2C的动作。   /*==================================*/ /* Network & Ethernet Configuration */ /*==================================*/ 网络配置也不需要修改   /*=====================*/ /* Flash & Environment */ /*=====================*/ 由于最开始我们已经定义好CFG_USE_NAND和CFG_NAND_LARGEPAGE的信息,所以这里也不需要修改;   /*==============================*/ /* U-Boot general configuration */ /*==============================*/ 这里主要定义UBOOT的一些操作,比如命令行显示字符串,delay等待时间的长短,这些根据个人要求修改,不改也可以。   /*===================*/ /* Linux Information */ /*===================*/ UBOOT要把一些参数信息传给内核linux使用,linux内核运行的时候需要这些配置信息,内核能够识别这些字符串信息。先把以下两个定义注释掉, //#define CONFIG_BOOTARGS           xxxxxxxxxxxxxx //#define CONFIG_BOOTCOMMAND     xxxxxxxxxxxxxxx 如果你要从NAND FLASH启动: #define CONFIG_BOOTARGS “mem=120M console=ttyS0,115200n8 noinitrd ip=off root=/dev/mtdblock3”(mtdblock3 表示文件系统放在LINUX内核分区) #define CONFIG_BOOTCOMMAND   " nboot 0x80008000 0x700000"(把linux 内核从FLASH BOOT起来,下面会介绍UBOOT的命令) 如果你还在调试阶段,建议你使用NFS文件系统: #define CONFIG_BOOTARGS “mem=120M console=ttyS0,115200n8 noinitrd rw ip=dhcp root=/dev/nfs nfsroot=192.168.1.251:/home/<useraccount>/nfs/tirootfs,nolock” #define CONFIG_BOOTCOMMAND   " nboot 0x80008000 0x700000"   本人的redhat linux的主机地址是:192.168.1.251,即SERVER IP=192.168.1.251 板子的IP是:192.168.1.188 如果你没有路由器给你分配IP地址,参数行里使用:ip=off mem=120M:本人定义前128M 给linux系统, 后128M 给DSP和图像缓冲区等; nboot 0x80008000 0x700000:讲明本人把内核放在nand 地址为0x700000,通过nand boot的命令把内核从nand 0x700000地址导入DDR 0x80008000地址   /*=================*/ /* U-Boot commands */ /*=================*/ 这里有很多功能的定义,包括#include <config_cmd_default.h>里边定义的,不需要的功能可以使用#undef ,从而减小UBOOT 生成BIN文件的尺寸。比如 #undef CONFIG_CMD_DHCP #undef CONFIG_CMD_DIAG #undef CONFIG_CMD_EEPROM #undef CONFIG_CMD_LOADB   /* loadb */ #undef CONFIG_CMD_LOADS    /* loads */   2、  修改board/davinci/dv-evm/dv_board.c里的有关自己板子的配置 在int board_init(void)函数里,因为本人的板子使用/EM_CS2作为NAND FLASH的片选信号,故在PINMUX0寄存器里,有关AEAW必须关掉。 /* Enable EMAC and AEMIF pins */ //REG(PINMUX0) = 0x80000c1f; REG(PINMUX0) = 0x80000000;只使用EMAC 否则UBOOT 启动不起来。   在int misc_init_r (void)函数里,因为本人没有使用I2C EEPROM存储MAC 地址,所以要注释掉 #if 0 /* Set Ethernet MAC address from EEPROM */ if (i2c_read(CFG_I2C_EEPROM_ADDR, 0x7f00, CFG_I2C_EEPROM_ADDR_LEN, buf, 6)) {      printf("\nEEPROM @ 0x%02x read FAILED!!!\n", CFG_I2C_EEPROM_ADDR); } else     {      tmp[0] = 0xff;      for (i = 0; i < 6; i++)          tmp[0] &= buf[i];        if ((tmp[0] != 0xff) && (getenv("ethaddr") == NULL)) {          sprintf((char *)&tmp[0], "%02x:%02x:%02x:%02x:%02x:%02x",              buf[0], buf[1], buf[2], buf[3], buf[4], buf[5]);          setenv("ethaddr", (char *)&tmp[0]);      } } #endif 一般MAC地址保存到NAND,降低成本。 ……… #if 0 i2c_read (0x39, 0x00, 1, (u_int8_t *)&i, 1); setenv ("videostd", ((i  & 0x80) ? "pal" : "ntsc")); #endif 如果你的板子没有TI 的视频采集芯片TVP5146之类的,上面的功能最好去掉。   3、  网口驱动移植:Cpu/arm926ejs/davinci/ ether.c 因为DM6446芯片上集成EMAC和MDIO,所以直接使用PHY芯片就可以了,驱动就使用UBOOT-1.3.4 TI 默认的驱动,而不是TI EVM使用的PHY_LXT972。直接使用GENERIC PHY,有个地方需要修改,否则网口工作不起来,本人也是从一个网友那里查到类似的信息,在static int dm644x_eth_phy_detect(void)函数里,改成: #if 0 for (i = 0; i < 32; i++) {         if (phy_act_state & (1 << i)) {                if (phy_act_state & ~(1 << i))                       return(0);              /* More than one PHY */                else {                       active_phy_addr = i;                       return(1);                }         } }   return(0);       /* Just to make GCC happy */ #else active_phy_addr = 1; return(1); #endif 由于本人的开发板使用GPIO对PHY芯片进行复位,所以在static int dm644x_eth_hw_init(void)函数里,加入GPIO复位的支持,同时文件头部加入#include <asm/arch/hardware.h>。 可以说, UBOOT移植到这里,基本上可以跑起网络了,TFTP应该没问题了,但是有关如何烧写UBL,烧写UBOOT,LINUX 内核等文件,以后再慢慢聊吧,一步一步来,《UBOOT移植(2)》以后会推出来,下一篇直接到MonaVista linux-2.6.18的移植(1),先让系统通过网络和串口跑起来,由简单到复杂。 继续make一下,生成u-boot.bin和u-boot.img,COPY u-boot.bin到WINDOWS底下,使用uart_load.exe和配套uart.bin文件,就可以把u-boot.bin给跑起来。如果有linux 内核编译出来并使用UBOOT tool目录下的mkimage工具生成 uImage,在UBOOT命令行下: U-Boot > tftp 80008000 uImage ####################################### U-Boot > bootm 80008000 ## Booting kernel from Legacy Image at 80008000 ...    Image Name:   linux-2.6.18    Image Type:   ARM Linux Kernel Image (uncompressed)    Data Size:    1509948 Bytes =  1.4 MB    Load Address: 80008000    Entry Point:  80008040    Verifying Checksum ... OK    XIP Kernel Image ... OK OK   Starting kernel ...   Uncompressing Linux...................................................................................................... done, booting the kernel. Linux version 2.6.18_pro500-davinci_evm-arm_v5t_le (root@xxxxx.com) (gcc version 4.2.0 (MontaVista 4.2.0-16.0.32.0801914 2008-08-30)) #1 PREEMPT Sun Mar 7 01:07:16 CST 2010 CPU: ARM926EJ-S [41069265] revision 5 (ARMv5TEJ), cr=00053177 Machine: DaVinci EVM …………………………………………………………… 当出现上面的信息后,说明以上u-boot-1.3.4的移植是成功的。有兴趣的朋友可以参考以上做法移植u-boot-2009.11-rc2,也可以跑起来,只不过CROSS_COMPILE ?= arm-linux-不是放在顶层makefile了,而是放在lib_arm/config.mk里,改成CROSS_COMPILE = arm_v5t_le-就可以编译了。          补充有关DM6446 BOOT的一点知识:如果板子没有任何程序,RBL会通过串口0发送BOOTME命令上来,运行uart_load.exe,会接到RBL的命令,然后握手通信,下载uart.bin到板子上,并运行起来,uart.bin程序就是小小的BOOT,通过串口0和PC通信,下载u-boot.bin,并把u-boot.bin给运行起来。所以UBOOT移植到上面的步骤,可以进入linux 内核移植的工作了。 本文出自 “集成系统-踏上文明的征程” 博客,请务必保留此出处http://zjbintsystem.blog.51cto.com/964211/282387 更多 0 wuyunzdh、xi2008、tsin 5人 了这篇文章 类别:DM6446┆技术圈( 0)┆阅读( 12705)┆评论( 26) ┆ 推送到技术圈┆返回首页 上一篇 TI Davinci DM6446开发攻略——UBL移植 下一篇 Davinci DM6446开发攻略——linux-2.6.18移植 相关文章 使用DataRowView时需要导入命名空间 Symbian v3 nokia 开发平台搭建 堆和栈的区别 MapEasy开发体会(二)——平滑移动 操作系统开发环境及工具 ClassPathXmlApplicationContext 和FileSyst.. 文章评论  <<   1   2   >>   页数 ( 1/2 )   [1楼]      lyblyxj 回复 2010-03-11 15:59:39 不错!学习了呵呵 很有难度的东西啊 [2楼]      邹盼盼 回复 2010-03-11 23:25:22 几个月前自己也装过一个“有奔头”玩了一下,感觉还可以。 [3楼]楼主      zjb_integrated 回复 2010-03-12 09:19:20 UBOOT在开源嵌入产品设计中,地位越来越重要,功能越来越丰富。 [4楼]      282409975 回复 2010-04-12 11:00:06 你好,我也是做dm6446,我们公司买的板子是ti原厂的,就是那个dvevm,我现在想移植下u-boot-1.3.4,就把根目录下的那个makefile的CROSS_COMPILE=arm_v5t_le-,然后就直接make distclean;make davinci_dvevm_config;make这样也生成了u-boot.bin以后,用DVFlasher下载到板子上了,发现uboot启动不了,打印信息如下:TI UBL Version: 1.12, Flash type: NANDBooting PSP Boot LoaderPSPBootMode = NANDStarting NAND Copy...Initializing NAND flash...Valid MagicNum found.NAND Boot success.  DONE然后就停止了,请问下为什么???谢谢了,我的QQ是451686458,谢谢了 [5楼]楼主      zjb_integrated 回复 2010-04-13 18:23:07 这个原因有很多,你要跟踪是否运行到board.c里的start_armboot()函数,等等。你可以参考人家给你的UBOOT是如何编译出u-boot.bin还是其他加有头信息的二进制文件 [6楼]      [匿名]晓风 回复 2010-05-12 12:15:12 你好,我的dvflasher不能运行。 [7楼]楼主      zjb_integrated 回复 2010-05-12 15:29:20 抱歉,没用过dvflasher,我们肯定不用这个dvflasher。 [8楼]      hejing_032 回复 2010-09-16 09:51:08 下载uboot之前有个flashwrite到什么地方能够下载源码?这个文件时通用的还是根据硬件决定 [9楼]楼主      zjb_integrated 回复 2010-09-16 10:56:13 这个估计是TI的DSP out文件,我们不关心这个。 [10楼]      wuyunzdh 回复 2011-01-03 18:27:12 你好,我的的uboot用老的环境是可以编译通过的并可以正常使用,最近从新装了一个编译环境,也是编译过去的,但是,下载到NORflash中美任何反应,但是可以用软件可以启动启动起来!!!请问这是什么原因引起来的!! [11楼]楼主      zjb_integrated 回复 2011-01-04 16:54:43 我们没有测试过NOR FLASH的应用。你的新编译环境.bashrc文件是否设置:PATH="/opt/mv_pro_5.0.0/montavista/pro/devkit/arm/v5t_le/bin:/opt/mv_pro_5.0.0/montavista/pro/bin:/opt/mv_pro_5.0.0/montavista/common/bin:$PATH"的路径 [12楼]      wuyunzdh 回复 2011-01-06 11:18:09 谢谢你的回复,路径肯定个配置了,在.bash_profile不然应该也编译不过去呀,是不是?这是我新环境的:cat /root/.bash_profile # .bash_profile# Get the aliases and functionsif [ -f ~/.bashrc ]; then    . ~/.bashrcfi# User specific environment and startup programsPATH=$PATH:$HOME/binPATH=/opt/montavista/pro/devkit/arm/v5t_le/bin:/opt/montavista/pro/bin:/opt/montavista/common/bin:/opt/3.4.1/bin:/opt/arm-920t-3.4.4/bin:$PATHexport PATHunset USERNAME下面是我老环境的:[root@myhost ~]# cat /root/.bash_profile alias rm='rm -i'alias ll='ls -l --color'alias cp='cp -i'alias mv='mv -i'PATH="${PATH}":/usr/local/arm/3.4.1/bin:/usr/local/920t_le/bin:/usr/local/bin:/opt/mv_pro_4.0/montavista/pro/devkit/arm/v5t_le/bin:/opt/mv_pro_4.0/montavista/common/bin:/opt/mv_pro_4.0/montavista/pro/bin:/opt/hisilicon/toolchains/arm-uclibc-linux-soft/bin:/opt/uClinux/bfin-uclinux/bin://opt/uClinux/bfin-linux-uclibc/binexport PATH. $HOME/.bashrc [13楼]      wuyunzdh 回复 2011-01-06 16:06:43 谢谢你的回复,肯定设置了,不然也编译不过去,我的是放在.bash_profile中的。新的编译环境见下:[root@localhost bin]# cat /root/.bash_profile # .bash_profile# Get the aliases and functionsif [ -f ~/.bashrc ]; then    . ~/.bashrcfi# User specific environment and startup programsPATH=$PATH:$HOME/binPATH=/opt/montavista/pro/devkit/arm/v5t_le/bin:/opt/montavista/pro/bin:/opt/montavista/common/bin:/opt/3.4.1/bin:/opt/arm-920t-3.4.4/bin:$PATHexport PATHunset USERNAME老的编译环境是:[root@myhost ~]# cat /root/.bash_profile alias rm='rm -i'alias ll='ls -l --color'alias cp='cp -i'alias mv='mv -i'PATH="${PATH}":/usr/local/arm/3.4.1/bin:/usr/local/920t_le/bin:/usr/local/bin:/opt/mv_pro_4.0/montavista/pro/devkit/arm/v5t_le/bin:/opt/mv_pro_4.0/montavista/common/bin:/opt/mv_pro_4.0/montavista/pro/bin:/opt/hisilicon/toolchains/arm-uclibc-linux-soft/bin:/opt/uClinux/bfin-uclinux/bin://opt/uClinux/bfin-linux-uclibc/binexport PATH. $HOME/.bashrc
文章
Linux · Shell · 芯片 · 内存技术 · Perl
2013-07-22
Fabric中的Transient Data与Private Data
在Hyperledger Fabric中有两个相关的概念:私有数据(Private Data)和暂态数据(Transient Data)。本文提供四个示例程序,分别对应私有数据和暂态数据的四种组合使用方式,并通过观察账本的交易以及世界状态数据库,理解为什么在使用私有数据时应当采用暂态数据作为输入。 从技术上讲,私有数据和暂态数据是两个不同的概念。私有数据是考虑如何在通道的部分机构之间共享数据,而暂态数据则是使用私有数据时的一种输入方法。有趣的是,这两者并没有直接的关系,虽然在现实中,当我们需要安全的使用私有数据时,通常都应当使用暂态数据作为输入。 Hyperledger Fabric区块链开发教程: Fabric Node.js开发详解 | Fabric Java开发详解 | Fabric Golang开发详解 1、基本概念 先让我们重温一下在演示程序中将要用到的一些核心概念。 账本:在Hyperledger Fabric中,当一个peer节点加入通道后,就会维护一个账本的副本。账本包含一个用于保存区块的区块链数据结构,以及一个用于保存最新状态的世界状态数据库。当peer节点从排序服务收到一个新的区块并且验证成功后,peer节点就将区块提交进账本,并根据区块中每个交易的RWSet来更新相应的世界状态。 基于共识机制,在一个通道中,不同的peer节点上的账本的大部分都是一致的。不过有一个例外,Fabric支持在部分通道之间存在的私有数据。 私有数据:在一个通道内有时会需要仅在部分机构之间共享数据。Hyperledger Fabric引入私有数据的目的就是满足这一需求。通过定义数据集,我们可以声明私有数据实现的机构子集,同时所有节点(包括在机构子集之外的其他节点)都会保存私有数据哈希的记录以便作为数据存在的证据或者用于审计目的。 可以利用链码API来使用私有数据。我们在示例代码中使用PutPrivateData和GetPrivateData这两个API。作为对比,我们使用PutState和GetState完成对公共状态的读写。 在Fabric 2.0中,会为每个机构准备一个隐含的数据集。本教程将利用这个隐含的数据集,因此我们不需要单独的数据集定义文件。 Fabric的账本结构以及私有数据的位置如下图所示,在后面的演示代码中,我们将查看交易以及世界状态数据库的内容: 暂态数据:许多链码函数在被调用时需要额外的输入数据。在大多数情况下我们会在调用函数时传入一组参数,而链码参数,包括函数名和函数参数,都会作为有效交易的一部分保存在区块内,因此将永久性的存在于账本中。如果出于某种原因我们不希望在链上永久保存参数列表,我们就可以使用暂态数据。暂态数据是一种可以向链码函数传参但不需要将其保存在交易记录中的输入方法。当使用暂态数据时,需要一个特殊的链码API即GetRansient方法来读取暂态数据。我们接下来将在演示代码中看到。 因此,链码的设计需要根据业务需求设计链码,决定哪些数据应当作为正常参数输入并记录在交易中,哪些数据应当作为暂态数据输入而不必记录在链上。 私有数据与暂态数据的关系:私有数据与暂态数据并不是直接相关的,我们可以只使用私有数据而不利用暂态数据作为输入,也可以在非私有数据中使用暂态数据。因此我们可以得到示例中的四种应用场景并观察每种场景下的结果。 私有数据与暂态数据的关系如下图所示: 输入方法的选择以及是否是否私有数据依赖于具体的业务需求,因为链码函数反应了真实的业务交易。我们可以选择普通的参数列表、暂态数据或者同时使用两种方式的输入,也可以向公共状态或者私有数据集写入数据。我们需要的是正确地选择需要使用的链码API。 2、应用场景概述 如前所述,我们将私有数据和暂态数据进行2X2的组合,概述如下: 场景1:不使用私有数据,不输入暂态数据 在这种场景下,数据被写入账本中的公开状态部分,所有的peer节点将保存相同的账本数据。在链码中使用PutState和GetState来访问这部分数据。当我们调用链码时,使用普通的参数列表指定输入数据。 当通道中的所有机构都需要相同的数据时,适合采用这种方式。 场景2:使用私有数据,不输入暂态数据 在这种场景下,数据被写入账本中的私有数据部分,并且只有在私有数据集定义的机构的peer节点上会保存数据。在链码中我们使用PutPrivateData和GetPrivateData来访问私有数据集。当我们调用链码时,使用普通的参数列表指定输入数据。 当应用中存在对部分数据的隐私需求,而对于输入数据不敏感时,可以采用这种方式。 场景3:使用私有数据,输入暂态数据 类似于场景2,数据被写入账本中的私有数据部分,只有在私有数据集定义中的那些peer节点会保存这部分私有数据。在链码中,我们使用PutPrivateData和GetPrivateData来访问数据集。当我们调用链码时,采用暂态数据作为输入,因此在链码中我们需要使用GetTransient来处理输入数据。 场景4:不使用私有数据,输入暂态数据 这是一个想象的场景,只是用来表明在不使用私有数据时,也可以使用暂态数据作为输入。我们在链码中使用PutState和GetState来将数据保存到账本的公共状态,而采用暂态数据作为链码调用的输入参数。和之前一样,我们在链码中使用GetTransient方法来处理输入数据。 3、演示环境搭建 在这些演示中我们使用Fabric 2.0,使用First Network作为Fabric网络。我们采用CouchDB选项启动网络,以便于查看世界状态数据库的内容。我们将重点关注peer0.org1.example.com (couchdb port 5984) 和 peer0.org2.example.com (couchdb port 7984) 以便查看两个机构中的节点的行为。 在私有数据部分,我们使用Org1内置的隐含私有数据集(_implicit_org_Org1MSP)。只有Org1中的peer节点可以保存私有数据,而Org1和Org2中的节点都可以保存数据哈希。 我们修改了fabric-samples中的SACC链码。SACC链码有两个函数set和get。为了展示私有数据和暂态数据,我们创建以下函数: setPrivate:使用相同的参数列表,数据保存在Org1隐含的私有数据集 setPrivateTransient:使用暂态数据输入,数据保存在Org1隐含的私有数据集 setTransient:使用暂态数据输入,数据保存在公共状态 getPrivate:提取保存在Org1隐含的私有数据集中的数据 修改后的SACC链码如下: /* * Copyright IBM Corp All Rights Reserved * * SPDX-License-Identifier: Apache-2.0 */ package main import ( "encoding/json" "fmt" "github.com/hyperledger/fabric-chaincode-go/shim" "github.com/hyperledger/fabric-protos-go/peer" ) // SimpleAsset implements a simple chaincode to manage an asset type SimpleAsset struct { } // Init is called during chaincode instantiation to initialize any // data. Note that chaincode upgrade also calls this function to reset // or to migrate data. func (t *SimpleAsset) Init(stub shim.ChaincodeStubInterface) peer.Response { // Get the args from the transaction proposal args := stub.GetStringArgs() if len(args) != 2 { return shim.Error("Incorrect arguments. Expecting a key and a value") } // Set up any variables or assets here by calling stub.PutState() // We store the key and the value on the ledger err := stub.PutState(args[0], []byte(args[1])) if err != nil { return shim.Error(fmt.Sprintf("Failed to create asset: %s", args[0])) } return shim.Success(nil) } // Invoke is called per transaction on the chaincode. Each transaction is // either a 'get' or a 'set' on the asset created by Init function. The Set // method may create a new asset by specifying a new key-value pair. func (t *SimpleAsset) Invoke(stub shim.ChaincodeStubInterface) peer.Response { // Extract the function and args from the transaction proposal fn, args := stub.GetFunctionAndParameters() var result string var err error if fn == "set" { result, err = set(stub, args) } else if fn == "setPrivate" { result, err = setPrivate(stub, args) } else if fn == "setTransient" { result, err = setTransient(stub, args) } else if fn == "setPrivateTransient" { result, err = setPrivateTransient(stub, args) } else if fn == "getPrivate" { result, err = getPrivate(stub, args) } else { // assume 'get' even if fn is nil result, err = get(stub, args) } if err != nil { return shim.Error(err.Error()) } // Return the result as success payload return shim.Success([]byte(result)) } // Set stores the asset (both key and value) on the ledger. If the key exists, // it will override the value with the new one func set(stub shim.ChaincodeStubInterface, args []string) (string, error) { if len(args) != 2 { return "", fmt.Errorf("Incorrect arguments. Expecting a key and a value") } err := stub.PutState(args[0], []byte(args[1])) if err != nil { return "", fmt.Errorf("Failed to set asset: %s", args[0]) } return args[1], nil } func setPrivate(stub shim.ChaincodeStubInterface, args []string) (string, error) { if len(args) != 2 { return "", fmt.Errorf("Incorrect arguments. Expecting a key and a value") } err := stub.PutPrivateData("_implicit_org_Org1MSP", args[0], []byte(args[1])) if err != nil { return "", fmt.Errorf("Failed to set asset: %s", args[0]) } return args[1], nil } func setTransient(stub shim.ChaincodeStubInterface, args []string) (string, error) { type keyValueTransientInput struct { Key string `json:"key"` Value string `json:"value"` } if len(args) != 0 { return "", fmt.Errorf("Incorrect arguments. Expecting no data when using transient") } transMap, err := stub.GetTransient() if err != nil { return "", fmt.Errorf("Failed to get transient") } // assuming only "name" is processed keyValueAsBytes, ok := transMap["keyvalue"] if !ok { return "", fmt.Errorf("key must be keyvalue") } var keyValueInput keyValueTransientInput err = json.Unmarshal(keyValueAsBytes, &keyValueInput) if err != nil { return "", fmt.Errorf("Failed to decode JSON") } err = stub.PutState(keyValueInput.Key, []byte(keyValueInput.Value)) if err != nil { return "", fmt.Errorf("Failed to set asset") } return keyValueInput.Value, nil } func setPrivateTransient(stub shim.ChaincodeStubInterface, args []string) (string, error) { type keyValueTransientInput struct { Key string `json:"key"` Value string `json:"value"` } if len(args) != 0 { return "", fmt.Errorf("Incorrect arguments. Expecting no data when using transient") } transMap, err := stub.GetTransient() if err != nil { return "", fmt.Errorf("Failed to get transient") } // assuming only "name" is processed keyValueAsBytes, ok := transMap["keyvalue"] if !ok { return "", fmt.Errorf("key must be keyvalue") } var keyValueInput keyValueTransientInput err = json.Unmarshal(keyValueAsBytes, &keyValueInput) if err != nil { return "", fmt.Errorf("Failed to decode JSON") } err = stub.PutPrivateData("_implicit_org_Org1MSP", keyValueInput.Key, []byte(keyValueInput.Value)) if err != nil { return "", fmt.Errorf("Failed to set asset") } return keyValueInput.Value, nil } // Get returns the value of the specified asset key func get(stub shim.ChaincodeStubInterface, args []string) (string, error) { if len(args) != 1 { return "", fmt.Errorf("Incorrect arguments. Expecting a key") } value, err := stub.GetState(args[0]) if err != nil { return "", fmt.Errorf("Failed to get asset: %s with error: %s", args[0], err) } if value == nil { return "", fmt.Errorf("Asset not found: %s", args[0]) } return string(value), nil } // Get returns the value of the specified asset key func getPrivate(stub shim.ChaincodeStubInterface, args []string) (string, error) { if len(args) != 1 { return "", fmt.Errorf("Incorrect arguments. Expecting a key") } value, err := stub.GetPrivateData("_implicit_org_Org1MSP", args[0]) if err != nil { return "", fmt.Errorf("Failed to get asset: %s with error: %s", args[0], err) } if value == nil { return "", fmt.Errorf("Asset not found: %s", args[0]) } return string(value), nil } // main function starts up the chaincode in the container during instantiate func main() { if err := shim.Start(new(SimpleAsset)); err != nil { fmt.Printf("Error starting SimpleAsset chaincode: %s", err) } } 4、Fabric私有数据和暂态数据演示 首先启动First Network,不要部署默认链码,启用CouchDB选项: cd fabric-samples/first-network ./byfn.sh up -n -s couchdb 当看到所有容器(5个排序节点,4个peer节点,4个couchdb,一个CLI)启动后: 创建一个新的链码目录: cd fabric-samples/chaincode cp -r sacc sacc_privatetransientdemo cd sacc_privatetransientdemo 然后使用上面的链码替换sacc.go。 在第一次运行之前我们需要先加载依赖的模块: GO111MODULE=on go mod vendor 最后我们使用lifecycle chaincode命令部署这个链码。 5、场景1演示:不使用Fabric私有数据和暂态数据输入 场景1时最常用的一种:使用普通的参数列表作为链码方法的输入,然后将其保存在公共状态中,所有peer节点持有完全相同的数据。我们调用链码的set和get方法。 docker exec cli peer chaincode invoke -o orderer.example.com:7050 --tls \ --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem \ --peerAddresses peer0.org1.example.com:7051 \ --tlsRootCertFiles /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt \ --peerAddresses peer0.org2.example.com:9051 \ --tlsRootCertFiles /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt \ -C mychannel -n mycc -c '{"Args":["set","name","alice"]}' docker exec cli peer chaincode query -C mychannel -n mycc -c '{"Args":["get","name"]}' 结果如下: 我们首先查看世界状态。这个数据同时保存在peer0.org1.example.com 和peer0.org2.example.com的公共状态中(mychannel_mycc)。 当查看区块链中的交易记录时,我么看到WriteSet中的键/值对是:name/alice,采用base64编码。 我们也可以看到调用链码时的参数列表,3个base64编码的参数分别是:set、name和alice。 和预期一样,RWSet更新了公开状态,输入参数被记录在交易中。 6、场景2演示:使用私有数据,不适用暂态数据输入 在场景2中,链码调用还是采用普通的参数列表,数据则保存在Org1的私有数据集。我们使用setPrivate和getPrivate访问链码 docker exec cli peer chaincode invoke -o orderer.example.com:7050 --tls \ --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem \ --peerAddresses peer0.org1.example.com:7051 \ --tlsRootCertFiles /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt \ --peerAddresses peer0.org2.example.com:9051 \ --tlsRootCertFiles /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt \ -C mychannel -n mycc -c '{"Args":["setPrivate","name","bob"]}' docker exec cli peer chaincode query -C mychannel -n mycc -c '{"Args":["getPrivate","name"]}' 我们首先查看世界状态。在peer0.org1.example.com中我们可以看到数据保存为私有数据,创建了两个数据库:一个用于实际数据,一个用于数据哈希。在peer0.org2.example.com上,我们看到只有哈希文件。 内容的哈希在两个机构的节点上都是一样的。此外,在peer0.org1.example.com中我们可以看到调用链码时输入的数据。 当查看区块链中的交易记录时,我们看到没有RWSet。相反我们看到数据被应用于Org1隐含的数据集,它指向已经保存在peer中的数据,通过hash得以保护这部分数据的隐私。 我们可以看到调用链码时的参数列表。3个base64编码的参数分别是:setPrivate、name、bob。 如果我们关系数据隐私,这可能存在问题。一方面数据保存在私有数据集中,这样只有限定的机构节点可以保存。另一方面,这部分隐私数据的链码输入却还是公开可见的,并且永久保存在所有peer节点的区块链中。如果这不是你期望的,那么我们还需要将输入数据隐藏掉。这就是使用暂态数据输入的原因。 6、场景3演示:使用私有数据和暂态数据输入 如果你希望确保数据输入不会保存在链上,那么场景3是推荐的方式。在这种场景下,采用暂态数据输入,并且数据保存在Org1的私有数据集。我们使用setPrivateTransient和getPrivate方法访问链码: 在我们的链码中,我们实现函数时将暂态数据编码为特定的JSON格式 {“key”:”some key”, “value”: “some value”} (链码134–137行)。我们也要求暂态数据包含一个keyvalue键(链码149行)。为了在命令行调用中使用暂态数据,我们需要首先将其进行base64编码。 export KEYVALUE=$(echo -n "{\"key\":\"name\",\"value\":\"charlie\"}" | base64 | tr -d \\n) docker exec cli peer chaincode invoke -o orderer.example.com:7050 --tls \ --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem \ --peerAddresses peer0.org1.example.com:7051 \ --tlsRootCertFiles /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt \ --peerAddresses peer0.org2.example.com:9051 \ --tlsRootCertFiles /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt \ -C mychannel -n mycc -c '{"Args":["setPrivateTransient"]}' \ --transient "{\"keyvalue\":\"$KEYVALUE\"}" docker exec cli peer chaincode query -C mychannel -n mycc -c '{"Args":["getPrivate","name"]}' 结果如下: 同样,我们首先查看世界状态。这类似于我们在场景2中看到的内容。实际的数据仅在peer0.org1.example.com保存,而哈希则在两个peer节点中都有保存。注意修订版本的值目前是2,而在场景2中的第一次修订的值是1。是链码调用促成了对数据的修订。 类似于场景2,在区块链上的交易记录中,我们可以看到没有Write Set。 我们也可以看到没有调用链码的参数列表。唯一的参数是链码函数名,setPrivateTransient。具体的调用数据{“key”:”name”, “value”:”charlie”}没有出现在区块链上。 我们看到私有数据和隐私数据的组合提供了某种程度的数据隐私。 7、场景4演示:不适用私有数据,使用暂态数据输入 最后我们看一下想象的这个场景的演示。在场景3中,采用暂态数据作为数据,然后保存在账本的公开状态。我们使用setTransient和get访问链码: export KEYVALUE=$(echo -n "{\"key\":\"name\",\"value\":\"david\"}" | base64 | tr -d \\n) docker exec cli peer chaincode invoke -o orderer.example.com:7050 --tls \ --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem \ --peerAddresses peer0.org1.example.com:7051 \ --tlsRootCertFiles /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt \ --peerAddresses peer0.org2.example.com:9051 \ --tlsRootCertFiles /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt \ -C mychannel -n mycc -c '{"Args":["setTransient"]}' \ --transient "{\"keyvalue\":\"$KEYVALUE\"}" docker exec cli peer chaincode query -C mychannel -n mycc -c '{"Args":["get","name"]}' 结果如下: 可以看到公开状态被更新,两个节点目前有同样的数据。注意修订版本更新为2. 我们看到Write Set中的键/值对是name和david,base64编码。 我们没有看到参数中的输入数据,只看到调用的方法名setTransient。 原文链接:Hyperledger Fabric私有数据与暂态数据 — 汇智网
文章
JSON · JavaScript · Java · API · Go · 区块链 · 数据库 · 数据格式 · Docker · 容器
2020-03-16
Ceph分布式存储实战.
云计算与虚拟化技术丛书 Ceph分布式存储实战 Ceph中国社区 著 图书在版编目(CIP)数据 Ceph分布式存储实战/Ceph中国社区著. —北京:机械工业出版社,2016.11 (云计算与虚拟化技术丛书) ISBN 978-7-111-55358-8 I. C… II. C… III. 分布式文件系统 IV. TP316 中国版本图书馆CIP数据核字(2016)第274895号 Ceph分布式存储实战 出版发行:机械工业出版社(北京市西城区百万庄大街22号 邮政编码:100037) 责任编辑:高婧雅 责任校对:殷 虹 印  刷: 版  次:2016年12月第1版第1次印刷 开  本:186mm×240mm 1/16 印  张:19.5 书  号:ISBN 978-7-111-55358-8 定  价:69.00元 凡购本书,如有缺页、倒页、脱页,由本社发行部调换 客服热线:(010)88379426 88361066 投稿热线:(010)88379604 购书热线:(010)68326294 88379649 68995259 读者信箱:hzit@hzbook.com 版权所有·侵权必究 封底无防伪标均为盗版 本书法律顾问:北京大成律师事务所 韩光/邹晓东 Praise 本书赞誉 正如OpenStack日渐成为开源云计算的标准软件栈,Ceph也被誉为软件定义存储开源项目的领头羊。细品本书,慢嗅“基础理论讲解简明扼要,技术实战阐述深入全面”之清香。千言万语,不如动手一战。Ceph爱好者们,请启动机器,拿起本书,早日踏上Ceph专家之路。 —陈绪,博士,英特尔中国云计算战略总监,中国开源软件推进联盟常务副秘书长,2015年中日韩东北亚开源论坛最高奖项“特别贡献奖”获得者 Ceph是主流的开源分布式存储操作系统。我们看到越来越多的云服务商和企业用户开始考察Ceph,把它作为构建“统一存储”和“软件定义存储”的可信赖解决方案。Ceph的CRUSH算法引擎,聪明地解决了数据分布效率问题,奠定了它胜任各种规模存储池集群的坚实基础。过去5年,在Red Hat、Intel等软硬件基础设施领导者的推动下,Ceph开源社区有超过10倍的增长—不仅仅具备广泛的硬件兼容性体系,大量上下游厂商添砖加瓦,也吸引了很多运营商、企业用户参与改进。XSKY很荣幸作为社区的一员,见证与实践着Ceph帮助用户进行存储基础架构革新的历程。我们欣喜地看到由Ceph中国社区撰写的本书的问世,这是一部在立意和实践方面均不输于同期几本英文书籍的作品,深入浅出,娓娓道来,凝结了作者的热情和心血。我们诚挚地向业内技术同行和Ceph潜在用户推荐此书!愿Ceph中国社区在推进开源事业的道路上取得更大的成功! —胥昕,XSKY星辰天合(北京)数据科技有限公司CEO 在开源软件定义存储(SDS)领域,Ceph是当之无愧的王者项目。随着IaaS技术的火热发展,越来越多的用户开始在生产环境中部署SDS。伴随着基础设施开源化的趋势,很多用户希望部署开源的SDS,Ceph成为了他们的第一选择。跟大多数开源软件项目一样,Ceph具有优秀的技术特性,但也存在着部署难、运维难的问题。在使用开源Ceph发行版时,用户需要对Ceph的实现原理、部署运维最佳实践有一定了解,才能在生产环境中稳定使用这一开源技术。长期以来,中文技术社区一直没有一本对Ceph的原理、生产实践、运维实践进行剖析的好书,本书的出现填补了这一空白。该书不仅从原理上对Ceph的核心技术进行了讲解,还介绍了将Ceph部署在OpenStack、ZStack等IaaS软件上的生产环境实践,最后着重介绍了Ceph的运维和排错,是一本不可多得的Ceph百科全书,是Ceph用户、IaaS开发人员必备的一本SDS工具书。 —张鑫,前CloudStack核心初创人员,开源IaaS项目ZStack创始人 开源系统是Linux的世界,开源管理平台是OpenStack的世界,开源存储是Ceph的世界。软件定义存储(SDS)是存储发展的必然趋势,最好的开源软件定义存储方案无疑就是Ceph,我身边好多朋友已经开始在生产环境中大量部署Ceph,Ceph也表现出卓越的稳定性和性能。但是Ceph的搭建和使用门槛比较高,很高兴看到Ceph中国社区组织编写的本书的出版,为Ceph搭建学习降低了门槛,是国内Ceph爱好者的福音。Ceph中国社区为Ceph在中国的普及做了大量非常重要的工作,本书是一个里程碑,相信Ceph中国社区会继续为Ceph做出更多的贡献。 —肖力,KVM云技术社区创始人 Ceph作为分布式存储开源项目的杰出代表,在各个领域已经得到了充分验证,可以预见,在未来的几年时间内,Ceph一定会得到更广泛的应用。本书作为国内为数不多阐述Ceph的著作,从基础、原理和实践多个层面进行了详尽讲解,是一本快速了解并掌握Ceph的力作。 —孙琦(Ray),北京休伦科技有限公司CTO 软件看开源,SDS看Ceph。Ceph是目前影响力最大的开源软件定义存储解决方案,其应用范围涵盖块存储、文件存储和对象存储,广泛被业界公司所采用。 很荣幸能在第一时间读到这本书,该书从Ceph的部署开始,阐明了Ceph各个主要模块及其功能,介绍了Ceph在块存储、文件存储和对象存储不同场景下的应用方式,指明了Ceph性能调优的方案。尤其是最后的生产环境应用案例,解了使用Ceph的技术人员的燃眉之急,给出了常见问题的解决思路,造福于整个开源云存储界。 无论是售前专家、开发架构师还是运维负责人,读一读Ceph中国社区编写的这本书,都可以细细地品一品,积极地拥抱开源、把握云存储的未来。 —楼炜,盘古数据资深云和大数据架构师 作为一名早期研究Ceph的人员,很高兴看到Ceph在近几年如火如荼的发展状态。在我刚接触Ceph时,很渴望得到系统化的介绍、培训或指导。但当时Ceph在国内还处于小众研究状态,高人难寻,深入全面的介绍资料更是没有。Ceph中国社区的朋友们出版这本介绍Ceph的书籍,为Ceph的广大研究者和爱好者做了一件很有意义的事情。相信本书一定能够成为Ceph发展的强力助推器! —温涛,新华三集团(H3C公司)ONEStor产品研发负责人  Ceph因其先进的设计思想,良好的可靠性、可扩展性,成为存储领域的研究热点,被誉为“存储的未来”,得到广泛的部署。由Ceph中国社区组织编写的这本书是国内第一本系统介绍Ceph的书籍,全书从Ceph的历史、架构、原理到部署、运维、应用案例,讲解全面深入,可操作性强。本书非常适合想要了解Ceph、使用Ceph的读者阅读,也可供分布式存储系统设计者参考。  —汪黎,KylinCloud团队存储技术负责人,Ceph代码贡献者 从实用价值上看,本书从Ceph的基本原理、Ceph的安装部署和Ceph的应用案例等方面进行了深入浅出的讲解,理论和实践完美结合,是难得的系统阐述Ceph的教科书,是广大Ceph爱好者的福音。 从理论价值上看,Ceph是超融合架构下首选的开源存储方案,本书详细阐述了存储相关的基本原理,不仅让你知其然,更能让你知其所以然。 —刘军卫,中国移动苏州研发中心云计算产品部技术总监 Ceph是当前最热门的分布式存储系统,在云技术领域获得了广泛的欢迎和支持。但是目前国内与此相关的书籍非常少。如果想学习Ceph,想更深入地了解Ceph,而又对密密麻麻的英文望而生畏,那么现在救星来了!本书从系统原理、基本架构、性能优化、应用实践、运维部署等各个方面对Ceph进行了全方位的介绍和分析。这是一本从入门到精通的好书,值得拥有!  —李响,博士,中兴通讯股份有限公司IaaS开源项目总监 一群开源的人用开源的方式去做一件开源的事儿,我想没有比这更合适的事情了。作为一名有着近10年的分布式存储研发和软件定义存储(SDS)产品及技术规划的先行者与践行者,很高兴看到同样已经十几岁的Ceph在众人之力和众人之智的推动下,吐故纳新,正以日新月异的速度蓬勃发展。 Ceph是每一个软件定义存储相关从业人员关注的重点,Ceph中国社区把国内广大的Ceph爱好者聚集到一起,分享“踩坑”的经验,承担了95%以上Ceph文档的本土化(翻译)工作,对Ceph在国内的发展扮演着非常重要的推动作用,非常感谢Ceph中国社区的每一位贡献者。 杉岩数据作为一家商用Ceph解决方案和服务提供商,随着Ceph商用产品越来越多地在企业级用户的生产环境中应用,一直期待能有一本适合国人阅读习惯且浅显易懂的Ceph书籍,让更多的人了解Ceph的功能特性。当我有幸阅读过此书后,我强烈建议广大SDS相关从业人员阅读此书,你一定会收获良多! —陈坚,深圳市杉岩数据技术有限公司总经理 Ceph从2012年开始拥抱OpenStack到现在已经成为OpenStack的首选后端存储。很高兴看到Ceph中国社区把国内广大的Ceph爱好者聚集到一起,分享技术与经验,对Ceph在国内的发展起到了非常重要的推动和落地作用。也一直期待国内能有一本Ceph入门相关的书籍,看到Ceph中国社区出版的这本书,很是欣慰。国内Ceph资料从稀缺到逐渐完善,这其中离不开Ceph中国社区的贡献和努力。 —朱荣泽,上海优铭云计算有限公司存储架构师 国内第一本对Ceph进行全面剖析的书籍,并辅以大量的实战操作,内容由浅入深,特别适合希望对Ceph进行系统性学习的工程师,是国内Ceph爱好者的福音。 —田亮,北京海云捷迅科技有限公司解决方案总监  Ceph是开源分布式存储领域的一颗当红明星,随着OpenStack如火如荼的发展,Ceph也逐渐成为了OpenStack的首选后端存储。国内目前缺乏Ceph入门以及相关运维书籍,Ceph中国社区出版的这本书填补了国内Ceph的空白,是国内Ceph爱好者的福音。 —陈沙克,浙江九州云信息科技有限公司副总裁 由于其出色的系统设计,Ceph正广泛部署于各大云计算厂商的生产环境中,为用户提供对象存储、云硬盘和文件系统存储服务。本书理论联系实际,除介绍Ceph的设计理念和原理之外,还系统介绍了Ceph的编程接口、上线部署、性能调优及应用场景,有利于读者快速掌握Ceph的运维和基于Ceph的开发。此书提供了深入理解云存储的捷径。 —吴兴义,乐视云技术经理 Ceph诞生于传统存储行业正处于巅峰之时,短短十多年间,闪存(如SSD)与软件定义存储(SDS)就联手颠覆了存储行业。作为软件定义存储领域的旗帜性项目,Ceph肩负着业界的厚望,也需要“与时俱进”,继续改进和完善,满足目标用户越来越高的要求。 众所周知,Ceph是个开源项目,成型于硬盘仍为主导的年代。如今,市场和用户需要Ceph更加产品化,同时充分利用闪存等固态存储介质带来的性能红利。这就要求业界精简过时的代码和不必要的中间层,并为Ceph加入新的功能和特性,对此,我个人归纳为“先做减法,再做加法”。要达到上述目标,必须让更多的人关注和了解Ceph,特别是吸引有一定存储经验和积累的人或组织加入Ceph生态圈。作为一本不可多得的系统介绍Ceph的书籍,本书的出版正逢其时,定会为Ceph生态的壮大贡献更多的有生力量。 —张广彬,北京企事录技术服务公司创始人 序 Preface Ceph是目前开源世界在存储领域的里程碑式项目,它所带来的分布式、无中心化设计是目前众多商用分布式存储模仿和学习的对象。Ceph社区经过十多年发展已经成为近几年参与度增长最快的开源社区之一,而Ceph中国社区正是背后的驱动力之一。从2015年开始,Ceph中国社区一直努力在国内普及Ceph的生态,并为广大Ceph爱好者提供了交流平台,使得众多开源爱好者能够进一步了解Ceph的魅力。在过去的10年,开源世界慢慢成为了IT创新的动力,而这10年也是国内技术爱好者受益于开源的最好时间。但是,从开源爱好者到社区的深度参与方面,尤其是在世界级开源项目上,我们还存在大缺失,而这些“沟壑”需要像Ceph中国社区这样的组织来弥补。我很欣喜地看到Ceph中国社区能在最合适的时间成立并迅速成长,而且受到Ceph官方社区的认可。 Ceph中国社区从论坛的搭建,微信群的建立,公众号的众包翻译和文章分析,到活动的组织都体现了一个开源社区最富有活力的价值。本书正是Ceph中国社区给国内Ceph爱好者的一份正当其时的“礼物”,本书是多位Ceph实战者在Ceph集群运维和问题讨论中形成的经验和锦囊之集合。毫不夸张地说,本书是我目前看到的最棒的Ceph入门工具书,可以帮助对分布式存储或者Ceph不太熟悉的读者真正零距离地接触并使用它。 王豪迈 2016年9月8日 Foreword 前言 随着信息化浪潮的到来,全球各行各业逐步借助信息技术深入发展。据悉,企业及互联网数据以每年50%的速率在增长。据权威调查机构Gartner预测,到2020年,全球数据量将达到35ZB,相当于80亿块4TB硬盘,数据结构的变化给存储系统带来了全新的挑战。那么有什么方法能够存储这些数据呢?我认为Ceph是解决未来十年数据存储需求的一个可行方案。Ceph是存储的未来!SDS是存储的未来! 为什么写这本书 目前,磁盘具备容量优势,固态硬盘具备速度优势。但能否让容量和性能不局限在一个存储器单元呢?我们很快联想到磁盘阵列技术(Redundant Array of Independent Disk,RAID,不限于HDD)。磁盘阵列技术是一种把多块独立的硬盘按不同的方式组合起来形成一个硬盘组(Disk Group,又称Virtual Disk),从而提供比单个硬盘更高的存储性能与数据备份能力的技术。磁盘阵列技术既可提供多块硬盘读写的聚合能力,又能提供硬盘故障的容错能力。 镜像技术(Mirroring)又称为复制技术(Replication),可提供数据冗余性和高可用性;条带(Striping),可提供并行的数据吞吐能力;纠删码(Erasure Code),把数据切片并增加冗余编码而提供高可用性和高速读写能力。镜像、条带和纠删码是磁盘阵列技术经典的数据分发方式,这3种经典的磁盘技术可通过组合方式提供更加丰富的数据读写性能。 传统的磁盘阵列技术的关注点在于数据在磁盘上的分发方式,随着通用磁盘、通用服务器,以及高速网络的成本降低,使数据在磁盘上的分发扩展到在服务器节点上的分发成为可能。镜像技术、条带技术和纠删码技术基于服务器节点的粒度实现后,这些技术的特点不再局限于单个设备的性能,而是具备“横向扩展”能力。我们暂且认为这是分布式存储本质的体现。 分布式存储解决了数据体量问题,对应用程序提供标准统一的访问接入,既能提升数据安全性和可靠性,又能提高存储整体容量和性能。可以预见,分布式存储是大规模存储的一个实现方向。分布式存储广泛地应用于航天、航空、石油、科研、政务、医疗、视频等高性能计算、云计算和大数据处理领域。目前行业应用对分布式存储技术需求旺盛,其处于快速发展阶段。 Ceph是加州大学圣克鲁兹分校的Sage Weil博士论文的研究项目,是一个使用自由开源协议(LGPLv2.1)的分布式存储系统。目前Ceph已经成为整个开源存储行业最热门的软件定义存储技术(Software Defined Storage,SDS)。它为块存储、文件存储和对象存储提供了统一的软件定义解决方案。Ceph旨在提供一个扩展性强大、性能优越且无单点故障的分布式存储系统。从一开始,Ceph就被设计为能在通用商业硬件上高度扩展。 由于其开放性、可扩展性和可靠性,Ceph成为了存储行业中的翘楚。这是云计算和软件定义基础设施的时代,我们需要一个完全软件定义的存储,更重要的是它要为云做好准备。无论运行的是公有云、私有云还是混合云,Ceph都非常合适。国内外有不少的Ceph应用方案,例如美国雅虎公司使用Ceph构建对象存储系统,用于Flickr、雅虎邮箱和Tumblr(轻量博客)的后端存储;国内不少公有云和私有云商选择Ceph作为云主机后端存储解决方案。 如今的软件系统已经非常智能,可以最大限度地利用商业硬件来运行规模庞大的基础设施。Ceph就是其中之一,它明智地采用商业硬件来提供企业级稳固可靠的存储系统。 Ceph已被不断完善,并融入以下建设性理念。 每个组件能够线性扩展。 无任何单故障点。 解决方案必须是基于软件的、开源的、适应性强的。 运行于现有商业硬件之上。 每个组件必须尽可能拥有自我管理和自我修复能力。 对象是Ceph的基础,它也是Ceph的构建部件,并且Ceph的对象存储很好地满足了当下及将来非结构化数据的存储需求。相比传统存储解决方案,对象储存有其独特优势:我们可以使用对象存储实现平台和硬件独立。Ceph谨慎地使用对象,通过在集群内复制对象来实现可用性;在Ceph中,对象是不依赖于物理路径的,这使其独立于物理位置。这种灵活性使Ceph能实现从PB(petabyte)级到EB(exabyte)级的线性扩展。 Ceph性能强大,具有超强扩展性及灵活性。它可以帮助用户摆脱昂贵的专有存储孤岛。Ceph是真正在商业硬件上运行的企业级存储解决方案;是一种低成本但功能丰富的存储系统。Ceph通用存储系统同时提供块存储、文件存储和对象存储,使客户可以按需使用。 由于国内许多企业决策者逐渐认识到Ceph的优势与前景,越来越多来自系统管理和传统存储的工程师使用Ceph,并有相当数量的企业基于Ceph研发分布式存储产品,为了更好地促进Ceph在国内传播和技术交流,我们几个爱好者成立了Ceph中国社区。目前,通过网络交流群、消息内容推送和问答互动社区,向国内关注Ceph技术的同行提供信息交流和共享平台。但是,由于信息在传递过程中过于分散,偶尔编写的文档内容并不完整,导致初学者在学习和使用Ceph的过程中遇到不少疑惑。同时,由于官方文档是通过英文发布的,对英语不太熟悉的同行难于学习。鉴于此,Ceph中国社区组织技术爱好者编写本书,本书主要提供初级和中级层面的指导。根据调查反馈以及社区成员的意见,我们确定了本书内容。 本书特色 在本书中,我们将采用穿插方式讲述Ceph分布式存储的原理与实战。本书侧重实战,循序渐进地讲述Ceph的基础知识和实战操作。从第1章起,读者会了解Ceph的前生今世。随着每章推进,读者将不断学习、不断深入。我希望,到本书的结尾,读者不论在概念上还是实战上,都能够成功驾驭Ceph。每个章节在讲述完基础理论知识后会有对应的实战操作。我们建议读者在自己的电脑上按部就班地进行实战操作。这样,一来读者不会对基础理论知识感到困惑,二来可让读者通过实战操作加深对Ceph的理解。同时,如果读者在阅读过程中遇到困难,我们建议再重温已阅章节或重做实验操作,这样将会加深理解,也可以加入Ceph中国社区QQ群(239404559)进行技术讨论。 读者对象 本书适用于以下读者。 Ceph爱好者。 云平台运维工程师。 存储系统工程师。 系统管理员。 高等院校的学生或者教师。 本书是专门对上述读者所打造的Ceph入门级实战书籍。如果你具备GNU/ Linux和存储系统的基本知识,却缺乏软件定义存储解决方案及Ceph相关的经验,本书也是不错的选择。云平台运维工程师、存储系统工程师读完本书之后能够深入了解Ceph原理、部署和维护好线上Ceph集群。同时,本书也适合大学高年级本科生和研究生作为Ceph分布式存储系统或者云计算相关课程的参考书籍,能够带领你进入一个开源的分布式存储领域,深入地了解Ceph,有助于你今后的工作。 如何阅读本书 由于Ceph是运行在GNU/Linux系统上的存储解决方案,我们假定读者掌握了存储相关知识并熟悉GNU/Linux操作系统。如果读者在这些方面知识有欠缺,可参照阅读其他书籍或专业信息网站。 本书将讲述如下的内容。 第1章 描述Ceph的起源、主要功能、核心组件逻辑、整体架构和设计思想,并通过实战的方式指导我们快速建立Ceph运行环境。 第2章 描述Ceph的分布式本质,深入分析Ceph架构,并介绍如何使用LIBRADOS库。 第3章 描述CRUSH的本质、基本原理,以及CRUSH作用下数据与对象的映射关系。 第4章 描述Ceph FS文件系统、RBD块存储和Object对象存储的建立以及使用。 第5章 描述Calamari的安装过程和基本使用操作。  第6章 描述Ceph FS作为高性能计算和大数据计算的后端存储的内容。 第7章 描述RBD在虚拟化和数据库场景下的应用,包括OpenStack、CloudStack和ZStack与RBD的结合。 第8章 描述基于Ceph的云盘技术方案和备份方案,描述网关的异地同步方案和多媒体转换网关设计。 第9章 描述Ceph的硬件选型、性能调优,以及性能测试方法。 第10章 描述CRUSH的结构,并给出SSD与SATA混合场景下的磁盘组织方案。 第11章 描述Ceph的缓冲池原理和部署,以及纠删码原理和纠删码库,最后描述纠删码池的部署方案。 第12章 对3种存储访问类型的生产环境案例进行分析。 第13章 描述Ceph日常运维细节,以及常见错误的处理方法。 勘误与支持 在本书的写作过程,我们也参考了Ceph中国社区往期沙龙一线工程师、专家分享的经验和Ceph官方文档。我们热切希望能够为读者呈现丰富而且权威的Ceph存储技术。由于Ceph社区不断发展,版本迭代速度快,笔者水平有限,书中难免存在技术延后和谬误,恳请读者批评指正。可将任何意见和建议发送到邮箱devin@ceph.org.cn或者star.guo@ceph.org.cn,也可以发布到Ceph中国社区问答系统http://bbs.ceph.org.cn/。我们将密切跟踪Ceph分布式存储技术的发展,吸收读者宝贵意见,适时编写本书的升级版本。Ceph中国社区订阅号为:“ceph_community”,二维码为: 欢迎读者扫描关注,“Ceph中国社区订阅号”会定期发送Ceph技术文章、新闻资讯。也欢迎读者通过这个微信订阅号进行本书勘误反馈,本书的勘误和更新也会通过订阅号发布。 致谢 首先要感谢我们社区的全体志愿者,社区的发展离不开全体志愿者们无怨无悔的奉献,正是有了你们才有了社区今日的繁荣,其次要感谢所有支持过我们的企业,是你们的慷慨解囊成就了Ceph中国社区今日的壮大,最后感谢陈晓熹的校稿以及所有为本书编写提供支持、帮助的人。未来,我们也非常欢迎有志将开源事业发扬光大的同学们积极加入我们的社区,和我们一起创造Ceph未来的辉煌。 Contents 目录 本书赞誉 序 前言 第1章 初识Ceph 1 1.1 Ceph概述 1 1.2 Ceph的功能组件 5 1.3 Ceph架构和设计思想 7 1.4 Ceph快速安装 9 1.4.1 Ubuntu/Debian安装 10 1.4.2 RHEL/CentOS安装 13 1.5 本章小结 16 第2章 存储基石RADOS 17 2.1 Ceph功能模块与RADOS 18 2.2 RADOS架构 20 2.2.1 Monitor介绍 20 2.2.2 Ceph OSD简介 22 2.3 RADOS与LIBRADOS 26 2.4 本章小结 31 第3章 智能分布CRUSH 32 3.1 引言 32 3.2 CRUSH基本原理 33 3.2.1 Object与PG 34 3.2.2 PG与OSD 34 3.2.3 PG与Pool 35 3.3 CRUSH关系分析 37 3.4 本章小结 41 第4章 三大存储访问类型 42 4.1 Ceph FS文件系统 42 4.1.1 Ceph FS和MDS介绍 43 4.1.2 部署MDS 45 4.1.3 挂载Ceph FS 46 4.2 RBD块存储 47 4.2.1 RBD介绍 47 4.2.2 librbd介绍 48 4.2.3 KRBD介绍 48 4.2.4 RBD操作 50 4.2.5 RBD应用场景 56 4.3 Object对象存储 57 4.3.1 RGW介绍 57 4.3.2 Amazon S3简介 58 4.3.3 快速搭建RGW环境 61 4.3.4 RGW搭建过程的排错指南 68 4.3.5 使用S3客户端访问RGW服务 71 4.3.6 admin管理接口的使用 75 4.4 本章小结 78 第5章 可视化管理Calamari 79 5.1 认识Calamari 79 5.2 安装介绍 79 5.2.1 安装calamari-server 80 5.2.2 安装romana(calamari-client) 82 5.2.3 安装diamond 85 5.2.4 安装salt-minion 86 5.2.5 重启服务 87 5.3 基本操作 87 5.3.1 登录Calamari 87 5.3.2 WORKBENCH页面 88 5.3.3 GRAPH页面 89 5.3.4 MANAGE页面 90 5.4 本章小结 92 第6章 文件系统—高性能计算与大数据 93 6.1 Ceph FS作为高性能计算存储 93 6.2 Ceph FS作为大数据后端存储 98 6.3 本章小结 101 第7章 块存储—虚拟化与数据库 102 7.1 Ceph与KVM 102 7.2 Ceph与OpenStack 106 7.3 Ceph与CloudStack 110 7.4 Ceph与ZStack 114 7.5 Ceph提供iSCSI存储  122 7.6 本章小结 128 第8章 对象存储—云盘与RGW异地灾备 129 8.1 网盘方案:RGW与OwnCloud的整合 129 8.2 RGW的异地同步方案 133 8.2.1 异地同步原理与部署方案设计 134 8.2.2 Region异地同步部署实战 137 8.3 本章小结 146 第9章 Ceph硬件选型、性能测试与优化 147 9.1 需求模型与设计 147 9.2 硬件选型 148 9.3 性能调优 151 9.3.1 硬件优化 152 9.3.2 操作系统优化 155 9.3.3 网络层面优化 161 9.3.4 Ceph层面优化 170 9.4 Ceph测试 174 9.4.1 测试前提 175 9.4.2 存储系统模型 175 9.4.3 硬盘测试 176 9.4.4 云硬盘测试 182 9.4.5 利用Cosbench来测试Ceph 185 9.5 本章小结 189 第10章 自定义CRUSH 191 10.1 CRUSH解析 191 10.2 CRUSH设计:两副本实例 201 10.3 CRUSH设计:SSD、SATA混合实例 207 10.3.1 场景一:快–慢存储方案 207 10.3.2 场景二:主–备存储方案 214 10.4 模拟测试CRUSH分布 217 10.5 本章小结 222 第11章 缓冲池与纠删码 223 11.1 缓冲池原理 223 11.2 缓冲池部署 225 11.2.1 缓冲池的建立与管理 226 11.2.2 缓冲池的参数配置 226 11.2.3 缓冲池的关闭 228 11.3 纠删码原理 229 11.4 纠删码应用实践 232 11.4.1 使用Jerasure插件配置纠删码 232 11.4.2 ISA-L插件介绍 234 11.4.3 LRC插件介绍 235 11.4.4 其他插件介绍 235 11.5 本章小结 235 第12章 生产环境应用案例 237 12.1 Ceph FS应用案例 237 12.1.1 将Ceph FS导出成NFS使用 238 12.1.2 在Windows客户端使用Ceph FS 239 12.1.3 OpenStack Manila项目对接Ceph FS案例 242 12.2 RBD应用案例 244 12.2.1 OpenStack对接RBD典型架构 244 12.2.2 如何实现Cinder Multi-Backend 246 12.3 Object RGW应用案例:读写分离方案 248 12.4 基于HLS的视频点播方案 249 12.5 本章小结 251 第13章 Ceph运维与排错 252 13.1 Ceph集群运维 252 13.1.1 集群扩展 252 13.1.2 集群维护 259 13.1.3 集群监控 266 13.2 Ceph常见错误与解决方案 277 13.2.1 时间问题 277 13.2.2 副本数问题 279 13.2.3 PG问题 282 13.2.4 OSD问题 286 13.3 本章小结 292 第1章 初识Ceph 1.1 Ceph概述 1. Ceph简介 从2004年提交第一行代码开始到现在,Ceph已经是一个有着十年之久的分布式存储系统软件,目前Ceph已经发展为开源存储界的当红明星,当然这与它的设计思想以及OpenStack的推动有关。 “Ceph is a unified, distributed storage system designed for excellent performance, reliability and scalability.”这句话说出了Ceph的特性,它是可靠的、可扩展的、统一的、分布式的存储系统。Ceph可以同时提供对象存储RADOSGW(Reliable、Autonomic、Distributed、Object Storage Gateway)、块存储RBD(Rados Block Device)、文件系统存储Ceph FS(Ceph Filesystem)3种功能,以此来满足不同的应用需求。 Ceph消除了对系统单一中心节点的依赖,从而实现了真正的无中心结构的设计思想,这也是其他分布式存储系统所不能比的。通过后续章节内容的介绍,你可以看到,Ceph几乎所有优秀特性的实现,都与其核心设计思想有关。 OpenStack是目前最为流行的开源云平台软件。Ceph的飞速发展离不开OpenStack的带动。目前而言,Ceph已经成为OpenStack的标配开源存储方案之一,其实际应用主要涉及块存储和对象存储,并且开始向文件系统领域扩展。这一部分的相关情况,在后续章节中也将进行介绍。 2. Ceph的发展 Ceph是加州大学Santa Cruz分校的Sage Weil(DreamHost的联合创始人)专为博士论文设计的新一代自由软件分布式文件系统。 2004年,Ceph项目开始,提交了第一行代码。 2006年,OSDI学术会议上,Sage发表了介绍Ceph的论文,并在该篇论文的末尾提供了Ceph项目的下载链接。 2010年,Linus Torvalds将Ceph Client合并到内核2.6.34中,使Linux与Ceph磨合度更高。 2012年,拥抱OpenStack,进入Cinder项目,成为重要的存储驱动。 2014年,Ceph正赶上OpenStack大热,受到各大厂商的“待见”,吸引来自不同厂商越来越多的开发者加入,Intel、SanDisk等公司都参与其中,同时Inktank公司被Red Hat公司1.75亿美元收购。 2015年,Red Hat宣布成立Ceph顾问委员会,成员包括Canonical、CERN、Cisco、Fujitsu、Intel、SanDisk和SUSE。Ceph顾问委员会将负责Ceph软件定义存储项目的广泛议题,目标是使Ceph成为云存储系统。 2016年,OpenStack社区调查报告公布,Ceph仍为存储首选,这已经是Ceph第5次位居调查的首位了。 3. Ceph 应用场景 Ceph可以提供对象存储、块设备存储和文件系统服务,其对象存储可以对接网盘(owncloud)应用业务等;其块设备存储可以对接(IaaS),当前主流的IaaS云平台软件,例如OpenStack、CloudStack、Zstack、Eucalyptus等以及KVM等,本书后续章节中将介绍OpenStack、CloudStack、Zstack和KVM的对接;其文件系统文件尚不成熟,官方不建议在生产环境下使用。 4. Ceph生态系统 Ceph作为开源项目,其遵循LGPL协议,使用C++语言开发,目前Ceph已经成为最广泛的全球开源软件定义存储项目,拥有得到众多IT厂商支持的协同开发模式。目前Ceph社区有超过40个公司的上百名开发者持续贡献代码,平均每星期的代码commits超过150个,每个版本通常在2 000个commits左右,代码增减行数在10万行以上。在过去的几个版本发布中,贡献者的数量和参与公司明显增加,如图1-1所示。 图1-1 部分厂商和软件 5. Ceph用户群 Ceph成为了开源存储的当红明星,国内外已经拥有众多用户群体,下面简单说一下Ceph的用户群。 (1)国外用户群 1)CERN:CERN IT部门在2013年年中开始就运行了一个单一集群超过10 000个VM和100 000个CPU Cores的云平台,主要用来做物理数据分析。这个集群后端Ceph包括3PB的原始容量,在云平台中作为1000多个Cinder卷和1500多个Glance镜像的存储池。在2015年开始测试单一30 PB的块存储RBD集群。 2)DreamHost:DreamHost从2012年开始运行基于Ceph RADOSGW的大规模对象存储集群,单一集群在3PB以下,大约由不到10机房集群组成,直接为客户提供对象存储服务。 3)Yahoo Flick:Yahoo Flick自2013年开始逐渐试用Ceph对象存储替换原有的商业存储,目前大约由10机房构成,每个机房在1PB~2PB,存储了大约2 500亿个对象。 4)大学用户:奥地利的因斯布鲁克大学、法国的洛林大学等。 (2)国内用户群 1)以OpenStack为核心的云厂商:例如UnitedStack、Awcloud等国内云计算厂商。 2)Ceph产品厂商:SanDisk、XSKY、H3C、杉岩数据、SUSE和Bigtera等Ceph厂商。 3)互联网企业:腾讯、京东、新浪微博、乐视、完美世界、平安科技、联想、唯品会、福彩网和魅族等国内互联网企业。 6. 社区项目开发迭代 目前Ceph社区采用每半年一个版本发布的方式来进行特性和功能的开发,每个版本发布需要经历设计、开发、新功能冻结,持续若干个版本的Bug修复周期后正式发布下一个稳定版本。其发布方式跟OpenStack差不多,也是每半年发布一个新版本。 Ceph会维护多个稳定版本来保证持续的Bug修复,以此来保证用户的存储安全,同时社区会有一个发布稳定版本的团队来维护已发布的版本,每个涉及之前版本的Bug都会被该团队移植回稳定版本,并且经过完整QA测试后发布下一个稳定版本。 代码提交都需要经过单元测试,模块维护者审核,并通过QA测试子集后才能合并到主线。社区维护一个较大规模的测试集群来保证代码质量,丰富的测试案例和错误注入机制保证了项目的稳定可靠。 7. Ceph版本 Ceph正处于持续开发中并且迅速提升。2012年7月3日,Sage发布了Ceph第一个LTS版本:Argonaut。从那时起,陆续又发布了9个新版本。Ceph版本被分为LTS(长期稳定版)以及开发版本,Ceph每隔一段时间就会发布一个长期稳定版。Ceph版本具体信息见表1-1。欲了解更多信息,请访问https://Ceph.com/category/releases/。 表1-1 Ceph版本信息 Ceph版本名称 Ceph版本号 发布时间 Argonaut V0.48 (LTS) 2012.6.3 Bobtail V0.56 (LTS) 2013.1.1 Cuttlefish V0.61 2013.5.7 Dumpling V0.67 (LTS) 2013.8.14 Emperor V0.72 2013.11.9 Firefly V0.80 (LTS) 2014.3.7 Giant V0.87.1 2015.2.26 Hammer V0.94 (LTS) 2015.4.7 Infernalis V9.0.0 2015.5.5 Jewel V10.0.0 2015.11 Jewel V10.2.0 2016.3 1.2 Ceph的功能组件 Ceph提供了RADOS、OSD、MON、Librados、RBD、RGW和Ceph FS等功能组件,但其底层仍然使用RADOS存储来支撑上层的那些组件,如图1-2所示。 图1-2 Ceph功能组件的整体架构 下面分为两部分来讲述Ceph的功能组件。 (1)Ceph核心组件 在Ceph存储中,包含了几个重要的核心组件,分别是Ceph OSD、Ceph Monitor和Ceph MDS。一个Ceph的存储集群至少需要一个Ceph Monitor和至少两个Ceph的OSD。运行Ceph文件系统的客户端时,Ceph的元数据服务器(MDS)是必不可少的。下面来详细介绍一下各个核心组件。 Ceph OSD:全称是Object Storage Device,主要功能包括存储数据,处理数据的复制、恢复、回补、平衡数据分布,并将一些相关数据提供给Ceph Monitor,例如Ceph OSD心跳等。一个Ceph的存储集群,至少需要两个Ceph OSD来实现active + clean健康状态和有效的保存数据的双副本(默认情况下是双副本,可以调整)。注意:每一个Disk、分区都可以成为一个OSD。 Ceph Monitor:Ceph的监控器,主要功能是维护整个集群健康状态,提供一致性的决策,包含了Monitor map、OSD map、PG(Placement Group)map和CRUSH map。 Ceph MDS:全称是Ceph Metadata Server,主要保存的是Ceph文件系统(File System)的元数据(metadata)。温馨提示:Ceph的块存储和Ceph的对象存储都不需要Ceph MDS。Ceph MDS为基于POSIX文件系统的用户提供了一些基础命令,例如ls、find等命令。 (2)Ceph功能特性 Ceph可以同时提供对象存储RADOSGW(Reliable、Autonomic、Distributed、Object Storage Gateway)、块存储RBD(Rados Block Device)、文件系统存储Ceph FS(Ceph File System)3种功能,由此产生了对应的实际场景,本节简单介绍如下。 RADOSGW功能特性基于LIBRADOS之上,提供当前流行的RESTful协议的网关,并且兼容S3和Swift接口,作为对象存储,可以对接网盘类应用以及HLS流媒体应用等。 RBD(Rados Block Device)功能特性也是基于LIBRADOS之上,通过LIBRBD创建一个块设备,通过QEMU/KVM附加到VM上,作为传统的块设备来用。目前OpenStack、CloudStack等都是采用这种方式来为VM提供块设备,同时也支持快照、COW(Copy On Write)等功能。 Ceph FS(Ceph File System)功能特性是基于RADOS来实现分布式的文件系统,引入了MDS(Metadata Server),主要为兼容POSIX文件系统提供元数据。一般都是当做文件系统来挂载。 后面章节会对这几种特性以及对应的实际场景做详细的介绍和分析。 1.3 Ceph架构和设计思想 1. Ceph架构 Ceph底层核心是RADOS。Ceph架构图如图1-3所示。 图1-3 Ceph架构图 RADOS:RADOS具备自我修复等特性,提供了一个可靠、自动、智能的分布式存储。 LIBRADOS:LIBRADOS库允许应用程序直接访问,支持C/C++、Java和Python等语言。 RADOSGW:RADOSGW 是一套基于当前流行的RESTful协议的网关,并且兼容S3和Swift。 RBD:RBD通过Linux 内核(Kernel)客户端和QEMU/KVM驱动,来提供一个完全分布式的块设备。 Ceph FS:Ceph FS通过Linux内核(Kernel)客户端结合FUSE,来提供一个兼容POSIX的文件系统。 具体的RADOS细节以及RADOS的灵魂CRUSH(Controlled Replication Under Scalable Hashing,可扩展哈希算法的可控复制)算法,这两个知识点会在后面的第2、3章详细介绍和分析。 2. Ceph设计思想 Ceph是一个典型的起源于学术研究课题的开源项目。虽然学术研究生涯对于Sage而言只是其光辉事迹的短短一篇,但毕竟还是有几篇学术论文可供参考的。可以根据Sage的几篇论文分析Ceph的设计思想。 理解Ceph的设计思想,首先还是要了解Sage设计Ceph时所针对的应用场景,换句话说,Sage当初做Ceph的初衷的什么? 事实上,Ceph最初针对的应用场景,就是大规模的、分布式的存储系统。所谓“大规模”和“分布式”,至少是能够承载PB级别的数据和成千上万的存储节点组成的存储集群。 如今云计算、大数据在中国发展得如火如荼,PB容量单位早已经进入国内企业存储采购单,DT时代即将来临。Ceph项目起源于2004年,那是一个商用处理器以单核为主流,常见硬盘容量只有几十GB的年代。当时SSD也没有大规模商用,正因如此,Ceph之前版本对SSD的支持不是很好,发挥不了SSD的性能。如今Ceph高性能面临的最大挑战正是这些历史原因,目前社区和业界正在逐步解决这些性能上的限制。 在Sage的思想中,我们首先说一下Ceph的技术特性,总体表现在集群可靠性、集群扩展性、数据安全性、接口统一性4个方面。 集群可靠性:所谓“可靠性”,首先从用户角度来说数据是第一位的,要尽可能保证数据不会丢失。其次,就是数据写入过程中的可靠性,在用户将数据写入Ceph存储系统的过程中,不会因为意外情况出现而造成数据丢失。最后,就是降低不可控物理因素的可靠性,避免因为机器断电等不可控物理因素而产生的数据丢失。 集群可扩展性:这里的“可扩展”概念是广义的,既包括系统规模和存储容量的可扩展,也包括随着系统节点数增加的聚合数据访问带宽的线性扩展。 数据安全性:所谓“数据安全性”,首先要保证由于服务器死机或者是偶然停电等自然因素的产生,数据不会丢失,并且支持数据自动恢复,自动重平衡等。总体而言,这一特性既保证了系统的高度可靠和数据绝对安全,又保证了在系统规模扩大之后,其运维难度仍能保持在一个相对较低的水平。 接口统一性:所谓“接口统一”,本书开头就说到了Ceph可以同时支持3种存储,即块存储、对象存储和文件存储。Ceph支持市面上所有流行的存储类型。 根据上述技术特性以及Sage的论文,我们来分析一下Ceph的设计思路,概述为两点:充分发挥存储本身计算能力和去除所有的中心点。 充分发挥存储设备自身的计算能力:其实就是采用廉价的设备和具有计算能力的设备(最简单的例子就是普通的服务器)作为存储系统的存储节点。Sage认为当前阶段只是将这些服务器当做功能简单的存储节点,从而产生资源过度浪费(如同虚拟化的思想一样,都是为了避免资源浪费)。而如果充分发挥节点上的计算能力,则可以实现前面提出的技术特性。这一点成为了Ceph系统设计的核心思想。 去除所有的中心点:搞IT的最忌讳的就是单点故障,如果系统中出现中心点,一方面会引入单点故障,另一方面也必然面临着当系统规模扩大时的可扩展性和性能瓶颈。除此之外,如果中心点出现在数据访问的关键路径上,也必然导致数据访问的延迟增大。虽然在大多数存储软件实践中,单点故障点和性能瓶颈的问题可以通过为中心点增加HA或备份加以缓解,但Ceph系统最终采用Crush、Hash环等方法更彻底地解决了这个问题。很显然Sage的眼光和设想还是很超前的。 1.4 Ceph快速安装 在Ceph官网上提供了两种安装方式:快速安装和手动安装。快速安装采用Ceph-Deploy工具来部署;手动安装采用官方教程一步一步来安装部署Ceph集群,过于烦琐但有助于加深印象,如同手动部署OpenStack一样。但是,建议新手和初学者采用第一种方式快速部署并且测试,下面会介绍如何使用Ceph-Deploy工具来快速部署Ceph集群。 1.4.1 Ubuntu/Debian安装 本节将介绍如何使用Ceph-Deploy工具来快速部署Ceph集群,开始之前先普及一下Ceph-Deploy工具的知识。Ceph-Deploy工具通过SSH方式连接到各节点服务器上,通过执行一系列脚本来完成Ceph集群部署。Ceph-Deploy简单易用同时也是Ceph官网推荐的默认安装工具。本节先来讲下在Ubuntu/Debian系统下如何快速安装Ceph集群。 1)配置Ceph APT源。 root@localhos`t:~# echo deb http://ceph.com/debian-{ceph-stable-release}/ $(lsb_release -sc) main | sudo tee /etc/apt/sources.list.d/ceph.list 2)添加APT源key。 root@localhost:~# wget –q –O -'https://ceph.com/git/?p=ceph.git;a=blob_plain;f=keys/release.asc' | sudo apt-key add - 3)更新源并且安装ceph-deploy。 root@localhost:~# sudo apt-get update &&sudo apt-get install ceph-deploy -y 4)配置各个节点hosts文件。 root@localhost:~# cat /etc/hosts 192.168.1.2  node1 192.168.1.3  node2 192.168.1.4  node3 5)配置各节点SSH无密码登录,这就是本节开始时讲到的Ceph-Deploy工具要用过SSH方式连接到各节点服务器,来安装部署集群。输完ssh-keygen命令之后,在命令行会输出以下内容。 root@localhost:~# ssh-keygen Generating public/private key pair. Enter file in which to save the key (/ceph-client/.ssh/id_rsa): Enter passphrase (empty for no passphrase): Enter same passphrase again: Your identification has been saved in /ceph-client/.ssh/id_rsa. Your public key has been saved in /ceph-client/.ssh/id_rsa.pub 6)复制key到各节点。 root@localhost:~# ssh-copy-idnode1 root@localhost:~# ssh-copy-idnode2 root@localhost:~# ssh-copy-idnode3 7)在执行ceph-deploy的过程中会生成一些配置文件,建议创建一个目录,例如my-cluster。 root@localhost:~# mkdir my-cluster root@localhost:~# cd my-cluster 8)创建集群(Cluster),部署新的monitor节点。 root@localhost:~# ceph-deploy new {initial-monitor-node(s)} 例如: root@localhost:~# ceph-deploy new node1 9)配置Ceph.conf配置文件,示例文件是默认的,可以根据自己情况进行相应调整和添加。具体优化情况本书后面会介绍。 [global] fsid = 67d997c9-dc13-4edf-a35f-76fd693aa118 mon_initial_members = node1,node2 mon_host = 192.168.1.2,192.168.1.3 auth_cluster_required = cephx auth_service_required = cephx auth_client_required = cephx filestore_xattr_use_omap = true <!-----以上部分都是ceph-deploy默认生成的----> public network = {ip-address}/{netmask} cluster network={ip-addesss}/{netmask} <!-----以上两个网络是新增部分,默认只是添加public network,一般生产都是定义两个网络,集群网络和数据网络分开--------> [osd] …… [mon] …… 这里配置文件不再过多叙述。 10)安装Ceph到各节点。 root@localhost:~# ceph-deploy install {ceph-node}[{ceph-node} ...] 例如: root@localhost:~# ceph-deploy install node1 node2 node3 11)获取密钥key,会在my-cluster目录下生成几个key。 root@localhost:~# ceph-deploy mon create-initial 12)初始化磁盘。 root@localhost:~# ceph-deploy disk zap {osd-server-name}:{disk-name} 例如: root@localhost:~# ceph-deploy disk zap node1:sdb 13)准备OSD。 root@localhost:~# ceph-deploy osd prepare {node-name}:{data-disk}[:{journal-disk}] 例如: root@localhost:~# ceph-deploy osd prepare node1:sdb1:sdc 14)激活OSD。 root@localhost:~# ceph-deploy osd activate {node-name}:{data-disk-partition}[:{journal-disk-partition}] 例如: root@localhost:~# ceph-deploy osd activate node1:sdb1:sdc 15)分发key。 root@localhost:~# ceph-deploy admin {admin-node} {ceph-node} 例如: root@localhost:~# ceph-deploy admin node1 node2 node3 16)给admin key赋权限。 root@localhost:~# sudo chmod +r /etc/ceph/ceph.client.admin.keyring 17)查看集群健康状态,如果是active+clean状态就是正常的。 root@localhost:~# ceph health 安装Ceph前提条件如下。 ① 时间要求很高,建议在部署Ceph集群的时候提前配置好NTP服务器。 ② 对网络要求一般,因为Ceph源在外国有时候会被屏蔽,解决办法多尝试机器或者代理。 1.4.2 RHEL/CentOS安装 本节主要讲一下在RHEL/CentOS系统下如何快速安装Ceph集群。 1)配置Ceph YUM源。 root@localhost:~# vim /etc/yum.repos.d/ceph.repo [ceph-noarch] name=Cephnoarch packages baseurl=http://ceph.com/rpm-{ceph-release}/{distro}/noarch enabled=1 gpgcheck=1 type=rpm-md gpgkey=https://ceph.com/git/?p=ceph.git;a=blob_plain;f=keys/release.asc 2)更新源并且安装ceph-deploy。 root@localhost:~# yum update &&yum install ceph-deploy -y 3)配置各个节点hosts文件。 root@localhost:~# cat /etc/hosts 192.168.1.2  node1 192.168.1.3  node2 192.168.1.4  node3 4)配置各节点SSH无密码登录,通过SSH方式连接到各节点服务器,以安装部署集群。输入ssh-keygen命令,在命令行会输出以下内容。 root@localhost:~# ssh-keygen Generating public/private key pair. Enter file in which to save the key (/ceph-client/.ssh/id_rsa): Enter passphrase (empty for no passphrase): Enter same passphrase again: Your identification has been saved in /ceph-client/.ssh/id_rsa. Your public key has been saved in /ceph-client/.ssh/id_rsa.pub 5)拷贝key到各节点。 root@localhost:~# ssh-copy-id node1 root@localhost:~# ssh-copy-id node2 root@localhost:~# ssh-copy-id node3 6)在执行ceph-deploy的过程中会生成一些配置文件,建议创建一个目录,例如my-cluster。 root@localhost:~# mkdir my-cluster root@localhost:~# cd my-cluster 7)创建集群(Cluster),部署新的monitor节点。 root@localhost:~# ceph-deploy new {initial-monitor-node(s)} 例如: root@localhost:~# ceph-deploy new node1 8)配置Ceph.conf配置文件,示例文件是默认的,可以根据自己情况进行相应调整和添加。具体优化情况本书后面会介绍。 [global] fsid = 67d997c9-dc13-4edf-a35f-76fd693aa118 mon_initial_members = node1,node2 mon_host = 192.168.1.2,192.168.1.3 auth_cluster_required = cephx auth_service_required = cephx auth_client_required = cephx filestore_xattr_use_omap = true <!-----以上部分都是ceph-deploy默认生成的----> public network = {ip-address}/{netmask} cluster network={ip-addesss}/{netmask} <!-----以上两个网络是新增部分,默认只是添加public network,一般生产都是定义两个网络,集群网络和数据网络分开--------> [osd] …… [mon] …… 这里配置文件不再过多叙述。 9)安装Ceph到各节点。 root@localhost:~# ceph-deploy install {ceph-node}[{ceph-node} ...] 例如: root@localhost:~# ceph-deploy install node1 node2 node3 10)获取密钥key,会在my-cluster目录下生成几个key。 root@localhost:~# ceph-deploy mon create-initial 11)初始化磁盘。 root@localhost:~# ceph-deploy disk zap {osd-server-name}:{disk-name} 例如: root@localhost:~# ceph-deploy disk zap node1:sdb 12)准备OSD。 root@localhost:~# ceph-deploy osd prepare {node-name}:{data-disk}[:{journal-disk}] 例如: root@localhost:~# ceph-deploy osd prepare node1:sdb1:sdc 13)激活OSD。 root@localhost:~# ceph-deploy osd activate {node-name}:{data-disk-partition}[:{journal-disk-partition}] 例如: root@localhost:~# ceph-deploy osd activate node1:sdb1:sdc 14)分发key。 root@localhost:~# ceph-deploy admin {admin-node} {ceph-node} 例如: root@localhost:~# ceph-deploy admin node1 node2 node3 15)给admin key赋权限。 root@localhost:~# sudo chmod +r /etc/ceph/ceph.client.admin.keyring 16)查看集群健康状态,如果是 active + clean 状态就是正常的。 root@localhost:~# ceph health 1.5 本章小结 本章主要从Ceph的历史背景、发展事件、Ceph的架构组件、功能特性以及Ceph的设计思想方面介绍了Ceph,让大家对Ceph有一个全新的认识,以便后面更深入地了解Ceph。 第2章 存储基石RADOS 分布式对象存储系统RADOS是Ceph最为关键的技术,它是一个支持海量存储对象的分布式对象存储系统。RADOS层本身就是一个完整的对象存储系统,事实上,所有存储在Ceph系统中的用户数据最终都是由这一层来存储的。而Ceph的高可靠、高可扩展、高性能、高自动化等特性,本质上也是由这一层所提供的。因此,理解RADOS是理解Ceph的基础与关键。 Ceph的设计哲学如下。 每个组件必须可扩展。 不存在单点故障。 解决方案必须是基于软件的。 可摆脱专属硬件的束缚即可运行在常规硬件上。 推崇自我管理。 由第1章的讲解可以知道,Ceph包含以下组件。 分布式对象存储系统RADOS库,即LIBRADOS。 基于LIBRADOS实现的兼容Swift和S3的存储网关系统RADOSGW。 基于LIBRADOS实现的块设备驱动RBD。 兼容POSIX的分布式文件Ceph FS。 最底层的分布式对象存储系统RADOS。 2.1 Ceph功能模块与RADOS Ceph中的这些组件与RADOS有什么关系呢,笔者手绘了一张简单的Ceph架构图,我们结合图2-1来分析这些组件与RADOS的关系。 图2-1 Ceph架构图 Ceph存储系统的逻辑层次结构大致划分为4部分:基础存储系统RADOS、基于RADOS实现的Ceph FS,基于RADOS的LIBRADOS层应用接口、基于LIBRADOS的应用接口RBD、RADOSGW。Ceph架构(见图1-1)我们在第1章有过初步的了解,这里详细看一下各个模块的功能,以此了解RADOS在整个Ceph起到的作用。 (1)基础存储系统RADOS RADOS这一层本身就是一个完整的对象存储系统,事实上,所有存储在Ceph系统中的用户数据最终都是由这一层来存储的。Ceph的很多优秀特性本质上也是借由这一层设计提供。理解RADOS是理解Ceph的基础与关键。物理上,RADOS由大量的存储设备节点组成,每个节点拥有自己的硬件资源(CPU、内存、硬盘、网络),并运行着操作系统和文件系统。本书后续章节将对RADOS进行深入介绍。 (2)基础库LIBRADOS LIBRADOS层的功能是对RADOS进行抽象和封装,并向上层提供API,以便直接基于RADOS进行应用开发。需要指明的是,RADOS是一个对象存储系统,因此,LIBRADOS实现的API是针对对象存储功能的。RADOS采用C++开发,所提供的原生LIBRADOS API包括C和C++两种。物理上,LIBRADOS和基于其上开发的应用位于同一台机器,因而也被称为本地API。应用调用本机上的LIBRADOS API,再由后者通过socket与RADOS集群中的节点通信并完成各种操作。 (3)上层应用接口 Ceph上层应用接口涵盖了RADOSGW(RADOS Gateway)、RBD(Reliable Block Device)和Ceph FS(Ceph File System),其中,RADOSGW和RBD是在LIBRADOS库的基础上提供抽象层次更高、更便于应用或客户端使用的上层接口。 其中,RADOSGW是一个提供与Amazon S3和Swift兼容的RESTful API的网关,以供相应的对象存储应用开发使用。RADOSGW提供的API抽象层次更高,但在类S3或Swift LIBRADOS的管理比便捷,因此,开发者应针对自己的需求选择使用。RBD则提供了一个标准的块设备接口,常用于在虚拟化的场景下为虚拟机创建volume。目前,Red Hat已经将RBD驱动集成在KVM/QEMU中,以提高虚拟机访问性能。 (4)应用层 应用层就是不同场景下对于Ceph各个应用接口的各种应用方式,例如基于LIBRADOS直接开发的对象存储应用,基于RADOSGW开发的对象存储应用,基于RBD实现的云主机硬盘等。 下面就来看看RADOS的架构。 2.2 RADOS架构 RADOS系统主要由两个部分组成,如图2-2所示。 1)OSD:由数目可变的大规模OSD(Object Storage Devices)组成的集群,负责存储所有的Objects数据。 2)Monitor:由少量Monitors组成的强耦合、小规模集群,负责管理Cluster Map。其中,Cluster Map是整个RADOS系统的关键数据结构,管理集群中的所有成员、关系和属性等信息以及数据的分发。 图2-2 RADOS系统架构图示 对于RADOS系统,节点组织管理和数据分发策略均由内部的Mon全权负责,因此,从Client角度设计相对比较简单,它给应用提供存储接口。 2.2.1 Monitor介绍 正如其名,Ceph Monitor是负责监视整个群集的运行状况的,这些信息都是由维护集群成员的守护程序来提供的,如各个节点之间的状态、集群配置信息。Ceph monitor map包括OSD Map、PG Map、MDS Map和CRUSH等,这些Map被统称为集群Map。 1)Monitor Map。Monitor Map包括有关monitor节点端到端的信息,其中包括Ceph集群ID,监控主机名和IP地址和端口号,它还存储了当前版本信息以及最新更改信息,可以通过以下命令查看monitor map。 #ceph mon dump 2)OSD Map。OSD Map包括一些常用的信息,如集群ID,创建OSD Map的版本信息和最后修改信息,以及pool相关信息,pool的名字、pool的ID、类型,副本数目以及PGP,还包括OSD信息,如数量、状态、权重、最新的清洁间隔和OSD主机信息。可以通过执行以下命令查看集群的OSD Map。 #ceph  osd dump 3)PG Map。PG Map包括当前PG版本、时间戳、最新的OSD Map的版本信息、空间使用比例,以及接近占满比例信息,同时,也包括每个PG ID、对象数目、状态、OSD的状态以及深度清理的详细信息,可以通过以下命令来查看PG Map。 #ceph pg dump 4)CRUSH Map。CRUSH Map包括集群存储设备信息,故障域层次结构和存储数据时定义失败域规则信息;可以通过以下命令查看CRUSH Map。 #ceph osd crush dump 5)MDS Map。MDS Map包括存储当前MDS Map的版本信息、创建当前Map的信息、修改时间、数据和元数据POOL ID、集群MDS数目和MDS状态,可通过以下命令查看集群MDS Map信息。 #ceph mds dump Ceph的MON服务利用Paxos的实例,把每个映射图存储为一个文件。Ceph Monitor并未为客户提供数据存储服务,而是为Ceph集群维护着各类Map,并服务更新群集映射到客户机以及其他集群节点。客户端和其他群集节点定期检查并更新于Monitor的集群Map最新的副本。 Ceph Monitor是个轻量级的守护进程,通常情况下并不需要大量的系统资源,低成本、入门级的CPU,以及千兆网卡即可满足大多数的场景;与此同时,Monitor节点需要有足够的磁盘空间来存储集群日志,健康集群产生几MB到GB的日志;然而,如果存储的需求增加时,打开低等级的日志信息的话,可能需要几个GB的磁盘空间来存储日志。 一个典型的Ceph集群包含多个Monitor节点。一个多Monitor的Ceph的架构通过法定人数来选择leader,并在提供一致分布式决策时使用Paxos算法集群。在Ceph集群中有多个Monitor时,集群的Monitor应该是奇数;最起码的要求是一台监视器节点,这里推荐Monitor个数是3。由于Monitor工作在法定人数,一半以上的总监视器节点应该总是可用的,以应对死机等极端情况,这是Monitor节点为N(N>0)个且N为奇数的原因。所有集群Monitor节点,其中一个节点为Leader。如果Leader Monitor节点处于不可用状态,其他显示器节点有资格成为Leader。生产群集必须至少有N/2个监控节点提供高可用性。 2.2.2 Ceph OSD简介 Ceph OSD是Ceph存储集群最重要的组件,Ceph OSD将数据以对象的形式存储到集群中每个节点的物理磁盘上,完成存储用户数据的工作绝大多数都是由OSD deamon进程来实现的。 Ceph集群一般情况都包含多个OSD,对于任何读写操作请求,Client端从Ceph Monitor获取Cluster Map之后,Client将直接与OSD进行I/O操作的交互,而不再需要Ceph Monitor干预。这使得数据读写过程更为迅速,因为这些操作过程不像其他存储系统,它没有其他额外的层级数据处理。 Ceph的核心功能特性包括高可靠、自动平衡、自动恢复和一致性。对于Ceph OSD而言,基于配置的副本数,Ceph提供通过分布在多节点上的副本来实现,使得Ceph具有高可用性以及容错性。在OSD中的每个对象都有一个主副本,若干个从副本,这些副本默认情况下是分布在不同节点上的,这就是Ceph作为分布式存储系统的集中体现。每个OSD都可能作为某些对象的主OSD,与此同时,它也可能作为某些对象的从OSD,从OSD受到主OSD的控制,然而,从OSD在某些情况也可能成为主OSD。在磁盘故障时,Ceph OSD Deamon的智能对等机制将协同其他OSD执行恢复操作。在此期间,存储对象副本的从OSD将被提升为主OSD,与此同时,新的从副本将重新生成,这样就保证了Ceph的可靠和一致。 Ceph OSD架构实现由物理磁盘驱动器、在其之上的Linux文件系统以及Ceph OSD服务组成。对Ceph OSD Deamon而言,Linux文件系统显性地支持了其扩展属性;这些文件系统的扩展属性提供了关于对象状态、快照、元数据内部信息;而访问Ceph OSD Deamon的ACL则有助于数据管理,如图2-3所示。 Ceph OSD操作必须在一个有效的Linux分区的物理磁盘驱动器上,Linux分区可以是BTRFS、XFS或者EXT4分区,文件系统是对性能基准测试的主要标准之一,下面来逐一了解。 1)BTRFS:在BTRFS文件系统的OSD相比于XFS和EXT4提供了最好的性能。BTRFS的主要优点有以下4点。 扩展性(scalability):BTRFS最重要的设计目标是应对大型机器对文件系统的扩展性要求。Extent、B-Tree和动态inode创建等特性保证了BTRFS在大型机器上仍有卓越的表现,其整体性能不会随着系统容量的增加而降低。 数据一致性(data integrity):当系统面临不可预料的硬件故障时,BTRFS采用 COW事务技术来保证文件系统的一致性。BTRFS还支持校验和,避免了silent corrupt(未知错误)的出现。而传统文件系统无法做到这一点。 多设备管理相关的特性:BTRFS支持创建快照(snapshot)和克隆(clone)。BTRFS还能够方便地管理多个物理设备,使得传统的卷管理软件变得多余。 结合Ceph,BTRFS中的诸多优点中的快照,Journal of Parallel(并行日志)等优势在Ceph中表现得尤为突出,不幸的是,BTRFS还未能到达生产环境要求的健壮要求。暂不推荐用于Ceph集群的生产使用。 2)XFS:一种高性能的日志文件系统,XFS特别擅长处理大文件,同时提供平滑的数据传输。目前CentOS 7也将XFS+LVM作为默认的文件系统。XFS的主要优点如下。 分配组:XFS文件系统内部被分为多个“分配组”,它们是文件系统中的等长线性存储区。每个分配组各自管理自己的inode和剩余空间。文件和文件夹可以跨越分配组。这一机制为XFS提供了可伸缩性和并行特性——多个线程和进程可以同时在同一个文件系统上执行I/O操作。这种由分配组带来的内部分区机制在一个文件系统跨越多个物理设备时特别有用,使得优化对下级存储部件的吞吐量利用率成为可能。 条带化分配:在条带化RAID阵列上创建XFS文件系统时,可以指定一个“条带化数据单元”。这可以保证数据分配、inode分配,以及内部日志被对齐到该条带单元上,以此最大化吞吐量。 基于Extent的分配方式:XFS文件系统中的文件用到的块由变长Extent管理,每一个Extent描述了一个或多个连续的块。对那些把文件所有块都单独列出来的文件系统来说,Extent大幅缩短了列表。 有些文件系统用一个或多个面向块的位图管理空间分配——在XFS中,这种结构被由一对B+树组成的、面向Extent的结构替代了;每个文件系统分配组(AG)包含这样的一个结构。其中,一个B+树用于索引未被使用的Extent的长度,另一个索引这些Extent的起始块。这种双索引策略使得文件系统在定位剩余空间中的Extent时十分高效。 扩展属性:XFS通过实现扩展文件属性给文件提供了多个数据流,使文件可以被附加多个名/值对。文件名是一个最大长度为256字节的、以NULL字符结尾的可打印字符串,其他的关联值则可包含多达64KB的二进制数据。这些数据被进一步分入两个名字空间中,分别为root和user。保存在root名字空间中的扩展属性只能被超级用户修改,保存在user名字空间中的可以被任何对该文件拥有写权限的用户修改。扩展属性可以被添加到任意一种 XFS inode上,包括符号链接、设备节点和目录等。可以使用attr命令行程序操作这些扩展属性。xfsdump和xfsrestore工具在进行备份和恢复时会一同操作扩展属性,而其他的大多数备份系统则会忽略扩展属性。 XFS作为一款可靠、成熟的,并且非常稳定的文件系统,基于分配组、条带化分配、基于Extent的分配方式、扩展属性等优势非常契合Ceph OSD服务的需求。美中不足的是,XFS不能很好地处理Ceph写入过程的journal问题。 3)Ext4:第四代扩展文件系统,是Linux系统下的日志文件系统,是Ext3文件系统的后继版本。其主要特征如下。 大型文件系统:Ext4文件系统可支持最高1 Exbibyte的分区与最大16 Tebibyte的文件。 Extents:Ext4引进了Extent文件存储方式,以替换Ext2/3使用的块映射(block mapping)方式。Extent指的是一连串的连续实体块,这种方式可以增加大型文件的效率并减少分裂文件。 日志校验和:Ext4使用校验和特性来提高文件系统可靠性,因为日志是磁盘上被读取最频繁的部分之一。 快速文件系统检查:Ext4将未使用的区块标记在inode当中,这样可以使诸如e2fsck之类的工具在磁盘检查时将这些区块完全跳过,而节约大量的文件系统检查的时间。这个特性已经在2.6.24版本的Linux内核中实现。 Ceph OSD把底层文件系统的扩展属性用于表示各种形式的内部对象状态和元数据。XATTR是以key/value形式来存储xattr_name和xattr_value,并因此提供更多的标记对象元数据信息的方法。Ext4文件系统提供不足以满足XATTR,由于XATTR上存储的字节数的限制能力,从而使Ext4文件系统不那么受欢迎。然而,BTRFS和XFS有一个比较大的限制XATTR。 Ceph使用日志文件系统,如增加了BTRFS和XFS的OSD。在提交数据到后备存储器之前,Ceph首先将数据写入称为一个单独的存储区,该区域被称为journal,这是缓冲器分区在相同或单独磁盘作为OSD,一个单独的SSD磁盘或分区,甚至一个文件文件系统。在这种机制下,Ceph任何写入首先是日志,然后是后备存储,如图2-4所示。 图2-4 IO流向图 journal持续到后备存储同步,每隔5s。默认情况下。10GB是该jouranl的常用的大小,但journal空间越大越好。Ceph使用journal综合考虑了存储速度和数据的一致性。journal允许Ceph OSD功能很快做小的写操作;一个随机写入首先写入在上一个连续类型的journal,然后刷新到文件系统。这给了文件系统足够的时间来合并写入磁盘。使用SSD盘作为journal盘能获得相对较好的性能。在这种情况下,所有的客户端写操作都写入到超高速SSD日志,然后刷新到磁盘。所以,一般情况下,使用SSD作为OSD的journal可以有效缓冲突发负载。 与传统的分布式数据存储不同,RADOS最大的特点如下。 ① 将文件映射到Object后,利用Cluster Map通过CRUSH计算而不是查找表方式定位文件数据到存储设备中的位置。优化了传统的文件到块的映射和BlockMap管理。 ② RADOS充分利用了OSD的智能特点,将部分任务授权给OSD,最大程度地实现可扩展。 2.3 RADOS与LIBRADOS LIBRADOS模块是客户端用来访问RADOS对象存储设备的。Ceph存储集群提供了消息传递层协议,用于客户端与Ceph Monitor与OSD交互,LIBRADOS以库形式为Ceph Client提供了这个功能,LIBRADOS就是操作RADOS对象存储的接口。所有Ceph客户端可以用LIBRADOS或LIBRADOS里封装的相同功能和对象存储交互,LIBRBD和LIBCEPHFS就利用了此功能。你可以用LIBRADOS直接和Ceph交互(如与Ceph兼容的应用程序、Ceph接口等)。下面是简单描述的步骤。 第1步:获取LIBRADOS。 第2步:配置集群句柄。 第3步:创建IO上下文。 第4步:关闭连接。 LIBRADOS架构图,如图2-5所示。 先根据配置文件调用librados创建一个rados,接下来为这个rados创建一个radosclient,radosclient包含3个主要模块(finisher、Messager、Objector)。再根据pool创建对应的ioctx,在ioctx中能够找到radosclient。再调用osdc对生成对应osd请求,与OSD进行通信响应请求。  下面分别介绍LIBRADOS的C语言、Java语言和Python语言示例。 1. LIBRADOS C语言示例 下面是LIBRADOS C语言示例。 #include <stdio.h> #include <string.h> #include <rados/librados.h> int main (int argc, char argv**) {         /*声明集群句柄以及所需参数 */         rados_t cluster;         char cluster_name[] = "ceph";        //集群名称         char user_name[] = "client.admin";   //指定访问集群的用户,这里用admin         uint64_t flags;         rados_ioctx_t io;                    //rados上下文句柄         char *poolname = "data";             //目标pool名         char read_res[100];         char xattr[] = "en_US";         /* 指定参数初始化句柄*/         int err;         err = rados_create2(&cluster, cluster_name, user_name, flags);         if (err < 0) {                 fprintf(stderr, "%s: Couldn't create the cluster handle! %s\n", argv[0], strerror(-err));                 exit(EXIT_FAILURE);         } else {                 printf("\nCreated a cluster handle.\n");         }         /* 读取配置文件用来配置句柄*/         err = rados_conf_read_file(cluster, "/etc/ceph/ceph.conf");         if (err < 0) {                 fprintf(stderr, "%s: cannot read config file: %s\n", argv[0], strerror(-err));                 exit(EXIT_FAILURE);         } else {                 printf("\nRead the config file.\n");         }         /* 分解参数 */         err = rados_conf_parse_argv(cluster, argc, argv);         if (err < 0) {                 fprintf(stderr, "%s: cannot parse command line arguments: %s\n", argv[0], strerror(-err));                 exit(EXIT_FAILURE);         } else {                 printf("\nRead the command line arguments.\n");         }         /* 连接集群*/         err = rados_connect(cluster);         if (err < 0) {                 fprintf(stderr, "%s: cannot connect to cluster: %s\n", argv[0], strerror(-err));                 exit(EXIT_FAILURE);         } else {                 printf("\nConnected to the cluster.\n");         }        //创建rados句柄上下文         err = rados_ioctx_create(cluster, poolname, &io);         if (err < 0) {                 fprintf(stderr, "%s: cannot open rados pool %s: %s\n", argv[0], poolname, strerror(-err));                 rados_shutdown(cluster);                 exit(EXIT_FAILURE);         } else {                 printf("\nCreated I/O context.\n");         }         //写对象         err = rados_write(io, "hw", "Hello World!", 12, 0);         if (err < 0) {                 fprintf(stderr, "%s: Cannot write object \"hw\" to pool %s: %s\n", argv[0], poolname, strerror(-err));                 rados_ioctx_destroy(io);                 rados_shutdown(cluster);                 exit(1);         } else {                 printf("\nWrote \"Hello World\" to object \"hw\".\n");         }        //设置对象属性         err = rados_setxattr(io, "hw", "lang", xattr, 5);         if (err < 0) {                 fprintf(stderr, "%s: Cannot write xattr to pool %s: %s\n", argv[0], poolname, strerror(-err));                 rados_ioctx_destroy(io);                 rados_shutdown(cluster);                 exit(1);         } else {                 printf("\nWrote \"en_US\" to xattr \"lang\" for object \"hw\".\n");         }         rados_completion_t comp;        //确认异步rados句柄成功创建         err = rados_aio_create_completion(NULL, NULL, NULL, &comp);         if (err < 0) {                 fprintf(stderr, "%s: Could not create aio completion: %s\n", argv[0], strerror(-err));                 rados_ioctx_destroy(io);                 rados_shutdown(cluster);                 exit(1);         } else {                 printf("\nCreated AIO completion.\n");         }         /* Next, read data using rados_aio_read. */         //异步读对象         err = rados_aio_read(io, "hw", comp, read_res, 12, 0);         if (err < 0) {                 fprintf(stderr, "%s: Cannot read object. %s %s\n", argv[0], poolname, strerror(-err));                 rados_ioctx_destroy(io);                 rados_shutdown(cluster);                 exit(1);         } else {                 printf("\nRead object \"hw\". The contents are:\n %s \n", read_res);         }        //等待对象上的操作完成         rados_wait_for_complete(comp);         // 释放complete句柄          rados_aio_release(comp);         char xattr_res[100];        //获取对象         err = rados_getxattr(io, "hw", "lang", xattr_res, 5);         if (err < 0) {                 fprintf(stderr, "%s: Cannot read xattr. %s %s\n", argv[0], poolname, strerror(-err));                 rados_ioctx_destroy(io);                 rados_shutdown(cluster);                 exit(1);         } else {                 printf("\nRead xattr \"lang\" for object \"hw\". The contents are:\n %s \n", xattr_res);         }        //移除对象属性         err = rados_rmxattr(io, "hw", "lang");         if (err < 0) {                 fprintf(stderr, "%s: Cannot remove xattr. %s %s\n", argv[0], poolname, strerror(-err));                 rados_ioctx_destroy(io);                 rados_shutdown(cluster);                 exit(1);         } else {                 printf("\nRemoved xattr \"lang\" for object \"hw\".\n");         }        //删除对象         err = rados_remove(io, "hw");         if (err < 0) {                 fprintf(stderr, "%s: Cannot remove object. %s %s\n", argv[0], poolname, strerror(-err));                 rados_ioctx_destroy(io);                 rados_shutdown(cluster);                 exit(1);         } else {                 printf("\nRemoved object \"hw\".\n");         }     rados_ioctx_destroy(io);                  //销毁io上下文     rados_shutdown(cluster);                  //销毁句柄 } 2. LIBRADOS Java语言示例 下面是LIBRADOS Java语言示例。 import com.ceph.rados.Rados; import com.ceph.rados.RadosException; import java.io.File; public class CephClient {         public static void main (String args[]){                 try {                         //获取句柄                         Rados cluster = new Rados("admin");                         System.out.println("Created cluster handle.");                         File f = new File("/etc/ceph/ceph.conf");                         //读取配置文件                         cluster.confReadFile(f);                         System.out.println("Read the configuration file.");                        //连接集群                         cluster.connect();                         System.out.println("Connected to the cluster.");                 } catch (RadosException e) {                         System.out.println(e.getMessage()+":"+e.getReturnValue());                 }         } } 3. LIBRADOS Python语言示例 下面是LIBRADOS Python语言示例。 #!/usr/bin/python #encoding=utf-8 import rados, sys cluster = rados.Rados(conffile='/etc/ceph/ceph.conf')   #获取句柄 print "\n\nI/O Context and Object Operations" print "=================================" print "\nCreating a context for the 'data' pool" if not cluster.pool_exists('data'):                   raise RuntimeError('No data pool exists') ioctx = cluster.open_ioctx('data')                  #获取pool的io上下文句柄 print "\nWriting object 'hw' with contents 'Hello World!' to pool 'data'." ioctx.write("hw", "Hello World!")                 #写入对象 print "Writing XATTR 'lang' with value 'en_US' to object 'hw'" ioctx.set_xattr("hw", "lang", "en_US")          #设置对象的属性 print "\nWriting object 'bm' with contents 'Bonjour tout le monde!' to pool 'data'." ioctx.write("bm", "Bonjour tout le monde!") print "Writing XATTR 'lang' with value 'fr_FR' to object 'bm'" ioctx.set_xattr("bm", "lang", "fr_FR") print "\nContents of object 'hw'\n------------------------" print ioctx.read("hw")                   #读取对象 print "\n\nGetting XATTR 'lang' from object 'hw'" print ioctx.get_xattr("hw", "lang")        #读取对象属性 print "\nContents of object 'bm'\n------------------------" print ioctx.read("bm")   print "\nClosing the connection." ioctx.close()                           #关闭io上下文 print "Shutting down the handle." cluster.shutdown()                      #销毁句柄 2.4 本章小结 本章从宏观角度剖析了Ceph架构,将Ceph架构分为基础存储系统RADOS、基于RADOS实现的CEPHFS,基于RADOS的LIBRADOS层应用接口、基于LIBRADOS的应用接口RBD、RADOSGW,其中着重讲解了RADOS的组成部分MON、OSD及其功能,最后解析LIBRADOS API的基本用法。 第3章 智能分布CRUSH 3.1 引言 数据分布是分布式存储系统的一个重要部分,数据分布算法至少要考虑以下3个因素。 1)故障域隔离。同份数据的不同副本分布在不同的故障域,降低数据损坏的风险。 2)负载均衡。数据能够均匀地分布在磁盘容量不等的存储节点,避免部分节点空闲,部分节点超载,从而影响系统性能。 3)控制节点加入离开时引起的数据迁移量。当节点离开时,最优的数据迁移是只有离线节点上的数据被迁移到其他节点,而正常工作的节点的数据不会发生迁移。 对象存储中一致性Hash和Ceph的CRUSH算法是使用比较多的数据分布算法。在Aamzon的Dyanmo键值存储系统中采用一致性Hash算法,并且对它做了很多优化。OpenStack的Swift对象存储系统也使用了一致性Hash算法。 CRUSH(Controlled Replication Under Scalable Hashing)是一种基于伪随机控制数据分布、复制的算法。Ceph是为大规模分布式存储系统(PB级的数据和成百上千台存储设备)而设计的,在大规模的存储系统里,必须考虑数据的平衡分布和负载(提高资源利用率)、最大化系统的性能,以及系统的扩展和硬件容错等。CRUSH就是为解决以上问题而设计的。在Ceph集群里,CRUSH只需要一个简洁而层次清晰的设备描述,包括存储集群和副本放置策略,就可以有效地把数据对象映射到存储设备上,且这个过程是完全分布式的,在集群系统中的任何一方都可以独立计算任何对象的位置;另外,大型系统存储结构是动态变化的(存储节点的扩展或者缩容、硬件故障等),CRUSH能够处理存储设备的变更(添加或删除),并最小化由于存储设备的变更而导致的数据迁移。 3.2 CRUSH基本原理 众所周知,存储设备具有吞吐量限制,它影响读写性能和可扩展性能。所以,存储系统通常都支持条带化以增加存储系统的吞吐量并提升性能,数据条带化最常见的方式是做RAID。与Ceph的条带化最相似的是RAID 0或者是“带区卷”。Ceph条带化提供了类似于RAID 0的吞吐量,N路RAID镜像的可靠性以及更快速的恢复能力。 在磁盘阵列中,数据是以条带(stripe)的方式贯穿在磁盘阵列所有硬盘中的。这种数据的分配方式可以弥补OS读取数据量跟不上的不足。 1)将条带单元(stripe unit)从阵列的第一个硬盘到最后一个硬盘收集起来,就可以称为条带(stripe)。有的时候,条带单元也被称为交错深度。在光纤技术中,一个条带单元被叫作段。 2)数据在阵列中的硬盘上是以条带的形式分布的,条带化是指数据在阵列中所有硬盘中的存储过程。文件中的数据被分割成小块的数据段在阵列中的硬盘上顺序的存储,这个最小数据块就叫作条带单元。 决定Ceph条带化数据的3个因素。 对象大小:处于分布式集群中的对象拥有一个最大可配置的尺寸(例如,2MB、4MB等),对象大小应该足够大以适应大量的条带单元。 条带宽度:条带有一个可以配置的单元大小,Ceph Client端将数据写入对象分成相同大小的条带单元,除了最后一个条带之外;每个条带宽度,应该是对象大小的一小部分,这样使得一个对象可以包含多个条带单元。 条带总量:Ceph客户端写入一系列的条带单元到一系列的对象,这就决定了条带的总量,这些对象被称为对象集,当Ceph客户端端写入的对象集合中的最后一个对象之后,它将会返回到对象集合中的第一个对象处。 3.2.1 Object与PG Ceph条带化之后,将获得N个带有唯一oid(即object的id)。Object id是进行线性映射生成的,即由file的元数据、Ceph条带化产生的Object的序号连缀而成。此时Object需要映射到PG中,该映射包括两部分。 1)由Ceph集群指定的静态Hash函数计算Object的oid,获取到其Hash值。 2)将该Hash值与mask进行与操作,从而获得PG ID。 根据RADOS的设计,假定集群中设定的PG总数为M(M一般为2的整数幂),则mask的值为M–1。由此,Hash值计算之后,进行按位与操作是想从所有PG中近似均匀地随机选择。基于该原理以及概率论的相关原理,当用于数量庞大的Object以及PG时,获得到的PG ID是近似均匀的。 计算PG的ID示例如下。 1)Client输入pool ID和对象ID(如pool=‘liverpool’,object-id=‘john’)。 2)CRUSH获得对象ID并对其Hash运算。 3)CRUSH计算OSD个数,Hash取模获得PG的ID(如0x58)。 4)CRUSH获得已命名pool的ID(如liverpool=4)。 5)CRUSH预先考虑到pool ID相同的PG ID(如4.0x58)。 3.2.2 PG与OSD 由PG映射到数据存储的实际单元OSD中,该映射是由CRUSH算法来确定的,将PG ID作为该算法的输入,获得到包含N个OSD的集合,集合中第一个OSD被作为主OSD,其他的OSD则依次作为从OSD。N为该PG所在POOL下的副本数目,在生产环境中N一般为3;OSD集合中的OSD将共同存储和维护该PG下的Object。需要注意的是,CRUSH算法的结果不是绝对不变的,而是受到其他因素的影响。其影响因素主要有以下两个。 一是当前系统状态。也就是上文逻辑结构中曾经提及的Cluster Map(集群映射)。当系统中的OSD状态、数量发生变化时,Cluster Map可能发生变化,而这种变化将会影响到PG与OSD之间的映射。 二是存储策略配置。这里的策略主要与安全相关。利用策略配置,系统管理员可以指定承载同一个PG的3个OSD分别位于数据中心的不同服务器乃至机架上,从而进一步改善存储的可靠性。 因此,只有在Cluster Map和存储策略都不发生变化的时候,PG和OSD之间的映射关系才是固定不变的。在实际使用中,策略一经配置通常不会改变。而系统状态的改变或者是因为设备损坏,或者是因为存储集群规模扩大。好在Ceph本身提供了对于这种变化的自动化支持,因而,即便PG与OSD之间的映射关系发生了变化,并不会对应用造成困扰。事实上,Ceph正是需要有目的的利用这种动态映射关系。正是利用了CRUSH的动态特性,Ceph才可以将一个PG根据需要动态迁移到不同的OSD组合上,从而自动化地实现高可靠性、数据分布re-blancing等特性。 之所以在此次映射中使用CRUSH算法,而不是其他Hash算法,原因之一是CRUSH具有上述可配置特性,可以根据管理员的配置参数决定OSD的物理位置映射策略;另一方面是因为CRUSH具有特殊的“稳定性”,也就是当系统中加入新的OSD导致系统规模增大时,大部分PG与OSD之间的映射关系不会发生改变,只有少部分PG的映射关系会发生变化并引发数据迁移。这种可配置性和稳定性都不是普通Hash算法所能提供的。因此,CRUSH算法的设计也是Ceph的核心内容之一。 3.2.3 PG与Pool Ceph存储系统支持“池”(Pool)的概念,这是存储对象的逻辑分区。 Ceph Client端从Ceph mon端检索Cluster Map,写入对象到Pool。Pool的副本数目,Crush规则和PG数目决定了Ceph将数据存储的位置,如图3-1所示。 Pool至少需要设定以下参数。 对象的所有权/访问权。 PG数目。 该pool使用的crush规则。 对象副本的数目。 Object、PG、Pool、OSD关系图,如图3-2所示。 图3-2 映射关系图 Pool相关的操作如下。 1)创建pool。 ceph osd pool create {pool-name} {pg-num} [{pgp-num}] [replicated] \      [crush-ruleset-name] [expected-num-objects] ceph osd pool create {pool-name} {pg-num}  {pgp-num}   erasure \      [erasure-code-profile] [crush-ruleset-name] [expected_num_objects] 2)配置pool配额。 ceph osd pool set-quota {pool-name} [max_objects {obj-count}] [max_bytes {bytes}] 3)删除pool。 ceph osd pool delete {pool-name} [{pool-name} --yes-i-really-really-mean-it] 4)重命名pool。 ceph osd pool rename {current-pool-name} {new-pool-name} 5)展示pool统计。 rados df 6)给pool做快照。 ceph osd pool mksnap {pool-name} {snap-name} 7)删除pool快照。 ceph osd pool rmsnap {pool-name} {snap-name} 8)配置pool的相关参数。 ceph osd pool set {pool-name} {key} {value} 9)获取pool参数的值。 ceph osd pool get {pool-name} {key} 10)配置对象副本数目。 ceph osd pool set {poolname} size {num-replicas} 11)获取对对象副本数目。 ceph osd dump | grep 'replicated size' 3.3 CRUSH关系分析 从本质上讲,CRUSH算法是通过存储设备的权重来计算数据对象的分布的。在计算过程中,通过Cluster Map(集群映射)、Data Distribution Policy(数据分布策略)和给出的一个随机数共同决定数据对象的最终位置。 1. Cluster Map Cluster Map记录所有可用的存储资源及相互之间的空间层次结构(集群中有多少个机架、机架上有多少服务器、每个机器上有多少磁盘等信息)。所谓的Map,顾名思义,就是类似于我们生活中的地图。在Ceph存储里,数据的索引都是通过各种不同的Map来实现的。另一方面,Map 使得Ceph集群存储设备在物理层作了一层防护。例如,在多副本(常见的三副本)的结构上,通过设置合理的Map(故障域设置为Host级),可以保证在某一服务器死机的情况下,有其他副本保留在正常的存储节点上,能够继续提供服务,实现存储的高可用。设置更高的故障域级别(如Rack、Row等)能保证整机柜或同一排机柜在掉电情况下数据的可用性和完整性。 (1)Cluster Map的分层结构 Cluster Map由Device和Bucket构成。它们都有自己的ID和权重值,并且形成一个以Device为叶子节点、Bucket为躯干的树状结构,如图3-3所示。 图3-3 CRUSH架构图 Bucket拥有不同的类型,如Host、Row、Rack、Room等,通常我们默认把机架类型定义为Rack,主机类型定义为Host,数据中心(IDC机房)定义为Data Center。Bucket的类型都是虚拟结构,可以根据自己的喜好设计合适的类型。Device节点的权重值代表了存储设备的容量与性能。其中,磁盘容量是权重大小的关键因素。 OSD 的权重值越高,对应磁盘会被分配写入更多的数据。总体来看,数据会被均匀写入分布于群集所有磁盘,从而提高整体性能和可靠性。无论磁盘的规格容量,总能够均匀使用。 关于OSD权重值的大小值的配比,官方默认值设置为1TB容量的硬盘,对应权重值为1。 可以在/etc/init.d/ceph 原码里查看相关的内容。 … 363             get_conf osd_weight "" "osd crush initial weight" 364             defaultweight="$(df -P -k $osd_data/. | tail -1 | awk '{ print sprintf("%.2f",$2/1073741824) }')"  ###此处就是ceph默认权重的设置值             get_conf osd_keyring "$osd_data/keyring" "keyring" 366             do_cmd_okfail "timeout 30 $BINDIR/ceph -c $conf --name=osd.$id --keyring=$osd_keyring     osd crush create-or-move -- $id ${osd_weight:-${defaultweight:-1}} $osd_location" … (2)恢复与动态平衡 在默认设置下,当集群里有组件出现故障时(主要是OSD,也可能是磁盘或者网络等),Ceph会把OSD标记为down,如果在300s内未能回复,集群就会开始进行恢复状态。这个“300s”可以通过“mon osd down out interval”配置选项修改等待时间。PG(Placement  Groups)是Ceph数据管理(包括复制、修复等动作)单元。当客户端把读写请求(对象单元)推送到Ceph时,通过CRUSH提供的Hash算法把对象映射到PG。PG在CRUSH策略的影响下,最终会被映射到OSD上。 2. Data Distribution Policy Data Distribution Policy由Placement Rules组成。Rule决定了每个数据对象有多少个副本,这些副本存储的限制条件(比如3个副本放在不同的机架中)。一个典型的rule如下所示。 rule replicated_ruleset {    ##rule名字     ruleset 0         # rule的Id     type replicated   ## 类型为副本模式,另外一种模式为纠删码(EC)     min_size 1        ## 如果存储池的副本数大于这个值,此rule不会应用     max_size 10       ## 如要存储池的副本数大于这个值,此rule不会应用     step take default  ## 以default root为入口     step chooseleaf firstn 0 type host  ## 隔离域为host级,即不同副本在不同的主机上     step emit        ## 提交 } 根据实际的设备环境,可以定制出符合自己需求的Rule,详见第10章。 3. CRUSH中的伪随机  先来看一下数据映射到具体OSD的函数表达形式。 CRUSH(x) -> (osd1, osd2, osd3.....osdn)   CRUSH使用了多参数的Hash函数在Hash之后,映射都是按既定规则选择的,这使得从x到OSD的集合是确定的和独立的。CRUSH只使用Cluster Map、Placement Rules、X。CRUSH是伪随机算法,相似输入的结果之间没有相关性。关于伪随机的确认性和独立性,以下我们以一个实例来展示。  [root@host-192-168-0-16 ~]# ceph osd tree  ID WEIGHT  TYPE NAME               UP/DOWN  REWEIGHT  PRIMARY-AFFINITY  -1 0.35999 root default                                                  -3 0.09000     host host-192-168-0-17                                     2 0.09000         osd.2                   up  1.00000          1.00000  -4 0.09000     host host-192-168-0-18                                     3 0.09000         osd.3                   up  1.00000          1.00000  -2 0.17999     host host-192-168-0-16                                     0 0.09000         osd.0                   up  1.00000          1.00000   1 0.09000         osd.1                   up  1.00000          1.00000 以上是某个Ceph集群的存储的CRUSH结构。在此集群里我们手动创建一个pool,并指定为8个PG和PGP。 [root@host-192-168-0-16 ~]# ceph  osd pool create  crush  8   8  pool 'crush' created PGP是PG的逻辑承载体,是CRUSH算法不可缺少的部分。在Ceph集群里,增加PG数量,PG到OSD的映射关系就会发生变化,但此时存储在PG里的数据并不会发生迁移,只有当PGP的数量也增加时,数据迁移才会真正开始。关于PG和PGP的关系,假如把PG比作参加宴会的人,那么PGP就是人坐的椅子,如果人员增加时,人的座位排序就会发生变化,只有增加椅子时,真正的座位排序变更才会落实。因此,人和椅子的数量一般都保持一致。所以,在Ceph里,通常把PGP和PG设置成一致的。 可以查看PG映射OSD的集合,即PG具体分配到OSD的归属。  [root@host-192-168-0-16 ~]# ceph pg dump | grep ^22\. | awk '{print $1 "\t"   $17}'    ## 22 表示 Pool id ## dumped all in format plain 22.1 [1,2,3]     22.0   [1,2,3] 22.3  [3,1,2] 22.2  [3,2,0] 22.5 [1,3,2] 22.4  [3,1,2] 22.7  [2,1,3] 22.6  [3,0,2] 上面示例中,第一列是PG的编号(其中22表示的Pool的ID),共计8个,第二列是PG映射到了具体的OSD集合。如“22.1 [1,2,3]”表示PG 22.1分别在OSD1、OSD2和OSD3分别存储副本。 在Ceph集群里,当有数据对象要写入集群时,需要进行两次映射。第一次从object→PG,第二次是PG→OSD set。每一次的映射都是与其他对象无相关的。以上充分体现了CRUSH的独立性(充分分散)和确定性(可确定的存储位置)。 3.4 本章小结 本章主要围绕Ceph的核心——CRUSH展开,讲述了CRUSH的基本原理以及CRUSH的特性。读者可以了解Ceph基本要素的概念,重点应该把握Ceph的Object、PG、Pool等基本核心概念,熟悉读写流程以及CRUSH算法的组成、影响因素及选择流程。基于以上掌握的要点,为后面的深入学习打好坚实的基础。
文章
存储 · 运维 · 算法 · 对象存储 · 块存储
2017-05-02
linux下bus、devices和platform的基础模型 【转】
转自:http://blog.chinaunix.net/uid-20672257-id-3147337.html 一、kobject的定义:kobject是Linux2.6引入的设备管理机制,在内核中由struct kobject结构表示,这个结构使所有设备在底层都具有统一的接口.kobject提供了基本的对象管理能力,是构成Linux2.6设备模型的核心结构,它与sysfs文件系统紧密联系,每个在内核中注册kobject对象都对应与sysfs文件系统中的一个目录;kobject--->sysfs.dir;其结构定义为:struct kobject{  const char*        k_name;              //指向设备名称的指针  char               name[KOBJ_NAME_LEN]; //设备名称  struct kref        kref;                //内核对象的引用计数  struct list_head   entry;               //挂接到当前内核对象所在kset中的单元  struct kobject*    parent;              //指向父对象的指针  struct kset*       kset;                //内核对象所属kset的指针  struct kobj_type*  ktype;               //指向内核对象类型描述符的指针  struct dentry*     dentry;              //sysfs文件系统中与该内核对象对应的文件节点路径的指针  wait_queue_head_t  poll;                //IO等待队列;};二、kobject相关函数:1、void kobject_init(struct kobject* kobj);   该函数用于初始化kobject对象,它设置kobject对象的引用计数为1,entry字段指向自身,其所属kset对象的引用计数加1;2、void kobject_cleanup(struct kobject* kobj);   void kobject_release(struct kref* ref);   这两个函数用于清除kobject对象,当其引用计数为0时,释放对象所占用的资源;3、int kobject_set_name(struct kobject* kobj, const char* format, ...);   该函数用于设置指定kobject对象的名称;4、const char* kobject_name(const struct kobject* kobj);   该函数用于返回指定kobject的名称;5、int kobject_rename(struct kobject* kobj, const char* new_name);   该函数用于为指定kobject对象重命名;6、struct kobject* kobject_get(struct kobject* kobj);   该函数用于将kobject对象的引用计数加1,相当于申请了一个kobject对象资源,同时返回该kobject对象的指针;7、void kobject_put(struct kobject* kobj);   该函数用于将kobject对象的引用计数减1,相当于释放了一个kobject对象资源;当引用计数为0时,则调用kobject_release()释放该kobject对象的资源;8、int kobject_add(struct kobject* kobj);   该函数用于注册kobject对象,即:加入到Linux的设备层次中,它会挂接该kobject对象到kset的list链中,增加父目录各级kobject对象的引用计数,在其parent字段指向的目录下创建对应的文件节点,并启动该类型kobject对象的hotplug()函数;9、void kobject_del(struct kobject* kobj);   该函数与kobject_add()相反,用于注销kobject对象,即:中止该kobject对象的hotplug()函数,从Linux的设备层次中删除该kobject对象,删除该kobject对象在sysfs文件系统中对应的文件节点;10、int kobject_register(struct kobject* obj);    该函数用于注册kobject对象,它首先会调用kobject_init()初始化kobj,然后再调用kobject_add()完成该内核对象的添加;11、void kobject_unregister(struct kobject* kobj);    该函数与kobject_register()相反,用于注销kobject对象,它首先调用kobject_del()从Linux的设备层次中删除kobject对象,再调用kobject_put()减少该kobject对象的引用计数,当引用计数为0时,则释放该kobject对象的资源;12、struct kobject* kobject_add_dir(struct kobject*, const char* path);    该函数用于在sysfs文件系统中为该kobject对象创建对应的目录;13、char* kobject_get_path(struct kobject* kobj);    该函数用于返回该kobject对象在sysfs文件系统中的对应目录路径;三、kobject的行为:typedef int __bitwise kobject_action_t;enum kobject_action{  KOBJ_ADD     = (__force kobject_action_t) 0x01, //exclusive to core  KOBJ_REMOVE  = (__force kobject_action_t) 0x02, //exclusive to core  KOBJ_CHANGE  = (__force kobject_action_t) 0x03, //device state change  KOBJ_MOUNT   = (__force kobject_action_t) 0x04, //mount event for block devices (broken)  KOBJ_UMOUNT  = (__force kobject_action_t) 0x05, //umount event for block devices (broken)  KOBJ_OFFLINE = (__force kobject_action_t) 0x06, //device offline  KOBJ_ONLINE  = (__force kobject_action_t) 0x07, //device online};该枚举类型用于定义kobject对象的状态更新消息码,也就是热插拔事件码;备注:struct kobject结构定义于文件include/linux/kobject.h下面转自:http://blog.chinaunix.net/u1/57901/showart_1803248.html 在LINUX中最让人不解的大概就是/sys下面的内容了 下面首先让我们来创建一个简单的platform设备,并从这个设备的视角进行深入,在此篇文章的深入过程中,我们只看kobeject的模型我所使用的内核版本号为2.6.26,操作系统的内核版本号为2.6.27-7,暂未发现2.6.27-7与2.6.26的重大不同 首先写一个简单的模块 #include #include #include static int __init test_probe(struct platform_device *pdev) {         int err = 0;         return err; } static int test_remove(struct platform_device *pdev) {         return 0; } static struct platform_device test_device = {         .name = "test_ts",         .id = -1, }; static struct platform_driver test_driver = {         .probe                = test_probe,         .remove                = test_remove,         .driver                = {                 .name        = "test_ts",                 .owner        = THIS_MODULE,         }, }; static int __devinit test_init(void) {         platform_device_register(&test_device);                 return platform_driver_register(&test_driver); } static void __exit test_exit(void) {         platform_device_unregister(&test_device);         platform_driver_unregister(&test_driver); } module_init(test_init); module_exit(test_exit); MODULE_AUTHOR("zwolf"); MODULE_DESCRIPTION("Module test"); MODULE_LICENSE("GPL"); MODULE_ALIAS("test"); 接下来是makefile #Makefile obj-m:=test.o KDIR:=/lib/modules/2.6.27-7-generic/build PWD:=$(shell pwd) default:         $(MAKE) -C $(KDIR) M=$(PWD) modules KDIR中的目录请改为各位实际运行中的内核目录make之后进行模块的加载 sudo insmod ./test.ko 现在到sys目录中查看我们的设备是否已经加载上了 首先是/sys/bus/platform/devices/在devices下,每一个连接文件都代表了一个设备ls可看见test_ts,进入test_ts,ls可发现driver这个链接文件,ls-l查看,发现这个文件是连到/sys/bus/platform/drivers/test_ts的 这里需要说明的是连接的含义,并不是driver驱动存在于test_ts这个设备中,而是test_ts使用的驱动为/sys/bus/platform/drivers/test_ts 现在换到/sys/bus/platform/drivers这个目录下 ls查看会发现这里的文件都为目录,而非连接文件,说明这是驱动真正放置的位置 现在进入test_ts目录,然后ls,发现有一个test_ts的连接文件,ls –l查看可发现该文件连接到/sys/devices/platform/test_ts下 回到/sys/bus/platform/devices/下ls –l也会发现test_ts连接到/sys/devices/platform/test_ts 为什么test_ts这个设备放置于/sys/devices/platform下,而不是/sys/bus/platform/devices下呢 我认为和直观性有关,在sys下有这么几个目录block  bus  class  dev  devices  firmware  kernel  module  fs power  devices很直观的说明了设备在这个目录下再来看组成这个目录图的核心,kobject图,我也叫他层次图不看大号绿色箭头右边的内容的话是不是发现两个架构相同? 对的,kobject的层次决定了目录的结构 kobeject图很大,但也不要担心,里面的内容其实不多,基础框架涉及3个主要结构kset kobject和ktype 在说明test_ts的注册之前,先让我们看一下sys下的两个基础目录bus,devices 首先是bus bus的注册在/drivers/base/bus.c里 int __init buses_ini    t(void) {         bus_kset = kset_create_and_add("bus", &bus_uevent_ops, NULL);         if (!bus_kset)                 return -ENOMEM;         return 0; } 先看bus_uevent_ops,这是一个uevent的操作集(我也还没清楚uevent的用途,所以uevent的内容先放着) 然后到kset_create_and_add struct kset *kset_create_and_add(const char *name,                                  struct kset_uevent_ops *uevent_ops,                                  struct kobject *parent_kobj) //传递进来的参数为("bus", &bus_uevent_ops, NULL) {         struct kset *kset;         int error;         //创建一个kset容器         kset = kset_create(name, uevent_ops, parent_kobj);         if (!kset)                 return NULL;         //注册创建的kset容器         error = kset_register(kset);         if (error) {                 kfree(kset);                 return NULL;         }         return kset; } 首先需要创建一个kset容器 static struct kset *kset_create(const char *name,                                 struct kset_uevent_ops *uevent_ops,                                 struct kobject *parent_kobj) //传递进来的参数为("bus", &bus_uevent_ops, NULL) {         struct kset *kset;         //为kset分配内存         kset = kzalloc(sizeof(*kset), GFP_KERNEL);         if (!kset)                 return NULL;         //设置kset中kobject的名字,这里为bus         kobject_set_name(&kset->kobj, name);         //设置uevent操作集,这里为bus_uevent_ops         kset->uevent_ops = uevent_ops;         //设置父对象,这里为NULL         kset->kobj.parent = parent_kobj;         //设置容器操作集         kset->kobj.ktype = &kset_ktype;         //设置父容器         kset->kobj.kset = NULL;         return kset; } 这里的ktype,也就是kset_ktype是一个操作集,用于为sys下文件的实时反馈做服务,例如我们cat name的时候就要通过ktype提供的show函数,具体什么怎么运用,将在后面讲解 现在回到kset_create_and_add中的kset_register,将建立好的kset添加进sys里 int kset_register(struct kset *k) {         int err;         if (!k)                 return -EINVAL;         //初始化         kset_init(k);         //添加该容器         err = kobject_add_internal(&k->kobj);         if (err)                 return err;         kobject_uevent(&k->kobj, KOBJ_ADD);         return 0; } kset_init进行一些固定的初始化操作,里面没有我们需要关心的内容 kobject_add_internal为重要的一个函数,他对kset里kobj的从属关系进行解析,搭建正确的架构 static int kobject_add_internal(struct kobject *kobj) {         int error = 0;         struct kobject *parent;         //检测kobj是否为空         if (!kobj)                 return -ENOENT;         //检测kobj名字是否为空         if (!kobj->name || !kobj->name[0]) {                 pr_debug("kobject: (%p): attempted to be registered with empty "                          "name!\n", kobj);                 WARN_ON(1);                 return -EINVAL;         }         //提取父对象         parent = kobject_get(kobj->parent);         /* join kset if set, use it as parent if we do not already have one */         //父容器存在则设置父对象         if (kobj->kset) {//在bus的kset中为空,所以不会进入到下面的代码                 //检测是否已经设置父对象                 if (!parent)                         //无则使用父容器为父对象                         parent = kobject_get(&kobj->kset->kobj);                 //添加该kobj到父容器的链表中                 kobj_kset_join(kobj);                 //设置父对象                 kobj->parent = parent;         }         pr_debug("kobject: '%s' (%p): %s: parent: '%s', set: '%s'\n",                  kobject_name(kobj), kobj, __func__,                  parent ? kobject_name(parent) : "",                  kobj->kset ? kobject_name(&kobj->kset->kobj) : "");         //建立相应的目录         error = create_dir(kobj);         if (error) {                 kobj_kset_leave(kobj);                 kobject_put(parent);                 kobj->parent = NULL;                 if (error == -EEXIST)                         printk(KERN_ERR "%s failed for %s with "                                "-EEXIST, don't try to register things with "                                "the same name in the same directory.\n",                                __func__, kobject_name(kobj));                 else                         printk(KERN_ERR "%s failed for %s (%d)\n",                                __func__, kobject_name(kobj), error);                 dump_stack();         } else                 kobj->state_in_sysfs = 1;         return error; } 至此bus的目录就建立起来了 模型如下接下来是devices,在/drivers/base/core.c里 int __init devices_init(void) {         devices_kset = kset_create_and_add("devices", &device_uevent_ops, NULL);         if (!devices_kset)                 return -ENOMEM;         return 0; } 过程和bus的注册一致,我就不复述了~ 模型如下然后是platform的注册 在platform的注册中,分为两个部分,一部分是注册到devices中,另一部分是注册到bus中,代码在/drivers/base/platform.c中 int __init platform_bus_init(void) {         int error;                  //注册到devices目录中         error = device_register(&platform_bus);         if (error)                 return error;         //注册到bus目录中         error =  bus_register(&platform_bus_type);          if (error)                 device_unregister(&platform_bus);         return error; } 首先是device_register,注册的参数为platform_bus,如下所示 struct device platform_bus = {         .bus_id                = "platform", }; 很简单,只有一个参数,表明了目录名 int device_register(struct device *dev) {         //初始化dev结构         device_initialize(dev);         //添加dev至目录         return device_add(dev); } void device_initialize(struct device *dev) {         //重要的一步,指明了父容器为devices_kset,而devices_kset的注册在前面已经介绍过了         dev->kobj.kset = devices_kset;         //初始化kobj的ktype为device_ktype         kobject_init(&dev->kobj, &device_ktype);         klist_init(&dev->klist_children, klist_children_get,                    klist_children_put);         INIT_LIST_HEAD(&dev->dma_pools);         INIT_LIST_HEAD(&dev->node);         init_MUTEX(&dev->sem);         spin_lock_init(&dev->devres_lock);         INIT_LIST_HEAD(&dev->devres_head);         device_init_wakeup(dev, 0);         set_dev_node(dev, -1); } int device_add(struct device *dev) {         struct device *parent = NULL;         struct class_interface *class_intf;         int error;         dev = get_device(dev);         if (!dev || !strlen(dev->bus_id)) {                 error = -EINVAL;                 goto Done;         }         pr_debug("device: '%s': %s\n", dev->bus_id, __func__);         parent = get_device(dev->parent);         setup_parent(dev, parent);         if (parent)                 set_dev_node(dev, dev_to_node(parent));         //设置dev->kobj的名字和父对象,并建立相应的目录         error = kobject_add(&dev->kobj, dev->kobj.parent, "%s", dev->bus_id);         if (error)                 goto Error;         if (platform_notify)                 platform_notify(dev);         if (dev->bus)                 blocking_notifier_call_chain(&dev->bus->p->bus_notifier,                                              BUS_NOTIFY_ADD_DEVICE, dev);         //建立uevent文件         error = device_create_file(dev, &uevent_attr);         if (error)                 goto attrError;         if (MAJOR(dev->devt)) {                 error = device_create_file(dev, &devt_attr);                 if (error)                         goto ueventattrError;         }         //建立subsystem连接文件连接到所属class,这里没有设置class对象所以不会建立         error = device_add_class_symlinks(dev);         if (error)                 goto SymlinkError;         //建立dev的描述文件,这里没有设置描述文件所以不会建立         error = device_add_attrs(dev);         if (error)                 goto AttrsError;         //建立链接文件至所属bus,这里没有设置所属bus所以不会建立         error = bus_add_device(dev);         if (error)                 goto BusError;         //添加power文件,因为platform不属于设备,所以不会建立power文件         error = device_pm_add(dev);         if (error)                 goto PMError;         kobject_uevent(&dev->kobj, KOBJ_ADD);         //检测驱动中有无适合的设备进行匹配,但没有设置bus,所以不会进行匹配         bus_attach_device(dev);         if (parent)                 klist_add_tail(&dev->knode_parent, &parent->klist_children);         if (dev->class) {                 down(&dev->class->sem);                 list_add_tail(&dev->node, &dev->class->devices);                 list_for_each_entry(class_intf, &dev->class->interfaces, node)                         if (class_intf->add_dev)                                 class_intf->add_dev(dev, class_intf);                 up(&dev->class->sem);         } Done:         put_device(dev);         return error; PMError:         bus_remove_device(dev); BusError:         if (dev->bus)                 blocking_notifier_call_chain(&dev->bus->p->bus_notifier,                                              BUS_NOTIFY_DEL_DEVICE, dev);         device_remove_attrs(dev); AttrsError:         device_remove_class_symlinks(dev); SymlinkError:         if (MAJOR(dev->devt))                 device_remove_file(dev, &devt_attr); ueventattrError:         device_remove_file(dev, &uevent_attr); attrError:         kobject_uevent(&dev->kobj, KOBJ_REMOVE);         kobject_del(&dev->kobj); Error:         cleanup_device_parent(dev);         if (parent)                 put_device(parent);         goto Done; } 在kobject_add-> kobject_add_varg-> kobject_add_internal中 //提取父对象,因为没有设置,所以为空 parent = kobject_get(kobj->parent); //父容器存在则设置父对象,在前面的dev->kobj.kset = devices_kset中设为了devices_kset if (kobj->kset) { //检测是否已经设置父对象         if (!parent)                 //无则使用父容器为父对象                 parent = kobject_get(&kobj->kset->kobj); //添加该kobj到父容器的链表中         kobj_kset_join(kobj);         //设置父对象         kobj->parent = parent; } 现在devices下的platform目录建立好了,模型如下,其中红线描绘了目录关系现在到bus_register了 注册的参数platform_bus_type如下所示 struct bus_type platform_bus_type = {         .name                = "platform",         .dev_attrs        = platform_dev_attrs,         .match                = platform_match,         .uevent                = platform_uevent,         .suspend                = platform_suspend,         .suspend_late        = platform_suspend_late,         .resume_early        = platform_resume_early,         .resume                = platform_resume, }; int bus_register(struct bus_type *bus) {         int retval;         //声明一个总线私有数据并分配空间         struct bus_type_private *priv;         priv = kzalloc(sizeof(struct bus_type_private), GFP_KERNEL);         if (!priv)                 return -ENOMEM;         //互相关联         priv->bus = bus;         bus->p = priv;         BLOCKING_INIT_NOTIFIER_HEAD(&priv->bus_notifier);         //设置私有数据中kobj对象的名字         retval = kobject_set_name(&priv->subsys.kobj, "%s", bus->name);         if (retval)                 goto out;         //设置父容器为bus_kset,操作集为bus_ktype         priv->subsys.kobj.kset = bus_kset;         priv->subsys.kobj.ktype = &bus_ktype;         priv->drivers_autoprobe = 1;         //注册bus容器         retval = kset_register(&priv->subsys);         if (retval)                 goto out;         //建立uevent属性文件         retval = bus_create_file(bus, &bus_attr_uevent);         if (retval)                 goto bus_uevent_fail;         //建立devices目录         priv->devices_kset = kset_create_and_add("devices", NULL,                                                  &priv->subsys.kobj);         if (!priv->devices_kset) {                 retval = -ENOMEM;                 goto bus_devices_fail;         }         //建立drivers目录         priv->drivers_kset = kset_create_and_add("drivers", NULL,                                                  &priv->subsys.kobj);         if (!priv->drivers_kset) {                 retval = -ENOMEM;                 goto bus_drivers_fail;         }         //初始化klist_devices和klist_drivers链表         klist_init(&priv->klist_devices, klist_devices_get, klist_devices_put);         klist_init(&priv->klist_drivers, NULL, NULL);         //增加probe属性文件         retval = add_probe_files(bus);         if (retval)                 goto bus_probe_files_fail;         //增加总线的属性文件         retval = bus_add_attrs(bus);         if (retval)                 goto bus_attrs_fail;         pr_debug("bus: '%s': registered\n", bus->name);         return 0; bus_attrs_fail:         remove_probe_files(bus); bus_probe_files_fail:         kset_unregister(bus->p->drivers_kset); bus_drivers_fail:         kset_unregister(bus->p->devices_kset); bus_devices_fail:         bus_remove_file(bus, &bus_attr_uevent); bus_uevent_fail:         kset_unregister(&bus->p->subsys);         kfree(bus->p); out:         return retval; } 在kset_register-> kobject_add_internal中 //提取父对象,因为没有设置父对象,所以为空 parent = kobject_get(kobj->parent); //父容器存在则设置父对象,在上文中设置了父容器priv->subsys.kobj.kset = bus_kset if (kobj->kset) {         //检测是否已经设置父对象         if (!parent)                 //无则使用父容器为父对象                 parent = kobject_get(&kobj->kset->kobj);         //添加该kobj到父容器的链表中         kobj_kset_join(kobj);         //设置父对象         kobj->parent = parent; } 在retval = kset_register(&priv->subsys)完成之后platform在bus下的模型如下图     有印象的话大家还记得在platform下面有两个目录devices和drivers吧~ 现在就到这两个目录的注册了 priv->devices_kset = kset_create_and_add("devices", NULL,&priv->subsys.kobj); priv->drivers_kset = kset_create_and_add("drivers", NULL, &priv->subsys.kobj); 注意这两条语句的头部 priv->devices_kset = kset_create_and_add priv->drivers_kset = kset_create_and_add 可以清楚的看到bus_type_private下的devices_kset, drivers_kset分别连接到了devices,drivers的kset上 现在来看kset_create_and_add("devices", NULL,&priv->subsys.kobj); struct kset *kset_create_and_add(const char *name,                                  struct kset_uevent_ops *uevent_ops,                                  struct kobject *parent_kobj) //参数为"devices", NULL,&priv->subsys.kobj {         struct kset *kset;         int error;         //创建一个kset容器         kset = kset_create(name, uevent_ops, parent_kobj);         if (!kset)                 return NULL;         //注册创建的kset容器         error = kset_register(kset);         if (error) {                 kfree(kset);                 return NULL;         }         return kset; } 在kset_create 中比较重要的操作为 kset->kobj.ktype = &kset_ktype //设置了ktype,为kset_ktype kset->kobj.parent = parent_kobj; //设置了父对象,为priv->subsys.kobj,也就是platform_bus_type->p->subsys.kobj kset->kobj.kset = NULL;    //设置父容器为空 在kset_register中 //提取父对象 parent = kobject_get(kobj->parent); //在之前设置为了 //父容器存在则设置父对象,由于父容器为空,不执行以下代码 if (kobj->kset) {         //检测是否已经设置父对象         if (!parent)                 //无则使用父容器为父对象                 parent = kobject_get(&kobj->kset->kobj);         //添加该kobj到父容器的链表中         kobj_kset_join(kobj);         //设置父对象         kobj->parent = parent; } 至此, devices的模型就建立好了,drivers模型的建立和devices是一致的,只是名字不同而已,我就不复述了,建立好的模型如下   好了~  到了这里,bus,devices和platform的基础模型就就建立好了,就等设备来注册了在platform模型设备的建立中,需要2个部分的注册,驱动的注册和设备的注册 platform_device_register(&test_device);         platform_driver_register(&test_driver); 首先看platform_device_register 注册参数为test_device,结构如下 static struct platform_device test_device = {         .name = "test_ts",         .id = -1,         //. resource         //.dev }; 这个结构主要描述了设备的名字,ID和资源和私有数据,其中资源和私有数据我们在这里不使用,将在别的文章中进行讲解 int platform_device_register(struct platform_device *pdev) {         //设备属性的初始化         device_initialize(&pdev->dev);         //将设备添加进platform里         return platform_device_add(pdev); } void device_initialize(struct device *dev) {         dev->kobj.kset = devices_kset;                   //设置kset为devices_kset,则将设备挂接上了devices目录         kobject_init(&dev->kobj, &device_ktype);                    //初始化kobeject,置ktype为device_ktype         klist_init(&dev->klist_children, klist_children_get,                    klist_children_put);         INIT_LIST_HEAD(&dev->dma_pools);         INIT_LIST_HEAD(&dev->node);         init_MUTEX(&dev->sem);         spin_lock_init(&dev->devres_lock);         INIT_LIST_HEAD(&dev->devres_head);         device_init_wakeup(dev, 0);         set_dev_node(dev, -1); } int platform_device_add(struct platform_device *pdev) {         int i, ret = 0;         if (!pdev)                 return -EINVAL;         //检测是否设置了dev中的parent,无则赋为platform_bus         if (!pdev->dev.parent)                 pdev->dev.parent = &platform_bus;         //设置dev中的bus为platform_bus_type         pdev->dev.bus = &platform_bus_type;         //检测id,id为-1表明该设备只有一个,用设备名为bus_id         //不为1则表明该设备有数个,需要用序号标明bus_id         if (pdev->id != -1)                 snprintf(pdev->dev.bus_id, BUS_ID_SIZE, "%s.%d", pdev->name,                          pdev->id);         else                 strlcpy(pdev->dev.bus_id, pdev->name, BUS_ID_SIZE);         //增加资源到资源树中         for (i = 0; i < pdev->num_resources; i++) {                 struct resource *p, *r = &pdev->resource;                 if (r->name == NULL)                         r->name = pdev->dev.bus_id;                 p = r->parent;                 if (!p) {                         if (r->flags & IORESOURCE_MEM)                                 p = &iomem_resource;                         else if (r->flags & IORESOURCE_IO)                                 p = &ioport_resource;                 }                 if (p && insert_resource(p, r)) {                         printk(KERN_ERR "%s: failed to claim resource %d\n",pdev->dev.bus_id, i);                         ret = -EBUSY;                         goto failed;                 }         }         pr_debug("Registering platform device '%s'. Parent at %s\n",pdev->dev.bus_id, pdev->dev.parent->bus_id);         //添加设备到设备层次中         ret = device_add(&pdev->dev);         if (ret == 0)                 return ret; failed:         while (--i >= 0)                 if (pdev->resource.flags & (IORESOURCE_MEM|IORESOURCE_IO))                         release_resource(&pdev->resource);         return ret; } int device_add(struct device *dev) {         struct device *parent = NULL;         struct class_interface *class_intf;         int error;         dev = get_device(dev);         if (!dev || !strlen(dev->bus_id)) {                 error = -EINVAL;                 goto Done;         }         pr_debug("device: '%s': %s\n", dev->bus_id, __func__);         //取得上层device,而dev->parent的赋值是在platform_device_add中的pdev->dev.parent = &platform_bus完成的         parent = get_device(dev->parent);         //以上层devices为准重设dev->kobj.parent         setup_parent(dev, parent);         if (parent)                 set_dev_node(dev, dev_to_node(parent));         //设置dev->kobj的名字和父对象,并建立相应目录         error = kobject_add(&dev->kobj, dev->kobj.parent, "%s", dev->bus_id);         if (error)                 goto Error;         if (platform_notify)                 platform_notify(dev);         //一种新型的通知机制,但是platform中没有设置相应的结构,所以在这里跳过         /* notify clients of device entry (new way) */         if (dev->bus)                 blocking_notifier_call_chain(&dev->bus->p->bus_notifier,BUS_NOTIFY_ADD_DEVICE, dev);         //建立uevent文件         error = device_create_file(dev, &uevent_attr);         if (error)                 goto attrError;         //设备有设备号则建立dev文件         if (MAJOR(dev->devt)) {                 error = device_create_file(dev, &devt_attr);                 if (error)                         goto ueventattrError;         }         //建立subsystem连接文件连接到所属class         error = device_add_class_symlinks(dev);         if (error)                 goto SymlinkError;         //添加dev的描述文件         error = device_add_attrs(dev);         if (error)                 goto AttrsError;         //添加链接文件至所属bus         error = bus_add_device(dev);         if (error)                 goto BusError;         //添加power文件         error = device_pm_add(dev);         if (error)                 goto PMError;         kobject_uevent(&dev->kobj, KOBJ_ADD);         //检测驱动中有无适合的设备进行匹配,现在只添加了设备,还没有加载驱动,所以不会进行匹配         bus_attach_device(dev);         if (parent)                 klist_add_tail(&dev->knode_parent, &parent->klist_children);         if (dev->class) {                 down(&dev->class->sem);                 list_add_tail(&dev->node, &dev->class->devices);                 list_for_each_entry(class_intf, &dev->class->interfaces, node)                         if (class_intf->add_dev)                                 class_intf->add_dev(dev, class_intf);                 up(&dev->class->sem);         } Done:         put_device(dev);         return error; PMError:         bus_remove_device(dev); BusError:         if (dev->bus)                 blocking_notifier_call_chain(&dev->bus->p->bus_notifier,BUS_NOTIFY_DEL_DEVICE, dev);         device_remove_attrs(dev); AttrsError:         device_remove_class_symlinks(dev); SymlinkError:         if (MAJOR(dev->devt))                 device_remove_file(dev, &devt_attr); ueventattrError:         device_remove_file(dev, &uevent_attr); attrError:         kobject_uevent(&dev->kobj, KOBJ_REMOVE);         kobject_del(&dev->kobj); Error:         cleanup_device_parent(dev);         if (parent)                 put_device(parent);         goto Done; } static void setup_parent(struct device *dev, struct device *parent) {         struct kobject *kobj;         //取得上层device的kobj         kobj = get_device_parent(dev, parent);         //kobj不为空则重设dev->kobj.parent         if (kobj)                 dev->kobj.parent = kobj; } static struct kobject *get_device_parent(struct device *dev,                                          struct device *parent) {         int retval;         //因为dev->class为空,所以跳过这段代码         if (dev->class) {                 struct kobject *kobj = NULL;                 struct kobject *parent_kobj;                 struct kobject *k;                 if (parent == NULL)                         parent_kobj = virtual_device_parent(dev);                 else if (parent->class)                         return &parent->kobj;                 else                         parent_kobj = &parent->kobj;                 spin_lock(&dev->class->class_dirs.list_lock);                 list_for_each_entry(k, &dev->class->class_dirs.list, entry)                         if (k->parent == parent_kobj) {                                 kobj = kobject_get(k);                                 break;                         }                 spin_unlock(&dev->class->class_dirs.list_lock);                 if (kobj)                         return kobj;                 k = kobject_create();                 if (!k)                         return NULL;                 k->kset = &dev->class->class_dirs;                 retval = kobject_add(k, parent_kobj, "%s", dev->class->name);                 if (retval < 0) {                         kobject_put(k);                         return NULL;                 }                 return k;         }         if (parent)                 //返回上层device的kobj                 return &parent->kobj;         return NULL; } 在bus_attach_device中虽然没有成功进行匹配,但是有很重要的一步为之后正确的匹配打下基础 void bus_attach_device(struct device *dev) {         struct bus_type *bus = dev->bus;         int ret = 0;         if (bus) {                 if (bus->p->drivers_autoprobe)                         ret = device_attach(dev);                 WARN_ON(ret < 0);                 if (ret >= 0)                         klist_add_tail(&dev->knode_bus, &bus->p->klist_devices);         } } klist_add_tail(&dev->knode_bus, &bus->p->klist_devices)就是这一行 在这一行代码中将设备挂载到了bus下的devices链表下,这样,当驱动请求匹配的时候,platform总线就会历遍devices链表为驱动寻找合适的设备 现在来看一下test_device的模型然后platform_driver_unregister,他的参数 test_driver的结构如下 static struct platform_driver test_driver = {         .probe                = test_probe,         .remove                = test_remove,         .driver                = {                 .name        = "test_ts",                 .owner        = THIS_MODULE,         }, }; int platform_driver_register(struct platform_driver *drv) {         drv->driver.bus = &platform_bus_type;         if (drv->probe)                 drv->driver.probe = platform_drv_probe;         if (drv->remove)                 drv->driver.remove = platform_drv_remove;         if (drv->shutdown)                 drv->driver.shutdown = platform_drv_shutdown;         if (drv->suspend)                 drv->driver.suspend = platform_drv_suspend;         if (drv->resume)                 drv->driver.resume = platform_drv_resume;         return driver_register(&drv->driver); } 从上面代码可以看出,在platform_driver中设置了probe, remove, shutdown, suspend或resume函数的话 则drv->driver也会设置成platform对应的函数 int driver_register(struct device_driver *drv) {         int ret;         struct device_driver *other;                  //检测总线的操作函数和驱动的操作函数是否同时存在,同时存在则提示使用总线提供的操作函数         if ((drv->bus->probe && drv->probe) ||             (drv->bus->remove && drv->remove) ||             (drv->bus->shutdown && drv->shutdown))                 printk(KERN_WARNING "Driver '%s' needs updating - please use ""bus_type methods\n", drv->name);         //检测是否已经注册过         other = driver_find(drv->name, drv->bus);         if (other) {                 put_driver(other);                 printk(KERN_ERR "Error: Driver '%s' is already registered, “"aborting...\n", drv->name);                 return -EEXIST;         }         //添加驱动到总线上         ret = bus_add_driver(drv);         if (ret)                 return ret;                  ret = driver_add_groups(drv, drv->groups);         if (ret)                 bus_remove_driver(drv);         return ret; } int bus_add_driver(struct device_driver *drv) {         struct bus_type *bus;         struct driver_private *priv;         int error = 0;         //取bus结构         bus = bus_get(drv->bus);         if (!bus)                 return -EINVAL;         pr_debug("bus: '%s': add driver %s\n", bus->name, drv->name);         //分配驱动私有数据         priv = kzalloc(sizeof(*priv), GFP_KERNEL);         if (!priv) {                 error = -ENOMEM;                 goto out_put_bus;         }         //初始化klist_devices链表         klist_init(&priv->klist_devices, NULL, NULL);         //互相关联         priv->driver = drv;         drv->p = priv;         //设置私有数据的父容器,在这一步中,设置了kset为platform下的drivers_kset结构,也就是drivers呢个目录         priv->kobj.kset = bus->p->drivers_kset;         //初始化kobj对象,设置容器操作集并建立相应的目录,这里由于没有提供parent,所以会使用父容器中的kobj为父对象         error = kobject_init_and_add(&priv->kobj, &driver_ktype, NULL,                                      "%s", drv->name);         if (error)                 goto out_unregister;         //检测所属总线的drivers_autoprobe属性是否为真         //为真则进行与设备的匹配,到这里,就会与我们之前注册的test_device连接上了,至于如何连接,进行了什么操作,将在别的文章中详细描述         if (drv->bus->p->drivers_autoprobe) {                 error = driver_attach(drv);                 if (error)                         goto out_unregister;         }         //挂载到所属总线驱动链表上         klist_add_tail(&priv->knode_bus, &bus->p->klist_drivers);         module_add_driver(drv->owner, drv);         //建立uevent属性文件         error = driver_create_file(drv, &driver_attr_uevent);         if (error) {                 printk(KERN_ERR "%s: uevent attr (%s) failed\n",                         __func__, drv->name);         }         //建立设备属性文件         error = driver_add_attrs(bus, drv);         if (error) {                 printk(KERN_ERR "%s: driver_add_attrs(%s) failed\n",__func__, drv->name);         }         error = add_bind_files(drv);         if (error) {                 printk(KERN_ERR "%s: add_bind_files(%s) failed\n",__func__, drv->name);         }         kobject_uevent(&priv->kobj, KOBJ_ADD);         return error; out_unregister:         kobject_put(&priv->kobj); out_put_bus:         bus_put(bus);         return error; } 到这里test_driver的模型就建立好了,图就是最上面的层次图,我就不再贴了 到这里一个基本的框架就建立起来了~   下面,我开始对kobject kset和ktype做分析 先说说关系,ktype与kobject和kset这两者之前的关系较少,让我画一个图,是这样的ktype依赖于kobject,kset也依赖于kobject,而kobject有时需要kset(所以用了一个白箭头),不一定需要ktype(真可怜,连白箭头都没有) 首先先说一下这个可有可无的ktype 到/sys/bus/platform下面可以看见一个drivers_autoprobe的文件 cat drivers_autoprobe可以查看这个文件的值 echo 0 > drivers_autoprobe则可以改变这个文件的值 drivers_autoprobe这个文件表示的是是否自动进行初始化 在 void bus_attach_device(struct device *dev) {         struct bus_type *bus = dev->bus;         int ret = 0;         if (bus) {                 if (bus->p->drivers_autoprobe)                         ret = device_attach(dev);                 WARN_ON(ret < 0);                 if (ret >= 0)                         klist_add_tail(&dev->knode_bus, &bus->p->klist_devices);         } } 中可以看见这么一段代码 if (bus->p->drivers_autoprobe)         ret = device_attach(dev); bus->p->drivers_autoprobe的值为真则进行匹配 而drivers_autoprobe这个文件则可以动态的修改这个值选择是否进行匹配 使用外部文件修改内核参数,ktype就是提供了这么一种方法 现在让我们看看ktype是怎么通过kobject进行运作的 首先是ktype及通过ktype进行运作的drivers_autoprobe的注册 ktype的挂载十分简单,因为他是和kobject是一体的 只有这么下面一句         priv->subsys.kobj.ktype = &bus_ktype; 这样就将bus_ktype挂载到了platform_bus_type的kobject上 drivers_autoprobe的注册如下 retval = bus_create_file(bus, &bus_attr_drivers_autoprobe); bus_attr_drivers_autoprobe这个结构由一系列的宏进行组装 static BUS_ATTR(drivers_autoprobe, S_IWUSR | S_IRUGO,                 show_drivers_autoprobe, store_drivers_autoprobe); #define BUS_ATTR(_name, _mode, _show, _store)        \ struct bus_attribute bus_attr_##_name = __ATTR(_name, _mode, _show, _store) #define __ATTR(_name,_mode,_show,_store) { \         .attr = {.name = __stringify(_name), .mode = _mode },        \         .show        = _show,                                        \         .store        = _store,                                        \ } 最后bus_attr_drivers_autoprobe的模型如下 struct bus_attribute  bus_attr_drivers_autoprobe  {         .attr = { .name = “drivers_autoprobe”, .mode = S_IWUSR | S_IRUGO  },                 .show        = show_drivers_autoprobe,                                                 .store        = store_drivers_autoprobe,                                         } 进入到bus_create_file中 int bus_create_file(struct bus_type *bus, struct bus_attribute *attr) //参数为(bus, &bus_attr_drivers_autoprobe) {         int error;         if (bus_get(bus)) {                 error = sysfs_create_file(&bus->p->subsys.kobj, &attr->attr);                 bus_put(bus);         } else                 error = -EINVAL;         return error; } int sysfs_create_file(struct kobject * kobj, const struct attribute * attr) //参数为(&bus->p->subsys.kobj, &attr->attr) {         BUG_ON(!kobj || !kobj->sd || !attr);         return sysfs_add_file(kobj->sd, attr, SYSFS_KOBJ_ATTR); } int sysfs_add_file(struct sysfs_dirent *dir_sd, const struct attribute *attr,int type) //参数为(&bus->p->subsys.kobj ->sd, &attr->attr, SYSFS_KOBJ_ATTR) {         return sysfs_add_file_mode(dir_sd, attr, type, attr->mode); } int sysfs_add_file_mode(struct sysfs_dirent *dir_sd,                         const struct attribute *attr, int type, mode_t amode) //整理一下参数,现在应该为 //(&platform_bus_type->p->subsys.kobj ->sd, &bus_attr_drivers_autoprobe->attr, SYSFS_KOBJ_ATTR, &bus_attr_drivers_autoprobe->attr->mode) {         umode_t mode = (amode & S_IALLUGO) | S_IFREG;         struct sysfs_addrm_cxt acxt;         struct sysfs_dirent *sd;         int rc;         //在这一步中可以看出新建了一个节点         sd = sysfs_new_dirent(attr->name, mode, type);         if (!sd)                 return -ENOMEM;                  //这一步挂载了&bus_attr_drivers_autoprobe->attr到节点中,为以后提取attr及上层结构做准备         sd->s_attr.attr = (void *)attr;         // dir_sd也就是上层目录,在这里为platform_bus_type->p->subsys.kobj ->sd         //也就是/sys/bus/platform这个目录         sysfs_addrm_start(&acxt, dir_sd);         rc = sysfs_add_one(&acxt, sd);         sysfs_addrm_finish(&acxt);         if (rc)                 sysfs_put(sd);         return rc; } struct sysfs_dirent *sysfs_new_dirent(const char *name, umode_t mode, int type) {         char *dup_name = NULL;         struct sysfs_dirent *sd;         if (type & SYSFS_COPY_NAME) {                 name = dup_name = kstrdup(name, GFP_KERNEL);                 if (!name)                         return NULL;         }         sd = kmem_cache_zalloc(sysfs_dir_cachep, GFP_KERNEL);         if (!sd)                 goto err_out1;         if (sysfs_alloc_ino(&sd->s_ino))                 goto err_out2;         atomic_set(&sd->s_count, 1);         atomic_set(&sd->s_active, 0);         sd->s_name = name;   //节点的名字为&bus_attr_drivers_autoprobe->attr->name  也就是drivers_autoprobe         sd->s_mode = mode; sd->s_flags = type;   //节点的type为SYSFS_KOBJ_ATTR         return sd; err_out2:         kmem_cache_free(sysfs_dir_cachep, sd); err_out1:         kfree(dup_name);         return NULL; } 现在一切准备就绪,来看看怎么读取吧 首先是open,大概流程可以看我的另一篇文章<从文件到设备>,一直看到ext3_lookup 这里和ext3_lookup不同的是,sys的文件系统是sysfs文件系统,所以应该使用的lookup函数为sysfs_lookup(/fs/sysfs/dir.c) static struct dentry * sysfs_lookup(struct inode *dir, struct dentry *dentry,                                 struct nameidata *nd) {         struct dentry *ret = NULL;         struct sysfs_dirent *parent_sd = dentry->d_parent->d_fsdata;         struct sysfs_dirent *sd;         struct inode *inode;         mutex_lock(&sysfs_mutex);         sd = sysfs_find_dirent(parent_sd, dentry->d_name.name);         if (!sd) {                 ret = ERR_PTR(-ENOENT);                 goto out_unlock;         }         //节点的初始化在这里         inode = sysfs_get_inode(sd);         if (!inode) {                 ret = ERR_PTR(-ENOMEM);                 goto out_unlock;         }         dentry->d_op = &sysfs_dentry_ops;         dentry->d_fsdata = sysfs_get(sd);         d_instantiate(dentry, inode);         d_rehash(dentry); out_unlock:         mutex_unlock(&sysfs_mutex);         return ret; } struct inode * sysfs_get_inode(struct sysfs_dirent *sd) {         struct inode *inode;         inode = iget_locked(sysfs_sb, sd->s_ino);         if (inode && (inode->i_state & I_NEW))                 //为节点赋值                 sysfs_init_inode(sd, inode);         return inode; } static void sysfs_init_inode(struct sysfs_dirent *sd, struct inode *inode) {         struct bin_attribute *bin_attr;         inode->i_blocks = 0;         inode->i_mapping->a_ops = &sysfs_aops;         inode->i_mapping->backing_dev_info = &sysfs_backing_dev_info;         inode->i_op = &sysfs_inode_operations;         inode->i_ino = sd->s_ino;         lockdep_set_class(&inode->i_mutex, &sysfs_inode_imutex_key);         if (sd->s_iattr) {                 set_inode_attr(inode, sd->s_iattr);         } else                 set_default_inode_attr(inode, sd->s_mode);         //判断类型         switch (sysfs_type(sd)) {         case SYSFS_DIR:                 inode->i_op = &sysfs_dir_inode_operations;                 inode->i_fop = &sysfs_dir_operations;                 inode->i_nlink = sysfs_count_nlink(sd);                 break;         //还记得在注册的时候有一个参数为SYSFS_KOBJ_ATTR赋到了sd->s_flags上面吧         case SYSFS_KOBJ_ATTR:                 inode->i_size = PAGE_SIZE;                 inode->i_fop = &sysfs_file_operations;                 break;         case SYSFS_KOBJ_BIN_ATTR:                 bin_attr = sd->s_bin_attr.bin_attr;                 inode->i_size = bin_attr->size;                 inode->i_fop = &bin_fops;                 break;         case SYSFS_KOBJ_LINK:                 inode->i_op = &sysfs_symlink_inode_operations;                 break;         default:                 BUG();         }         unlock_new_inode(inode); } sysfs_file_operations的结构如下,之后open和read,write都明了了 const struct file_operations sysfs_file_operations = {         .read                = sysfs_read_file,         .write                = sysfs_write_file,         .llseek                = generic_file_llseek,         .open                = sysfs_open_file,         .release        = sysfs_release,         .poll                = sysfs_poll, }; 有关在哪调用open还是请查阅我的另一篇文章<从文件到设备>中 nameidata_to_filp之后的操作 好的~  现在进入到了sysfs_open_file中 static int sysfs_open_file(struct inode *inode, struct file *file) {         struct sysfs_dirent *attr_sd = file->f_path.dentry->d_fsdata;         //要重的取值,在这里取得了drivers_autoprobe的目录platform的kproject         struct kobject *kobj = attr_sd->s_parent->s_dir.kobj;         struct sysfs_buffer *buffer;         struct sysfs_ops *ops;         int error = -EACCES;         if (!sysfs_get_active_two(attr_sd))                 return -ENODEV;         if (kobj->ktype && kobj->ktype->sysfs_ops)                 //这里可谓是ktype实现中的核心,在这里ops设置成了platform_bus_type中kobject->ktype的sysfs_ops                 ops = kobj->ktype->sysfs_ops;         else {                 printk(KERN_ERR "missing sysfs attribute operations for ""kobject: %s\n", kobject_name(kobj));                 WARN_ON(1);                 goto err_out;         }         if (file->f_mode & FMODE_WRITE) {                 if (!(inode->i_mode & S_IWUGO) || !ops->store)                         goto err_out;         }         if (file->f_mode & FMODE_READ) {                 if (!(inode->i_mode & S_IRUGO) || !ops->show)                         goto err_out;         }         error = -ENOMEM;         buffer = kzalloc(sizeof(struct sysfs_buffer), GFP_KERNEL);         if (!buffer)                 goto err_out;         mutex_init(&buffer->mutex);         buffer->needs_read_fill = 1;         //然后将设置好的ops挂载到buffer上         buffer->ops = ops;         //再将buffer挂载到file->private_data中         file->private_data = buffer;         error = sysfs_get_open_dirent(attr_sd, buffer);         if (error)                 goto err_free;         sysfs_put_active_two(attr_sd);         return 0; err_free:         kfree(buffer); err_out:         sysfs_put_active_two(attr_sd);         return error; } 现在已经为read和write操作准备好了 马上进入到read操作中整个流程如上图所示,如何进入到sysfs_read_file在上面open的操作中已经说明了 我们就从sysfs_read_file开始分析(该文件在/fs/sysfs/file.c中) sysfs_read_file(struct file *file, char __user *buf, size_t count, loff_t *ppos) {         struct sysfs_buffer * buffer = file->private_data;         ssize_t retval = 0;         mutex_lock(&buffer->mutex);         if (buffer->needs_read_fill || *ppos == 0) {                 //主要操作在fill_read_buffer中                 retval = fill_read_buffer(file->f_path.dentry,buffer);                 if (retval)                         goto out;         }         pr_debug("%s: count = %zd, ppos = %lld, buf = %s\n",__func__, count, *ppos, buffer->page);         retval = simple_read_from_buffer(buf, count, ppos, buffer->page,                                          buffer->count); out:         mutex_unlock(&buffer->mutex);         return retval; } static int fill_read_buffer(struct dentry * dentry, struct sysfs_buffer * buffer) {         struct sysfs_dirent *attr_sd = dentry->d_fsdata;         //取得父目录的kobject,也就是platform的kobject         struct kobject *kobj = attr_sd->s_parent->s_dir.kobj;         //还记得这个buffer->ops在什么时候进行赋值的么?         struct sysfs_ops * ops = buffer->ops;         int ret = 0;         ssize_t count;         if (!buffer->page)                 buffer->page = (char *) get_zeroed_page(GFP_KERNEL);         if (!buffer->page)                 return -ENOMEM;         if (!sysfs_get_active_two(attr_sd))                 return -ENODEV;         buffer->event = atomic_read(&attr_sd->s_attr.open->event);         //调用ops->show  也就是bus_sysfs_ops->show    具体就是bus_attr_show了         //参数为父目录的kobject, bus_attr_drivers_autoprobe->attr,和一段char信息         count = ops->show(kobj, attr_sd->s_attr.attr, buffer->page);         sysfs_put_active_two(attr_sd);         if (count >= (ssize_t)PAGE_SIZE) {                 print_symbol("fill_read_buffer: %s returned bad count\n",                         (unsigned long)ops->show);                 /* Try to struggle along */                 count = PAGE_SIZE - 1;         }         if (count >= 0) {                 buffer->needs_read_fill = 0;                 buffer->count = count;         } else {                 ret = count;         }         return ret; } 现在进入bus_attr_show中 static ssize_t bus_attr_show(struct kobject *kobj, struct attribute *attr,char *buf) {         //提取attr的上层结构,也就是bus_attr_drivers_autoprobe         struct bus_attribute *bus_attr = to_bus_attr(attr);         //提取kobj的上上层结构,也就是bus_type_private         struct bus_type_private *bus_priv = to_bus(kobj);         ssize_t ret = 0;         if (bus_attr->show)                 //终于到了这里,最后的调用,调用bus_attr_drivers_autoprobe.show ,也就是show_drivers_autoprobe                 //参数为bus_priv->bus,也就是platform_bus_type , 及一段char信息                 ret = bus_attr->show(bus_priv->bus, buf);         return ret; } static ssize_t show_drivers_autoprobe(struct bus_type *bus, char *buf) {         return sprintf(buf, "%d\n", bus->p->drivers_autoprobe); } 没什么好介绍了就是打印 buf + bus->p->drivers_autoprobe   从结果来看~ buf是空的 到这里,终于把内核的信息给打印出来了,千辛万苦,层层调用,就是为了取得上层kobject结构,逆运算再取得kobject的上层结构 大家是否对kobject有所了解了呢?~   在对kobject进行介绍之前  还是先把write操作讲完吧 哈哈~ write操作和read操作重要的步骤基本是一致的,只不过在最后的调用中 static ssize_t store_drivers_autoprobe(struct bus_type *bus,                                        const char *buf, size_t count) {         if (buf[0] == '0')                 bus->p->drivers_autoprobe = 0;         else                 bus->p->drivers_autoprobe = 1;         return count; } 不进行打印而对内核的参数进行了修改而已 好~ 现在让我们来看看kobject吧 kobject的结构如下 struct kobject {         const char                *name;          //kobject的名字         struct kref                kref;                                //kobject的原子操作         struct list_head        entry;         struct kobject                *parent;                        //父对象         struct kset                *kset;                        //父容器         struct kobj_type        *ktype;                        //ktype         struct sysfs_dirent        *sd;                                //文件节点         unsigned int state_initialized:1;         unsigned int state_in_sysfs:1;         unsigned int state_add_uevent_sent:1;         unsigned int state_remove_uevent_sent:1; }; kobject描述的是较具体的对象,一个设备,一个驱动,一个总线,一类设备 在层次图上可以看出,每个存在于层次图中的设备,驱动,总线,类别都有自己的kobject kobject与kobject之间的层次由kobject中的parent指针决定 而kset指针则表明了kobject的容器 像platform_bus 和test_device的kset都是devices_kset 呢parent和kset有什么不同呢 我认为是人工和默认的区别,看下面这张图 ,蓝框为kset,红框为kobject容器提供了一种默认的层次~  但也可以人工设置层次 对于kobject现在我只理解了这么多,欢迎大家指出有疑问的地方 最后是kset,kset比较简单,看下面的结构 struct kset {         struct list_head list;         spinlock_t list_lock;         struct kobject kobj;         struct kset_uevent_ops *uevent_ops; }; 对于kset的描述,文档里也有介绍 /** * struct kset - a set of kobjects of a specific type, belonging to a specific subsystem. * * A kset defines a group of kobjects.  They can be individually * different "types" but overall these kobjects all want to be grouped * together and operated on in the same manner.  ksets are used to * define the attribute callbacks and other common events that happen to * a kobject. 翻译过来大概就是 结构kset,一个指定类型的kobject的集合,属于某一个指定的子系统 kset定义了一组kobject,它们可以是不同类型组成但却希望捆在一起有一个统一的操作 kset通常被定义为回调属性和其他通用的事件发生在kobject上 可能翻译的不是很好,望大家见谅 从结构中能看出kset比kobject多了3个属性 list_head                                //列表 spinlock_t                        //共享锁 kset_uevent_ops                //uevent操作集 list_head        连接了所有kobject中kset属性指向自己的kobject 而kset_uevent_ops则用于通知机制,由于uevent的作用我也没接触过,所以暂不解析uevent的机制了 【作者】张昺华 【出处】http://www.cnblogs.com/sky-heaven/ 【博客园】 http://www.cnblogs.com/sky-heaven/ 【新浪博客】 http://blog.sina.com.cn/u/2049150530 【知乎】 http://www.zhihu.com/people/zhang-bing-hua 【我的作品---旋转倒立摆】 http://v.youku.com/v_show/id_XODM5NDAzNjQw.html?spm=a2hzp.8253869.0.0&from=y1.7-2 【我的作品---自平衡自动循迹车】 http://v.youku.com/v_show/id_XODM5MzYyNTIw.html?spm=a2hzp.8253869.0.0&from=y1.7-2 【新浪微博】 张昺华--sky 【twitter】 @sky2030_ 【facebook】 张昺华 zhangbinghua 本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利.
文章
Linux · 容器 · Shell · SDN
2016-05-11
阿里云物联网
5576 人关注 | 178 讨论 | 355 内容
+ 订阅
  • 聚焦安全,德施曼极致的工匠精神
  • 一人一卡一密打造园区统一管理平台
  • 点点未来:幼儿园遇上物联网,智慧幼儿园感知教育的诞生
查看更多 >
阿里云Serverless Compute
159 人关注 | 3 讨论 | 305 内容
+ 订阅
  • 独家对话阿里云函数计算负责人不瞋:你所不知道的 Serverless
  • AI 事件驱动场景 Serverless 实践
  • 中国唯一入选 Forrester 领导者象限,阿里云 Serverless 全球领先
查看更多 >
华章出版社
336 人关注 | 1 讨论 | 10124 内容
+ 订阅
  • 带你读《Java并发编程的艺术》之一:并发编程的挑战
查看更多 >
开发与运维
3848 人关注 | 92010 讨论 | 88661 内容
+ 订阅
  • 低成本可复用前端框架——Linke
  • NET SDK连接阿里云ElasticSearch示例
  • Fluid给数据弹性一双隐形的翅膀 (1) -- 自定义弹性伸缩
查看更多 >
人工智能
2038 人关注 | 7300 讨论 | 33547 内容
+ 订阅
  • [leetcode/lintcode 题解] 算法面试真题详解:最终优惠价
  • 阿里云新品发布会周刊第96期 丨 AIoT春季发布_工业&农业数字化解决方案
  • 4.24 上海站 | 阿里云 Serverless Developer Meetup 开放报名!
查看更多 >