目录

1. Task 1:Alarm Clock

1.1 实验要求

1.2 基础知识

1.3 设计思路

1.4 实验过程

1.5实验结果:

1.6 收获与总结

2. Task 2:优先级调度

2.1 实验要求

2.2 实验过程

2.3 实验结果

3. Task 3:多级反馈调度

3.1 实验要求

3.2 基础知识

3.3 实验过程

3.4 实验结果

3.5 收获与总结


1. Task 1:Alarm Clock

1.1 实验要求

重新实现timer_sleep()。

将“忙等待“变为可避免繁忙的等待

暂停执行调用线程,直到时间提前至少x个计时单位。除非系统处于空闲状态,否则线程在x个计时单位之后才唤醒。在等待一定时间后,将它放在就绪队列中。

1.2 基础知识

timer_sleep()函数解析:

void timer_sleep (int64_t ticks)           //在ticks时间内,如果线程处于running状态就不断把他扔到就绪队列不让他执行{int64_t start = timer_ticks ();         //返回自OS启动以来的计时器tick数。禁止当前行为被中断,保存禁止被中断前的中断状态(用old_level储存)。ASSERT (intr_get_level () == INTR_ON);           //如果它的条件返回错误,则终止程序执行。//intr_get_level ()这个函数一样是调用了汇编指令,把标志寄存器的东西放到处理器棧上,然后把值pop到flags(代表标志寄存器IF位)上,通过判断flags来返回当前终端状态(intr_level)。while (timer_elapsed (start) < ticks)   //返回了当前时间距离start的时间间隔。thread_yield();              //把当前线程扔到就绪队列里,然后重新schedule,注意这里如果ready队列为空的话当前线程会继续在cpu执行。}

1.3 设计思路

调用timer_sleep的时候直接把线程阻塞掉,然后给线程结构体加一个成员ticks_blocked来记录这个线程被sleep了多少时间,然后利用操作系统自身的时钟中断(每个tick会执行一次)加入对线程状态的检测,每次检测将ticks_blocked减1, 如果减到0就唤醒这个线程。

1.4 实验过程

  1. 为线程增加一个新的状态,表示线程正处在sleep中。
  2. 在内核中增加一个列表sleep_list,用于存放处于THREAD_SLEEP状态的线程。
  3. 在struct thread结构体中添加两个成员变量,用于保存与THREAD_SLEEP状态相关的信息。
  4. 为了实现非忙等待的timer_sleep(),需要实现几个函数,具体功能下文会解释。
  5. 实现thread_sleep()函数,该函数会将当前线程放入sleep_list队列中,并更新线程信息,最后调用schedule()进行新线程调度。该函数会在timer_sleep()中被调用。
  6. 实现thread_foreach_sleep()函数,该函数会遍历sleep_list队列,并将其中的每个线程的sleep_ticks减1;若某个线程的sleep_ticks为0,即该线程不需要再继续等待,便将该线程从sleep_list队列移至就绪队列中。该函数会在timer_interrupt()中被调用。
  7. 实现thread_less_priority()函数,该函数会比较传入线程a和b的优先级,若a的优先级小于b的,则返回真,否则返回假。该函数会在next_thread_to_run中被调用,用来从就绪队列中选择优先级最高的线程。
  8. 修改timer_sleep()函数:一是要对输入参数进行合法性检查,输入参数ticks必须大于0才有意义;二是要将其改为非忙等待,看了上面的内容,其实思路已经很清楚了,只要调用thread_sleep()将当前线程扔到sleep_list队列中就行了。
  9. 修改timer_interrupt()函数:当每个ticks中断到来时,需要对sleep_list中的线程进行更新,否则sleep中的线程将永远不会被唤醒。
  10. 需要对新加入的sleep_list在系统启动后进行初始化。
  11. 收尾工作,因为project要求在调度下一个进程时,选择优先级最高的来执行,所以需要修改next_thread_to_run()函数。该函数原先选择就绪队列中的第一个线程来执行,现在我们利用库函数中提供的list_max()来选择其中优先级最高的线程。

1.5实验结果:

1.6 收获与总结

通过配置pintos学习到了一些bochs的配置方法。通过完成实验一,更深层次地理解了阻塞线线程的过程。

2. Task 2:优先级调度

2.1 实验要求

在Pintos中实现优先级调度。当一个线程被添加到具有比当前运行的线程更高优先级的就绪列表时,当前线程应该立即将处理器放到新线程中。类似地,当线程正在等待锁,信号量或条件变量时,应首先唤醒优先级最高的等待线程。线程可以随时提高或降低自己的优先级,但降低其优先级以使其不再具有最高优先级必须使其立即产生CPU。

