lab8 lock

image-20230827174215466

PreRead

  1. 第六章
  2. 3.5节:物理内存分配器
  3. 8.1-8.3

文章目录

  • PreRead
  • Memory allocator
    • tasks
    • hints
    • 思路
  • Buffer cache
    • task
    • hints
    • 思路
    • 实现

这次的lab,本质上都是通过将锁的粒度减小来获得性能的提升

  1. 第一个task,可以简单地按cpu划分,因为本来就是空闲页面,谁拥有都一样
  2. 第二个task,本质上也可以简单地按某种性质划分,但是因为我们不只需要分配,我们 还需要查找。如果随便分成若干部分,那么查找起来就非常慢了。所以这也是为什么hints里提示我们用哈希表来划分

Memory allocator

tasks

  1. 你的任务是去实现per-cpu空闲链表,并且在一个cpu的空闲链表空着的时候去偷另一个cpu的空闲链表

  2. 你的所有锁的名字都应该以kmem开头,即在initlock中设置

  3. 你必须通过

    kalloctest,make grade会提醒你它通过了

    usertests,可以先检查一下sbrkmuch

hints

  1. 你可以使用kernel/param.h中的NCPU常数

  2. freerange将所有的空闲内存都给正在运行的free range

  3. cpuid函数会返回当前的cpu号,但是它必须在中断被关闭的时候使用

    因此你需要使用push_offpop_off

  4. 看一下snprintf,学习怎么格式化字符

思路

首先,我们需要以不同的cpu号去访问不同的freelist,最方便的方法就是用一个数组,如下所示。其中count是为了借空闲页面准备的。

struct {struct spinlock lock;struct run *freelist;int count;
} kmem[NCPU];

然后,我们应该在kinit中先初始化各种cpu对应的lock,然后将所有空闲页面都放到运行kinit的cpu上。

这里有几个细节

  1. 首先,我是希望kinit只被一个cpu执行,这样才能保证freerange将所有页面都放到这个cpu上,因此,我需要使用push_offpop_off将kinit包围起来
  2. 对于b_lock这个锁,也是为了借空闲页面准备的,否则可能发生死锁
void kinit() {push_off();for (int i = 0; i < NCPU; i++) {initlock(&kmem[i].lock, "kmem");kmem[i].count = 0;}initlock(&b_lock, "borrow");freerange(end, (void *)PHYSTOP);pop_off();
}

freerange函数不需要修改

在kfree函数中,当我们准备将这个空闲页面加入到一个freelist时,先关闭中断,然后获取当前cpu号,加入到对应的freelist,还是比较简单的。

其中,如果是freerange调用的kfree,可能会有push_off的嵌套,不过这没关系,只要pop_off成对出现即可

void kfree(void *pa) {struct run *r;if (((uint64)pa % PGSIZE) != 0 || (char *)pa < end || (uint64)pa >= PHYSTOP)panic("kfree");// Fill with junk to catch dangling refs.memset(pa, 1, PGSIZE);r = (struct run *)pa;push_off();int id = cpuid();acquire(&kmem[id].lock);r->next = kmem[id].freelist;kmem[id].freelist = r;kmem[id].count++;release(&kmem[id].lock);pop_off();
}

kalloc函数,如果当前cpu有空闲页面,则正常操作,否则的话,需要去借页面。我这里采用的借的策略是遍历所有cpu,如果某个cpu有空闲页面,那我就借一半,如果有3个,那我就借2个

// Allocate one 4096-byte page of physical memory.
// Returns a pointer that the kernel can use.
// Returns 0 if the memory cannot be allocated.
void *
kalloc(void) {struct run *r;push_off();int id = cpuid();acquire(&kmem[id].lock);r = kmem[id].freelist;if (r) {kmem[id].freelist = r->next;kmem[id].count--;}release(&kmem[id].lock);if (!r) {acquire(&b_lock);r = borrow(id);release(&b_lock);}if (r)memset((char *)r, 5, PGSIZE); // fill with junkpop_off();return (void *)r;
}

具体的borrow函数的实现如下

可以发现,在进入borrow函数之前,我就将当前cpu的freelist的锁给释放了。这是因为我进入borrow之后,会去获取其他freelist的锁,假如我是cpu a,我在borrow里要获取cpu b的锁。而b此时也在运行borrow,那它可能也在获取我的锁。如果我和b在进入borrow前都没有释放自己的锁,那必然就死锁了

