当前位置: 首页 > 编程日记 > 正文

Linux 内核进程管理之进程ID

Linux 内核使用 task_struct 数据结构来关联所有与进程有关的数据和结构,Linux 内核所有涉及到进程和程序的所有算法都是围绕该数据结构建立的,是内核中最重要的数据结构之一。该数据结构在内核文件 include/linux/sched.h 中定义,在Linux 3.8 的内核中,该数据结构足足有 380 行之多,在这里我不可能逐项去描述其表示的含义,本篇文章只关注该数据结构如何来组织和管理进程ID的。

进程ID类型

要想了解内核如何来组织和管理进程ID,先要知道进程ID的类型:

  • PID:这是 Linux 中在其命名空间中唯一标识进程而分配给它的一个号码,称做进程ID号,简称PID。在使用 fork 或 clone 系统调用时产生的进程均会由内核分配一个新的唯一的PID值。
  • TGID:在一个进程中,如果以CLONE_THREAD标志来调用clone建立的进程就是该进程的一个线程,它们处于一个线程组,该线程组的ID叫做TGID。处于相同的线程组中的所有进程都有相同的TGID;线程组组长的TGID与其PID相同;一个进程没有使用线程,则其TGID与PID也相同。
  • PGID:另外,独立的进程可以组成进程组(使用setpgrp系统调用),进程组可以简化向所有组内进程发送信号的操作,例如用管道连接的进程处在同一进程组内。进程组ID叫做PGID,进程组内的所有进程都有相同的PGID,等于该组组长的PID。
  • SID:几个进程组可以合并成一个会话组(使用setsid系统调用),可以用于终端程序设计。会话组中所有进程都有相同的SID。

PID 命名空间

命名空间是为操作系统层面的虚拟化机制提供支撑,目前实现的有六种不同的命名空间,分别为mount命名空间、UTS命名空间、IPC命名空间、用户命名空间、PID命名空间、网络命名空间。命名空间简单来说提供的是对全局资源的一种抽象,将资源放到不同的容器中(不同的命名空间),各容器彼此隔离。命名空间有的还有层次关系,如PID命名空间,图1 为命名空间的层次关系图。

图1 命名空间的层次关系

在上图有四个命名空间,一个父命名空间衍生了两个子命名空间,其中的一个子命名空间又衍生了一个子命名空间。以PID命名空间为例,由于各个命名空间彼此隔离,所以每个命名空间都可以有 PID 号为 1 的进程;但又由于命名空间的层次性,父命名空间是知道子命名空间的存在,因此子命名空间要映射到父命名空间中去,因此上图中 level 1 中两个子命名空间的六个进程分别映射到其父命名空间的PID 号5~10。

命名空间增大了 PID 管理的复杂性,对于某些进程可能有多个PID——在其自身命名空间的PID以及其父命名空间的PID,凡能看到该进程的命名空间都会为其分配一个PID。因此就有:

  • 全局ID:在内核本身和初始命名空间中唯一的ID,在系统启动期间开始的 init 进程即属于该初始命名空间。系统中每个进程都对应了该命名空间的一个PID,叫全局ID,保证在整个系统中唯一。
  • 局部ID:对于属于某个特定的命名空间,它在其命名空间内分配的ID为局部ID,该ID也可以出现在其他的命名空间中。

进程ID管理数据结构

Linux 内核在设计管理ID的数据结构时,要充分考虑以下因素:

  1. 如何快速地根据进程的 task_struct、ID类型、命名空间找到局部ID
  2. 如何快速地根据局部ID、命名空间、ID类型找到对应进程的 task_struct
  3. 如何快速地给新进程在可见的命名空间内分配一个唯一的 PID

如果将所有因素考虑到一起,将会很复杂,下面将会由简到繁设计该结构。

一个PID对应一个task_struct

如果先不考虑进程之间的关系,不考虑命名空间,仅仅是一个PID号对应一个task_struct,那么我们可以设计这样的数据结构:

struct task_struct {//...struct pid_link pids;//...
};struct pid_link {struct hlist_node node;  struct pid *pid;          
};struct pid {struct hlist_head tasks;        //指回 pid_link 的 nodeint nr;                       //PIDstruct hlist_node pid_chain;    //pid hash 散列表结点
};

每个进程的 task_struct 结构体中有一个指向 pid 结构体的指针,pid 结构体包含了 PID 号。结构示意图如图2。

图2 一个task_struct对应一个PID

