首頁 後端開發 php教程 nginx的資料結構1——ngx_int_t與ngx_rbtree_t

nginx的資料結構1——ngx_int_t與ngx_rbtree_t

Aug 08, 2016 am 09:18 AM
nbsp node parent temp

    面對./src/core子目錄中71個源文件,有點無從下手。瀏覽包含主函數的nginx.c文件,發現nginx使用了很多自行封裝的資料結構,不弄清楚這是些什麼的資料結構就很難理解主函數中操作的意義。於是我們挑看起來基礎的資料結構開始研究。組織nginx所有資料結構的是ngx_core.h檔案。它首先包含了ngx_config.h,我們在ngx_config.h中發現了三種類型定義。

1、ngx_int_tngx_uint_tngx_flag_t

. ,在nginx_config.h中找到了它的定義。

typedef intptr_t        ngx_int_t;
typedef uintptr_t       ngx_uint_t;
typedef intptr_t        ngx_flag_t;
登入後複製
    順藤摸瓜找到了三種資料類型的定義。本科c

入門教學中並沒有對

intptr_t/uintptr_t的介紹,我在cstdint.
/* Types for `void *' pointers.  */
#if __WORDSIZE == 64
# ifndef __intptr_t_defined
typedef long int               intptr_t;
#  define __intptr_t_defined
# endif
typedef unsigned long int    uintptr_t;
#else
# ifndef __intptr_t_defined
typedef int                    intptr_t;
#  define __intptr_t_defined
# endif
typedef unsigned int        uintptr_t;
#endif
登入後複製
    首先註解說這兩種類型是
「void *」的指針型,雖然字面上看,

intptr_t整數指標型,但是讓人摸不著頭腦,為什麼要用整數型作為整數型的指標型呢?先放一放,看後面的宏,機器是

64位字長則intptr_tlong int ,正好我機器上是64位元編譯器,sizeof()了一下,是8個字節648個字節 intptr_tintuintptr_tunsigned int,查表得知unsigned 4個位元組,16位元編譯器下為2個位元組。那麼intptr_t/uintptr_t應該是會隨著平台字長變化而發生對應變化的整數型別。經過了解,發現《深入分析Linux內核源碼》中對此的解釋是,系統內核在操作內存時,將內存當做一個大數組,而指針就是數組索引/下標,內核程式設計師使用這種特殊的整數來接受記憶體位址值、操作記憶體相比使用指標更加直觀,不容易犯錯。看起來,nginx中,只是單純的想要使用一些平台相關的intunsigned int類型變數而已。 2、ngx_rbtree_t2.1、什麼是紅黑樹 ACM比賽裡划水的退休隊員,對紅黑樹這樣的有名資料結構還是比較敏感的。紅黑樹是一種特殊約束形式下的平衡二元查找樹實現。學過資料結構課的同學應該知道,課本上的最早的自平衡二叉樹AVL樹嚴格的要求子樹的高度差不超過2,以獲得根結點到所有葉結點距離基本上相同(平衡)的特性。     紅黑樹不追求嚴格的平衡,而是通過5個約束實現基本平衡:

    ①結點是紅色或黑色;    ②根是黑色;

    ③葉結點是黑色;

    ④紅色結點的子結點都是黑色;    ⑤任一結點至其葉結點的簡單路徑中黑色結點數相同。

    AVL樹根到葉結點最長距離與最短距離的比不超過2。紅黑樹的約束也保證了這個特性(最長路徑是紅黑相間,最短路徑是全黑,這種情況下最長路徑剛好是最短路徑的2倍長)。

    既然是平衡二元查找樹的一種實現,那麼紅黑樹自然是內部有序的,同時跟AVL樹一樣支持O(log2n)時間複雜度的查找、插入和刪除。

    相比AVL树,红黑可以保证在每次插入或删除操作之后的重平衡过程中,全树拓扑结构的更新仅涉及常数个结点。尽管最坏情况下需对O(log2n)个结点重染色,但就分摊意义(平均效率)而言,仅为O(1)个。但是因为没有严格约束树的平衡特性,红黑树的左右子树高度差比AVL要大。

