日期: 2021 年 8 月 2 日

Windows Server 2012 R2 中有个文件怎么都删不掉

就是它,以前遇到删不掉的拖到左侧显示的.bat 上就能删掉,而这个没用。

大家有什么办法?

%title插图%num

第 1 条附言  ·  2018-01-09 16:34:44 +08:00

找到个类似案例,而他是格盘才解决,

http://www.liqingbin.com/post/498.html

第 2 条附言  ·  2018-01-09 18:08:24 +08:00

已解决 unlocker 搞定的

8 条回复    2018-01-09 20:13:10 +08:00

jimmy2010
    1

jimmy2010   2018-01-09 18:02:51 +08:00 via Android

看起来这是个隐藏文件,你可以右键看下是不是还加了系统属性,如果是的话用 attrib 命令去掉就可以了。话说木马传上来怎么还
jimmy2010
    2

jimmy2010   2018-01-09 18:04:08 +08:00 via Android

接上:怎么还能自己改属性,莫非木马已经执行了,或者被杀毒软件控制了也是不能操作的。
imydou
    3

imydou   2018-01-09 18:06:51 +08:00

@jimmy2010 #2 attrib 提示找不到文件
plesk 的面板,是木马不假,虚拟主机都是独立用户,权限限制在域名目录里。
imydou
    4

imydou   2018-01-09 18:10:22 +08:00

@jimmy2010 #2 没有系统属性,也没杀软,文件没有在使用,是利用 windows 命名相关的漏洞。用 unlocker 删了,至于具体原理,不懂
jimmy2010
    5

jimmy2010   2018-01-09 18:15:31 +08:00 via Android

哦,明白了,确实是用了 Windows 系统不允许使用的文件名 nul,类似 aux 之类一样不允许这类字符做文件名
https://www.zhihu.com/question/38751059
imydou
    6

imydou   2018-01-09 18:18:17 +08:00

@jimmy2010 #5 遇到的其他文件都是利用单个漏洞,比如 nul .. ;
而这个文件利用了*少两个
h4lbhg1G
    7

h4lbhg1G   2018-01-09 20:09:09 +08:00

我直接的反应是,用 PE 系统或者 Linux 的 LiveCD 进去删。
imydou
    8

imydou   2018-01-09 20:13:10 +08:00

@h4lbhg1G #7 阿里云 ecs

变量作用域❤️

变量作用域❤️
**题目:**学习使用auto定义变量的用法。
1
**程序分析:**python中的变量作用域。
1
i=0
n=0
def dummy():
    i=0
    print(i)
    i+=1
def dummy2():
    global n
    print(n)
    n+=1
print(‘函数内部的同名变量’)
for j in range(20):
    print(i)
    dummy()
    i+=1
print(‘global声明同名变量’)
for k in range(20):
    print(n)
    dummy2()
    n+=10

深度剖析 Linux 的 3 种“拷贝”命令

概述

Linux 下有 3 种“拷贝”,分别是 ln,cp,mv,这 3 个命令貌似都能 copy 出一个新的文件出来。

细心的小伙伴看到我给 “拷贝” 打上了双引号?因为 Linux 的这 3 个命令有*大的区别,虽然用户看起来是拷贝出了新文件。

你是否曾经遇到过以下问题,想通原因了吗?:

  1. ln 创建链接文件,软链接可以跨文件系统,硬链接跨文件系统会报错,为什么?;
  2. mv 好像有时候快,有时候非常慢,有些时候还会残留垃圾,为什么?;
  3. cp 拷贝数据有时快,有时候非常慢,源文件和目标文件所占物理空间竟然不一致?

本篇文章看完,希望你以上问题不再有疑问,从容使用 ln,mv,cp 命令。

温馨提示:

  1. 以下我们只讨论文件的简单操作,关于目录操作或者复杂参数的操作不在我们本次主题以内,我们忽略;
  2. coreutils 库的代码版本用的是 8.3;

我们来看下简单的 3 个命令操作。首先在执行以下命令之前,准备一个不小的 test 的普通文件(比如 1G )。

“拷贝”命令一:ln

  1. # 创建一个软链接文件
  2. ln -s ./test ./test_soft_link
  3. # 创建一个硬连接文件
  4. ln ./test ./test_hard_link

你会发现当前目录出现了两个新文件 test_soft_link ,test_hard_link 。并且你会发现拷贝速度好快?为什么呢?

“拷贝”命令二:mv

把 test 文件”拷贝”到 ./backup/ 目录

mv ./test ./backup/

更神奇的是,好像 copy 一个 1 G 的文件,速度也贼快?

“拷贝”命令三:cp

把 test 文件”拷贝”到 ./backup/ 目录

cp ./test ./backup/

上面我们看到,好像 ln,mv,cp 这 3 个命令都是“拷贝”?好像都进行了数据复制出了新的文件?

答案:当然不是。这 3 个看起来都是复制出了新文件,但其实天壤之别。我们一个个来揭秘。

在揭秘这 3 个命令之前,我们必须先复习文件的基础知识点,Linux 的文件和目录的关系。

%title插图%num

Linux 的文件和目录

在深度剖析 Linux cp 的秘密一文中,我们详细剖析了文件系统的形态。有几个关键知识点:

  1. 文件系统内有 3 个关键区域:超级块区域,inode 区域,数据 block 区域;
  2. 其中一个 inode 和一个文件对应,包含了文件的元数据信息;
  3. 一个 inode 有唯一的编号,可以理解成就是单调递增的整数。比如 1,2,3,4,5,6,,,,;

关于上面,我们注意到 inode 其实标识的是一个平坦的结构,inode 索引到数据 data 区域,每个 inode 都有唯一编号。

问题来了:Linux 的目录是一个倒挂的树形结构呀,为什么上面说 inode 是平坦的结构?如下:

%title插图%num

Linux 的文件确实是树形结构,inode 也确实是平坦的结构。你会感觉到因为是因为之前故意忽略了一个几个东西:目录文件和 dentry 项。这是两个非常重要的概念,我们逐个解释下。

文件系统中其实有两种文件类型,分为:

  • 普通文件(这里把链接文件包含在普通文件以内)
  • 目录文件

可以通过 inode->i_mode 字段,使用 S_ISREG,S_ISDIR 这两个宏来判断是哪个类型。普通文件很容易理解,就是普通的数据文件,inode 里面存储元数据,inode 可以索引到 block,block 里面存储用户的数据。目录文件 inode 存储元数据,block 里面存储的是目录条目。目录条目是什么样子的东西?

举个形象的例子:在当前 testdir 目录下,有 dir1,dir2,dir3 这三个文件。假设 dir1 的 inode 编号是 1024,dir2 是 1025,dir3 是 1026。

那么现实是这样的:

  1. testdir 这个目录首先会对应有一个 inode,inode->i_mode 的类型是目录,并且还会有 block 块,通过 inode->i_blocks 能索引到这些 block;
  2. block 里面存储的内容很简单,是一个个目录条目,内核的名字缩写为 dirent,每一个 dirent 本质就是一个 文件名字 到 inode 编号的映射,所以,testdir 这个目录文件的 block 里存了 3 条记录 [dir1, 1024],[dir2, 1025],[dir3, 1026];

%title插图%num

所以,目录到底是什么呢?就存储形态而已,目录也是文件,存储的是 名字 到 inode number 的映射表。dirent 其实就是 directory entry 的缩写。

好像还没讲到树形结构?

其实已经讲了一半了,树形结构的数据结构基础已经有了,就是目录文件和 dirent 的实现。

假设叶子结点的为普通文件

针对开篇的图,其实磁盘上存储了 3 个目录文件

%title插图%num

这个时候,读者朋友你是不是都可以用笔画出一个树形结构了,内存的树形结构也是这么来的。通过磁盘的映射数据构造出来。在内存中,这个树形结构的节点用 dentry 来表示(通常翻译成目录项,但是笔者认为这个翻译很容易让人误解)。

以下是笔者从内核精简出来的 dentry 结构体,通过这个总结到几个信息:

  1. dentry 绑定到唯一一个 inode 结构体;
  2. dentry 有父,子,兄弟的索引路径,有这个就足够在内存中构建一个树了,并且事实也确实如此;
  1. struct dentry {
  2.    // …
  3.    struct dentry  *d_parent;   /* 父节点 */
  4.    struct qstr    d_name;      // 名字
  5.    struct inode   *d_inode;    // inode 结构体
  6.    struct list_head d_child;     /* 兄弟节点 */
  7.    struct list_head d_subdirs;   /* 子节点 */
  8. };

所以,看到现在理解了吗?父、子 指针,这就是经典的树形结构需要的字段呀。目录文件类型为树形结构提供了存储到磁盘持久化的一种形态,是一种 map 表项的形态,每一个表项我们叫做 dirent 。文件树的结构在内存中以 dentry 结构体体现。

划重点:仔细理解下 dirent 和 dentry 的概念和形态,仔细理解磁盘的数据形态和内存的数据结构形态,后面要考的。

%title插图%num

ln 命令

ln 是 Linux 的基础命令之一,是 link 的缩写,顾名思义就是跟链接文件相关的一个命令。一般语法如下:

ln [OPTION]... TARGET LINK_NAME