图中还有两个结构上面未提及:

  • pid_hash[]: 这是一个hash表的结构,根据 pid 的 nr 值哈希到其某个表项,若有多个 pid 结构对应到同一个表项,这里解决冲突使用的是散列表法。这样,就能解决开始提出的第2个问题了,根据PID值怎样快速地找到task_struct结构体:
    • 首先通过 PID 计算 pid 挂接到哈希表 pid_hash[] 的表项
    • 遍历该表项,找到 pid 结构体中 nr 值与 PID 值相同的那个 pid
    • 再通过该 pid 结构体的 tasks 指针找到 node
    • 最后根据内核的 container_of 机制就能找到 task_struct 结构体
  • pid_map:这是一个位图,用来唯一分配PID值的结构,图中灰色表示已经分配过的值,在新建一个进程时,只需在其中找到一个为分配过的值赋给 pid 结构体的 nr,再将pid_map 中该值设为已分配标志。这也就解决了上面的第3个问题——如何快速地分配一个全局的PID。

至于上面的第1个问题就更加简单,已知 task_struct 结构体,根据其 pid_link 的 pid 指针找到 pid 结构体,取出其 nr 即为 PID 号。

进程ID有类型之分

如果考虑进程之间有复杂的关系,如线程组、进程组、会话组,这些组均有组ID,分别为 TGID、PGID、SID,所以原来的 task_struct 中pid_link 指向一个 pid 结构体需要增加几项,用来指向到其组长的 pid 结构体,相应的 struct pid 原本只需要指回其 PID 所属进程的task_struct,现在要增加几项,用来链接那些以该 pid 为组长的所有进程组内进程。数据结构如下:

enum pid_type
{PIDTYPE_PID,PIDTYPE_PGID,PIDTYPE_SID,PIDTYPE_MAX
};struct task_struct {//...pid_t pid;     //PIDpid_t tgid;    //thread group idstruct task_struct *group_leader;   // threadgroup leaderstruct pid_link pids[PIDTYPE_MAX];//...
};struct pid_link {struct hlist_node node;  struct pid *pid;          
};struct pid {struct hlist_head tasks[PIDTYPE_MAX];int nr;                         //PIDstruct hlist_node pid_chain;    // pid hash 散列表结点
};

上面 ID 的类型 PIDTYPE_MAX 表示 ID 类型数目。之所以不包括线程组ID,是因为内核中已经有指向到线程组的 task_struct 指针 group_leader,线程组 ID 无非就是 group_leader 的PID。

假如现在有三个进程A、B、C为同一个进程组,进程组长为A,这样的结构示意图如图3。

图3 增加ID类型的结构

关于上图有几点需要说明:

  • 图中省去了 pid_hash 以及 pid_map 结构,因为第一种情况类似;
  • 进程B和C的进程组组长为A,那么 pids[PIDTYPE_PGID] 的 pid 指针指向进程A的 pid 结构体;
  • 进程A是进程B和C的组长,进程A的 pid 结构体的 tasks[PIDTYPE_PGID] 是一个散列表的头,它将所有以该pid 为组长的进程链接起来。

再次回顾本节的三个基本问题,在此结构上也很好去实现。

增加进程PID命名空间

若在第二种情形下再增加PID命名空间,一个进程就可能有多个PID值了,因为在每一个可见的命名空间内都会分配一个PID,这样就需要改变 pid 的结构了,如下:

struct pid
{unsigned int level;  /* lists of tasks that use this pid */struct hlist_head tasks[PIDTYPE_MAX];struct upid numbers[1];
};struct upid {int nr;struct pid_namespace *ns;struct hlist_node pid_chain;
};

在 pid 结构体中增加了一个表示该进程所处的命名空间的层次level,以及一个可扩展的 upid 结构体。对于struct upid,表示在该命名空间所分配的进程的ID,ns指向是该ID所属的命名空间,pid_chain 表示在该命名空间的散列表。

举例来说,在level 2 的某个命名空间上新建了一个进程,分配给它的 pid 为45,映射到 level 1 的命名空间,分配给它的 pid 为 134;再映射到 level 0 的命名空间,分配给它的 pid 为289,对于这样的例子,如图4所示为其表示:

图4 增加PID命名空间之后的结构图

图中关于如果分配唯一的 PID 没有画出,但也是比较简单,与前面两种情形不同的是,这里分配唯一的 PID 是有命名空间的容器的,在PID命名空间内必须唯一,但各个命名空间之间不需要唯一。