另外,为什么在borrow之前要获得一个borrow的大锁呢?这是因为如果我在borrow里如果找到了一个可以借的freelist,那么我还是会获取两个锁,这也是有可能造成问题的,因为我们没有限制获取锁的顺序。为了避免可能的情况,我是用这个大锁来保平安,不过好像不会出现这种情况

void *borrow(int id) {for (int i = 0; i < NCPU; i++) {acquire(&kmem[i].lock);if (kmem[i].count != 0) {int b_count = (kmem[i].count + 1) / 2;struct run *r = kmem[i].freelist;struct run *temp = r;for (int i = 0; i < b_count - 1; i++) {temp = temp->next;}kmem[i].freelist = temp->next;kmem[i].count -= b_count;acquire(&kmem[id].lock);if (b_count != 1) {temp->next = kmem[id].freelist;kmem[id].freelist = r->next;kmem[id].count += b_count - 1;}release(&kmem[id].lock);release(&kmem[i].lock);return r;}release(&kmem[i].lock);}return 0;
}

image-20230827120002931

Buffer cache

task

  1. 修改bgetbrelse,使得对磁盘块的查找和释放在lock上等待的时间越少越好
  2. 通过bcachetestusertests
  3. 请给你的所有lock一个以bcache开头的名字,在initlock中实现它
  4. 这玩意比kalloc要难太多,因为buffer是必须被所有cpu共享的,不能每个cpu一份,因此建议使用一个哈希表,给哈希表的每个桶都设置一个锁
  5. 以下情况发生冲突是没关系的,因为测试不会有这些情况
    1. 两个进程访问同一个磁盘block
    2. 两个进程同时miss然后需要找到一个没用过的block
    3. 两个进程同时操作block,但是它们恰好在你的hash策略中碰撞了,那么你应该避免这种情况,比如调大你的哈希表的size

hints

  1. 阅读xv6的8.1到8.3
  2. 你可以使用固定长度的哈希表,同时选择一个质数去做哈希,比如13
  3. 在哈希表中查找一个buffer和为这个buffer分配一个entry必须是原子性的
  4. 删除所有缓存的链表(bcache.head),时间戳缓存使用它们上一次使用的时间(trap.c中的ticks)。有了这个改变之后,brelse不需要获得bcache的lock,bget可以基于时间戳选择最近最少使用的块
  5. 在bget中使用顺序查找实现LRU是可以的
  6. 你有时可能需要持有两个锁,即bcache锁和每个bucket的锁,保证你可以避免死锁
  7. 当你替换某一块的内容时,需要将buf从一个bucket移到另一个,记得处理这两个bucket相同的情况,否则就死锁了

思路

hints里其实就提供了一个思路,用哈希表去存可用的buf。但是到底怎么实现呢?我觉得这里的思路应该有很多,这里提供一种。

  1. 首先,我们通过blockno % prime为key,构造一个哈希表,其中prime可以取hints里的13
    1. 每一个哈希表的表槽都是一个buf链表+一个表槽锁
    2. 这个链表的结构可以按照原来的bcache里那个head来
    3. 表槽锁就是保护这个表槽里的这个链表
  2. 然后,我们在binit中先将所有的空闲buf都放到key=0的链表中,其实放到哪都可以,平均放到每个表槽也行
  3. bget的时候,先根据blockno计算出key,然后去对应的表槽里找是否这个block已经被取出来了
    1. 如果已经取出来了,则直接返回buf指针,这一个逻辑和原来的bget很像
    2. 如果这个block还没有被取出来,那么我们就去找一个引用数为0的buf,将这个buf的内容换成我们这个block。这里又有两种情况,因此我们需要遍历整个哈希表的表槽,并遍历每个表槽的链表,在链表上执行lru算法,找到一个buf,将这个buf修改为我们的内容,然后移动到key对应的表槽
      1. 这个引用数为0的buf在我们这个表槽的链表里
      2. 这个引用数为0的buf在别的表槽里
  4. brelse中,就很简单,只需要将refcnt减1就行了,都不用将这个buf移动

思路就是这样,不过有一个关键点没有涉及,那就是锁,该如何安排锁呢?