实施优先捐赠。您需要考虑需要优先捐赠的所有不同情况。务必处理多个捐赠,其中多个优先级捐赠给单个线程。您还必须处理嵌套捐赠:如果H等待M持有的锁定且M正在等待L 持有的锁定,则M和L应该被提升为H的优先级。如有必要,您可以对嵌套优先级捐赠的深度施加合理限制,例如8级。

您必须为锁实现优先捐赠。您无需为其他Pintos同步构造实现优先级捐赠。您确实需要在所有情况下实施优先级安排。

最后,实现以下允许线程检查和修改自己的优先级的函数。这些函数的骨架在threads / thread.c中提供。2

2.2 实验过程

这个测试集主要是要求实现锁的优先级捐赠,及将cond改为按优先级选择下一线程,并且保证执行中的线程始终是优先级最高的。

因为原先的实现中是将lock视为值为1的信号量来处理的,为了实现优先级捐赠,需要用到其中的等待队列,但这样包裹一层结构体觉得很麻烦,所以对lock做了较大改动。

1.为了实现优先级捐赠,我们先考虑struct thread需要增加哪些变量:

1)  我们需要记录当前线程的基础优先级,这样当捐赠结束时才能恢复原先的优先级;

2)  我们需要记录线程已经持有的锁,这样才能在需要的时候(获得或释放一个锁时),根据这些锁来决定新的捐赠的优先级大小;

3)  还需要记录线程正在等待的锁,这样当发生嵌套捐赠时,可以根据这个变量不断向前找到下一个被捐赠对象。

thread.h:修改后的struct thread结构体:

struct thread{/* Owned by thread.c. */tid_t tid;                         /* Thread identifier. */enum thread_status status;         /* Thread state. */char name[16];                      /* Name (for debugging purposes). */uint8_t *stack;                    /* Saved stack pointer. */int priority;                      /* Priority. */struct list_elem allelem;          /* List element for all threads list. *//* Shared between thread.c and synch.c. */struct list_elem elem;             /* List element. */struct list_elem slpelem;int64_t sleep_ticks;struct lock *lock_waiting;//线程正在等待的锁struct list locks;//线程持有的锁int locks_priority;//线程持有的锁locks中的最高优先级int base_priority;//线程的基础优先级#ifdef USERPROG/* Owned by userprog/process.c. */uint32_t *pagedir;                 /* Page directory. */#endif/* Owned by thread.c. */unsigned magic;                    /* Detects stack overflow. */};

2.为了实现优先级捐赠,再考虑需要对struct lock做的修改:

1)  需要记录锁的持有者是哪个线程,这样当发生嵌套捐赠时,可以根据这个变量不断向前找到下一个被捐赠对象;

2)  需要记录等待该锁的线程,这样当该锁被释放时,可以从中选择下一个锁的持有者;

3)  需要记录当前锁的优先级,便于优先级捐赠的实现。

synch.h:修改后的struct lock结构体:

struct lock {struct thread *holder;//持有当前锁的线程struct list waiters;//等待当前锁的线程列表int priority;//当前锁的优先级,就是waiters中最高的优先级struct list_elem elem;//struct thread中locks的链表元素};

3.锁在初始化时,是没有优先级的,所以需要增加一个无效的优先级。

thread.h:在文件靠前添加宏定义:

#define PRI_UNVALID -1

4.对锁进行初始化。

synch.c:修改后的lock_init()函数:

void lock_init (struct lock *lock){ASSERT (lock != NULL);lock->holder = NULL;list_init (&lock->waiters);lock->priority=PRI_UNVALID;}

5.修改线程的初始化函数。

thread.c:修改后的init_thread()函数:

static void init_thread (struct thread *t, const char *name, int priority){ASSERT (t != NULL);ASSERT (PRI_MIN <= priority && priority <= PRI_MAX);ASSERT (name != NULL);memset (t, 0, sizeof *t);t->status = THREAD_BLOCKED;strlcpy (t->name, name, sizeof t->name);t->stack = (uint8_t *) t + PGSIZE;t->priority = priority;t->lock_waiting=NULL;list_init(&t->locks);t->locks_priority=PRI_UNVALID;t->base_priority=priority;t->magic = THREAD_MAGIC;list_push_back (&all_list, &t->allelem);}

6.为了实现优先级(嵌套)捐赠,需要实现几个函数

thread.h:

void thread_priority_donate_nest(struct thread *t);void thread_update_priority(struct thread *t);void lock_update_priority(struct lock *l);

1)实现thread_priority_donate_nest()函数,该函数实现了嵌套优先级捐赠,从传入的线程t开始,逐级向上进行优先级捐赠,且当无法进行捐赠时,及时退出循环。该函数会在lock_acquire()中被调用,因为只有当请求的锁被占用时,才会发生优先级捐赠。