至此,已经与 Linux 内核中数据结构相差不多了。

进程ID管理函数

有了上面的复杂的数据结构,再加上散列表等数据结构的操作,就可以写出我们前面所提到的三个问题的函数了:

获得局部ID

根据进程的 task_struct、ID类型、命名空间,可以很容易获得其在命名空间内的局部ID:

  1. 获得与task_struct 关联的pid结构体。辅助函数有 task_pid、task_tgid、task_pgrp和task_session,分别用来获取不同类型的ID的pid 实例,如获取 PID 的实例:

    static inline struct pid *task_pid(struct task_struct *task)
    {return task->pids[PIDTYPE_PID].pid;
    }
    

    获取线程组的ID,前面也说过,TGID不过是线程组组长的PID而已,所以:

    static inline struct pid *task_tgid(struct task_struct *task)
    {return task->group_leader->pids[PIDTYPE_PID].pid;
    }
    

    而获得PGID和SID,首先需要找到该线程组组长的task_struct,再获得其相应的 pid:

    static inline struct pid *task_pgrp(struct task_struct *task)
    {return task->group_leader->pids[PIDTYPE_PGID].pid;
    }static inline struct pid *task_session(struct task_struct *task)
    {return task->group_leader->pids[PIDTYPE_SID].pid;
    }
    
  2. 获得 pid 实例之后,再根据 pid 中的numbers 数组中 uid 信息,获得局部PID。

    pid_t pid_nr_ns(struct pid *pid, struct pid_namespace *ns)
    {struct upid *upid;pid_t nr = 0;if (pid && ns->level <= pid->level) {upid = &pid->numbers[ns->level];if (upid->ns == ns)nr = upid->nr;}return nr;
    }
    

    这里值得注意的是,由于PID命名空间的层次性,父命名空间能看到子命名空间的内容,反之则不能,因此,函数中需要确保当前命名空间的level 小于等于产生局部PID的命名空间的level。
    除了这个函数之外,内核还封装了其他函数用来从 pid 实例获得 PID 值,如 pid_nr、pid_vnr 等。在此不介绍了。

结合这两步,内核提供了更进一步的封装,提供以下函数:

pid_t task_pid_nr_ns(struct task_struct *tsk, struct pid_namespace *ns);
pid_t task_tgid_nr_ns(struct task_struct *tsk, struct pid_namespace *ns);
pid_t task_pigd_nr_ns(struct task_struct *tsk, struct pid_namespace *ns);
pid_t task_session_nr_ns(struct task_struct *tsk, struct pid_namespace *ns);

从函数名上就能推断函数的功能,其实不外于封装了上面的两步。

查找进程task_struct

根据局部ID、以及命名空间,怎样获得进程的task_struct结构体呢?也是分两步:

  1. 获得 pid 实体。根据局部PID以及命名空间计算在 pid_hash 数组中的索引,然后遍历散列表找到所要的 upid, 再根据内核的 container_of 机制找到 pid 实例。代码如下:

    struct pid *find_pid_ns(int nr, struct pid_namespace *ns)
    {struct hlist_node *elem;struct upid *pnr;//遍历散列表hlist_for_each_entry_rcu(pnr, elem,&pid_hash[pid_hashfn(nr, ns)], pid_chain)     //pid_hashfn() 获得hash的索引if (pnr->nr == nr && pnr->ns == ns)     //比较 nr 与 ns 是否都相同return container_of(pnr, struct pid,     //根据container_of机制取得pid 实体numbers[ns->level]);return NULL;
    }
    
  2. 根据ID类型取得task_struct 结构体。

    struct task_struct *pid_task(struct pid *pid, enum pid_type type)
    {struct task_struct *result = NULL;if (pid) {struct hlist_node *first;first = rcu_dereference_check(hlist_first_rcu(&pid->tasks[type]),lockdep_tasklist_lock_is_held());if (first)result = hlist_entry(first, struct task_struct, pids[(type)].node);}return result;
    }
    

内核还提供其它函数用来实现上面两步:

struct task_struct *find_task_by_pid_ns(pid_t nr, struct pid_namespace *ns);
struct task_struct *find_task_by_vpid(pid_t vnr);
struct task_struct *find_task_by_pid(pid_t vnr);

具体函数实现的功能也比较简单。

生成唯一的PID

内核中使用下面两个函数来实现分配和回收PID的:

static int alloc_pidmap(struct pid_namespace *pid_ns);
static void free_pidmap(struct upid *upid);

在这里我们不关注这两个函数的实现,反而应该关注分配的 PID 如何在多个命名空间中可见,这样需要在每个命名空间生成一个局部ID,函数 alloc_pid 为新建的进程分配PID,简化版如下:

struct pid *alloc_pid(struct pid_namespace *ns)
{struct pid *pid;enum pid_type type;int i, nr;struct pid_namespace *tmp;struct upid *upid;tmp = ns;pid->level = ns->level;// 初始化 pid->numbers[] 结构体for (i = ns->level; i >= 0; i--) {nr = alloc_pidmap(tmp);            //分配一个局部IDpid->numbers[i].nr = nr;pid->numbers[i].ns = tmp;tmp = tmp->parent;}// 初始化 pid->task[] 结构体for (type = 0; type < PIDTYPE_MAX; ++type)INIT_HLIST_HEAD(&pid->tasks[type]);// 将每个命名空间经过哈希之后加入到散列表中upid = pid->numbers + ns->level;for ( ; upid >= pid->numbers; --upid) {hlist_add_head_rcu(&upid->pid_chain,&pid_hash[pid_hashfn(upid->nr, upid->ns)]);upid->ns->nr_hashed++;}return pid;
}

参考资料

  • 深入Linux 内核架构(以前不觉得这本书写得多好,现在倒发现还不错,本文很多都是照抄上面的)
  • 周徐达师弟的PPT(让我受益匪浅的一次讨论,周由浅入深告诉我们该数据结构是如何设计出来的,本文主思路就是按照该PPT,在此 特别感谢!)

转载于:https://www.cnblogs.com/hazir/p/linux_kernel_pid.html

相关文章:

【青少年编程竞赛交流】12月份微信图文索引

12月份微信图文索引 由于“组队学习”这个公众号的功能主要是组织Datawhale社群中的学习者们每个月的组队学习&#xff0c;所以&#xff0c;我另外新建了这个微信公众号“青少年编程竞赛交流”&#xff0c;在这个公众号上分享有关青少年编程方面的知识&#xff0c;带小朋友们参…

获取BT节点信息bittorrent-discovery

2019独角兽企业重金招聘Python工程师标准>>> 获取BT节点信息bittorrent-discovery BT/磁力都是常见的P2P下载方式。用户作为一个节点node从其他用户node或者peer获取文件数据&#xff0c;以完成下载。bittorren-discovery脚本可以探测目标主机通过BT/磁力方式分享所…

面向对象方法综述(工具<方法<思维<价值观)

思想起源于上世纪六十年代&#xff08;和结构化方法一样&#xff09; 最早的OOPL&#xff1a;Simula67 最纯的OOPL&#xff1a;Smalltalk smalltalk的贡献&#xff1a;它在系统设计中强调对象概念的统一&#xff0c;引入对象&#xff0c;对象类&#xff0c;方法&#xff0c;实…

第二章例2-2

#include<stdio.h>int main(void){ printf("Programming is fun.\n"); printf("And programming in C is even morn fun!\n"); return 0;}转载于:https://www.cnblogs.com/jiangjiali/p/3352576.html

【组队学习】十二月微信图文索引

十二月微信图文索引 一、组队学习相关 周报&#xff1a; Datawhale组队学习周报&#xff08;第042周&#xff09;Datawhale组队学习周报&#xff08;第043周&#xff09;Datawhale组队学习周报&#xff08;第044周&#xff09;Datawhale组队学习周报&#xff08;第045周&…

shell编程--case判断

case基础语法&#xff1a;格式 case 变量名 invalue1)command;;value2)command;;*)commond;;esac在case程序中&#xff0c;可以在条件中使用|&#xff0c;表示或的意思&#xff0c; 比如 2|3)command;;脚本 [rootlynn-04 shell]# vim case.sh#!/bin/bash read -p "Ple…

【delphi】Byte数组与String类型的转换

string string AnsiString 长字符串&#xff0c;理论上长度不受限制&#xff0c;但其实受限于最大寻址范围2的32次方4G字节&#xff1b; 变量Str名字是一个指针&#xff0c;指向位于堆内存的字符序列&#xff0c;字符序列起始于Str[1]&#xff0c;Str[1]偏移负16个字节的空间…

VC解析XML--使用CMarkup类解析XML