2.2、ngx_rbtree.h

    机会难得,我们就把nginx的源码作为素材来深入了解一下红黑树的实现。首先是结点的结构:

typedef ngx_uint_t  ngx_rbtree_key_t;
typedef ngx_int_t   ngx_rbtree_key_int_t;


typedef struct ngx_rbtree_node_s  ngx_rbtree_node_t;

struct ngx_rbtree_node_s {
    ngx_rbtree_key_t       key;//平台相关的无符号整型关键字
    ngx_rbtree_node_t     *left;//左子结点指针
    ngx_rbtree_node_t     *right;//<span style="font-family:宋体;">右</span>子结点指针
    ngx_rbtree_node_t     *parent;//父结点指针
    u_char                 color;//结点颜色
    u_char                 data;//结点数据
};
登入後複製

    然后是红黑树的结构定义:

typedef struct ngx_rbtree_s  ngx_rbtree_t;	//“_s”是结构体“_t”是类型
//下面是一个函数指针变量类型的定义,是红黑树插入函数的指针
//参数有树根结点、插入结点和哨兵结点的指针
typedef void (*ngx_rbtree_insert_pt) (ngx_rbtree_node_t *root,
    ngx_rbtree_node_t *node, ngx_rbtree_node_t *sentinel);

struct ngx_rbtree_s {
    ngx_rbtree_node_t     *root;	//根节点指针
    ngx_rbtree_node_t     *sentinel;	//哨兵结点指针
    ngx_rbtree_insert_pt   insert;	//插入函数指针
};
登入後複製
    将函数指针变量作为结构体成员变量以达成可以把结构体当做类来使用(既有成员变量又有成员方法)的效果,这种手法在nginx的源码中相当普遍。关于函数,nginx还有一种更神奇的手段——宏:
#define ngx_rbtree_init(tree, s, i)                                         \
    ngx_rbtree_sentinel_init(s);                                               \
    (tree)->root = s;                                                                  \
    (tree)->sentinel = s;                                                            \
    (tree)->insert = i//这里insert函数指针的赋值实现了多态
登入後複製

    借助宏来达成内联函数的效果(函数实现如果比较简单,就干脆把实现过程整个搬到类中),令人费解的是,C不是没有内联关键字,甚至同一个头文件中就有一个内联函数的定义。研究内联函数之前,下面还有几个宏要看一看:

#define ngx_rbt_red(node)               ((node)->color = 1)
#define ngx_rbt_black(node)             ((node)->color = 0)
#define ngx_rbt_is_red(node)            ((node)->color)
#define ngx_rbt_is_black(node)          (!ngx_rbt_is_red(node))
#define ngx_rbt_copy_color(n1, n2)      (n1->color = n2->color) 

/* a sentinel must be black */
#define ngx_rbtree_sentinel_init(node)  ngx_rbt_black(node)
登入後複製

    nginx源码中的变量都很容易看懂以至于我们不怎么需要查资料或找注释。color1染红置0染黑,color1则结点为红色,不为红色的则为黑色,复制结点颜色即复制color值,哨兵结点一定要染成黑色。

static ngx_inline ngx_rbtree_node_t *
ngx_rbtree_min(ngx_rbtree_node_t *node, ngx_rbtree_node_t *sentinel)
{
    while (node->left != sentinel) {
        node = node->left;
    } 
    return node;
}
登入後複製

    ngx_inline是一个宏,实际值就是关键字inline。这个内联函数非常好懂,目的看起来是寻找以任意结点为根结点的子树中结点值最小的结点。实现方法是找到红黑树子树最边缘的左子结点。那么我们有理由猜测,哨兵结点是空结点或边缘标识。

2.3、红黑树的结点插入

    接下来我们来深入ngx_rbtree.c看看nginx如何实现几个关键的红黑树方法。