首先,锁肯定是要去保护一些东西的,之前的bcache的那个大锁,是因为保护的东西太多了,所有buf都是被它保护着,这就导致很慢了,因为可能不同的cpu没有冲突,但依然要等很久。

因此,我们这里采用一种哈希表的方法,使得锁管理的范围变小。对于某个key对应的表槽的那个锁,它只需要管理blockno%prime==key的block,也就是说,我们将原来的一个锁,变成了prime个锁,使得它们管理的范围缩小了prime倍。当然了,这是对于那些存储了某些block的内容的buf而言的,如果它存储了,那么它肯定就在对应的表槽中。至于那些没有存储的,或者说引用数为0的,我们可以称为空闲buf,它们按什么方式组织都行,甚至可以专门搞一个空闲链表都可以。但是这里采用的方式比较偷懒,也比较巧,即没有存储的一开始就放在key=0的表槽链表,引用计数为0的,直接不处理,反正它们都可能在bget中被访问到

最后,锁的作用呢?我们这里有两个锁,一个锁是表槽对应的锁,一个是每个buf对应的锁,它们分别保护了什么?

  1. 表槽锁当然是保护了表槽里的那个链表,也就是保护了链表的每个节点,即一个个buf,使得链表或者每个buf在被修改时,只会有一个线程对它们进行修改
  2. 而每个buf对应的锁,它的作用是使得,在某一刻,它永远只会被一个线程所拥有,不会同时被多个线程拥有。所以这个锁使用起来非常简单,我们只需要在我们找到了一个正确的buf,将它作为res在bget中返回之前调用这个buf的锁即可

实现

首先是整体的布局

  1. 这里的bcache最好不删,因为这个变量默认就开辟了NBUFstruct buf,省的我们自己申请空间创造了
  2. 哈希表有prime个表槽,每个表槽一个链表+一个锁,链表的结构和之前的一样,一个head作为dummynode,方便操作
  3. 一些宏,主要是方便,省的后面输入一大串代码来获取锁和释放锁
#define prime 13struct {struct buf buf[NBUF];
} bcache;struct {struct spinlock lock;struct buf head;
} ht[prime];#define LOCK(i) (acquire(&ht[i].lock));
#define UNLOCK(i) (release(&ht[i].lock));

binit函数

  1. 首先给每个表槽的锁给初始化,然后初始化这个head
  2. 将所有的buf都放到key=0的表槽中

这个过程很像之前binit,抄就完事了

void binit(void) {struct buf *b;char a[20];for (int i = 0; i < prime; i++) {snprintf(a, sizeof(a), "bcache_%d", i);initlock(&ht[i].lock, a);ht[i].head.prev = &ht[i].head;ht[i].head.next = &ht[i].head;}// Create linked list of buffersfor (b = bcache.buf; b < bcache.buf + NBUF; b++) {initsleeplock(&b->lock, "buffer");insert_into_ht(b, 0);}
}

可以发现,这里用到了一个insert_into_ht的操作,定义如下

  1. 可以从原来的brelse
void insert_into_ht(struct buf *b, int key) {b->next = ht[key].head.next;b->prev = &ht[key].head;ht[key].head.next->prev = b;ht[key].head.next = b;
}void delete_from_ht(struct buf *b) {b->next->prev = b->prev;b->prev->next = b->next;
}

brelse函数的实现也非常简单

  1. 释放这个buf的锁,其实这个释放放在哪一行都没问题
    1. 因为它的refcnt还没减1,就注定了它不会被别人给夺舍
    2. 只要unlock不取消掉,就没有人能够访问到它
void brelse(struct buf *b) {releasesleep(&b->lock);int key = b->blockno % prime;LOCK(key);b->refcnt -= 1;UNLOCK(key);
}

bpinbunpin的实现也很简单

  1. 首先,这两个函数肯定是在一个buf已经有了一个block,并且refcnt不为0的情况下调用的
  2. 我们只需要先获得对应表槽的锁,即获得对这个buf的修改权,然后修改,就可以了
void bpin(struct buf *b) {int key = b->blockno % prime;LOCK(key);b->refcnt++;UNLOCK(key);
}void bunpin(struct buf *b) {int key = b->blockno % prime;LOCK(key);b->refcnt--;UNLOCK(key);
}

