本篇关键词:、、、

下载 >> 离线文档.鸿蒙内核源码分析(百篇博客分析.挖透鸿蒙内核).pdf

任务管理相关篇为:

什么是远程登录?

  • 每个人都有上门做客的经历,抖音也一直在教我们做人,做客不要空手去,总得带点东西,而对中国人你就不能送,不能送,最好也别送,因他们与 终 离 邪 谐音,犯忌讳。 这是人情世故,叫礼仪,是中华文明圈的共识,是相互交流信任的基础。

  • 那互联网圈有没有这种共识呢? 当然有,互联网世界的人情世故就是协议, 种种协议映射到人类社会来说就是种种礼仪,协议有TCPHTTPSSHTelnet等等,就如同礼仪分商业礼仪,外交礼仪,校园礼仪,家庭礼仪等等。 孔圣人不也说 不学礼,无以立 应该就是这个道理, 登门拜访的礼仪可类比远程登录协议Telnet。 来了就跟自己家一样, 我家的东西就是你家的,随便用,甭客气。

Telnet协议的具体内容可以查看以下文档。

协议 时间 英文版 中文版 标题
Telnet 1983 rfc854 rfc854 TELNET PROTOCOL SPECIFICATION(远程登录协议规范)
Telnet 1983 rfc855 rfc855 TELNET OPTION SPECIFICATIONS(远程登录选项规范)

Telnet协议细节不是本篇讨论的重点,后续会有专门的 Lwip协议栈 系列博客说清楚。本篇要说清楚的是内核如何接待远方的客人。

Shell | 控制台 | 远程登录模型

对远程登录来有客户端和服务端的说法,跟别人来你家你是主人和你去别人家你是客人一样,身份不同,职责不同,主人要做的事明显要更多,本篇只说鸿蒙对telnet服务端的实现,说清楚它是如何接待外面来的客人。至于图中提到的客户端任务是指主人为每个客人专门提供了一个对接人的意思。下图为看完三部分源码后整理的模型图

模型解释

  • 通过本地的shell命令telnet on启动远程登录模块,由此创建Telnet的服务任务TelnetServer
  • TelnetServer任务,创建socket监听23端口,接受来自远程终端的 telnet xx.xx.xx.xx 23请求
  • 收到请求后创建一个TelnetClientLoop用于接待客户的任务,对接详细的客户需求。
  • 在接待客户期间创建一个远程登录类型的控制台,来处理和转发远程客户的请求给shell进程最终执行远程命令。
  • shell处理完成后通过专门的任务SendToSer回写远程终端,控制台部分详细看 系列篇的(控制台篇)

鸿蒙是如何实现的?

1. 启动 Telnet