void
ngx_rbtree_insert(ngx_rbtree_t *tree, ngx_rbtree_node_t *node)
{
    //根结点指针的指针,或者根结点指针数组,会有多个根结点吗,令人费解
    //临时结点指针
    //哨兵结点指针,推测哨兵在每次查询时可能都不一样,也许指待插位置
    //变量不分行,我写注释都很不方便
    ngx_rbtree_node_t  **root, *temp, *sentinel;
 
    /* a binary tree insert */
 
    root = (ngx_rbtree_node_t **) &tree->root;//树根指针的指针赋给了root
    sentinel = tree->sentinel;//哨兵指针赋给了哨兵指针
 
    if (*root == sentinel) {//特判,如果根是哨兵,即树是空的
        node->parent = NULL;//新插入的结点变成了根
        node->left = sentinel;//新结点的左子结点是哨兵
        node->right = sentinel;//新结点的右子结点也是哨兵
        ngx_rbt_black(node);//新根染黑
        *root = node;//确认新结点为新根
 
        return;//插入结束
    }
 
    //树初始化时给了insert指针一个函数地址
    //查看前面的宏ngx_rbtree_init(tree, s, i)
    //发现只是把指定结点染黑,同时赋为根和哨兵,给insert指针指定一个函数
    //ngx_rbtree.c中有两个参数表符合的可选函数:插入值、插入计时器值
    //稍后来看两种插入分别如何实现又有什么区别
    tree->insert(*root, node, sentinel);
 
    /* re-balance tree */
    //如果新结点不是根且其父结点是红的,循环
    while (node != *root && ngx_rbt_is_red(node->parent)) {
        //如果父结点是左子结点,获得父结点的右兄弟
        if (node->parent == node->parent->parent->left) {
            temp = node->parent->parent->right;
            //如果父结点的右兄弟是红的
            if (ngx_rbt_is_red(temp)) {
                ngx_rbt_black(node->parent);//父结点染黑
                ngx_rbt_black(temp);//父结点的右兄弟染黑
                ngx_rbt_red(node->parent->parent);//父结点的父结点染红
                node = node->parent->parent;//父结点的父结点成为当前结点
 
            } else {//如果父结点的右兄弟是黑的
                if (node == node->parent->right) {//如果新结点是右子结点
                    node = node->parent;//父结点成为新node
                    ngx_rbtree_left_rotate(root, sentinel, node);//node左旋
 
                }
 
                ngx_rbt_black(node->parent);//node的父结点染黑
                //node的父结点的父结点染红
                ngx_rbt_red(node->parent->parent);
                ngx_rbtree_right_rotate(root, sentinel, node->parent->parent);//node的父结点的父结点右旋
            }
 
        } else {//如果父结点是右子结点,获得父结点的左兄弟
            temp = node->parent->parent->left;
            //如果父结点的左兄弟是红的
            if (ngx_rbt_is_red(temp)) {
                ngx_rbt_black(node->parent);//父结点染黑
                ngx_rbt_black(temp);//父结点的左兄弟染黑
                ngx_rbt_red(node->parent->parent);//父结点的父结点染红
                node = node->parent->parent;
 
            } else {//如果父结点的左兄弟是黑的
                if (node == node->parent->left) {//如果新结点是左子结点
                    node = node->parent;//父结点成为当前结点
                    ngx_rbtree_right_rotate(root, sentinel, node);
                    //当前结点右旋
                }
 
                ngx_rbt_black(node->parent);//当前结点染黑
                //当前结点父结点的父结点染红
                ngx_rbt_red(node->parent->parent);
                ngx_rbtree_left_rotate(root, sentinel, node->parent->parent);//当前结点的父结点的父结点左旋
            }
        }
    }
 
    ngx_rbt_black(*root);//根结点染黑
}
登入後複製

    然后是对应ngx_rbtree_insert_pt指针的基础的结点插入函数:

void
ngx_rbtree_insert_value(ngx_rbtree_node_t *temp, ngx_rbtree_node_t *node,
    ngx_rbtree_node_t *sentinel)
{
    ngx_rbtree_node_t  **p;//虽然无关紧要,但两层指针令人费解
 
    for ( ;; ) {//无条件循环或者说死循环,等同于while(1)但节省了一个字符
 
        p = (node->key < temp->key) ? &temp->left : &temp->right;
 
        if (*p == sentinel) {//在二叉树中查找新结点合适的叶结点位置
            break;
        }
 
        temp = *p;
    }
    //令新结点占据合适的哨兵位置成为新的叶结点,染红,产生新哨兵
    *p = node;
    node->parent = temp;
    node->left = sentinel;
    node->right = sentinel;
    ngx_rbt_red(node);
}
登入後複製

    ngx_rbtree_insert_timer_value函数跟ngx_rbtree_insert_value函数唯一区别就是判断大小时,采用了两个值相减,避免溢出。

    以上是插入结点涉及的函数,老实说我不太喜欢这么长的函数实现,换我自己写肯定分块了。分支操作太多,看代码逻辑已经乱了,我们需要画几个图。首先,如果树为空:


    如果树中只有一个根结点:

    如果C>A

    如果CC染红,B染黑A染红,A右旋。右旋函数如下:

static ngx_inline void
ngx_rbtree_right_rotate(ngx_rbtree_node_t **root, ngx_rbtree_node_t *sentinel,
    ngx_rbtree_node_t *node)
{
    ngx_rbtree_node_t  *temp;
 
    temp = node->left;
    node->left = temp->right;//左子结点指向原左子结点的右结点
 
    if (temp->right != sentinel) {//如果左子结点的右结点不为哨兵
        temp->right->parent = node;//左子结点的右子结点挂在右旋结点上
    }
 
    temp->parent = node->parent;//左子结点挂在右旋结点的父结点上
 
    if (node == *root) {//如果右旋结点为根节点
        *root = temp;//根节点赋为左子结点
 
    } else if (node == node->parent->right) {//如果右旋结点为右子结点
        node->parent->right = temp;//左子结点挂父结点右边
 
    } else {//否则左子结点挂父结点左边
        node->parent->left = temp;
    }
 
    temp->right = node;//右旋结点挂左子结点右边
    node->parent = temp;
}
<p>    显然<span>B</span><span>将成为新的根,左</span><span>C</span><span>右</span><span>A</span><span>:</span></p>
<img src="/static/imghw/default1.png" data-src="http://image.codes51.com/Article/image/20150806/20150806084733_7641.png" class="lazy" alt=""><br><p>    如果<span>B<C<A</span><span>,会先做一次左旋再做一次右旋,其实除开染色过程,我觉得这跟</span><span>AVL</span><span>树的插入过程没有什么区别:</span></p><img src="/static/imghw/default1.png"  data-src="http://image.codes51.com/Article/image/20150806/20150806084733_8423.png"  class="lazy" alt=""><p>    其他的插入情景要么与以上几个对称,要么发生在树的其他子树中,实际过程完全一样。LL<span>型右旋,</span><span>RR</span><span>型左旋,</span><span>LR</span><span>型先右旋后左旋,</span><span>RL</span><span>型先左旋后右旋。</span>与<span>AVL</span><span>树不同的是,插入结点时红黑树左旋或右旋的判定条件明确为附近一两个结点的颜色,其他过程没有任何区别。</span></p><p>2.4、红黑树的结点删除</p><p>    据说红黑树和<span>AVL</span><span>树的区别主要体现在删除节点时,我们就来看一看。</span>我刚说什么来着,删除结点的函数体更长了,足足<span>165</span><span>行,我决定分段研究,</span>先看第一部分:</p><pre name="code">if (node->left == sentinel) {//如果左子结点是哨兵或左右子结点都是哨兵
    temp = node->right;//获得右子结点,后面让它接替node位置
    subst = node;//node赋给subst
 
} else if (node->right == sentinel) {//如果右子结点是哨兵
    temp = node->left;//获得左子结点,后面让它接替node位置
    subst = node;//node赋给subst
 
} else {//如果左右子结点都不是哨兵
    subst = ngx_rbtree_min(node->right, sentinel);//获得右子树中最小的结点
 
    if (subst->left != sentinel) {//如果右子树的最小结点的左子结点不是哨兵
        temp = subst->left;//获得右子树的最小结点的左子结点
    } else {//否则获得右子树最小结点的右子结点
        temp = subst->right;
    }//看起来subst将被从原位置删掉然后接替node的位置
}
<p>    下面我们来看看<span>temp</span><span>和</span><span>subst</span><span>要干什么用:</span></p>
<pre name="code">if (subst == *root) {//如果subst是根
    *root = temp;//temp接替根
    ngx_rbt_black(temp);//染黑temp
 
    /* DEBUG stuff */
    node->left = NULL;//清空了待删结点
    node->right = NULL;
    node->parent = NULL;
    node->key = 0;
 
    return;
}
 