ln 可以用来创建一个链接文件,有趣的是,链接文件有两个不同的类别:

  • 软链接文件
  • 硬链接文件

 1   什么是软链接文件?

无论是软链接还是硬链接都是“链接”文件,也就是说,通过这个链接文件都能找到背后的那个“源文件”。首先说结论:

  • 软链接文件是一个全新的文件,有独立的 inode,有自己的 block ,而这个文件类型是“链接文件”的类型而已;
  • 这个软链接文件的内容是一段 path 路径,这个路径直接指向源文件;

所以,你明白了吗?软链接文件就是一个文件而已,文件里面存储的是一个路径字符串。所以软链接文件可以非常灵活,链接文件本身和源解耦,只通过一段路径字符串寻路。

所以,软链接文件是可以跨文件系统创建的。

%title插图%num

有兴趣的小伙伴可以去看源码实现,在 coreutils 库里,调用栈如下:

main -> do_link -> force_symlinkat -> symlinkat

也就是说*终调用的是系统调用 symlinkat 来完成创建,而这个 symlinkat 系统调用在内核由不同的文件系统实现。举个例子,如果是 minix 文件系统,那么对应的函数就是 minix_symlink。minix_symlink 这个函数上来就是新建一个 inode ,然后在对应的目录文件中添加一个 dirent 。来来来,我们看一眼 minix_symlink 的主干代码:

  1. static int minix_symlink(struct inode * dir, struct dentry *dentry,
  2.           const char * symname)
  3. {
  4.  // …
  5.  // 新建一个 inode,inode 类型为 S_IFLNK 链接类型
  6.  inode = minix_new_inode(dir, S_IFLNK | 0777, &err);
  7.  if (!inode)
  8.    goto out;
  9.  // 填充链接文件内容
  10.  minix_set_inode(inode, 0);
  11.  err = page_symlink(inode, symname, i);
  12.  if (err)
  13.    goto out_fail;
  14.  // 绑定 dentry 和 inode
  15.  err = add_nondir(dentry, inode);
  16.  //…
  17. }

划重点:软链接文件是新建了一个文件,文件类型是链接文件,文件内容就是一段字符串路径。分配新的 inode,内存对应新的 dentry ,当然了,也新增了一个 dirent 。软链文件可以跨越不同的文件系统。

2  什么是硬链接文件?

现在我们知道了,软链接文件怎么找到源文件的?通过路径找到的,路径就存储在软链接文件中。硬链接文件又怎么办到的呢?

硬链接很神奇,硬链接其实是新建了一个 dirent 而已。下面是重点:

  1. 硬链接文件其实并没有新建文件(也就是说,没有消耗 inode 和 文件所需的 block 块);
  2. 硬链接其实是修改了当前目录所在的目录文件,加了一个 dirent 而已,这个 dirent 用一个新的 name 名字指向原来的 inode number;

%title插图%num

重点来了,由于新旧两个 dirent 都是指向同一个 inode,那么就导致了一个限制:不能跨文件系统。因为,不同文件系统的 inode 管理都是独立的。

感兴趣的同学可以试下,跨文件系统创建硬链接就会报告如下错误:Invalid cross-device link

  1. sh-4.4# ln /dev/shm/source.txt ./dest.txt
  2. ln: failed to create hard link ‘./dest.txt’ => ‘/dev/shm/source.txt’: Invalid cross-device link

有兴趣的小伙伴可以去看源码实现,在 coreutils 库里,调用栈如下:

main -> do_link -> force_linkat -> linkat

也就是说*终调用的是系统调用 linkat 来完成创建,而这个 linkat 系统调用在内核由不同的文件系统实现。举个例子,如果是 minix 文件系统,那么对应的函数就是 minix_link。这个函数从内存上来讲是把一个 dentry 和 inode 关联起来。从磁盘数据结构上来讲,会在对应目录文件中增加一个 dirent 项。

划重点:硬链接只增加了一个 dirent 项,只修改了目录文件而已。不涉及到 inode 数量的变化。新的 name 指向原来的 inode。

%title插图%num

mv 命令

mv 是 move 的缩写,从效果上来看,是把源文件搬移到另一个位置。

你是否思考过 mv 命令内部是怎么实现的呢?

是把源文件拷贝到目标位置,然后删除源文件吗?所以,说 mv 貌似也是“拷贝”?

其实,并不是,准确的说不完全是。

对于 mv 的讨论,要拆分成源和目的文件是否在同一个文件系统。

1 源 和 目的 在同一个文件系统

mv 命令的核心操作是系统调用 rename ,rename 从内核实现来说只涉及到元数据的操作,只涉及到 dirent 的增删(当然不同的文件系统可能略有不同,但是大致如是)。通常操作是删除源文件所在目录文件中的 dirent,在目标目录文件中添加一个新的 dirent 项。

划重点:inode number 不变,inode 不变,不增不减,还是原来的 inode 结构体,所以数据完全没有拷贝。

mv 的调用栈如下,感兴趣的可以自己调试。

  1. main -> renameat2
  2. main -> movefile -> do_move -> copy -> copy_internal -> renameat2

我们用例子来直观看下,首先准备好一个 source.txt 文件,用 stat 命令看下元数据信息:

  1. sh-4.4# stat source.txt
  2.   File: source.txt
  3.   Size: 0          Blocks: 0          IO Block: 4096   regular empty file
  4. Device: 78h/120d Inode: 3156362     Links: 1
  5. Access: (0644/-rw-r–r–)  Uid: (    0/    root)   Gid: (    0/    root)

我们看到 inode 编号是:3156362 。然后执行 mv 命令:

sh-4.4# mv source.txt dest.txt

然后 stat 看下 dest.txt 文件的信息:

  1. sh-4.4# stat dest.txt
  2.   File: dest.txt
  3.   Size: 0          Blocks: 0          IO Block: 4096   regular empty file
  4. Device: 78h/120d Inode: 3156362     Links: 1
  5. Access: (0644/-rw-r–r–)  Uid: (    0/    root)   Gid: (    0/    root)

发现没?inode 编号还是 3156362 。

2 源和目的在不同的文件系统

还记得之前我们提过,由于硬链接是直接在目录文件中添加一个 dirent,名字直接指向源文件的 inode ,不同文件系统都是独立的一套 inode 管理系统,所以硬链接不能跨文件系统。

那么问题来了,mv 遇到跨文件系统的场景呢,怎么处理?是否还是 rename ?

举个例子,如下命令,源和目的是不同的文件系统。我虚拟机的挂载点如下:

  1. sh-4.4# df -h
  2. Filesystem      Size  Used Avail Use% Mounted on
  3. overlay          59G  3.5G   52G   7% /
  4. tmpfs            64M     0   64M   0% /dev
  5. shm              64M     0   64M   0% /dev/shm

我故意挑选 /home/qiya/testdir 和 /dev/shm/ ,这两个目录分别对应了 “/” 和 “/dev/shm/” 的挂载点的文件系统,分属两个不同的文件系统。我们先提前看下源文件的信息(主要是 inode 信息):

  1. sh-4.4# stat /dev/shm/source.txt
  2.   File: /dev/shm/source.txt
  3.   Size: 0          Blocks: 0          IO Block: 4096   regular empty file
  4. Device: 7fh/127d Inode: 163990      Links: 1
  5. Access: (0644/-rw-r–r–)  Uid: (    0/    root)   Gid: (    0/    root)

我们执行以下 mv 命令:

sh-4.4# mv /dev/shm/source.txt /home/qiya/testdir/dest.txt

然后看下目的文件信息:

  1. sh-4.4# stat dest.txt
  2.   File: dest.txt
  3.   Size: 0          Blocks: 0          IO Block: 4096   regular empty file
  4. Device: 78h/120d Inode: 3155414     Links: 1
  5. Access: (0644/-rw-r–r–)  Uid: (    0/    root)   Gid: (    0/    root)

对比有没有发现,inode 的信息是不一样的,inode number 是不一样的(是不是跟上面同一文件系统下的 mv 现象不一致)什么原因呢?我下面一一道来,从原理出剖析。

当系统调用 rename 的时候,如果目的不在同一文件系统时,会报告 EXDEV 的错误码,提示该调用不能跨文件系统。

#define EXDEV           18      /* Cross-device link */

所以,rename 是不能用于跨文件系统的,这个时候怎么办?