经过今天尝试MFC解析XML串&#xff0c;也算有了不少收获&#xff0c;总结一下。 我是使用的CMarkup类对XML进行操作。 CMarkup好象都是先从一个xml文件里面把内容读出来&#xff0c;再进行解析&#xff0c;搞得我恨不得要把我的CString写到xml文件…

Spring原理总结

写在前面&#xff1a;技术常新&#xff0c;思想常存。 Spring全家桶过于庞大&#xff0c;学习时难以抓住重点&#xff0c;希望通过此文章&#xff0c;让大家更好地去学习这一框架技术。 Spring有两大内功&#xff1a;IoC容器和AOP。这两个东西是不管什么时候都不会改变的。即便…

尚国栋:金融风控贷款违约预测(天池学习赛)

尚国栋是华北电力大学数理系大三的学生&#xff0c;LSGO软件技术团队&#xff08;Dreamtech算法组&#xff09;成员&#xff0c;参加了多期Datawhale的组队学习&#xff0c;荣获多期优秀队长的称号。 希望参与我们线下组队学习的同学&#xff0c;可以在微信公众号后台回复 线下…

树莓派修改密码(有单独屏幕)

在树莓派终端输入 sudo passwd pi改的是派的密码。 注意&#xff1a;和 sudo passwd root区分 Linux有密码保护&#xff0c;输入了看上去和没输入一样&#xff0c;其实已经输入了

Python每日一练0023

问题 如何判断一个文件是否存在 解决方案 这个问题可以分成几类问题 如果这里的文件指的是文件或目录&#xff0c;我们可以用os.path.exists()方法 >>> import os >>> os.path.exists(test) True 如果这里的文件指的是普通的文件&#xff0c;我们可以用os.pa…

火狐拓展开发 基础知识

平时经常碰到一些零碎的知识&#xff0c;或者其他什么好文章/知识点/插件/库等等&#xff0c;因为实在太多书签已插乱...于是想着干脆写个火狐小add-on。 首先找到了这里ADD-ON SDK&#xff1a; Using the Add-on SDK you can create Firefox add-ons using standard Web techn…

尚育鹏:Leetcode刷题总结(数组)

尚育鹏是华北电力大学数理系大二的学生&#xff0c;LSGO软件技术团队&#xff08;Dreamtech算法组&#xff09;成员&#xff0c;参加了多期Datawhale的组队学习。 希望参与我们线下组队学习的同学&#xff0c;可以在微信公众号后台回复 线下组队学习&#xff0c;进入线下组队学…

树莓派实现人脸识别需要做的那些事

1.连接数据库&#xff0c;建表&#xff0c;用来存放图像转码后的字符 2.用Pycharm连接上树莓派 3.下载安装face_recognition需要的依赖 4.配置好环境后运行代码

spring boot 实战 / 可执行war启动参数详解

概述 上一篇文章《spring boot 实战 / mvn spring-boot:run 参数详解》主要讲解了spring boot 项目基于maven插件启动过程中借助profiles的切换工作环境的问题。  这里我们讲一下spring boot项目基于可执行war包启动过程中借助profiles切换工作环境的问题。 配置 这里我们修改…

1476. Lunar Code

http://acm.timus.ru/problem.aspx?space1&num1476 由于前一列对后一列有影响&#xff0c;所以需要保持前一列的状态&#xff0c; 但无需用状态压缩来保存&#xff08;也保存不了&#xff09; 只需要保存前一列以 k 个0结尾的个数就可以 代码&#xff1a; import java.mat…

【组队学习】【33期】吃瓜教程——西瓜书+南瓜书

吃瓜教程——西瓜书南瓜书 航路开辟者&#xff1a;谢文睿、秦州领航员&#xff1a;潘磊航海士&#xff1a;谢文睿、秦州 基本信息 开源内容&#xff1a;https://github.com/datawhalechina/pumpkin-bookB 站视频&#xff1a;https://www.bilibili.com/video/BV1Mh411e7VU内容…

FIRST集与FOLLOW集构造步骤

首先&#xff0c;这两个集主语是候选式&#xff0c;是V*中的一个终结符/非终结符。 由于FOLLOW集的定义和构造步骤里面都涉及FIRST集&#xff0c;故先介绍FIRST集。 一.FIRST集的定义如下&#xff1a; FIRST(α){a|α>aβ, a∈Vt, α, β∈V*},若α>(*)ε则规定ε∈FRIS…

Bossie Awards 2013:最佳开源数据中心和云软件