red = ngx_rbt_is_red(subst);//获得subst是否是红色
 
if (subst == subst->parent->left) {//如果subst是左子结点
    subst->parent->left = temp;//把接替结点挂到subst位置
 
} else {//如果subst是右子结点
    subst->parent->right = temp;//把接替结点挂到subst位置
}
登入後複製
    下一段:

if (subst == node) {//如果subst是待删结点
    temp->parent = subst->parent;//接替结点直接接替,删除完成
 
} else {//如果subst不是待删结点
     if (subst->parent == node) {//如果subst的父结点就是待删结点
        temp->parent = subst;//接替结点挂在subst上
     } else {//如果待删结点比subst的父结点更高
        temp->parent = subst->parent;//把接替结点挂在subst的父结点上
    }
    //subst接替待删结点node的位置,复制待删结点跟周围结点的关系
    subst->left = node->left;
    subst->right = node->right;
    subst->parent = node->parent;
    ngx_rbt_copy_color(subst, node);//复制颜色
 
    if (node == *root) {//如果待删结点是根
        *root = subst;//subst接替根
    } else {//如果待删结点不是根,subst接替它
        if (node == node->parent->left) {
            node->parent->left = subst;
        } else {
            node->parent->right = subst;
        }
    }
 
    if (subst->left != sentinel) {//如果subst左子结点不是哨兵
        subst->left->parent = subst;//subst的左子结点放弃node,挂上来
    }
 
    if (subst->right != sentinel) {//如果subst右子结点不是哨兵
        subst->right->parent = subst;//subst右子结点放弃node,挂上来
    }
}
//清空待删结点node
/* DEBUG stuff */
node->left = NULL;
node->right = NULL;
node->parent = NULL;
node->key = 0;
//如果subst是红色,红黑树约束依然被遵守,删除工作就可以结束了
if (red) {
    return;
}
登入後複製

    看起来结点的删除过程已经顺利完成了,但是如果subst是黑色,我们需要修复红黑树的约束。下面这一段代码的主角是接替subst位置的temp结点:

//当subst的接替结点不是根且为黑色,循环
while (temp != *root && ngx_rbt_is_black(temp)) {
        if (temp == temp->parent->left) {//如果temp是左子结点
            w = temp->parent->right;//获得其右兄弟
 
            if (ngx_rbt_is_red(w)) {//如果temp的右兄弟是红色
                ngx_rbt_black(w);//染黑temp的右兄弟
                ngx_rbt_red(temp->parent);//染红temp的父结点
                //temp的父结点左旋
                ngx_rbtree_left_rotate(root, sentinel, temp->parent);
                w = temp->parent->right;//获得temp的新右兄弟
            }
            //如果temp右兄弟的左右子结点都是黑的
            if (ngx_rbt_is_black(w->left) && ngx_rbt_is_black(w->right)) {
                ngx_rbt_red(w);//染红temp的右兄弟
                temp = temp->parent;//获得temp的父结点为新temp
 
            } else {//如果temp右兄弟的子结点不全为黑
                if (ngx_rbt_is_black(w->right)) {//如果其右子结点是黑色
                    ngx_rbt_black(w->left);//染黑左子结点
                    ngx_rbt_red(w);//染红temp的右兄弟
                    ngx_rbtree_right_rotate(root, sentinel, w);//右兄弟右旋
                    w = temp->parent->right;//获得temp的新右兄弟
                }
                //temp右兄弟复制temp父结点颜色
                ngx_rbt_copy_color(w, temp->parent);
                ngx_rbt_black(temp->parent);//染黑temp父结点
                ngx_rbt_black(w->right);//染黑temp右兄弟的右子结点
                //temp父结点左旋
                ngx_rbtree_left_rotate(root, sentinel, temp->parent);
                temp = *root;//获得根
            }
 
        } else {//如果temp是右子结点,做对称的事
            w = temp->parent->left;
 
            if (ngx_rbt_is_red(w)) {
                ngx_rbt_black(w);
                ngx_rbt_red(temp->parent);
                ngx_rbtree_right_rotate(root, sentinel, temp->parent);
                w = temp->parent->left;
            }
 
            if (ngx_rbt_is_black(w->left) && ngx_rbt_is_black(w->right)) {
                ngx_rbt_red(w);
                temp = temp->parent;
 
            } else {
                if (ngx_rbt_is_black(w->left)) {
                    ngx_rbt_black(w->right);
                    ngx_rbt_red(w);
                    ngx_rbtree_left_rotate(root, sentinel, w);
                    w = temp->parent->left;
                }
 
                ngx_rbt_copy_color(w, temp->parent);
                ngx_rbt_black(temp->parent);
                ngx_rbt_black(w->left);
                ngx_rbtree_right_rotate(root, sentinel, temp->parent);
                temp = *root;
            }
        }
    }