大头戏bget来了

  1. 首先通过search_in_ht尝试去找找这个block是不是已经被读入了某个buf里,这种情况如果成功,那就和之前bget前一部分逻辑一模一样
  2. 如果失败了,那么就需要通过search_in_other去整个哈希表中找一个空闲的buf,这个操作一定会成功,否则在xv6里就直接给它来一个panic,原函数也是这么写的
static struct buf *
bget(uint dev, uint blockno) {struct buf *b;int key = blockno % prime;// 尝试去对应的哈希表槽查找LOCK(key);b = search_in_ht(dev, blockno, key);if (b) {UNLOCK(key);acquiresleep(&b->lock);return b;}// 至此,没有在对应的表槽找到,遍历所有哈希表的表槽,不过优先处理自己表槽的// 这里是带着key对应的锁去查找的b = search_in_other(dev, blockno, key);// 这个b不可能为0,否则直接panic了UNLOCK(key);acquiresleep(&b->lock);return b;
}

search_in_ht的实现如下所示,就是遍历链表,如果找到了,更新属性,然后返回。其中更新属性会用到update_time

void update_time(struct buf *b) {acquire(&tickslock);b->timestamp = ticks;release(&tickslock);
}
struct buf *search_in_ht(uint dev, uint blockno, int key) {struct buf *b;for (b = ht[key].head.next; b != &ht[key].head; b = b->next) {if (b->dev == dev && b->blockno == blockno) {b->refcnt++;update_time(b);return b;}}return 0;
}

search_in_other就比较复杂

  1. 这里采取的遍历顺序是从自己这里开始遍历,用一个cycle来控制遍历prime次,之所以这样做,是为了避免每次都是0开始遍历。这样操作相对来说会提高点性能,不会出现前面的表槽没有空闲的,后面的表槽全是空闲的

  2. 如果我们要进入的某个表槽不是自己,那么就需要获取那个表槽的锁

    1. 这里是有可能死锁的
      1. 因为我们进入这个函数的时候,是带着key对应的锁的,现在又去请求i对应的锁
      2. 假如某个cpu是带着i对应的锁进入这个函数,正在请求key对应的锁,岂不是就死锁了?
      3. 感觉是自带的评测没有检查出来,这里还是有点问题的。不过懒得改了
  3. 接下来就是通过search_lru_free_in_ht去这个兄弟那里找一找有没有空闲的

    struct buf *search_lru_free_in_ht(uint dev, uint blockno, int key) {struct buf *b;struct buf *lru_b = 0;for (b = ht[key].head.next; b != &ht[key].head; b = b->next) {if (b->refcnt == 0 && (lru_b == 0 || lru_b->timestamp > b->timestamp)) {lru_b = b;}}return lru_b;
    }
    
  4. 如果没有,那么视情况释放锁,然后continue

  5. 如果有的话

    1. 更新各种属性
    2. 如果这个buf是别的表槽,将这个buf挪到key对应的表槽
    3. 最后视情况释放这个兄弟锁,返回答案