当Facebook 的开源计算项目&#xff08;OCP&#xff09;酝酿着设计更好的服务器和网络时&#xff0c;其他开源项目也纷纷重塑数据库&#xff0c;应用平台以及下一代应用程序的虚拟化层。你还不知道吧&#xff0c;下一代的“云”基础设施管理工具终将来自开源产品。 近日&#x…

Laravel开启跨域的方法

1、建立中间件Cors.php 命令&#xff1a;php artisan make:middleware Cors 在/app/Http/Middleware/ 目录下会出现一个Cors.php 文件。 内容如下&#xff1a; <?phpnamespace App\Http\Middleware;use Closure;class Cors {/*** Handle an incoming request.** param \Il…

【组队学习】【33期】动手学数据分析

动手学数据分析 航路开辟者&#xff1a;陈安东、金娟娟、杨佳达、老表、李玲、张文涛、高立业领航员&#xff1a;张文恺航海士&#xff1a;武帅、戴治旭、初晓宇 基本信息 内容属性&#xff1a;精品入门课系列开源内容&#xff1a;https://github.com/datawhalechina/hands-…

LL(1)预测分析表的构造

LL(1)分析法&#xff08;即预测分析法&#xff09;是自上而下文法中的一种&#xff0c;使用这种方法需要用到LL(1)预测分析表。 前提&#xff1a;掌握了FIRST集和FOLLOW集的构造。 步骤&#xff1a;对于每一个产生式A→α &#xff08;1&#xff09; 对每个终结符a∈FIRST(α)…

新的sublime text已经上传网盘,地址写在下面

注&#xff1a;新网盘地址&#xff0c;之前的关于sublime text的网盘地址已效 网盘地址&#xff1a;http://pan.baidu.com/s/1oVHAm 压缩文件结构 从上到下依次是&#xff1a; 1.sublime text3 32bit便携版本的压缩包&#xff0c;解压可用. 64bit的用户可以将&#xff1a;http:…

WebAssembly Studio:Mozilla提供的WASM工具

\看新闻很累&#xff1f;看技术新闻更累&#xff1f;试试下载InfoQ手机客户端&#xff0c;每天上下班路上听新闻&#xff0c;有趣还有料&#xff01;\\\WebAssembly Studio是Mozilla开发的一款在线工具&#xff0c;用于将C/C和Rust代码编译为WASM格式。\\WebAssembly Studio是M…

【组队学习】【33期】3. 李宏毅机器学习(含深度学习)

李宏毅机器学习&#xff08;含深度学习&#xff09; 航路开辟者&#xff1a;王茂霖、陈安东&#xff0c;刘峥嵘&#xff0c;李玲领航员&#xff1a;宋泽山航海士&#xff1a;汪健麟、叶梁 基本信息 开源内容&#xff1a;https://github.com/datawhalechina/leeml-notes开源内…

Linux 引导和系统启动

bootstrap 引导程序;鞋带 -> 简称 boot 启动 pull oneself up by one’s bootstraps.&#xff08;体现计算机系统启动的难处&#xff09; Linux系统启动分为两大部分&#xff1a; 一&#xff0e; 第一部分&#xff1a;机器启动&#xff08;BIOS到 加载内核 &#xff0c;…

【数据结构】支持四则混合运算的计算器(转)

1.给出两个数&#xff0c;用户再指定操作符&#xff0c;要求计算结果&#xff0c;这实现起来很容易&#xff1b; 2.多个数&#xff0c;但只涉及同一优先级的操作符&#xff0c;做起来也很容易&#xff1b; 3.多个数&#xff0c;不同优先级的操作符&#xff0c;怎么办呢&#xf…

TypeScript学习笔记之 接口(Interface)

在java中&#xff0c;接口是用来定义一些规范&#xff0c;使用这些接口&#xff0c;就必须实现接口中的方法&#xff0c;而且接口中的属性必须是常量。javascript中是没有接口的概念的。所以TypeScript在编译成 JavaScript 的时候&#xff0c;所有的接口都会被擦除掉。 而TypeS…

【组队学习】【33期】数据可视化(Matplotlib)

数据可视化&#xff08;Matplotlib&#xff09; 航路开辟者&#xff1a;杨剑砺、杨煜、耿远昊、李运佳、居凤霞领航员&#xff1a;王森航海士&#xff1a;肖明远、郭棉昇 基本信息 开源内容&#xff1a;https://github.com/datawhalechina/fantastic-matplotlib开源内容&…