ngx_rbt_black(temp);//染黑当前temp
登入後複製

    跟插入结点时一样乱,我们梳理一下。

    首先忽略红黑树的约束进行删除:

    ①如果删除的是一个叶结点,即没有后继或后继全为哨兵的结点,直接删除即可;

    ②如果只有一个后继,让其替换待删除结点即可;

    ③如果有两个后继,需要从树的边缘选择一个结点,有两种等价的选择,待删结点左子树的最大结点和右子树的最小结点,nginx选择的是后者,以这个结点的键与值(keyvalue/data)替换待删结点的键与值,然后删除这个替身。

    不论是①、②情景中的待删结点还是③情景中替身,在源码中都是subst。下面要围绕着它来进行讨论。

    以上是不考虑红黑树平衡性的纯拓扑结构变动。下面要考虑是否调整树的拓扑结构使树重新平衡,是否调整结点的颜色使树重新符合红黑树的约束条件。我们知道红黑树有一条关键约束是任意结点到其子树中叶结点的简单路径中黑色结点数相同。那么如果subst是一个红色结点,我们不需要对红黑树做任何调整,它仍是一棵红黑树;如果subst是黑色的,所有经过subst的简单路径上都会少一个黑色结点数,所以需要进行调整。

    下面来根据不同情景分情况讨论,因为二叉树的情景左右颠倒时调整方式也可以左右颠倒,我们只讨论subst是左子结点的情况。设刚接替substtempXX的新右兄弟为W。从经过简化的源码来看,关于结点颜色的变化很令人费解,我们不妨先来看一看:

    ①W为红色:将W染黑,将X与W的父结点X->parent染红,X->parent左旋,W重设为X的新右兄弟,然后转入情景①、②或③;

    ②W为黑色,W两个后继都是黑色:将W染红,X重设为X->parent;

    ③W为黑色,W右子结点为黑色:将W左子结点染黑,将W染红,W右旋,W重设为X的新右兄弟,然后将X->parent的颜色赋给W,将X->parent染黑,X->parent左旋,根赋给temp;

    ④W为黑色,W右子结点为红色:将W左子结点染黑,将W染红,W右旋,W重设为X的新右兄弟,然后将X->parent的颜色赋给W,将X->parent染黑,将W右子结点染黑,X->parent左旋,根赋给temp。

    最后还要把temp染黑。我们可以看到情景①中进行了一次左旋,情景②只进行了染色,情景③、④都进行了一次右旋和一次左旋。情景①处理结束时一定还要转入别的情景,情景②、③、④的出现则标志着本次调整的结束。那么,红黑树删除结点后的调整过程中,依情景①循环出现的次数,调整过程中旋转的最多见的次数将是1次、2次、3次,再往上次数越多越罕见(依情景①循环出现的次数),最多旋转次数将可能到达树高即log2n次。生产环境中,删除结点后平均每次调整中旋转的次数就像分析源码之前提到的,将是常数规模的。

    接下來我打算以逐步翻新版本的方式重寫紅黑樹,更精細、直觀地了解紅黑樹這一資料結構。而在重寫之前,我們需要了解,nginx的紅黑中所有的葉結點,都是哨兵(sentinel),這在調整紅黑樹時達成了對紅黑樹的一種優化。透過增加一層全黑的子結點,紅黑樹中實際有值的子樹裡,就允許在子結點出現紅色結點了。雖然我沒有證明,但這常數規模地增加了刪除結點時的旋轉次數,也促進了插入新結點時進行調整的機率(增加了在紅色結點下插入新結點的機率),同樣增加了旋轉的次數。而旋轉將壓縮紅黑樹子樹的高度,提高查詢效率。

    在由樸素到精緻地重寫紅黑樹的過程中,我將由少到多地考慮使用nginx對紅黑樹的優化,或者加入我自己的優化。