thread.c:

void thread_priority_donate_nest(struct thread *t){struct lock *l = t->lock_waiting;//获取当前线程等待的锁while(l){if(t->priority > l->priority)//判断当前线程是否能提高等待锁的优先级l->priority = t->priority;//若能,则进行优先级捐赠elsebreak;//若不能,则结束优先级捐赠t = l->holder;//获得占有锁的线程if(l->priority > t->locks_priority)//判断当前锁是否能提高占有线程的锁优先级t->locks_priority = l->priority;//若能,则进行优先级捐赠elsebreak;//若不能,则结束优先级捐赠if(l->priority > t->priority)//因为锁优先级可能低于优先级(因为基础优先级很高),所以这里需要再判断一次t->priority = l->priority;elsebreak;l = t->lock_waiting;}}

2)实现thread_update_priority()函数,该函数根据线程持有的锁更新线程的锁优先级,并与基础优先级比较来更新优先级。

thread.c:在文件靠后添加以下代码:

void thread_update_priority(struct thread *t){t->locks_priority = PRI_UNVALID;struct lock *l;struct list_elem *e;for (e = list_begin (&t->locks); e != list_end (&t->locks);e = list_next (e))//遍历线程持有的锁{l = list_entry(e, struct lock, elem);if(l->priority > t->locks_priority)t->locks_priority = l->priority;//找到其中最高的优先级作为锁优先级}if(t->base_priority > t->locks_priority)//更新优先级t->priority = t->base_priority;elset->priority = t->locks_priority;}

3)实现lock_update_priority()函数,该函数根据等待线程来更新锁的优先级。

thread.c:

void lock_update_priority(struct lock *l){l->priority = PRI_UNVALID;struct thread *t;struct list_elem *e;for (e = list_begin (&l->waiters); e != list_end (&l->waiters);e = list_next (e))//遍历等待该锁的线程{t = list_entry(e, struct thread, elem);if(t->priority > l->priority)l->priority = t->priority;//找到其中最高的优先级}}

7.修改lock_acquire()函数,实现优先级捐赠:

1)  首先判断锁是否被占用,若被占用,则进行优先级捐赠并将当前线程扔到阻塞队列中;

2)  若不被占用,便获取该锁,并更新相关的状态信息。

synch.c:修改后的lock_acquire()函数:

void lock_acquire (struct lock *lock){ASSERT (lock != NULL);ASSERT (!intr_context ());ASSERT (!lock_held_by_current_thread (lock));struct thread *cur_t;enum intr_level old_level;old_level = intr_disable ();while (lock->holder != NULL) //判断请求的锁是否被占用{//锁被占用cur_t = thread_current();list_push_back (&lock->waiters, &cur_t->elem);//将当前线程放入锁的等待队列中cur_t->lock_waiting = lock;//设置当前线程正等待该锁thread_priority_donate_nest(cur_t);//进行优先级捐赠thread_block ();//将当前线程扔到就阻塞列中}cur_t = thread_current();lock->holder = cur_t;//当前线程获得该锁cur_t->lock_waiting = NULL;list_push_back(&cur_t->locks,&lock->elem);//将该锁放入当前线程的持有锁队列中if(lock->priority > cur_t->locks_priority)//因为有新锁加入,所以要更新优先级cur_t->locks_priority = lock->priority;if(lock->priority > cur_t->priority)//理由同上cur_t->priority = lock->priority;intr_set_level (old_level);}

8.修改lock_try_acquire()函数,实现优先级捐赠:思路跟上面的一样,只不过这里当锁被占用时,不会进入阻塞状态,而是返回false。

synch.c:修改后的lock_try_acquire()函数:

bool lock_try_acquire (struct lock *lock){bool success;ASSERT (lock != NULL);ASSERT (!lock_held_by_current_thread (lock));struct thread *cur_t;enum intr_level old_level;old_level = intr_disable ();if (lock->holder == NULL){cur_t = thread_current();lock->holder = cur_t;cur_t->lock_waiting = NULL;list_push_back(&cur_t->locks,&lock->elem);if(lock->priority > cur_t->locks_priority)cur_t->locks_priority = lock->priority;if(lock->priority > cur_t->priority)cur_t->priority = lock->priority;success = true; }elsesuccess = false;intr_set_level (old_level);return success;}

9.修改lock_release()函数,实现优先级捐赠中,当锁被释放时,还原优先级的操作。

synch.c:修改后的lock_release()函数:

void lock_release (struct lock *lock) {ASSERT (lock != NULL);ASSERT (lock_held_by_current_thread (lock));struct list_elem *e;struct thread *t;enum intr_level old_level;old_level = intr_disable();lock->holder = NULL;//将锁的持有者设为NULL,释放锁list_remove(&lock->elem);//同时将该锁从线程的持有锁队列中删除thread_update_priority(thread_current());//因为线程的锁队列发生变化,所以需要更新线程的优先级if(!list_empty(&lock->waiters)){//如果锁的等待队列不空e = list_max(&lock->waiters,&thread_less_priority,NULL);//从锁的等待队列中找到优先级最高的线程list_remove(e);//将该线程从锁的等待队列中移出lock_update_priority(lock);//因为锁的等待队列发生变化,所以需要更新所的优先级t = list_entry(e,struct thread,elem);thread_unblock(t);//将选出的线程放入到就绪队列中}thread_yield();//因为在释放锁的过程中,当前线程的优先级可能不再是所有线程中优先级最高的线程,所以需要交出cpu进行一次线程调度intr_set_level(old_level);}

10.   做到这里,关于lock的优先级捐赠已经完成了。

在线程调度中,有要求当前执行的线程一定所有线程中优先级最高的,所以需要在线程优先级发生发生变化的地方进行优先级调度。

首先修改thread_create()函数,因为新创建的线程可能具有更高的优先级,所以需要进行线程调度,以使新线程执行。

thread.c:修改后的thread_create()函数:

tid_t thread_create (const char *name, int priority,thread_func *function, void *aux) {/*前面代码太长了,这里省了,要加的就是下面两行*/if(priority > thread_current()->priority)thread_yield();return tid;}

11.修改thread_set_priority()函数,因为该函数也会修改线程的优先级,不过需要注意这里修改的是线程的基础优先级,所以还需要另加判断进行优先级更新。

thread.c:修改后的thread_set_priority()函数:

void thread_set_priority (int new_priority) {struct thread *t;enum intr_level old_level;old_level = intr_disable();t = thread_current();t->base_priority = new_priority;if(t->base_priority > t->locks_priority)t->priority = t->base_priority;elset->priority = t->locks_priority;thread_yield();intr_set_level(old_level);}

12.下面来修改semaphore的相关操作,保证在每次信号量增加时,选择的是等待队列中优先级最高的线程。

synch.c:修改后的sema_up()函数:

void sema_up (struct semaphore *sema) {struct list_elem *e;struct thread *t;enum intr_level old_level;ASSERT (sema != NULL);old_level = intr_disable ();if (!list_empty (&sema->waiters)){//如果等待队列非空,从中选择优先级最高的线程放入到就绪队列中e = list_max(&sema->waiters, &thread_less_priority, NULL);list_remove(e);t = list_entry(e, struct thread, elem);thread_unblock(t);}sema->value++;intr_set_level (old_level);thread_yield();}

13.剩下的就是实现cond的优先级版本了。使用semaphore来实现cond。

synch.h:修改后的struct condition结构体:

struct condition {struct semaphore sema;};

14.修改cond_init()函数,利用semaphore实现。

synch.c:修改后的cond_init()函数:

void cond_init (struct condition *cond){ASSERT (cond != NULL);sema_init(&cond->sema,0);}

15.修改cond_wait()函数,利用semaphore实现。

synch.c:修改后的cond_wait()函数:

void cond_wait (struct condition *cond, struct lock *lock) {ASSERT (cond != NULL);ASSERT (lock != NULL);ASSERT (!intr_context ());ASSERT (lock_held_by_current_thread (lock));lock_release (lock);sema_down (&cond->sema);lock_acquire (lock);}

16.修改cond_signal()函数,利用semaphore实现。

synch.c:修改后的cond_signal()函数:

void cond_signal (struct condition *cond, struct lock *lock UNUSED) {ASSERT (cond != NULL);ASSERT (lock != NULL);ASSERT (!intr_context ());ASSERT (lock_held_by_current_thread (lock));if (!list_empty (&cond->sema.waiters)) sema_up(&cond->sema);}

17.修改cond_broadcast()函数,利用semaphore实现。

synch.c:修改后的cond_broadcast()函数:

void cond_broadcast (struct condition *cond, struct lock *lock) {ASSERT (cond != NULL);ASSERT (lock != NULL);while (!list_empty (&cond->sema.waiters))cond_signal (cond, lock);}

2.3 实验结果

3. Task 3:多级反馈调度

3.1 实验要求

1.在Pintos启动时选择调度算法策略;

2.默认情况下,优先级调度程序处于活动状态,能选择BSD 调度;

3.高级调度程序不执行优先级捐赠;

4.启用BSD调度程序后,线程不再直接控制自己的优先级。

5.线程通过thread_get_priority()调度设置应该线程的当前优先级。

3.2 基础知识

1)进程的基本状态及切换

在Pintos中,线程切换的触发如下所示

其中shedule即表示线程切换;thread_block表示线程阻塞;thread_exit表示线程退出,thread_schedule_tail在线程切换前使用,湖区当前线程,分配并恢复之前执行的状态和现场,如果当前线程死亡,清空资源;thread_yield表示线程让步,主动让出时间片;

2)多级反馈队列调度算法

Multilevel Feedback是UNIX的一个分支BSD(加州大学伯克利分校开发和发布的)5.3版所采用的调度算法,是一个综合调度算法。

  1. 算法详细描述
  • 设置多个就绪队列,第一级队列优先级最高
  • 给不同就绪队列中的进程分配长度不同的时间片,第一级队列时间片最小;随着队列优先级别的降低,时间片增大
  • 当第一级队列为空时,在第二级队列调度,以此类推
  • 各级队列按照时间片轮转方式进行调度
  • 当一个新创建进程就绪后,进入第一级队列
  • 进程用完时间片而放弃CPU,进入下一级就绪队列
  • 由于阻塞而放弃CPU的进程进入相应的等待队列,一旦等待的事件发生,该进程回到原来一级就绪队列
  • 若允许抢占:当有一个优先级更高的进程就绪时,可以抢占CPU。被抢占的进程回到原来一级就绪队列末尾
  1. 计算线程优先级

优先级计算公式:priority=PRI_MAX-(recent_cpu/4)-(nice*2)

其中:

1.  recent_cpu是线程最近使用的CPU时间的估计值

recent_cpu=(2*load_avg)/(2*load_avg+1)*recent_cpu+nice

2.  load_avg=(59/60)*load_avg+(1/60)*ready_threads

3.  其中recent_cpu、load_avg均采用指数加权移动平均算法。

3.3 实验过程

1.首先,实现定点小数运算fixed_point.h,并在thread.c、thread.h中引入

2.  添加变量——在thread.c中定义全局变量load_avg,在thread.h中的struct thread结构体中增加nice和recent_cpu定量,并在thread.c中的init_thread()函数对其初始化。

3.在/src/devices/timer.c中的timer_interrupt函数中实现线程优先级定时更新功能,根据实验要求,线程的优先级priority每4 ticks更新一次,recent_cpu、load_avg、nice每1 ticks更新一次,我们通过修改timer_interrupt实现该线程优先级更新功能。

4.  实现上述的thread_mlfqs_increase_recent_cpu_by_one函数

5.实现上述thread_mlfq_update_load_avg_and_recent_cpu函数

6.  实现BSD调度下的优先级更新

7.  实现线程中的thread_set_nice、thread_get_nice、thread_get_load_avg、thread_get_recent_cpu待填充函数

8.在thread.h中加入本次添加的函数

3.4 实验结果

3.5 收获与总结

通过完成本次实验,我收获了很多,本次实验主要在ubuntu系统上对Pintos操作系统进行调整测试。我的任务主要是实现Thread任务中的高级线程调度(Mission 3),帮助进一步巩固了进程的基本状态及切换、多级反馈队列调度算法和CPU调度算法的知识。

进程的基本状态反映了进程执行过程的变化。包括就绪状态、执行状态、阻塞状态、终止状态,分别对应了thread状态中的thread_ready、thread_running、thread_block、thread_dying。贯穿进程调度的整个过程,是进程调度的基础。

CPU调度算法-BSD,较好平衡了现场的不同需求,其中priority的根据recent_cpu、nice解决。其中recent_cpu是线程最近使用的CPU时间的估计值。近期recent_cpu越大优先级越低。

但是遗憾的是,对于多级反馈队列,我并没有在pintos源代码里面找到过于过个队列的描述以及时间片轮转算法的事项等内容。

PINTOS——Project 1: Threads相关推荐

  1. Pintos project 1 实验报告(代码分享)

    任务一 Alarm Clock 用到的相干目录有: pintos/src/devices time.h && time.c pintos/src/threads thread.h &a ...

  2. pintos project (2) Project 1 Thread -Mission 1 Code

    上一篇博客 分析了一下Mission 1中的代码,现在我们来开始正式实现. 首先,给线程结构体 struct thread{}加上ticks_blocked成员,在threads/thread.h中 ...

  3. 操作系统课程项目 OS project —— Pintos from Project 1 to Project 3

    Pintos Project 陪伴我们操作系统课程大半个学期了-- 虽然做了这么长时间,个人能力有限,pintos代码,看过的可能也就看懂了一半吧,更不用说没看过的了-- 但是也找到了一些有价值的资料 ...

  4. Pintos Project1:Thread 实验报告

    Pintos Project1:Thread 实验报告 Pintos Project1:Thread 实验报告 一.Pintos简介 二.Ubuntu下安装Pintos 1.安装bochs 2.安装P ...

  5. python多线程守护线程_Python守护程序线程

    python多线程守护线程 In this tutorial we will be learning about Python Daemon Thread. In our previous tutor ...

  6. java加法的底层_常见开发语言加减乘除底层是如何做到的?

    谢邀 语言运行有两大类:解释器,编译器. 编译 需要编译的语言,以 c.c++ 为代表,你写的高级语言先经过一个编译器转为汇编语言,再用汇编器转为机器语言.常见的汇编语言有 Intel 和 AT&am ...

  7. SQLDirect组件库用户指南

    SQLDirect组件库 For Delphi 4, 5, 6, 7, 8, 9 (2005), 10 (2006), 2007 和C++ Builder 4, 5, 6, 2006, 2007 ve ...

  8. 【UCB操作系统CS162项目】Pintos Lab1:线程调度 Threads

    实验文档链接:Lab1: Threads 我的实现(更新至Lab 2):Altair-Alpha/pintos 开始之前 如文档所述,在开始编写代码之前需要先阅读掌握 GETTING STARTED ...

  9. 操作系统课程设计报告2021-2022——pintos

    操作系统课程设计报告 2021-2022 目录 操作系统课程设计报告 2021-2022 第一章 实验项目介绍 环境配置 ( 一 ). Ubuntu 服务器搭建 图形界面搭建 ( 二 ). Pinto ...

最新文章

  1. 某程序员感叹:本是家族边缘人,但亲戚得知自己年入二百万后,都上来巴结!...
  2. mongodb 压缩——3.0+支持zlib和snappy
  3. powerdesigner 生成数据库脚本
  4. Go 语言能取代 Java,成为下一个 10 年的王者吗?
  5. 理解进程调度时机跟踪分析进程调度与进程切换的过程
  6. 前端学习笔记--HTTP缓存
  7. 国内外研究主页集合:计算机视觉-机器学习-模式识别
  8. webservice小坑
  9. python写webservice接口_Python开发WebService系列教程之REST,web.py,eurasia,Django
  10. 前端特效-霓虹灯按钮
  11. Android动态换肤框架PrettySkin原理篇(一)- LayoutInflater的理解及使用
  12. C语言中文件包含#include使用尖括号和双引号的区别
  13. android 自定义view实现仿QQ运动步数进度效果
  14. 测绘资质在线处理资质问题
  15. 无人驾驶引发的变革比想象的更快,留给车企和老司机的时间已不多
  16. 病毒丨熊猫烧香病毒分析
  17. Broadcast 和 BroadcastReceiver
  18. 亿级流量电商详情页系统的大型高并发与高可用缓存架构实战 目录
  19. 实用的链上数据查询工具——链数查
  20. 解决pip install ninjia 后,依旧报错的问题

热门文章

  1. 汇聚数据库创新力量,加速企业数字化转型
  2. C语言-打卡机(sqlite数据库、多线程)
  3. 计算机化工应用答案,计算机化工应用习题与解答.pdf
  4. HBase系列2-HBase快速入门
  5. hmmlearn使用简介
  6. wincc逻辑运算符_工控随笔_11_西门子_WinCC的VBS脚本_02_运算符
  7. 字节跳动自研线上引流回放系统的架构演进
  8. Android - View - ViewPager
  9. 《Android游戏编程之从零开始》书评之基础的魅力
  10. 关于context:property-placeholder的一个有趣现象