//SHELLCMD_ENTRY(telnet_shellcmd, CMD_TYPE_EX, "telnet", 1, (CmdCallBackFunc)TelnetCmd);/// 以静态方式注册shell 命令
/// 本命令用于启动或关闭telnet server服务 
INT32 TelnetCmd(UINT32 argc, const CHAR **argv)
{
    if (strcmp(argv[0], "on") == 0) { // 输入 telnet on
        /* telnet on: try to start telnet server task */
        TelnetdTaskInit(); //启动远程登录 服务端任务
        return 0;
    }
    if (strcmp(argv[0], "off") == 0) {// 输入 telnet off
        /* telnet off: try to stop clients, then stop server task */
        TelnetdTaskDeinit();//关闭所有的客户端,并关闭服务端任务
        return 0;
    }
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

2. 创建Telnet服务端任务

STATIC VOID TelnetdTaskInit(VOID)
{
    UINT32 ret;
    TSK_INIT_PARAM_S initParam = {0};
    initParam.pfnTaskEntry = (TSK_ENTRY_FUNC)TelnetdMain; // telnet任务入口函数
    initParam.uwStackSize = TELNET_TASK_STACK_SIZE;	// 8K
    initParam.pcName = "TelnetServer"; //任务名称
    initParam.usTaskPrio = TELNET_TASK_PRIORITY; //优先级 9,和 shell 的优先级一样
    initParam.uwResved = LOS_TASK_STATUS_DETACHED; //独立模式
    if (atomic_read(&g_telnetTaskId) != 0) {//只支持一个 telnet 服务任务
        PRINT_ERR("telnet server is already running!\n");
        return;
    }
    ret = LOS_TaskCreate((UINT32 *)&g_telnetTaskId, &initParam);//创建远程登录服务端任务并发起调度
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

3. Telnet服务端任务入口函数

//远程登录操作命令
STATIC const struct file_operations_vfs g_telnetOps = {
    TelnetOpen,
    TelnetClose,
    TelnetRead,
    TelnetWrite,
    NULL,
    TelnetIoctl,
    NULL,
#ifndef CONFIG_DISABLE_POLL
    TelnetPoll,
#endif
    NULL,
};
STATIC INT32 TelnetdMain(VOID)
{
    sock = TelnetdInit(TELNETD_PORT);//1.初始化创建 socket ,socket的本质就是打开了一个虚拟文件
    TelnetLock();
    ret = TelnetedRegister();//2.注册驱动程序 /dev/telnet ,g_telnetOps g_telnetDev
    TelnetUnlock();
    TelnetdAcceptLoop(sock);//3.等待连接,处理远程终端过来的命令 例如#task 命令
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

4. 循环等待远程终端的连接请求

STATIC VOID TelnetdAcceptLoop(INT32 listenFd)
{
    while (g_telnetListenFd >= 0) {//必须启动监听
        TelnetUnlock();
        (VOID)memset_s(&inTelnetAddr, sizeof(inTelnetAddr), 0, sizeof(inTelnetAddr));
        clientFd = accept(listenFd, (struct sockaddr *)&inTelnetAddr, (socklen_t *)&len);//接收数据
        if (TelnetdAcceptClient(clientFd, &inTelnetAddr) == 0) {//
            /*
             * Sleep sometime before next loop: mostly we already have one connection here,
             * and the next connection will be declined. So don't waste our cpu.
             | 在下一个循环来临之前休息片刻,因为鸿蒙只支持一个远程登录,此时已经有一个链接,
             在TelnetdAcceptClient中创建线程不会立即调度, 休息下任务会挂起,重新调度
             */
            LOS_Msleep(TELNET_ACCEPT_INTERVAL);//以休息的方式发起调度. 直接申请调度也未尝不可吧 @note_thinking 
        } else {
            return;
        }
        TelnetLock();
    }
    TelnetUnlock();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

5. 远方的客人到来,安排专人接待

鸿蒙目前只支持接待一位远方的客人,g_telnetClientFd是个全局变量,创建专门任务接待客人。

STATIC INT32 TelnetdAcceptClient(INT32 clientFd, const struct sockaddr_in *inTelnetAddr)
{
    g_telnetClientFd = clientFd;
	//创建一个线程处理客户端的请求
    if (pthread_create(&tmp, &useAttr, TelnetClientLoop, (VOID *)(UINTPTR)clientFd) != 0) {
        PRINT_ERR("Failed to create client handle task\n");
        g_telnetClientFd = -1;
        goto ERROUT_UNLOCK;
    }
}
1
2
3
4
5
6
7
8
9
10

6. 接待员做好接待工作

因接待工作很重要,这边把所有代码贴出来,并加上了大量的注释,目的只有一个,让咱客人爽。

STATIC VOID *TelnetClientLoop(VOID *arg)
{
    struct pollfd pollFd;
    INT32 ret;
    INT32 nRead;
    UINT32 len;
    UINT8 buf[TELNET_CLIENT_READ_BUF_SIZE];
    UINT8 *cmdBuf = NULL;
    INT32 clientFd = (INT32)(UINTPTR)arg;
    (VOID)prctl(PR_SET_NAME, "TelnetClientLoop", 0, 0, 0);
    TelnetLock();
    if (TelnetClientPrepare(clientFd) != 0) {//做好准备工作
        TelnetUnlock();
        (VOID)close(clientFd);
        return NULL;
    }
    TelnetUnlock();
    while (1) {//死循环接受远程输入的数据
        pollFd.fd = clientFd;
        pollFd.events = POLLIN | POLLRDHUP;//监听读数据和挂起事件
        pollFd.revents = 0;
		/*
		POLLIN 普通或优先级带数据可读
		POLLRDNORM 普通数据可读
		POLLRDBAND 优先级带数据可读
		POLLPRI 高优先级数据可读
		POLLOUT 普通数据可写
		POLLWRNORM 普通数据可写
		POLLWRBAND 优先级带数据可写
		POLLERR 发生错误
		POLLHUP 发生挂起
		POLLNVAL 描述字不是一个打开的文件
		poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,
		如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,
		直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。
	  这个过程经历了多次无谓的遍历。
	  poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。
	  poll与select的不同,通过一个pollfd数组向内核传递需要关注的事件,故没有描述符个数的限制,
			pollfd中的events字段和revents分别用于标示关注的事件和发生的事件,故pollfd数组只需要被初始化一次
	  poll的实现机制与select类似,其对应内核中的sys_poll,只不过poll向内核传递pollfd数组,
			然后对pollfd中的每个描述符进行poll,相比处理fdset来说,poll效率更高。poll返回后,
			需要对pollfd中的每个元素检查其revents值,来得指事件是否发生。
			优点
			1)poll() 不要求开发者计算最大文件描述符加一的大小。
			2)poll() 在应付大数目的文件描述符的时候速度更快,相比于select。
			3)它没有最大连接数的限制,原因是它是基于链表来存储的。
			缺点
			1)大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。
			2)与select一样,poll返回后,需要轮询pollfd来获取就绪的描述符
		*/
        ret = poll(&pollFd, 1, TELNET_CLIENT_POLL_TIMEOUT);//等2秒钟返回
        if (ret < 0) {//失败时,poll()返回-1
            break;
			/*	ret < 0 各值
			  EBADF  		一个或多个结构体中指定的文件描述符无效。
			  EFAULTfds   指针指向的地址超出进程的地址空间。
			  EINTR      请求的事件之前产生一个信号,调用可以重新发起。
			  EINVALnfds  参数超出PLIMIT_NOFILE值。
			  ENOMEM  	   可用内存不足,无法完成请求

			*/
        }
        if (ret == 0) {//如果在超时前没有任何事件发生,poll()返回0
            continue;
        }
        /* connection reset, maybe keepalive failed or reset by peer | 连接重置,可能keepalive失败或被peer重置*/
        if ((UINT16)pollFd.revents & (POLLERR | POLLHUP | POLLRDHUP)) {
            break;
        }
        if ((UINT16)pollFd.revents & POLLIN) {//数据事件
            nRead = read(clientFd, buf, sizeof(buf));//读远程终端过来的数据
            if (nRead <= 0) {
                /* telnet client shutdown */
                break;
            }
            cmdBuf = ReadFilter(buf, (UINT32)nRead, &len);//对数据过滤
            if (len > 0) {
                (VOID)TelnetTx((CHAR *)cmdBuf, len);//对数据加工处理
            }
        }
    }
    TelnetLock();
    TelnetClientClose();
    (VOID)close(clientFd);
    clientFd = -1;
    g_telnetClientFd = -1;
    TelnetUnlock();
    return NULL;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89

最后结语

理解远程登录的实现建议结合 shell编辑篇shell执行篇控制台篇 三篇来理解,实际上它们是上中下三层。

百文说内核 | 抓住主脉络

  • 百文相当于摸出内核的肌肉和器官系统,让人开始丰满有立体感,因是直接从注释源码起步,在加注释过程中,每每有心得处就整理,慢慢形成了以下文章。内容立足源码,常以生活场景打比方尽可能多的将内核知识点置入某种场景,具有画面感,容易理解记忆。说别人能听得懂的话很重要! 百篇博客绝不是百度教条式的在说一堆诘屈聱牙的概念,那没什么意思。更希望让内核变得栩栩如生,倍感亲切。
  • 与代码需不断debug一样,文章内容会存在不少错漏之处,请多包涵,但会反复修正,持续更新,v**.xx 代表文章序号和修改的次数,精雕细琢,言简意赅,力求打造精品内容。
  • 百文在 < 鸿蒙研究站 | 开源中国 | 博客园 | 51cto | csdn | 知乎 | 掘金 > 站点发布,百篇博客系列目录如下。

按功能模块:

基础知识 进程管理 任务管理 内存管理
双向链表
内核概念
源码结构
地址空间
计时单位
优雅的宏
钩子框架
位图管理
POSIX
main函数
调度故事
进程控制块
进程空间
线性区
红黑树
进程管理
Fork进程
进程回收
Shell编辑
Shell解析
任务控制块
并发并行
就绪队列
调度机制
任务管理
用栈方式
软件定时器
控制台
远程登录
协议栈
内存规则
物理内存
内存概念
虚实映射
页表管理
静态分配
TLFS算法
内存池管理
原子操作
圆整对齐
通讯机制 文件系统 硬件架构 内核汇编
通讯总览
自旋锁
互斥锁
快锁使用
快锁实现
读写锁
信号量
事件机制
信号生产
信号消费
消息队列
消息封装
消息映射
共享内存
文件概念
文件故事
索引节点
VFS
文件句柄
根文件系统
挂载机制
管道文件
文件映射
写时拷贝
芯片模式
ARM架构
指令集
协处理器
工作模式
寄存器
多核管理
中断概念
中断管理
编码方式
汇编基础
汇编传参
链接脚本
内核启动
进程切换
任务切换
中断切换
异常接管
缺页中断
编译运行 调测工具
编译过程
编译构建
GN语法
忍者无敌
ELF格式
ELF解析
静态链接
重定位
动态链接
进程映像
应用启动
系统调用
VDSO
模块监控
日志跟踪
系统安全
测试用例

百万注源码 | 处处扣细节

  • 百万汉字注解内核目的是要看清楚其毛细血管,细胞结构,等于在拿放大镜看内核。内核并不神秘,带着问题去源码中找答案是很容易上瘾的,你会发现很多文章对一些问题的解读是错误的,或者说不深刻难以自圆其说,你会慢慢形成自己新的解读,而新的解读又会碰到新的问题,如此层层递进,滚滚向前,拿着放大镜根本不愿意放手。

  • < gitee | github | coding | gitcode > 四大码仓推送 | 同步官方源码。

关注不迷路 | 代码即人生

  • 互联网从业十五年,计算机硕士,技术副总裁
  • 关注我,持续更新四十年,即聊技术也聊人生
  • 交有趣靠谱的人;做难而正确的事
  • 不做作,不炒作,只唯真
  • 不唯上,不唯书,只唯实

>> 捐助名单

据说喜欢 点赞 + 分享 的,后来都成了大神。😃