struct buf *search_in_other(uint dev, uint blockno, int key) {struct buf *b;for (int i = key, cycle = 0; cycle < prime; cycle++, i = (i + 1) % prime) {// 如果不是自己,则给这个兄弟上个锁if (i != key) {LOCK(i);}// 在这个兄弟里去找一下b = search_lru_free_in_ht(dev, blockno, i);// 这个兄弟里没有空闲页面if (!b) {if (i != key) {UNLOCK(i);}continue;}// 在这个兄弟里找到了空闲页面// 先更新属性b->dev = dev;b->blockno = blockno;b->valid = 0;b->refcnt = 1;update_time(b);// 如果不是自己的哈希槽里的,将这个页面放到自己哈希表槽中if (i != key) {delete_from_ht(b);insert_into_ht(b, key);}// 释放哈希表的锁if (i != key) {UNLOCK(i);}return b;}panic("no free buf");
}

整体代码如下

// Buffer cache.
//
// The buffer cache is a linked list of buf structures holding
// cached copies of disk block contents.  Caching disk blocks
// in memory reduces the number of disk reads and also provides
// a synchronization point for disk blocks used by multiple processes.
//
// Interface:
// * To get a buffer for a particular disk block, call bread.
// * After changing buffer data, call bwrite to write it to disk.
// * When done with the buffer, call brelse.
// * Do not use the buffer after calling brelse.
// * Only one process at a time can use a buffer,
//     so do not keep them longer than necessary.#include "types.h"
#include "param.h"
#include "spinlock.h"
#include "sleeplock.h"
#include "riscv.h"
#include "defs.h"
#include "fs.h"
#include "buf.h"
#include <x86_64-linux-gnu/sys/types.h>#define prime 13struct {struct buf buf[NBUF];
} bcache;struct {struct spinlock lock;struct buf head;
} ht[prime];#define LOCK(i) (acquire(&ht[i].lock));
#define UNLOCK(i) (release(&ht[i].lock));void update_time(struct buf *b) {acquire(&tickslock);b->timestamp = ticks;release(&tickslock);
}void insert_into_ht(struct buf *b, int key) {b->next = ht[key].head.next;b->prev = &ht[key].head;ht[key].head.next->prev = b;ht[key].head.next = b;
}void delete_from_ht(struct buf *b) {b->next->prev = b->prev;b->prev->next = b->next;
}void binit(void) {struct buf *b;char a[20];for (int i = 0; i < prime; i++) {snprintf(a, sizeof(a), "bcache_%d", i);initlock(&ht[i].lock, a);ht[i].head.prev = &ht[i].head;ht[i].head.next = &ht[i].head;}// Create linked list of buffersfor (b = bcache.buf; b < bcache.buf + NBUF; b++) {initsleeplock(&b->lock, "buffer");insert_into_ht(b, 0);}
}struct buf *search_in_ht(uint dev, uint blockno, int key) {struct buf *b;for (b = ht[key].head.next; b != &ht[key].head; b = b->next) {if (b->dev == dev && b->blockno == blockno) {b->refcnt++;update_time(b);return b;}}return 0;
}struct buf *search_lru_free_in_ht(uint dev, uint blockno, int key) {struct buf *b;struct buf *lru_b = 0;for (b = ht[key].head.next; b != &ht[key].head; b = b->next) {if (b->refcnt == 0 && (lru_b == 0 || lru_b->timestamp > b->timestamp)) {lru_b = b;}}return lru_b;
}struct buf *search_in_other(uint dev, uint blockno, int key) {struct buf *b;for (int i = key, cycle = 0; cycle < prime; cycle++, i = (i + 1) % prime) {// 如果不是自己,则给这个兄弟上个锁if (i != key) {LOCK(i);}// 在这个兄弟里去找一下b = search_lru_free_in_ht(dev, blockno, i);// 这个兄弟里没有空闲页面if (!b) {if (i != key) {UNLOCK(i);}continue;}// 在这个兄弟里找到了空闲页面// 先更新属性b->dev = dev;b->blockno = blockno;b->valid = 0;b->refcnt = 1;update_time(b);// 如果不是自己的哈希槽里的,将这个页面放到自己哈希表槽中if (i != key) {delete_from_ht(b);insert_into_ht(b, key);}// 释放哈希表的锁if (i != key) {UNLOCK(i);}return b;}panic("no free buf");
}static struct buf *
bget(uint dev, uint blockno) {struct buf *b;int key = blockno % prime;// 尝试去对应的哈希表槽查找LOCK(key);b = search_in_ht(dev, blockno, key);if (b) {UNLOCK(key);acquiresleep(&b->lock);return b;}// 至此,没有在对应的表槽找到,遍历所有哈希表的表槽,不过优先处理自己表槽的// 这里是带着key对应的锁去查找的b = search_in_other(dev, blockno, key);// 这个b不可能为0,否则直接panic了UNLOCK(key);acquiresleep(&b->lock);return b;
}// Return a locked buf with the contents of the indicated block.
struct buf *
bread(uint dev, uint blockno) {struct buf *b;b = bget(dev, blockno);if (!b->valid) {virtio_disk_rw(b, 0);b->valid = 1;}return b;
}// Write b's contents to disk.  Must be locked.
void bwrite(struct buf *b) {if (!holdingsleep(&b->lock))panic("bwrite");virtio_disk_rw(b, 1);
}// Release a locked buffer.
// Move to the head of the most-recently-used list.
void brelse(struct buf *b) {releasesleep(&b->lock);int key = b->blockno % prime;LOCK(key);b->refcnt -= 1;UNLOCK(key);
}void bpin(struct buf *b) {int key = b->blockno % prime;LOCK(key);b->refcnt++;UNLOCK(key);
}void bunpin(struct buf *b) {int key = b->blockno % prime;LOCK(key);b->refcnt--;UNLOCK(key);
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://xiahunao.cn/news/1620668.html

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

GiD 前处理 实例

目录 Blog Links一、前言二、创建 1/8 半球体2.1 创建圆弧及圆球面2.2 创建半球面等分点2.3 连接等分线2.4 生成 1/8 半球体 三、创建整个球体四、划分网格五、尾声六、参考文献 Blog Links DalNur | 博客总目录 GiD 实用教程 GiD 前处理 实例 GiD 自定义 简介 GiD 后处理 …

Linux 建立用户和修改用户UID,GID

预设用户名为tom&#xff0c;UID222&#xff0c;GID222 首先我们来创建一个用户&#xff0c;创建用户命令&#xff1a; useradd tom&#xff08;useradd 用户名&#xff09; 创建好用户名后&#xff0c;再来创建用户的密码&#xff0c;于是接着创建密码&#xff0c;创建密码命…

Android中UID、GID和PID的讲解

一、概述 在实际的开发中经常会碰到各种ID&#xff0c;这是由于在计算机的发展过程中&#xff0c;需要对程序执行的每一步做标记&#xff0c;通过这些标记的关联便于系统的统一管理。像PID、UID、GID、和EUID等&#xff0c;其实对于这些ID不需要刻意记忆&#xff0c;只需要了解…

Linux中的UID、GID和SID

一、 UID和GID vi /etc/passwd查看用户配置情况 GID 是组ID (Group Identify)&#xff0c;表示组的身份唯一标识 UID 是用户ID (User Identify)&#xff0c;表示用户身份唯一标识 用户分类 centos6 超级用户 UID0 root 普通用户 UID500起 oldboy 虚拟用户 UID1-499 存在满足…

GiD初步使用

GiD软件具有全面的几何建模、网格划分、CAD数据导入、后处理结果显示等功能。GiD采用类似于CAD的操作模式&#xff0c;用户在使用GiD创建复杂模型问题时&#xff0c;会感受到前所未有的方便和轻松。它易于操作、方便灵活、直观便捷。 1. 下载与安装 到官方网站下载适合电脑配…

用户账号-用户标识符:UID与GID

虽然我们登录Linux主机的时候&#xff0c;输入的是我们的账号&#xff0c;但是其实Linux主机并不会直接认识你的“账号名称”的&#xff0c;它仅认识ID。ID与账号的对应关系在/etc/passwd当中。 每个登录的用户至少都会取得两个ID&#xff0c;一个是用户ID&#xff08;UserID&…

Android 安全机制(1)uid 、 gid 与 pid

1、概述 Android 安全机制来源于Linux&#xff0c;并且以Linux权限管理为基础&#xff0c;要了解Android的安全机制&#xff0c;需要从linux中的安全机制了解开始&#xff0c;而用户的权限管理又是linux安全机制的最基本的一个组成. Android的创新之处是在linux用户权限管理的…

UID、EUID、GID和EGID

UID、EUID、GID和EGID Linux中id真是太多了进程有pid&#xff0c;然后用户还有UID这种&#xff0c;真是有点绕。 在Linux当中一个进程&#xff08;程序&#xff09;拥有四个ID:真实用户UID、有效用户EUID、真实组GID和有效组EGID。 这里以真实用户UID和有效用户EUID为例&…

用户和用户组-UID和GID

用户和用户组-UID和GID Linux用户和用户组用户UID用户组GID/etc/passwd 文件结构/etc/shadow 文件结构 Linux用户和用户组 Linux采用一个32位的整数记录和区分不同的用户。这个区分不同用户的数字被称为User ID&#xff0c;简称UID。Linux系统中用户分为3类&#xff0c;即普通…

linux uid gid 作用,Linux uid和gid

Linux uid和gid教程 我们在登陆 Linux 系统时&#xff0c;虽然输入的是自己的用户名和密码&#xff0c;但其实 Linux 并不认识你的用户名称&#xff0c;它只认识用户名对应的 ID 号(也就是一串数字)。Linux 系统将所有用户的名称与 ID 的对应关系都存储在 /etc/passwd 文件中。…

GiD 自定义 简介

目录 Blog Links一、前言二、GiD的程序架构三、问题类型系统四、主配置文件/.spd文件4.1 单位制4.2 截面属性4.3 局部轴 五、Tcl文件5.1 GiD程序调用GiD-Tcl5.2 执行程序的命名空间 六、参考文献 Blog Links DalNur | 博客总目录 GiD 实用教程 GiD 前处理 实例 GiD 自定义 简…

GiD 实用教程

目录 Blog Links一、前言二、用户界面2.1 界面组成2.2 快捷键2.3 模式切换2.4 图层功能2.5 删除功能2.6 视图切换2.7 渲染视图 三、帮助与实例3.1 帮助文档3.2 官方实例 四、GiD Basics五、几何模型5.1 点的定义5.2 线的创建5.3 面的创建5.4 体的创建 六、网格划分6.1 布设种子…

python 双向链表

双向链表基本介绍 双向链表增删改查操作思路分析 双向链表增删改查操作代码实现 """ 双向链表的增删改查 """# 英雄类 class HeroNode:next None # 指向下一个节点&#xff0c;默认为空pre None # 指向前一个节点&#xff0c;默认为空def …

小马哥的CSS驿站

目录 第一章 CSS概述 1.1语法 1.2注释 1.3CSS的创建 第二章 CSS选择器 1.id选择器 2.class选择器 3.标签选择器 4.子代选择器 5.后代选择器 6.相邻兄弟选择器 7.后续兄弟选择器 8.交集选择器 9.并集选择器 第三章 CSS样式 1.文本与文字样式 &#xff08;1&…

35 岁财务自由的小马哥,我想跟他学学!

如果说 Java 工程师&#xff0c;有什么一定要“死磕”拿下的东西&#xff0c;那一定是 Spring 无疑了。 众所周知&#xff0c;Spring 无论在 Java 生态系统&#xff0c;还是在就业市场&#xff0c;是绝对的王者。Spring AOP 作为 Spring 框架的核心内容之一&#xff0c;其重要性…

计算机网络(速率、宽带、吞吐量、时延、发送时延)

速率&#xff1a; 最重要的一个性能指标。 指的是数据的传送速率&#xff0c;也称为数据率 (data rate) 或比特率 (bit rate)。 单位&#xff1a;bit/s&#xff0c;或 kbit/s、Mbit/s、 Gbit/s 等。 例如 4 1010 bit/s 的数据率就记为 40 Gbit/s。 速率往往是指额定速率或…

什么盒模型

一、盒模型 1.什么是盒模型 在我们HTML页面中&#xff0c;每一个元素都可以被看作一个盒子&#xff0c;而这个盒子由&#xff1a;内容区&#xff08;content&#xff09;、填充区&#xff08;padding&#xff09;、边框区&#xff08;border&#xff09;、外边界区&#xff0…

1.1 编辑楼层标高

在任意视图内批量编辑楼层。点击 按钮&#xff0c;弹出楼层管理器界面&#xff0c;可以对模型中已有的楼层高度进行修改、批量修改楼层名称。在创建楼层时&#xff0c;可以设定起始楼层序号&#xff0c;后续楼层将自动排序,当前文件中的新建楼层在“确定”完成前允许自由删除操…

2.3 轴生墙

按照已创建的弧形/ 直线轴生成墙。点击 按钮&#xff0c;弹出轴线生墙对话框&#xff0c;如图所示&#xff1a;在基本墙中选择需要添加的墙类型&#xff0c;在顶高和底高中选择楼层&#xff0c;并可以勾选是否按楼层切分墙。可在墙上定位线中选择墙的中心或者外边缘等为定位线…

3.5 连接管线

通过Revit管线建模功能&#xff0c;或是通过翻模软件创建的管线往往是不连接的&#xff0c;如果要把它们连接起来很费力&#xff0c;同时也不容易指定连接的角度。模盒提供的连接管线”功能可以很好地连接各种管线&#xff0c;并根据所选管线的空间位置&#xff0c;智能提示可以…