版權聲明:本文為部落客原創文章,未經部落客允許不得轉載。

以上就介紹了nginx的資料結構1——ngx_int_t與ngx_rbtree_t,包含了方面的內容,希望對PHP教程有興趣的朋友有所幫助。

本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱門文章

<🎜>:泡泡膠模擬器無窮大 - 如何獲取和使用皇家鑰匙
3 週前 By 尊渡假赌尊渡假赌尊渡假赌
北端:融合系統,解釋
3 週前 By 尊渡假赌尊渡假赌尊渡假赌
Mandragora:巫婆樹的耳語 - 如何解鎖抓鉤
3 週前 By 尊渡假赌尊渡假赌尊渡假赌

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

熱門話題

Java教學
1665
14
CakePHP 教程
1424
52
Laravel 教程
1322
25
PHP教程
1270
29
C# 教程
1249
24
解決方法:您的組織要求您更改 PIN 碼 解決方法:您的組織要求您更改 PIN 碼 Oct 04, 2023 pm 05:45 PM

「你的組織要求你更改PIN訊息」將顯示在登入畫面上。當在使用基於組織的帳戶設定的電腦上達到PIN過期限制時,就會發生這種情況,在該電腦上,他們可以控制個人設備。但是,如果您使用個人帳戶設定了Windows,則理想情況下不應顯示錯誤訊息。雖然情況並非總是如此。大多數遇到錯誤的使用者使用個人帳戶報告。為什麼我的組織要求我在Windows11上更改我的PIN?可能是您的帳戶與組織相關聯,您的主要方法應該是驗證這一點。聯絡網域管理員會有所幫助!此外,配置錯誤的本機原則設定或不正確的登錄項目也可能導致錯誤。即

Windows 11 上調整視窗邊框設定的方法:變更顏色和大小 Windows 11 上調整視窗邊框設定的方法:變更顏色和大小 Sep 22, 2023 am 11:37 AM

Windows11將清新優雅的設計帶到了最前沿;現代介面可讓您個性化和更改最精細的細節,例如視窗邊框。在本指南中,我們將討論逐步說明,以協助您在Windows作業系統中建立反映您的風格的環境。如何更改視窗邊框設定?按+開啟“設定”應用程式。 WindowsI前往個人化,然後按一下顏色設定。顏色變更視窗邊框設定視窗11「寬度=」643「高度=」500「&gt;找到在標題列和視窗邊框上顯示強調色選項,然後切換它旁邊的開關。若要在「開始」功能表和工作列上顯示主題色,請開啟「在開始」功能表和工作列上顯示主題

如何在 Windows 11 上變更標題列顏色? 如何在 Windows 11 上變更標題列顏色? Sep 14, 2023 pm 03:33 PM

預設情況下,Windows11上的標題列顏色取決於您選擇的深色/淺色主題。但是,您可以將其變更為所需的任何顏色。在本指南中,我們將討論三種方法的逐步說明,以更改它並個性化您的桌面體驗,使其具有視覺吸引力。是否可以更改活動和非活動視窗的標題列顏色?是的,您可以使用「設定」套用變更活動視窗的標題列顏色,也可以使用登錄編輯程式變更非活動視窗的標題列顏色。若要了解這些步驟,請前往下一部分。如何在Windows11中變更標題列的顏色? 1.使用「設定」應用程式按+開啟設定視窗。 WindowsI前往“個人化”,然

Windows 11 上啟用或停用工作列縮圖預覽的方法 Windows 11 上啟用或停用工作列縮圖預覽的方法 Sep 15, 2023 pm 03:57 PM