划重点:这个时候操作分成两步走,先 copy ,后 remove 。

  1. *步:走不了 rename ,那么就退化成 copy ,也就是真正的拷贝。读取源文件,写入目标位置,生成一个全新的目标文件副本;
  • 这里调用的 copy_reg 的函数封装(要知道这个函数是 cp 命令的核心函数,在 深度剖析 Linux cp 的秘密 有深入剖析过 );
  • ln,mv,cp 是在 coreutils 库里的命令,公用函数本身就是可以复用的;
  • 第二步:删除源文件,使用 rm 函数删除;
  • 思考问题:mv 跨文件系统的时候,如果*步成功了,第二步失败了(比如没有删除权限)会怎么样?

    会导致垃圾。也就是说,目标处创建了一个新文件,源文件并没有删除。这个小实验有兴趣的可以试下。

    %title插图%num

    cp 命令

    cp 命令才是真正的数据拷贝命令,即拷贝元数据,也会拷贝数据。cp 命令也是我之前花了万字篇幅分析的命令,详细可见:深度剖析 Linux cp 的秘密。这里就不再赘述,下面提炼出关于拷贝的 3 种模式。

    涉及到数据拷贝的,关键有个 –sparse 参数,可以控制拷贝数据的 IO 次数。

     1 auto 模式

    重点:跳过文件空洞。是 cp 默认的模式

    cp src.txt dest.txt

    %title插图%num2 always 模式

    重点:跳过文件空洞,还会跳过全 0 数据,是空间*省的模式。

    cp --sparse=always src.txt dest.txt

    %title插图%num3 never 模式

    重点:无脑拷贝,从头拷贝到尾,不识别物理空洞和全 0 数据,是速度*慢的一种模式。

    cp --sparse=never src.txt dest.txt
    

    %title插图%num
    复用之前画的这 3 张图,很形象的体现了 cp 的行为。
    %title插图%num

    总结

    1. 目录文件是一种特殊的文件,可以理解成存储的是 dirent 列表。dirent 只是名字到 inode 的映射,这个是树形结构的基础;
    2. 常说目录树在内存中确实是一个树的结构,每个节点由 dentry 结构体表示;
    3. ln -s 创建软链接文件,软链接文件是一个独立的新文件,有一个新的 inode ,有新的 dentry,文件类型为 link,文件内容就是一条指向源的路径,所以软链的创建可以无视文件系统,跨越山河;
    4. ln 默认创建硬连接,硬链接文件只在目录文件里添加了一个新 dirent 项 <新name:原inode>,文件 inode 还是和原文件同一个,所以硬链接不能跨文件系统(因为不同的文件系统是独立的一套 inode 管理方式,不同的文件系统实例对 inode number 的解释各有不同);
    5. ln 命令貌似创建出了新文件,但其实不然,ln 只跟元数据相关,涉及到 dirent  的变动,不涉及到数据的拷贝,起不到数据备份的目的;
    6. mv 其实是调用 rename 调用,在同一个文件系统中不涉及到数据拷贝,只涉及到元数据变更( dirent 的增删 ),所以速度也很快。但如果 mv 的源和目的在不同的文件系统,那么就会退化成真正的 copy ,会涉及到数据拷贝,这个时候速度相对慢一些,慢成什么样子?就跟 cp 命令一样;
    7. cp 命令才是真正的数据拷贝命令,速度可能相对慢一些,但是 cp 命令有 –spare 可以优化拷贝速度,针对空洞和全 0 数据,可以跳过,从而针对稀疏文件可以节省大量磁盘 IO

Kubernetes 上容器的启动顺序如何把控?

为什么要做容器启动顺序控制?我们都知道 Pod 中除了 init-container 之外,是允许添加多个容器的。类似 TektonCD 中 task 和 step 的概念就分别与 pod 和 container 对应,而 step 是按照顺序执行的。此外还有服务网格的场景,sidecar 容器需要在服务容器启动之前完成配置的加载,也需要对容器的启动顺序加以控制。否则,服务容器先启动,而 sidecar 还无法提供网络上的支持。

现实

%title插图%num

 

期望

%title插图%num

 

到了这里肯定有同学会问,spec.containers[] 是一个数组,数组是有顺序的。Kubernetes 也确实是按照顺序来创建和启动容器,但是 容器启动成功,并不表示容器可以对外提供服务。

在 Kubernetes 1.18 非正式版中曾在 Lifecycle 层面提供了对 sidecar 类型容器的 支持,但是*终该功能并没有落地[2]。

那到底该怎么做?

%title插图%num

TL;DR

笔者准备了一个简单的 go 项目[3],用于模拟 sidecar 的启动及配置加载。

克隆代码后可以通过 make build 构建出镜像,假如你是用的 minikube 进行的实验,可以通过命令 make load-2-minikube 将镜像加载到 minikube 节点中。

使用 Deployment 的方式进行部署,直接用 Pod 也可以。

  1. apiVersion: apps/v1
  2. kind:Deployment
  3. metadata:
  4. creationTimestamp:null
  5. labels:
  6. app: sample
  7. name: sample
  8. spec:
  9. replicas:1
  10. selector:
  11. matchLabels:
  12. app: sample
  13. strategy:{}
  14. template:
  15. metadata:
  16. creationTimestamp:null
  17. labels:
  18. app: sample
  19. spec:
  20. containers:
  21. image: addozhang/k8s-container-sequence-sidecar:latest
  22. name: sidecar
  23. imagePullPolicy:IfNotPresent
  24. lifecycle:
  25. postStart:
  26. exec:
  27. command:
  28. -/entrypoint
  29. wait
  30. image: busybox:latest
  31. name: app
  32. imagePullPolicy:IfNotPresent
  33. command:[“/bin/sh”,”-c”]
  34. args:[“date; echo ‘app container started’; tail -f /dev/null”]

下面的截图中,演示了在 sample 命名空间中,pod 内两个容器的执行顺序。

%title插图%num

%title插图%num

Kubernetes 源码

在 kubelet 的源码 pkg/kubelet/kuberuntime/kuberuntime_manager.go 中,#SyncPod 方法用于创建 Pod,步骤比较繁琐,直接看第 7 步:创建普通容器。

  1. // SyncPod syncs the running pod into the desired pod by executing following steps:
  2. //
  3. // 1. Compute sandbox and container changes.
  4. // 2. Kill pod sandbox if necessary.
  5. // 3. Kill any containers that should not be running.
  6. // 4. Create sandbox if necessary.
  7. // 5. Create ephemeral containers.
  8. // 6. Create init containers.
  9. // 7. Create normal containers.
  10. func (m *kubeGenericRuntimeManager)SyncPod(pod *v1.Pod, podStatus *kubecontainer.PodStatus, pullSecrets []v1.Secret, backOff *flowcontrol.Backoff)(result kubecontainer.PodSyncResult){
  11. // Step 7: start containers in podContainerChanges.ContainersToStart.
  12. for _, idx := range podContainerChanges.ContainersToStart{
  13. start(“container”, containerStartSpec(&pod.Spec.Containers[idx]))
  14. }
  15. return
  16. }

在 #start 方法中调用了 #startContainer 方法,该方法会启动容器,并返回容器启动的结果。注意,这里的结果还 包含了容器的 Lifecycle hooks 调用。

也就是说,假如容器的 PostStart hook 没有正确的返回,kubelet 便不会去创建下一个容器。

  1. // startContainer starts a container and returns a message indicates why it is failed on error.
  2. // It starts the container through the following steps:
  3. // * pull the image
  4. // * create the container
  5. // * start the container
  6. // * run the post start lifecycle hooks (if applicable)
  7. func (m *kubeGenericRuntimeManager) startContainer(podSandboxID string, podSandboxConfig *runtimeapi.PodSandboxConfig, spec *startSpec, pod *v1.Pod, podStatus *kubecontainer.PodStatus, pullSecrets []v1.Secret, podIP string, podIPs []string)(string, error){
  8. // Step 4: execute the post start hook.
  9. if container.Lifecycle!=nil&& container.Lifecycle.PostStart!=nil{
  10. kubeContainerID := kubecontainer.ContainerID{
  11. Type: m.runtimeName,
  12. ID: containerID,
  13. }
  14. msg, handlerErr := m.runner.Run(kubeContainerID, pod, container, container.Lifecycle.PostStart)
  15. if handlerErr !=nil{
  16. m.recordContainerEvent(pod, container, kubeContainerID.ID, v1.EventTypeWarning, events.FailedPostStartHook, msg)
  17. if err := m.killContainer(pod, kubeContainerID, container.Name,“FailedPostStartHook”, reasonFailedPostStartHook,nil); err !=nil{
  18. klog.ErrorS(fmt.Errorf(“%s: %v”,ErrPostStartHook, handlerErr),“Failed to kill container”,“pod”, klog.KObj(pod),
  19. “podUID”, pod.UID,“containerName”, container.Name,“containerID”, kubeContainerID.String())
  20. }
  21. return msg, fmt.Errorf(“%s: %v”,ErrPostStartHook, handlerErr)
  22. }
  23. }
  24. return“”,nil
  25. }

%title插图%num

实现方案

%title插图%num

cmd/entrypoint/wait.go#L26[4] (这里参考了 Istio 的 pilot-agent 实现 )

在 PostStart 中持续的去检查 /ready 断点,可以 hold 住当前容器的创建流程。保证 /ready 返回 200 后,kubelet 才会去创建下一个容器。

这样就达到了前面截图中演示的效果。

  1. for time.Now().Before(timeoutAt){
  2. err = checkIfReady(client, url)
  3. if err ==nil{
  4. log.Println(“sidecar is ready”)
  5. returnnil
  6. }
  7. log.Println(“sidecar is not ready”)
  8. time.Sleep(time.Duration(periodMillis)* time.Millisecond)
  9. }
  10. return fmt.Errorf(“sidecar is not ready in %d second(s)”, timeoutSeconds)

六个问题让你更懂 React Fiber

React 的设计理念是什么?

React官网在React哲学[4]一节开篇提到:

我们认为,React 是用 JavaScript 构建快速响应的大型 Web 应用程序的首选方式。它在 Facebook 和 Instagram 上表现优秀。React *棒的部分之一是引导我们思考如何构建一个应用。

由此可见,React 追求的是 “快速响应”,那么,“快速响应“的制约因素都有什么呢?

  • CPU的瓶颈:当项目变得庞大、组件数量繁多、遇到大计算量的操作或者设备性能不足使得页面掉帧,导致卡顿。
  • IO的瓶颈:发送网络请求后,由于需要等待数据返回才能进一步操作导致不能快速响应。

本文要聊的fiber 架构主要就是用来解决 CPU 和网络的问题,这两个问题一直也是*影响前端开发体验的地方,一个会造成卡顿,一个会造成白屏。为此 react 为前端引入了两个新概念:Time Slicing 时间分片和Suspense。

%title插图%num

React的“先天不足” —— 听说 Vue 3.0 采用了动静结合的 Dom diff,React 为何不跟进?

Vue 3.0 动静结合的 Dom diff

Vue3.0 提出动静结合的 DOM diff 思想,动静结合的 DOM diff其实是在预编译阶段进行了优化。之所以能够做到预编译优化,是因为 Vue core 可以静态分析 template,在解析模版时,整个 parse 的过程是利用正则表达式顺序解析模板,当解析到开始标签、闭合标签和文本的时候都会分别执行对应的回调函数,来达到构造 AST 树的目的。

借助预编译过程,Vue 可以做到的预编译优化就很强大了。比如在预编译时标记出模版中可能变化的组件节点,再次进行渲染前 diff 时就可以跳过“永远不会变化的节点”,而只需要对比“可能会变化的动态节点”。这也就是动静结合的 DOM diff 将 diff 成本与模版大小正相关优化到与动态节点正相关的理论依据。

React 能否像 Vue 那样进行预编译优化?

Vue 需要做数据双向绑定,需要进行数据拦截或代理,那它就需要在预编译阶段静态分析模版,分析出视图依赖了哪些数据,进行响应式处理。而 React 就是局部重新渲染,React 拿到的或者说掌管的,所负责的就是一堆递归 React.createElement 的执行调用(参考下方经过Babel转换的代码),它无法从模版层面进行静态分析。JSX 和手写的 render function[5] 是完全动态的,过度的灵活性导致运行时可以用于优化的信息不足。

JSX 写法:

  1. <div>
  2.   <h1>六个问题助你理解 React Fiber</h1>
  3.   <ul>
  4.     <li>React</li>
  5.     <li>Vue</li>
  6.   </ul>
  7. </div>

递归 React.createElement:

  1. // Babel转换后
  2. React.createElement(
  3.   “div”,
  4.   null,
  5.  React.createElement(
  6.     “h1”,
  7.     null,
  8.     “\u516D\u4E2A\u95EE\u9898\u52A9\u4F60\u7406\u89E3 React Fiber”
  9.  ),
  10.  React.createElement(
  11.     “ul”,
  12.     null,
  13.    React.createElement(“li”null“React”),
  14.    React.createElement(“li”null“Vue”)
  15.  )
  16. );

JSX vs Template

%title插图%num

jsx and Templates

  • JSX 具有 JavaScript 的完整表现力,可以构建非常复杂的组件。但是灵活的语法,也意味着引擎难以理解,无法预判开发者的用户意图,从而难以优化性能。
  • Template 模板是一种非常有约束的语言,你只能以某种方式去编写模板。

既然存在以上编译时先天不足,在运行时优化方面,React一直在努力。比如,React15实现了batchedUpdates(批量更新)。即同一事件回调函数上下文中的多次setState只会触发一次更新。

但是,如果单次更新就很耗时,页面还是会卡顿(这在一个维护时间很长的大应用中是很常见的)。这是因为React15的更新流程是同步执行的,一旦开始更新直到页面渲染前都不能中断。

资料参考:以 React 为例,说说框架和性能(下)[6] | 新兴前端框架 Svelte 从入门到原理


%title插图%num

从架构演变看不断进击的 React 都做过哪些优化?

React渲染页面的两个阶段

  • 调度阶段(reconciliation):在这个阶段 React 会更新数据生成新的 Virtual DOM,然后通过Diff算法,快速找出需要更新的元素,放到更新队列中去,得到新的更新队列。
  • 渲染阶段(commit):这个阶段 React 会遍历更新队列,将其所有的变更一次性更新到DOM上。

React 15 架构

React15架构可以分为两层:

  • Reconciler(协调器)—— 负责找出变化的组件;
  • Renderer(渲染器)—— 负责将变化的组件渲染到页面上;

在React15及以前,Reconciler采用递归的方式创建虚拟DOM,递归过程是不能中断的。如果组件树的层级很深,递归会占用线程很多时间,递归更新时间超过了16ms,用户交互就会卡顿。

为了解决这个问题,React16将递归的无法中断的更新重构为异步的可中断更新,由于曾经用于递归的虚拟DOM数据结构已经无法满足需要。于是,全新的Fiber架构应运而生。

React 16 架构

为了解决同步更新长时间占用线程导致页面卡顿的问题,也为了探索运行时优化的更多可能,React开始重构并一直持续至今。重构的目标是实现Concurrent Mode(并发模式)。

从v15到v16,React团队花了两年时间将源码架构中的Stack Reconciler重构为Fiber Reconciler。

React16架构可以分为三层:

  • Scheduler(调度器)—— 调度任务的优先级,高优任务优先进入Reconciler;
  • Reconciler(协调器)—— 负责找出变化的组件:更新工作从递归变成了可以中断的循环过程。Reconciler内部采用了Fiber的架构;
  • Renderer(渲染器)—— 负责将变化的组件渲染到页面上。

React 17 优化

React16的expirationTimes模型只能区分是否 >=expirationTimes 决定节点是否更新。React17的lanes模型可以选定一个更新区间,并且动态的向区间中增减优先级,可以处理更细粒度的更新。

Lane用二进制位表示任务的优先级,方便优先级的计算(位运算),不同优先级占用不同位置的“赛道”,而且存在批的概念,优先级越低,“赛道”越多。高优先级打断低优先级,新建的任务需要赋予什么优先级等问题都是Lane所要解决的问题。

Concurrent Mode的目的是实现一套可中断/恢复的更新机制。其由两部分组成:

  • 一套协程架构:Fiber Reconciler
  • 基于协程架构的启发式更新算法:控制协程架构工作方式的算法

资料参考:React17新特性:启发式更新算法[7]

%title插图%num

浏览器一帧都会干些什么以及requestIdleCallback的启示

浏览器一帧都会干些什么?

我们都知道,页面的内容都是一帧一帧绘制出来的,浏览器刷新率代表浏览器一秒绘制多少帧。原则上说 1s 内绘制的帧数也多,画面表现就也细腻。目前浏览器大多是 60Hz(60帧/s),每一帧耗时也就是在 16.6ms 左右。那么在这一帧的(16.6ms) 过程中浏览器又干了些什么呢?

%title插图%num浏览器一帧都会干些什么

通过上面这张图可以清楚的知道,浏览器一帧会经过下面这几个过程:

  1. 接受输入事件
  2. 执行事件回调
  3. 开始一帧
  4. 执行 RAF (RequestAnimationFrame)
  5. 页面布局,样式计算
  6. 绘制渲染
  7. 执行 RIC (RequestIdelCallback)

第七步的 RIC 事件不是每一帧结束都会执行,只有在一帧的 16.6ms 中做完了前面 6 件事儿且还有剩余时间,才会执行。如果一帧执行结束后还有时间执行 RIC 事件,那么下一帧需要在事件执行结束才能继续渲染,所以 RIC 执行不要超过 30ms,如果长时间不将控制权交还给浏览器,会影响下一帧的渲染,导致页面出现卡顿和事件响应不及时。

requestIdleCallback 的启示