工作列縮圖可能很有趣,但它們也可能分散注意力或煩人。考慮到您將滑鼠懸停在該區域的頻率,您可能無意中關閉了重要視窗幾次。另一個缺點是它使用更多的系統資源,因此,如果您一直在尋找一種提高資源效率的方法,我們將向您展示如何停用它。不過,如果您的硬體規格可以處理它並且您喜歡預覽版,則可以啟用它。如何在Windows11中啟用工作列縮圖預覽? 1.使用「設定」應用程式點擊鍵並點選設定。 Windows按一下系統,然後選擇關於。點選高級系統設定。導航至“進階”選項卡,然後選擇“效能”下的“設定”。在「視覺效果」選

OOBELANGUAGE錯誤Windows 11 / 10修復中出現問題的問題 OOBELANGUAGE錯誤Windows 11 / 10修復中出現問題的問題 Jul 16, 2023 pm 03:29 PM

您是否在Windows安裝程式頁面上看到「出現問題」以及「OOBELANGUAGE」語句? Windows的安裝有時會因此類錯誤而停止。 OOBE表示開箱即用的體驗。正如錯誤提示所表示的那樣,這是與OOBE語言選擇相關的問題。沒有什麼好擔心的,你可以透過OOBE螢幕本身的漂亮註冊表編輯來解決這個問題。快速修復–1.點選OOBE應用底部的「重試」按鈕。這將繼續進行該過程,而不會再打嗝。 2.使用電源按鈕強制關閉系統。系統重新啟動後,OOBE應繼續。 3.斷開系統與網際網路的連接。在脫機模式下完成OOBE的所

Windows 11 上的顯示縮放比例調整指南 Windows 11 上的顯示縮放比例調整指南 Sep 19, 2023 pm 06:45 PM

在Windows11上的顯示縮放方面,我們都有不同的偏好。有些人喜歡大圖標,有些人喜歡小圖標。但是,我們都同意擁有正確的縮放比例很重要。字體縮放不良或圖像過度縮放可能是工作時真正的生產力殺手,因此您需要知道如何自訂以充分利用系統功能。自訂縮放的優點:對於難以閱讀螢幕上的文字的人來說,這是一個有用的功能。它可以幫助您一次在螢幕上查看更多內容。您可以建立僅適用於某些監視器和應用程式的自訂擴充功能設定檔。可以幫助提高低階硬體的效能。它使您可以更好地控制螢幕上的內容。如何在Windows11

10種在 Windows 11 上調整亮度的方法 10種在 Windows 11 上調整亮度的方法 Dec 18, 2023 pm 02:21 PM

螢幕亮度是使用現代計算設備不可或缺的一部分,尤其是當您長時間注視螢幕時。它可以幫助您減輕眼睛疲勞,提高易讀性,並輕鬆有效地查看內容。但是,根據您的設置,有時很難管理亮度,尤其是在具有新UI更改的Windows11上。如果您在調整亮度時遇到問題,以下是在Windows11上管理亮度的所有方法。如何在Windows11上變更亮度[10種方式解釋]單一顯示器使用者可以使用下列方法在Windows11上調整亮度。這包括使用單一顯示器的桌上型電腦系統以及筆記型電腦。讓我們開始吧。方法1:使用操作中心操作中心是訪問

如何修復Windows伺服器中的啟動錯誤代碼0xc004f069 如何修復Windows伺服器中的啟動錯誤代碼0xc004f069 Jul 22, 2023 am 09:49 AM

Windows上的啟動過程有時會突然轉向顯示包含此錯誤代碼0xc004f069的錯誤訊息。雖然啟動程序已經聯機,但一些運行WindowsServer的舊系統可能會遇到此問題。透過這些初步檢查,如果這些檢查不能幫助您啟動系統,請跳到主要解決方案以解決問題。解決方法–關閉錯誤訊息和啟動視窗。然後,重新啟動電腦。再次從頭開始重試Windows啟動程序。修復1–從終端啟動從cmd終端啟動WindowsServerEdition系統。階段–1檢查Windows伺服器版本您必須檢查您使用的是哪種類型的W

See all articles