我们以浏览器是否有剩余时间作为任务中断的标准,那么我们需要一种机制,当浏览器有剩余时间时通知我们。

  1. requestIdleCallback((deadline) => {
  2. // deadline 有两个参数
  3.   // timeRemaining(): 当前帧还剩下多少时间
  4.   // didTimeout: 是否超时
  5. // 另外 requestIdleCallback 后如果跟上第二个参数 {timeout: …} 则会强制浏览器在当前帧执行完后执行。
  6.  if (deadline.timeRemaining() > 0) {
  7.    // TODO
  8.  } else {
  9.   requestIdleCallback(otherTasks);
  10.  }
  11. });
  12. // 用法示例
  13. var tasksNum = 10000
  14. requestIdleCallback(unImportWork)
  15. function unImportWork(deadline{
  16.   while (deadline.timeRemaining() && tasksNum > 0) {
  17.     console.log(`执行了 ${10000 – tasksNum + 1}个任务`)
  18.     tasksNum–
  19.   }
  20.   if (tasksNum > 0) { // 在未来的帧中继续执行
  21.     requestIdleCallback(unImportWork)
  22.   }
  23. }

其实部分浏览器已经实现了这个API,这就是requestIdleCallback。但是由于以下因素,Facebook 抛弃了 requestIdleCallback 的原生 API:

  • 浏览器兼容性;
  • 触发频率不稳定,受很多因素影响。比如当我们的浏览器切换tab后,之前tab注册的requestIdleCallback触发的频率会变得很低。

参考:requestIdleCallback 的 FPS 只有 20[8]

基于以上原因,在React中实现了功能更完备的requestIdleCallbackpolyfill,这就是Scheduler。除了在空闲时触发回调的功能外,Scheduler还提供了多种调度优先级供任务设置。

资料参考:requestIdleCallback-后台任务调度[9]

%title插图%num

Fiber 为什么是 React 性能的一个飞跃?

什么是 Fiber

Fiber 的英文含义是“纤维”,它是比线程(Thread)更细的线,比线程(Thread)控制得更精密的执行模型。在广义计算机科学概念中,Fiber 又是一种协作的(Cooperative)编程模型(协程),帮助开发者用一种【既模块化又协作化】的方式来编排代码。

在 React 中,Fiber 就是 React 16 实现的一套新的更新机制,让 React 的更新过程变得可控,避免了之前采用递归需要一气呵成影响性能的做法。

React Fiber 中的时间分片

把一个耗时长的任务分成很多小片,每一个小片的运行时间很短,虽然总时间依然很长,但是在每个小片执行完之后,都给其他任务一个执行的机会,这样唯一的线程就不会被独占,其他任务依然有运行的机会。

React Fiber 把更新过程碎片化,每执行完一段更新过程,就把控制权交还给 React 负责任务协调的模块,看看有没有其他紧急任务要做,如果没有就继续去更新,如果有紧急任务,那就去做紧急任务。

Stack Reconciler

基于栈的 Reconciler,浏览器引擎会从执行栈的顶端开始执行,执行完毕就弹出当前执行上下文,开始执行下一个函数,直到执行栈被清空才会停止。然后将执行权交还给浏览器。由于 React 将页面视图视作一个个函数执行的结果。每一个页面往往由多个视图组成,这就意味着多个函数的调用。

如果一个页面足够复杂,形成的函数调用栈就会很深。每一次更新,执行栈需要一次性执行完成,中途不能干其他的事儿,只能”一心一意”。结合前面提到的浏览器刷新率,JS 一直执行,浏览器得不到控制权,就不能及时开始下一帧的绘制。如果这个时间超过 16ms,当页面有动画效果需求时,动画因为浏览器不能及时绘制下一帧,这时动画就会出现卡顿。不仅如此,因为事件响应代码是在每一帧开始的时候执行,如果不能及时绘制下一帧,事件响应也会延迟。

Fiber Reconciler

链表结构

在 React Fiber 中用链表遍历的方式替代了 React 16 之前的栈递归方案。在 React 16 中使用了大量的链表。

  • 使用多向链表的形式替代了原来的树结构;
  1.   <div id=“A”>
  2.   A1
  3.   <div id=“B1”>
  4.     B1
  5.     <div id=“C1”></div>
  6.   </div>
  7.   <div id=“B2”>
  8.     B2
  9.   </div>
  10.   </div>

%title插图%num

多向链表

  • 副作用单链表;

%title插图%num

副作用单链表

  • 状态更新单链表;

%title插图%num

状态更新单链表

链表是一种简单高效的数据结构,它在当前节点中保存着指向下一个节点的指针;遍历的时候,通过操作指针找到下一个元素。

%title插图%num

链表

链表相比顺序结构数据格式的好处就是:

  1. 操作更高效,比如顺序调整、删除,只需要改变节点的指针指向就好了。
  2. 不仅可以根据当前节点找到下一个节点,在多向链表中,还可以找到他的父节点或者兄弟节点。

但链表也不是完美的,缺点就是:

  1. 比顺序结构数据更占用空间,因为每个节点对象还保存有指向下一个对象的指针。
  2. 不能自由读取,必须找到他的上一个节点。

React 用空间换时间,更高效的操作可以方便根据优先级进行操作。同时可以根据当前节点找到其他节点,在下面提到的挂起和恢复过程中起到了关键作用。

斐波那契数列的 Fiber

递归形式的斐波那契数列写法:

  1. function fib(n{
  2.   if (n <= 2) {
  3.     return 1;
  4.   } else {
  5.     return fib(n – 1) + fib(n – 2);
  6.   }
  7. }

采用 Fiber 的思路将其改写为循环(这个例子并不能和 React Fiber 的对等):

  1. function fib(n{
  2.   let fiber = { arg: n, returnAddr: null, a: 0 }, consoled = false;
  3.   // 标记循环
  4.   rec: while (true) {
  5.     // 当展开完全后,开始计算
  6.     if (fiber.arg <= 2) {
  7.       let sum = 1;
  8.       // 寻找父级
  9.       while (fiber.returnAddr) {
  10.         if(!consoled) {
  11.           // 在这里打印查看形成的链表形式的 fiber 对象
  12.           consoled=true
  13.           console.log(fiber)
  14.         }
  15.         fiber = fiber.returnAddr;
  16.         if (fiber.a === 0) {
  17.           fiber.a = sum;
  18.           fiber = { arg: fiber.arg – 2, returnAddr: fiber, a: 0 };
  19.           continue rec;
  20.         }
  21.         sum += fiber.a;
  22.       }
  23.       return sum;
  24.     } else {
  25.       // 先展开
  26.       fiber = { arg: fiber.arg – 1, returnAddr: fiber, a: 0 };
  27.     }
  28.   }
  29. }

%title插图%num

React Fiber 是如何实现更新过程可控?

更新过程的可控主要体现在下面几个方面:

  • 任务拆分
  • 任务挂起、恢复、终止
  • 任务具备优先级

任务拆分

在 React Fiber 机制中,它采用”化整为零”的思想,将调和阶段(Reconciler)递归遍历 VDOM 这个大任务分成若干小任务,每个任务只负责一个节点的处理。

任务挂起、恢复、终止

workInProgress tree

workInProgress 代表当前正在执行更新的 Fiber 树。在 render 或者 setState 后,会构建一颗 Fiber 树,也就是 workInProgress tree,这棵树在构建每一个节点的时候会收集当前节点的副作用,整棵树构建完成后,会形成一条完整的副作用链。

currentFiber tree

currentFiber 表示上次渲染构建的 Filber 树。在每一次更新完成后 workInProgress 会赋值给 currentFiber。在新一轮更新时 workInProgress tree 再重新构建,新 workInProgress 的节点通过 alternate 属性和 currentFiber 的节点建立联系。

在新 workInProgress tree 的创建过程中,会同 currentFiber 的对应节点进行 Diff 比较,收集副作用。同时也会复用和 currentFiber 对应的节点对象,减少新创建对象带来的开销。也就是说无论是创建还是更新、挂起、恢复以及终止操作都是发生在 workInProgress tree 创建过程中的。workInProgress tree 构建过程其实就是循环的执行任务和创建下一个任务。

挂起

当*个小任务完成后,先判断这一帧是否还有空闲时间,没有就挂起下一个任务的执行,记住当前挂起的节点,让出控制权给浏览器执行更高优先级的任务。

恢复

在浏览器渲染完一帧后,判断当前帧是否有剩余时间,如果有就恢复执行之前挂起的任务。如果没有任务需要处理,代表调和阶段完成,可以开始进入渲染阶段。

  • 如何判断一帧是否有空闲时间的呢?

使用前面提到的 RIC (RequestIdleCallback) 浏览器原生 API,React 源码中为了兼容低版本的浏览器,对该方法进行了 Polyfill。

  • 恢复执行的时候又是如何知道下一个任务是什么呢?

答案是在前面提到的链表。在 React Fiber 中每个任务其实就是在处理一个 FiberNode 对象,然后又生成下一个任务需要处理的 FiberNode。

终止

其实并不是每次更新都会走到提交阶段。当在调和过程中触发了新的更新,在执行下一个任务的时候,判断是否有优先级更高的执行任务,如果有就终止原来将要执行的任务,开始新的 workInProgressFiber 树构建过程,开始新的更新流程。这样可以避免重复更新操作。这也是在 React 16 以后生命周期函数 componentWillMount 有可能会执行多次的原因。

%title插图%num

workInProgress tree 构建

任务具备优先级

React Fiber 除了通过挂起,恢复和终止来控制更新外,还给每个任务分配了优先级。具体点就是在创建或者更新 FiberNode 的时候,通过算法给每个任务分配一个到期时间(expirationTime)。在每个任务执行的时候除了判断剩余时间,如果当前处理节点已经过期,那么无论现在是否有空闲时间都必须执行该任务。过期时间的大小还代表着任务的优先级。

任务在执行过程中顺便收集了每个 FiberNode 的副作用,将有副作用的节点通过 firstEffect、lastEffect、nextEffect 形成一条副作用单链表 A1(TEXT)-B1(TEXT)-C1(TEXT)-C1-C2(TEXT)-C2-B1-B2(TEXT)-B2-A。

其实*终都是为了收集到这条副作用链表,有了它,在接下来的渲染阶段就通过遍历副作用链完成 DOM 更新。这里需要注意,更新真实 DOM 的这个动作是一气呵成的,不能中断,不然会造成视觉上的不连贯(commit)。

  1. <div id=“A1”>
  2.   A1
  3.   <div id=“B1”>
  4.     B1
  5.     <div id=“C1”>C1</div>
  6.     <div id=“C2”>C2</div>
  7.   </div>
  8.   <div id=“B2”>
  9.     B2
  10.   </div>
  11. </div>

%title插图%num

副作用链

直观展示

正是基于以上这些过程,使用Fiber,我们就有了在社区经常看到的两张对比图[10]。

清晰展示及交互、源码可通过下面两个链接进入,查看网页源代码。

  • Stack Example[11]
  • Fiber Example[12]

为了方便大家对比,我就直接放上两张对比图吧,大家自行比对,差别还是很明显的

%title插图%num

%title插图%num

Fiber 结构长什么样?

基于时间分片的增量更新需要更多的上下文信息,之前的vDOM tree显然难以满足,所以扩展出了fiber tree(即Fiber上下文的vDOM tree),更新过程就是根据输入数据以及现有的fiber tree构造出新的fiber tree(workInProgress tree)。

FiberNode 上的属性有很多,根据笔者的理解,以下这么几个属性是值得关注的:return、child、sibling(主要负责fiber链表的链接);stateNode;effectTag;expirationTime;alternate;nextEffect。各属性介绍参看下面的class FiberNode:

  1. class FiberNode {
  2.   constructor(tag, pendingProps, key, mode) {
  3.     // 实例属性
  4.     this.tag = tag; // 标记不同组件类型,如函数组件、类组件、文本、原生组件…
  5.     this.key = key; // react 元素上的 key 就是 jsx 上写的那个 key ,也就是*终 ReactElement 上的
  6.     this.elementType = null// createElement的*个参数,ReactElement 上的 type
  7.     this.type = null// 表示fiber的真实类型 ,elementType 基本一样,在使用了懒加载之类的功能时可能会不一样
  8.     this.stateNode = null// 实例对象,比如 class 组件 new 完后就挂载在这个属性上面,如果是RootFiber,那么它上面挂的是 FiberRoot,如果是原生节点就是 dom 对象
  9.     // fiber
  10.     this.return = null// 父节点,指向上一个 fiber
  11.     this.child = null// 子节点,指向自身下面的*个 fiber
  12.     this.sibling = null// 兄弟组件, 指向一个兄弟节点
  13.     this.index = 0//  一般如果没有兄弟节点的话是0 当某个父节点下的子节点是数组类型的时候会给每个子节点一个 index,index 和 key 要一起做 diff
  14.     this.ref = null// reactElement 上的 ref 属性
  15.     this.pendingProps = pendingProps; // 新的 props
  16.     this.memoizedProps = null// 旧的 props
  17.     this.updateQueue = null// fiber 上的更新队列执行一次 setState 就会往这个属性上挂一个新的更新, 每条更新*终会形成一个链表结构,*后做批量更新
  18.     this.memoizedState = null// 对应  memoizedProps,上次渲染的 state,相当于当前的 state,理解成 prev 和 next 的关系
  19.     this.mode = mode; // 表示当前组件下的子组件的渲染方式
  20.     // effects
  21.     this.effectTag = NoEffect; // 表示当前 fiber 要进行何种更新(更新、删除等)
  22.     this.nextEffect = null// 指向下个需要更新的fiber
  23.     this.firstEffect = null// 指向所有子节点里,需要更新的 fiber 里的*个
  24.     this.lastEffect = null// 指向所有子节点中需要更新的 fiber 的*后一个
  25.     this.expirationTime = NoWork; // 过期时间,代表任务在未来的哪个时间点应该被完成
  26.     this.childExpirationTime = NoWork; // child 过期时间
  27.     this.alternate = null// current 树和 workInprogress 树之间的相互引用
  28.   }
  29. }

%title插图%num

fiber-tree

图片来源:完全理解React Fiber[13]

  1. function performUnitWork(currentFiber){
  2.     //beginWork(currentFiber) //找到儿子,并通过链表的方式挂到currentFiber上,每一偶儿子就找后面那个兄弟
  3.   //有儿子就返回儿子
  4.   if(currentFiber.child){
  5.     return currentFiber.child;
  6.   }
  7.   //如果没有儿子,则找弟弟
  8.   while(currentFiber){//一直往上找
  9.     //completeUnitWork(currentFiber);//将自己的副作用挂到父节点去
  10.     if(currentFiber.sibling){
  11.       return currentFiber.sibling
  12.     }
  13.     currentFiber = currentFiber.return;
  14.   }
  15. }

Concurrent Mode (并发模式)

Concurrent Mode 指的就是 React 利用上面 Fiber 带来的新特性的开启的新模式 (mode)。react17开始支持concurrent mode,这种模式的根本目的是为了让应用保持cpu和io的快速响应,它是一组新功能,包括Fiber、Scheduler、Lane,可以根据用户硬件性能和网络状况调整应用的响应速度,核心就是为了实现异步可中断的更新。concurrent mode也是未来react主要迭代的方向。

目前 React 实验版本允许用户选择三种 mode:

  1. Legacy Mode: 就相当于目前稳定版的模式
  2. Blocking Mode: 应该是以后会代替 Legacy Mode 而长期存在的模式
  3. Concurrent Mode: 以后会变成 default 的模式

Concurrent Mode 其实开启了一堆新特性,其中有两个*重要的特性可以用来解决我们开头提到的两个问题:

  1. Suspense[14]:Suspense 是 React 提供的一种异步处理的机制, 它不是一个具体的数据请求库。它是React 提供的原生的组件异步调用原语。
  2. useTrasition[15]:让页面实现 Pending -> Skeleton -> Complete 的更新路径, 用户在切换页面时可以停留在当前页面,让页面保持响应。相比展示一个无用的空白页面或者加载状态,这种用户体验更加友好。

其中 Suspense 可以用来解决请求阻塞的问题,UI 卡顿的问题其实开启 concurrent mode 就已经解决的,但如何利用 concurrent mode 来实现更友好的交互还是需要对代码做一番改动的。

资料参考:Concurrent 模式介绍 (实验性)[16] | 理解 React Fiber & Concurrent Mode[17] | 11.concurrent mode(并发模式是什么样的)[18] | 人人都能读懂的react源码解析[19]

未来可期

Concurrent Mode只是并发,既然任务可拆分(只要*终得到完整effect list就行),那就允许并行执行,(多个Fiber reconciler + 多个worker),首屏也更容易分块加载/渲染(vDOM森林。

并行渲染的话,据说Firefox测试结果显示,130ms的页面,只需要30ms就能搞定,所以在这方面是值得期待的,而React已经做好准备了,这也就是在React Fiber上下文经常听到的待unlock的更多特性之一。

isInputPending —— Fiber架构思想对前端生态的影响

Facebook 在 Chromium 中提出并实现了 isInputPending() API,它可以提高网页的响应能力,但是不会对性能造成太大影响。Facebook 提出的 isInputPending API 是*个将中断的概念用于浏览器用户交互的的功能,并且允许 JavaScript 能够检查事件队列而不会将控制权交于浏览器。

目前 isInputPending API 仅在 Chromium 的 87 版本开始提供,其他浏览器并未实现。

%title插图%num

isInputPending

资料参考:Facebook 将对 React 的优化实现到了浏览器![20]

Svelte 对固有模式的冲击

当下前端领域,三大框架React、Vue、Angular版本逐渐稳定,如果说前端行业会出现哪些框架有可能会挑战React或者Vue呢?很多人认为Svelte 应该是其中的选项之一。

Svelte叫法是[Svelte], 本意是苗条纤瘦的,是一个新兴热门的前端框架。在开发者满意度、兴趣度、市场占有率上均名列前茅,同时,它有更小的打包体积,更少的开发代码书写,在性能测评中,与React、Vue相比,也不遑多让。

Svelte 的核心思想在于『通过静态编译减少框架运行时的代码量』

Svelte 优势有哪些

  • No Runtime —— 无运行时代码
  • Less-Code —— 写更少的代码
  • Hight-Performance —— 高性能

Svelte 劣势

  • 社区
  • 社区
  • 社区

原理概览

Svelte 在编译时,就已经分析好了数据 和 DOM 节点之间的对应关系,在数据发生变化时,可以非常高效的来更新DOM节点。

  • Rich Harris 在进行Svelte的设计的时候没有采用 Virtual DOM,主要是因为他觉得Virtual DOM Diff 的过程是非常低效的。具体可参考Virtual Dom 真的高效吗[21]一文;Svelte 采用了Templates语法,在编译的过程中就进行优化操作;
  • Svelte 记录脏数据的方式:位掩码(bitMask);
  • 数据和DOM节点之间的对应关系:React 和 Vue 是通过 Virtual Dom 进行 diff 来算出来更新哪些 DOM 节点效率*高。Svelte 是在编译时候,就记录了数据 和 DOM 节点之间的对应关系,并且保存在 p 函数中。

全员编程时代,人类高质量程序员应具备哪三大特质?

在美国公布的《新兴科技趋势报告》里,2045年,*保守预测也认为将会有超过1千亿的设备连接在互联网上,这些设备包括了移动设备、可穿戴设备、家用电器、医疗设备、工业探测器、监控摄像头、汽车,以及服装等。

不久的将来,我们的工作和生活将会迎来一场新的技术革命。

在管理、维修以及监视等需要人力的工作在联网设备中进行自动化的同时,人们可以节省出更多的时间接收来自物联网的信息,海量的信息通过算法、大数据等技术精准投放到人们的移动设备上,信息的获取变得愈发简单,如何利用信息更好地为自身服务成为人们日常生活的基础议题。而如何完善自己产品的服务,使得自己的产品跟上时代的需求,是每个企业议事日程上的重中之重。

%title插图%num

与此同时,企业要想站在时代风口把握机遇,就必须抓好技术这项重点议题。在这个时代,抢占技术高地的企业势必会势如破竹,而技术创新落后的企业注定会被时代淘汰。

例如拥有165年悠久历史的诺基亚(NOKIA)企业,提到“诺基亚”很多人不可避免地会联想到它的手机,作为早期经营木材和橡胶产品为主的企业,诺基亚在合适的时间进入了手机市场,它的股票市值一度超过2000亿欧元,一度成为欧洲*大的上市公司。然而,由于企业战略失败加上技术创新跟不上时代步伐,诺基亚逐渐被市场淘汰。2013年,它以*低的价格出售了旗下*核心的手机业务。

在技术迅猛发展的今天,如果你的产品无法带给消费者全新的体验,那么终将其他的产品所取代。

同理,人也是如此。在这个时代,如果你没有过硬的本领,不仅会面临着与他人的激烈竞争,而且在不久的将来,还有可能被人工智能抢走工作。

物联网、数据分析、以及人工智能这三大技术之间的合作将会在世界上创造出一个巨大的智能机器网络,在不需人力介入的情况下实现巨量的商业交易。

%title插图%num

未来是人工智能时代,万物互联、人工智能将彻底改变人们生活的方方面面,我们的出行、学习、工作都将发生翻天覆地的变化。

而就现在的社会环境来说,小到计算器计算,大到火箭升空、城市运转,这些通通需要编程技术来实现,随着智能机器人、无人驾驶、智慧城市、智能家居等在人们的生活中越来越普及,编程能力在人才供需市场上就显得尤为重要。

美国苹果公司联合创始人史蒂夫·乔布斯提到:“每个人都应该学习如何为计算机编写程序……因为它能教你如何思考”。随着科技的发展,编程已经不再是少数人才能进行的精英活动,在未来,编程也许会成为每个人生活中不可缺少的部分。

再谈“去虚拟化”对深度学习系统的必要性

“虚拟化”溯源

 

“虚拟化”可能是计算机科学历史上*伟大的思想之一,对此,计算机先驱David Wheeler有一句名言:

All problems in computer science can be solved by another level of indirection, except for the problem of too many layers of indirection.

这句话被尊称为“软件工程的基本定理”,并被C++之父Bjarne Stroustrup在专著《The C++ Programming Language》的序言处引用。不过,大部分人记住了前半句,却忽略了后半句,而后半句正是本文想展开讨论的。

“another level of indirection”刻画了“虚拟化”的精髓:通过引入一层新的抽象,把与上层应用无关的细节隐藏掉,有选择的给上层用户暴露一些功能供其使用,也称底层细节对用户透明,既不损害上层应用的功能,又能享受“关注点分离”(separation of concerns)的好处,增加易用性

可以说:虚拟化的案例无处不在,无往而不利。

操作系统是对硬件资源的虚拟化:计算核心被虚拟化成进程;硬件内存变成虚拟内存;存储介质被虚拟化成文件系统;网络传输通过多层协议栈被虚拟化成文件描述符,使得数据传输就像读写普通的文件一样。

分布式存储把通过网络互联多个单机文件系统虚拟化成的一个网络文件系统,使得用户不用再关心网络数据传输的细节,可以像访问本地文件一样访问其它节点上的数据。

当然,虚拟化造就了今天伟大的云计算技术和市场,云原生如火如荼,开发者只需要基于云服务的API编程,而不需要关于API 之下的物理细节,成本低,可靠性又高。

虚拟化的成功不可否认。但是,成也萧何,败也萧何,虚拟化也有其代价。

%title插图%num

“虚拟化”和*致性能的矛盾

 

一层层的抽象层次,一步步降低编程的复杂度,每一层都向上隐藏了一些东西,上层就相应地丢失一些操控能力。每经过一层抽象,就引入一些对上层而言的“不确定性”,*终的结果是,性能的天花板一步一步下降。

以Hadoop的分布式存储为例,虚拟化的结果是掩盖了数据真实的存储位置,不管在哪个节点上,访问接口都是一样的。不过,当向这个集群调度一些MapReduce的数据处理任务时,如果Task被调度到数据所在的节点上,那么就不需要网络传输,如果调度到另外的节点,就需要把数据读到内存并通过网络发送到计算所在的节点。也就是,被隐藏掉的数据位置信息有可能被调度器用来提升系统效率。

随着摩尔定律放缓,不止在深度学习领域,在任何追求*致性能的场景就出现了一种击穿、击碎中间抽象层次,一竿子插到底进行协同优化的强烈需求。 

让我们看六个例子。

1、操作系统把CPU资源抽象成被操作系统调度和管理的内核线程,避免了用户调度计算资源的麻烦,但是,在高并发场景,用户级线程(如coroutine)越来越多,计算资源更多的在用户态(user mode)来调度,而不是完全依靠内核的调度。

2、操作系统通过页表机制实现虚拟内存,神不知鬼不觉实现数据的换入换出,但在高性能场景,用户程序会通过编程接口禁止这一行为,譬如在RDMA和GPU异步数据传输都依赖于锁页内存,确保操作系统不会帮倒忙。

3、传统的网络传输协议栈TCP/IP都在操作系统内核,为了避免内核态和用户态的数据拷贝和上下文切换,人们先是使用用户态协议栈(如Intel dpdk),仍无法满足需求的话,就使用支持内核旁路(bypass)的RDMA技术,完全跳过了操作系统这一层。

4、缓存(Cache)技术也可以视为一种虚拟化,命中就直接使用,没命中就花时间从更慢的存储取过来,在局部性较好的负载下工作得很好。但在特定领域,譬如深度学习领域,工作负载有特别的规律,如果用软件来控制缓存数据的弹出以及读取甚至可以做到100%的命中率,这种办法被称为Scratchpad,现在几乎所有的AI芯片都没有用Cache,而是使用Scratchpad技术。

5、操作系统的文件系统或者网络文件系统,把底层硬件的细节隐藏了,编程时不需要考虑数据在哪台机器的哪块磁盘上,编程更简单了,但访问近处的数据速度快,访问远处的数据速度慢,为了提高效率,有人就提出了locality aware(局部性感知)的调度,把计算任务尽可能调度到存储数据的节点上去。

6、工业级的大型软件系统里,通常不会使用库函数,恨不得把底层代码库重新打造一遍,譬如Chromium, OceanBase等大型C++项目,我想,这也可以算作反虚拟化的例子。

%title插图%num

“虚拟化”的舒适区

 

虚拟化试图向上层提供一剂一劳永逸的灵丹妙药,既解决易用性,又不损害上层应用对性能的需求。但是,“虚拟化”到底好不好应该具体问题具体分析,那该如何判断虚拟化到底合不合适呢?

这里尝试抛出一个用来判断是否有必要引入一层抽象(虚拟化)的量化指标,供参考。每引入一层虚拟化,就向上层隐藏了一些东西,向上提供的服务的延迟就因此引入了不确定性(以分布式存储为例,有的数据近,有的数据远;以分布式GPU资源池为例,有的近,有的远),服务的响应延迟有一个波动范围,也就是[min, max],延迟越小越好。

%title插图%num如果上层某一个应用需要保证的*低延迟仍大于max,那么这层虚拟化对这个应用就没有损害,可以大胆的引入这个虚拟化技术(也就是上图的C区域)。

如果上层某一个应用需要保证的*低延迟介于min和max之间,那么引入这层虚拟化就对这个应用是有损害的,把这层虚拟化敲掉,就可以确保以min值满足这个应用对延迟的需求(也就是上图的B区域)。

如果上层某一个应用需要保证的*低延迟小于min,那么即使敲掉当前层的虚拟化,仍不能满足这个应用,就需要继续向下敲,进行联合优化,直到延迟得到满足(也就是上图的A区域)。

%title插图%num

鱼与熊掌兼得?

 

如上所述,虚拟化的基本思路是通过“隐藏细节”给上层应用提供一种假象,降低上层应用使用底层资源的复杂度。不过,有时候,“隐藏”掉的信息会阻碍上层应用挖掘*致的性能。有没有两全其美的办法呢?

David Patterson在《计算机体系结构的黄金时代》一文中开出的药方是从算法到硬件直接打通,结合算法和硬件的特点全部搞定制。定制表现在DSL和DSA,一方面是设计领域特定语言,方便编程,另一方面为面向领域应用设计领域特定架构,挖掘*致效率。

David Patterson的药方既不是在已有方案中插入新的抽象层次,也不是完全不要中间抽象,而是把原有的抽象层次全部敲碎重建,这种重建是基于对算法和硬件特点的充分挖掘。我理解这里有俩关键:软硬件协同设计(分工),以及编译器技术。

一方面,这里的思路和虚拟化不同之处是:不是向上层应用隐藏什么,而是强调要向上层暴露什么,或者说向上层让渡什么职责。

另一方面,这里没有期待只要提供一层API的抽象就包打天下,而是对从算法到硬件的映射复杂性有充足的认识。这种映射既包含任务相关但硬件无关的问题,也包含硬件相关的问题,本质等同于人们熟知的编译器技术。

说到这里,想到另一个“去虚拟化”的*佳例子,就是软件定义网络(Software defined network,SDN)。对于同一套硬件基础设施,承接不同的工作负载,交换机的*优转发规则是不同的。SDN的思路是,令交换机的转发规则是可编程的,对每一个不同的业务负载,都用静态分析得到*优的转发规则或策略(control plane),并按照这个规则对交换机编程,交换机在转发数据时只需要按照已编程的规则执行即可(data plane)。

在SDN的例子里,交换机让渡了一部分职责给软件,软件根据不同的业务负载都生成不一样的路由规则(策略),这是SDN平衡灵活性和*致性能的关键。

在OneFlow解决分布式深度学习的难题时,也是类似的思路,它不是靠额外引入的一个抽象层次实现,而是分成了控制平面和数据平面。在控制平面,编译器根据特定深度学习模型的任务负载和底层硬件拓扑生成对这个配置*优的执行计划(execution plan), 这个plan不是一劳永逸的,它需要上层模型的知识,也需要底层硬件的知识,模型或硬件拓扑一旦变化,plan就会变化。但是,编译器生成plan的机制可以认为是不变的,也是整套系统的精髓。

 

%title插图%num

结语

 

这篇文章补充解释了我对“通过向上层算法隐藏硬件信息”的虚拟化思路,以及“通过向上层算法暴露硬件信息和让渡职责”的去虚拟化的思路的理解。

简单来说,虚拟化的思想强调的是隐藏(底层细节)和限制(上层的功能范围),潜台词是:我认为对上层应用的需求足够了解,告诉上层应用太多细节也没有用,我把底层的东西一揽子处理好了,你只管调用我为你提供的API就可以了。

软件定义的思想强调的是暴露(底层细节)和让渡(硬件的策略可被软件分析和配置),潜台词是:我对上层应用的需求了解不足,没有办法为上层应用提供一个足够令人满意的一揽子的策略,于是干脆把底层策略暴露给上层,上层应用对自己的需求和任务负载*清楚,上层自己来负责生成策略并来配置底层。

致力于提升深度学习系统性能的朋友对“去虚拟化”的思路应该是习以为常的,这应该是整个社区经过探索和碰壁逐渐收敛出来的主流思路,不是我个人的发明,我只是把观察和理解写下来而已。

顺着这个思路,算法-软件-硬件协同优化领域正在发生一些令人兴奋的进展。毫无疑问,广义上的编译器技术(无论是单设备代码生成,还是分布式执行计划生成)是里面*核心的部分。

ios 汉字转拼音

from:http://blog.csdn.net/meegomeego/article/details/23253939

之前做通讯录相关的一些App时,有一个比较常用的算法是将汉字转换成拼音。当时采用的做法是:将各个拼音段的首个汉字(按Unicode排序)做成两个数组,一个数组存拼音,另一个数组存拼音对应首个汉字的Unicode。如果要获取某个汉字的拼音,可以折半查找法找的对应拼音。*近无意间发现CFStringTransform这篇文章后,发现系统本身已经提供了一个这样的函数。

CFStringTransform

iOS在CoreFoundation中提供了CFStringTransform函数,但在Foundation中却没有相对应的方法。它的定义如下:

Boolean CFStringTransform(CFMutableStringRef string, CFRange *range, CFStringRef transform, Boolean reverse);

其中string参数是要转换的string,比如要转换的中文,同时它是mutable的,因此也直接作为*终转换后的字符串。range是要转换的范围,同时输出转换后改变的范围,如果为NULL,视为全部转换。transform可以指定要进行什么样的转换,这里可以指定多种语言的拼写转换。reverse指定该转换是否必须是可逆向转换的。如果转换成功就返回true,否则返回false

如果要进行汉字到拼音的转换,我们只需要将transform设定为kCFStringTransformMandarinLatin或者kCFStringTransformToLatinkCFStringTransformToLatin也可适用于非汉字字符串):

  1. CFMutableStringRef string = CFStringCreateMutableCopy(NULL, 0, CFSTR(“中国”));
  2. CFStringTransform(string, NULL, kCFStringTransformMandarinLatin, NO);
  3. NSLog(@”%@”, string);

这段代码将输出:

2013-11-22 14:41:14.644 Test[2436:907] zhōng guó

可以看出,CFStringTransform正确的输出了“中国”的拼音,而且还带上了音标。有时候我们不需要音标怎么办?还好CFStringTransform同时提供了将音标字母转换为普通字母的方法kCFStringTransformStripDiacritics。我们在上面的代码基础上再加上这个:

  1. CFStringTransform(string, NULL, kCFStringTransformStripDiacritics, NO);
  2. NSLog(@”%@”, string);

那么*终将输出:

2013-11-22 14:47:00.380 Test[2470:907] zhong guo

refence doc:http://nshipster.cn/cfstringtransform/

IOS开发中判断文件是否存在,不存在则拷贝

  1.     首先,先总结一下如何获取Documents目录,在ios开发中,我们经常需要检索Documents目录的完整路径以便读取和写入文件,我总结了以下两种方法:
  2. 1NSString *documentsDirectory = [NSHomeDirectory() stringByAppendingPathComponent:@”Documents”];
  3. 2NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
  4.    NSString *documentsDirectory = [paths objectAtIndex:0];
  5.  以上documentsDirectory就是获取的Documents的完整路径。
  6.  程序的所有资源文件,存储在程序包中,获取程序包路径的方法是:
  7. NSString *appDirectory = [[NSBundle mainBundle] bundlePath];
  8. 在实际开发中我们有时需要判断Documents下的某个资源文件是否存在,如果不存在,则从程序包中拷贝进去,现在假设要判定的文件是“xxx.txt”,判定代码如下:
  9. NSFileManager *fileManager = [NSFileManager defaultManager];
  10. NSString *filePath = [self [documentsDirectory stringByAppendingPathComponent:@”xxx.txt”]];
  11. if(![fileManager fileExistsAtPath:filePath]) //如果不存在
  12. {
  13.      NSLog(@”xxx.txt is not exist”);
  14.      NSString *dataPath = [[[NSBundle mainBundle] bundlePath] stringByAppendingString:@”/xxx.txt”];//获取程序包中相应文件的路径
  15.      NSError *error;
  16.      if([fileManager copyItemAtPath:dataPath toPath:filePath error:&error]) //拷贝
  17.      {
  18.           NSLog(@”copy xxx.txt success”);
  19.      }
  20.      else
  21.      {
  22.           NSLog(@”%@”,error);
  23.      }
  24. }

ios判断程序文件夹下是否存在指定文件

  1. NSString *fileName=@”04.jpg”;
  2. NSArray *DocumentPath= NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
  3. NSString *filepath=[[NSString alloc] initWithString:[NSString stringWithFormat:@”%@/%@”,DocumentPath[0],fileName]];
  4. NSFileManager *fileManager = [NSFileManager defaultManager];
  5. if ([fileManager fileExistsAtPath:filepath]) {
  6. NSLog(@”file is exists”);
  7. } else{
  8. NSLog(@”file is not exists”);
  9. };
友情链接: SITEMAP | 旋风加速器官网 | 旋风软件中心 | textarea | 黑洞加速器 | jiaohess | 老王加速器 | 烧饼哥加速器 | 小蓝鸟 | tiktok加速器 | 旋风加速度器 | 旋风加速 | quickq加速器 | 飞驰加速器 | 飞鸟加速器 | 狗急加速器 | hammer加速器 | trafficace | 原子加速器 | 葫芦加速器 | 麦旋风 | 油管加速器 | anycastly | INS加速器 | INS加速器免费版 | 免费vqn加速外网 | 旋风加速器 | 快橙加速器 | 啊哈加速器 | 迷雾通 | 优途加速器 | 海外播 | 坚果加速器 | 海外vqn加速 | 蘑菇加速器 | 毛豆加速器 | 接码平台 | 接码S | 西柚加速器 | 快柠檬加速器 | 黑洞加速 | falemon | 快橙加速器 | anycast加速器 | ibaidu | moneytreeblog | 坚果加速器 | 派币加速器 | 飞鸟加速器 | 毛豆APP | PIKPAK | 安卓vqn免费 | 一元机场加速器 | 一元机场 | 老王加速器 | 黑洞加速器 | 白石山 | 小牛加速器 | 黑洞加速 | 迷雾通官网 | 迷雾通 | 迷雾通加速器 | 十大免费加速神器 | 猎豹加速器 | 蚂蚁加速器 | 坚果加速器 | 黑洞加速 | 银河加速器 | 猎豹加速器 | 海鸥加速器 | 芒果加速器 | 小牛加速器 | 极光加速器 | 黑洞加速 | movabletype中文网 | 猎豹加速器官网 | 烧饼哥加速器官网 | 旋风加速器度器 | 哔咔漫画 | PicACG | 雷霆加速