Pintos project 1 实验报告(代码分享)
任务一 Alarm Clock
用到的相干目录有: pintos/src/devices time.h && time.c
pintos/src/threads thread.h && thread.c
预期pass希望:暂无,安装pintos之后,第一次对threads进行make check操作,可以发现以alarm--开头的前六个文件,以及mlfqs-fair-2和mlfqs-fair-20文件,一开始就测试成功pass。但是为了避免对后期的修改有影响,这里还是要进行修改。
修改目的:可以正确阻塞线程,记录阻塞线程的时间,确保正确唤醒线程
具体修改算法
timer.c中实现了线程休眠的函数thread_sleep
(),当线程休眠时必须保证中断是打开的。当前执行进程调用timer_sleep(ticks)
时,函数通过不断轮询检查经过时间是否达到ticks,若还没达到则调用thread_yield
()函数,达到了ticks就会结束休眠。这个函数作用是实现让某个线程睡眠ticks时间,即让该线程暂时放弃CPU。
实现思路: 调用timer_sleep
()的时候直接把线程阻塞掉,然后给线程结构体加一个成员ticks_blocked
()来记录这个线程被sleep了多少时间, 然后利用操作系统自身的时钟中断(每个tick会执行一次)加入对线程状态的检测, 每次检测将ticks_blocked
减1, 如果减到0就唤醒这个线程。
数据结构修改
<thread.h>
struct thread{
//--------------
//--------------
/* 记录线程被阻塞的时间. */
int64_t ticks_blocked;}
函数修改
<time.c>
修改函数
timer_sleep()
修改函数
timer_interrupt()
<thread.c>
修改函数
init_thread()
添加函数
check_blocked_thread()
①在<timer.c>中修改timer_sleep()
函数
检测ticks是否有效 ,记录当前睡眠时间
void
timer_sleep (int64_t ticks)
{if (ticks <= 0)//检测ticks是否有效{return;}ASSERT (intr_get_level () == INTR_ON);enum intr_level old_level = intr_disable ();struct thread *current_thread = thread_current ();current_thread->ticks_blocked = ticks;//当前线程睡眠的时间thread_block ();//进程阻塞intr_set_level (old_level);
}
②修改<time.c> 中的timer_interrupt()
函数
修改时钟中断处理函数, 加入线程sleep时间的检测, 检测哪些进程使用block状态并且含有剩余的休眠时间。加在timer_interrupt
内。此处的检测必须同时满足两个条件,因为操作系统中存在进程因为等待锁而阻塞的状态,而不是主动调取timer_sleep
函数的结果
timer_interrupt (struct intr_frame *args UNUSED)
{ticks++;thread_tick ();
/*添加了 thread_foreach (check_blocked_thread, NULL);这句话*/
thread_foreach (check_blocked_thread, NULL);
}
③修改<thread.c>中的init_thread
()函数
添加新属性
/* 记录线程被阻塞的时间. */
int64_t ticks_blocked;
我们利用操作系统自身的时钟中断(每个tick会执行一次)加入对线程状态的检测, 每次检测将ticks_blocked
减1, 如果减到0就唤醒这个线程。
为了线程可以顺利创建,所以我们要保证刚创建时线程对应的ticks_blocked
为0, 加在init_thread
()函数内:
t->ticks_blocked = 0;//初始化为0
④添加<thread.c>新函数 check_blocked_thread
()
当前待检进程为阻塞状态并且阻塞时间剩余大于0个计时器刻度时执行,剩余刻度自减。若减为0,调用thread_unblock
函数将当前进程加入到就绪队列。aux就是传给这个函数的参数。
/*新函数*/
/* Check the blocked thread */
void
check_blocked_thread(struct thread *t, void *aux UNUSED)
{if (t->status == THREAD_BLOCKED && t->ticks_blocked > 0){t->ticks_blocked--;if (t->ticks_blocked == 0){thread_unblock(t);}}
}
同时在<thread.h>中申明这个函数
void check_blocked_thread(struct thread *t, void *aux UNUSED);
任务二 实现优先级调度
用到的相干目录:pintos/src/threads thread.h && thread.c
预期pass希望: alarm-priority priority-xxx
目的:在Pintos中实现优先级调度。当一个线程被添加到具有比当前运行的线程更高优先级的就绪列表时,当前线程应该立即将处理器放到新线程中。类似地,当线程正在等待锁,信号量或条件变量时,应首先唤醒优先级最高的等待线程。线程可以随时提高或降低自己的优先级,但降低其优先级以使其不再具有最高优先级必须使其立即产生CPU。
实施优先级捐赠,需要考虑需要优先捐赠的所有不同情况。务必处理多个捐赠,其中多个优先级捐赠给单个线程。
将任务二分为两个部分:
①实现优先级捐赠
②维持就绪队列为一个优先级队列,并且能够抢占CPU
具体修改算法:
先总结一下所有测试整合的逻辑:
在一个线程获取一个锁的时候, 如果拥有这个锁的线程优先级比自己低就提高它的优先级,并且如果这个锁还被别的锁锁着, 将会递归地捐赠优先级, 然后在这个线程释放掉这个锁之后恢复未捐赠逻辑下的优先级。
如果一个线程被多个线程捐赠, 维持当前优先级为捐赠优先级中的最大值(acquire和release之时)。
在对一个线程进行优先级设置的时候, 如果这个线程处于被捐赠状态, 则对original_priority进行设置, 然后如果设置的优先级大于当前优先级, 则改变当前优先级, 否则在捐赠状态取消的时候恢复original_priority。
在释放锁对一个锁优先级有改变的时候应考虑其余被捐赠优先级和当前优先级。
将信号量的等待队列实现为优先级队列。
将condition的waiters队列实现为优先级队列。
释放锁的时候若优先级改变则可以发生抢占
数据结构修改
<thread.h>修改thread数据结构, 加入以下成员:
int base_priority; /* 优先级 */
struct list locks; /* 当前进程持有的锁*/
struct lock *lock_waiting; /* 当前进程正在被什么锁阻碍 */
<synch.h>修改lock数据结构,加入以下成员:
struct list_elem elem; /* struct thread中locks的链表元素 */
int max_priority; /* 获取线程最大优先级 */
优先级捐赠函数功能修改
<synch.c>
修改函数
lock_acquire
()修改函数
lock_release
()新增函数
lock_cmp_priority
()
<thread.c>
修改函数
init_thread
()修改函数
thread_set_priority
()新增函数
thread_donate_priority
()新增函数
thread_remove_lock
()新增函数
thread_hold_the_lock
()新增函数
thread_cmp_priority
()修改函数
thread_unblock
()修改函数
thread_create
()
优先级捐赠关键实现函数实现
lock_acquire
()函数
功能:在P操作之前递归地实现优先级捐赠, 然后在被唤醒之后(此时这个线程已经拥有了这个锁),成为这个锁的拥有者。
void
lock_acquire (struct lock *lock)
{struct thread *current_thread = thread_current ();struct lock *l;enum intr_level old_level;ASSERT (lock != NULL);ASSERT (!intr_context ());ASSERT (!lock_held_by_current_thread (lock));if (lock->holder != NULL && !thread_mlfqs){current_thread->lock_waiting = lock;//成为锁的拥有者l = lock;while (l && current_thread->priority > l->max_priority){l->max_priority = current_thread->priority;//设置优先级thread_donate_priority (l->holder);//捐赠优先级l = l->holder->lock_waiting;}}sema_down (&lock->semaphore);old_level = intr_disable ();current_thread = thread_current ();if (!thread_mlfqs){current_thread->lock_waiting = NULL;lock->max_priority = current_thread->priority;thread_hold_the_lock (lock);}lock->holder = current_thread;intr_set_level (old_level);
}
lock_release
()函数实现
重新实现lock_release
函数,在释放锁对一个锁优先级有改变的时候还应考虑其余被捐赠优先级和当前优先级
if(!thread_mlfqs) {//将锁从相应的队列中移除list_remove(&(lock->holder_elem));lock->lock_priority = PRI_MIN-1;}sema_up (&lock->semaphore);//pro1 addedif(!thread_mlfqs) {//如果当前的线程持有的锁的队列为空,就恢复到原来的优先级if(list_empty(&curr->locks)) {curr->donated = false;thread_set_priority (curr->old_priority);}//否则将锁队列头(优先级最高)的优先级捐赠给它——donate-multipleelse{I = list_front(&curr->locks);another = list_entry(I, struct lock, holder_elem);thread_set_other_priority (thread_current(), another->lock_priority, false);}
list_insert_ordered
()函数
功能:实现队列的线程按照优先级高低排序
同时这里对thread_unblock
() && thread_yield
()进行修改,把list_push_back
改成
list_insert_ordered (&ready_list, &t->elem, (list_less_func *) &thread_cmp_priority, NULL);
- thread_hold_the_lock()函数
功能: 如果一个线程被多个线程捐赠, 维持当前优先级为捐赠优先级中的最大值
enum intr_level old_level = intr_disable ();
//实现优先级排序,选最大的
list_insert_ordered (&thread_current ()->locks, &lock->elem, lock_cmp_priority, NULL);if (lock->max_priority > thread_current ()->priority)
{//维持最大捐赠优先级thread_current ()->priority = lock->max_priority;thread_yield ();
}
thread_donate_priority
()函数介绍
功能:实现优先级捐赠,通过直接修改锁的最高优先级, 然后调用update的时候把现成优先级更 新实现的
//前面需判断是否是多级反馈调度,如果是就要禁止优先级捐赠
enum intr_level old_level = intr_disable ();thread_update_priority (t);
//如果设置的线程是ready的状态,因为优先级改变了,需要重新按照优先级的顺序放入ready队列中if (t->status == THREAD_READY){list_remove (&t->elem);list_insert_ordered (&ready_list, &t->elem, thread_cmp_priority, NULL);}
priority_higher
()函数介绍功能:实现判断两个线程的优先级高低函数
thread_unblock
() &&thread_create
() &&thread_yield
修改实现抢占式调度,如果新建的线程优先级高于正在运行的线程,则当前线程需要让出CPU。这里只需要在
thread_create
函数中的thread_unblock
之后比较一下新创建的线程的优先级与当前正在运行的线程的优先级的高低,然后决定是否需要让出CPU。创建的线程的优先级比当前线程的高的话,当前线程就要让出CPU,在
thread_create
函数中添加一下代码
if (thread_current ()->priority < priority){thread_yield ();}
lock_cmp_priority
()函数介绍功能:实现锁队列排序
thread_set_priority
()函数实现功能:为确保最高优先级的线程运行,需要重新计算调度的时刻有:创建新线程,设置线程优先级
void
thread_set_priority (int new_priority)
{if (thread_mlfqs)return;enum intr_level old_level = intr_disable ();struct thread *current_thread = thread_current ();int old_priority = current_thread->priority;current_thread->base_priority = new_priority;if (list_empty (¤t_thread->locks) || new_priority > old_priority){current_thread->priority = new_priority;thread_yield ();}intr_set_level (old_level);
}
优先级队列函数实现
<synch.c>
修改函数
cond_signal
()修改函数
sema_up
()修改函数
sema_down
()新增函数
cond_sema_cmp_priority
()
①cond_signal
()函数修改
功能:调用list_sort()
函数,将condition的waiters队列实现为优先级队列
list_sort (&cond->waiters, cond_sema_cmp_priority, NULL);
②cond_sema_cmp_priority
()函数介绍
功能:信号量比较函数
③sema_up
()函数
内部调用list_sort ()
函数,将sema的waiters队列实现为优先级队列
void
sema_up (struct semaphore *sema) //相当于signal操作
{enum intr_level old_level;ASSERT (sema != NULL);//pro1 addedstruct thread *wake_up; //需要唤醒的线程wake_up = NULL;//pro1 addedold_level = intr_disable ();if (!list_empty (&sema->waiters)) {/*thread_unblock (list_entry (list_pop_front (&sema->waiters),struct thread, elem));*///pro1 added//对sema的waiters队列按照优先级进行排序list_sort (&sema->waiters, priority_higher, NULL); //唤醒队列头,也就是队列中优先级最高的线程wake_up = list_entry (list_pop_front (&sema->waiters), struct thread, elem);thread_unblock (wake_up);//pro1 added}sema->value++; //V//pro1 added//如果当前线程的优先级比唤醒的线程的低,就要放弃CPUif(wake_up != NULL && thread_current()->priority < wake_up->priority)thread_yield();//pro1 addedintr_set_level (old_level);
}
④sema_down
()函数实现
实现wait操作,使value--
任务三 实现反馈调度
涉及到的目录:pintos/src/threads/ thread.h && thread.c 预期pass:mlfqs-xxx
目的:实现多级反馈队列调度程序,减少在系统上运行作业的平均响应时间。
具体修改算法
实现思路: 在timer_interrupt中固定一段时间计算更新线程的优先级,这里是每TIMER_FREQ时间更新一次系统load_avg和所有线程的recent_cpu, 每4个timer_ticks更新一次线程优先级, 每个timer_tick running线程的recent_cpu加一, 虽然这里说的是维持64个优先级队列调度, 其本质还是优先级调度, 我们保留之前写的优先级调度代码即可, 去掉优先级捐赠。
计算公式
priority = PRI_MAX - (recent_cpu / 4) - (nice * 2)
nice
是线程自己控制的一个值,范围在-20~20。优先级每四个tick重新计算并排列一次。recent_cpu=(2load_avg)/(2load_avg+1)*recent_cpu+nice
recent_cpu
测试当前线程在当前时钟周期(或当前很小的一个时间单位里)被分到的CPU时间。每次时钟tick
,recent_cpu
加1,recent_cpud
的值每秒钟刷新一次。load_avg = (59/60)*load_avg + (1/60)*ready_threads
load_avg
估计CPU需要处理的线程数目,其值初始为0,每秒钟重新计算一次定点小数及其计算
定点小数及其计算
在上面的公式中,priority
、nice
和ready_thread
是整数,但是recent_cpu
和load_avg
是实数。 但是,Pintos不支持内核中的浮点运算,因为它会使内核变得复杂和变慢。 出于同样的原因,真正的内核通常具有相同的限制。 这意味着必须使用整数模拟实数的计算。 本节介绍基础知识。
其基本思想是将整数的最右边的几位视为表示分数。例如,我们可以将带符号的32位整数的最低14位指定为小数位,这样整数x代表实数 x/214 。这个叫做 17.14 定点数,最大能够表示 (231-1)/214 ,近似 131071.999 。
我们知道bool
变量thread_mlfqs
指示是否启用高级调度程序,并且高级调度程序不应包含优先级捐赠的内容,所以,在Mission_2中实现的优先级捐赠代码,需要使用if
判断以保证在使用高级调度程序时,不启用优先级捐赠。随后,我们可以根据实验指导中的详细说明一步步完成本次实验。
首先,根据上述给定点数的计算方法,编写定点数的计算方法
/* Basic definitions of fixed point. */
typedef int fixed_t;
/* 16 LSB used for fractional part. */
#define FP_SHIFT_AMOUNT 16
/* Convert a value to fixed-point value. */
#define FP_CONST(A) ((fixed_t)(A << FP_SHIFT_AMOUNT))
/* Add two fixed-point value. */
#define FP_ADD(A,B) (A + B)
/* Add a fixed-point value A and an int value B. */
#define FP_ADD_MIX(A,B) (A + (B << FP_SHIFT_AMOUNT))
/* Substract two fixed-point value. */
#define FP_SUB(A,B) (A - B)
/* Substract an int value B from a fixed-point value A */
#define FP_SUB_MIX(A,B) (A - (B << FP_SHIFT_AMOUNT))
/* Multiply a fixed-point value A by an int value B. */
#define FP_MULT_MIX(A,B) (A * B)
/* Divide a fixed-point value A by an int value B. */
#define FP_DIV_MIX(A,B) (A / B)
/* Multiply two fixed-point value. */
#define FP_MULT(A,B) ((fixed_t)(((int64_t) A) * B >> FP_SHIFT_AMOUNT))
/* Divide two fixed-point value. */
#define FP_DIV(A,B) ((fixed_t)((((int64_t) A) << FP_SHIFT_AMOUNT) / B))
/* Get integer part of a fixed-point value. */
#define FP_INT_PART(A) (A >> FP_SHIFT_AMOUNT)
/* Get rounded integer of a fixed-point value. */
#define FP_ROUND(A) (A >= 0 ? ((A + (1 << (FP_SHIFT_AMOUNT - 1))) >> FP_SHIFT_AMOUNT) \: ((A - (1 << (FP_SHIFT_AMOUNT - 1))) >> FP_SHIFT_AMOUNT))
由于本实验涉及到的公式均与时钟中断有关,所以,我们将上述的公式迭代计算的相关代码,写入我们在任务一中详细介绍过的timer_interrupt()
时钟中断处理函数中。
数据结构修改
<thread.h>
struct thread{int nice; /*mlfqs中和线程优先级相关的变量,记录线程的友好程度 */fixed_t recent_cpu; /*最近使用CPU的时间 */
}
线程初始化时,给这两个变量赋初值 修改init_thread()
t->nice = 0;
t->recent_cpu = FP_CONST (0);
函数功能修改
<thread.c>
添加函数
thread_mlfqs_increase_recent_cpu
() 计算recent_cpu添加函数
thread_mlfqs_update_priority
() 更新优先级添加函数
thread_mlfqs_update_load_avg_and_recent_cpu
()添加函数
thread_set_nice
()添加函数
thread_get_nice
()添加函数
thread_get_load_avg
() 计算load_avg添加函数
thread_get_recent_cpu
() 计算recent_cpu添加全局变量
fixed_t load_avg;//全局变量
初始化
load_avg = FP_CONST (0);//在thread_start中
<timer.c>
修改函数
timer_interrupt
功能:每隔一段时间计算当前优先级并刷新
if (thread_mlfqs){thread_mlfqs_increase_recent_cpu();if (ticks % TIMER_FREQ==0)//刷新所有线程的load_avg和recent_cpu参数thread_mlfqs_update_load_avg_and_recent_cpu ();else if (ticks%4 == 0)thread_mlfqs_update_priority (thread_current ());}
①thread_mlfqs_update_load_avg_and_recent_cpu
()函数实现
功能:首先根据就绪队列的大小计算load_avg
的值,随后根据load_avg
的值,更新所有进程的recent_cpu
值及priority
值
/* 每秒刷新所有线程的 load_avg 和 recent_cpu */
void
thread_mlfqs_update_load_avg_and_recent_cpu (void)
{ASSERT (thread_mlfqs);ASSERT (intr_context ());size_t ready_threads = list_size (&ready_list);if (thread_current () != idle_thread)ready_threads++;//重新计算load_avgload_avg = FP_ADD (FP_DIV_MIX (FP_MULT_MIX (load_avg, 59), 60), FP_DIV_MIX (FP_CONST (ready_threads), 60));struct thread *t;struct list_elem *e = list_begin (&all_list);for (; e != list_end (&all_list); e = list_next (e)){t = list_entry(e, struct thread, allelem);if (t != idle_thread){//重新计算recent_cput->recent_cpu = FP_ADD_MIX (FP_MULT (FP_DIV (FP_MULT_MIX (load_avg, 2), FP_ADD_MIX (FP_MULT_MIX (load_avg, 2), 1)), t->recent_cpu), t->nice);//更新优先级thread_mlfqs_update_priority (t);}}
}
②thread_mlfqs_update_priority
()函数实现
功能:更新当前进程的priority
值,注意,一定要保证每个线程的优先级介于0(PRI_MIN
)到63(PRI_MAX
)之间。
t->priority = FP_INT_PART (FP_SUB_MIX (FP_SUB (FP_CONST (PRI_MAX), FP_DIV_MIX (t->recent_cpu, 4)), 2 * t->nice));
t->priority = t->priority < PRI_MIN ? PRI_MIN : t->priority;
//计算出来的优先级如果比最大值大就赋为最大值,比最小值小就赋为最小值
t->priority = t->priority > PRI_MAX ? PRI_MAX : t->priority;
③thread_set_nice
() &&thread_get_nice
()函数设置
thread_set_nice
()功能:将当前线程的 nice 值设置为 NICE
设置当前线程的nice,并根据nice的值来设置更新recent_cpu
和优先级
如果优先级改变后当前线程的优先级比ready队列里的线程的低,就要让出CPU
thread_get_nice
()功能:返回当前进程的nice值
④thread_get_load_avg
return FP_ROUND (FP_MULT_MIX (load_avg, 100));//计算公式实现
⑤thread_get_recent_cpu
()函数
return FP_ROUND (FP_MULT_MIX (thread_current ()->recent_cpu, 100));//计算公式
⑥变量添加
fixed_t load_avg;
在thread_start
中初始化:
load_avg = FP_CONST (0);
结果:27 pass 代码可以私信我,带上邮箱号
Pintos project 1 实验报告(代码分享)相关推荐
- 【计算机网络实验】动态路由协议——华为eNSP(详细实验报告+代码)
文章目录 3 实验四:动态路由协议 3.1 实验目的和内容 3.1.1实验目的 3.1.2 实验内容 3.2 实验过程 3.2.1 基于RIP动态路由协议的网络互联 3.2.2 基于OSPF动态路由协 ...
- 【计算机网络实验】访问控制列表NAT应用——华为eNSP(详细实验报告+代码)
文章目录 4 实验五:访问控制列表---NAT应用 4.1 实验目的和内容 4.2 实验过程 4.2.1 基本ACL 4.2.2 高级ACL 4.3 实验分析 4.4 实验心得 4 实验五:访问控制列 ...
- 【计算机网络实验】静态路由基础——华为eNSP(详细实验报告+代码)
文章目录 2.实验三:静态路由基础 2.1 实验目的和内容 2.1.1 实验目的 2.1.2 实验内容 2.2 实验过程 2.2.1 静态路由配置 2.2.2 负载分担 2.2.3 路由备份 2.2. ...
- c语言八数码A星算法代码解析,八数码问题c语言a星算法详细实验报告含代码解析...
八数码问题c语言a星算法详细实验报告含代码解析 (13页) 本资源提供全文预览,点击全文预览即可全文预览,如果喜欢文档就下载吧,查找使用更方便哦! 14.9 积分 一.实验内容和要求 八数码问题:在3 ...
- 传感与检测技术,光电二极管和光敏电阻的特性研究实验报告,江南大学物联网工程学院自动化
江南大学物联网工程学院传感与检测技术<光电二极管和光敏电阻的特性研究实验报告>分享,包括实验目的.实验仪器设备.实验原理及数据分析和思考与总结.仅供参考借鉴.
- java实验报告系统分析怎么写_20155218 《Java程序设计》实验二(Java面向对象程序设计)实验报告...
20155218 <Java程序设计>实验二(Java面向对象程序设计)实验报告 一.实验内容及步骤 (一)单元测试 主要学习安装和使用junit来测试编写的程序,并学习以TDD(Test ...
- java实验报告:实验一 基于控制台的购书系统
注:博主为初学java,此实验报告代码仅供参考,如有错误,还望指正. 水平有限,仅供参考 实验目的及要求 1.理解和掌握面向对象的设计程序 2.会用类图进行面向对象设计 3.掌握封装的实现及好处 4. ...
- c语言指针部分上机,北科大C语言程序设计实验报告8-指针-练习题代码(2次上机课内容)--...
北科大C语言程序设计实验报告8-指针-练习题代码(2次上机课内容)-- 下载提示(请认真阅读)1.请仔细阅读文档,确保文档完整性,对于不预览.不比对内容而直接下载带来的问题本站不予受理. 2.下载的文 ...
- 【ACM程序设计实验】高分课程实验报告分享
title : [ACM程序设计实验]高分课程实验报告分享 tags : 大学课程结项 date : 2022-7-8 author : Linno 笔者本身在ACM集训队训练,这门课是选上了之后教练 ...
最新文章
- R语言return返回值的形式实战
- TensorFlow基础5-可训练变量和自动求导机制
- Docker 镜像优化:从 1.16GB 到 22.4MB
- VC中如何使某一窗口始终居于其它窗口前面?
- mysql性能优化的一些建议
- python教程list类型_Python数据类型之list相关常用操作
- centos7 mysql tar_CentOS7中mysql-5.7.21-el7-x86_64.tar.gz版MySQL的安装与配置
- 计算机网络(七)-物理层设备
- 撸表情开发过程中使用腾讯云存储的接入实例分享
- jmeter利用influxdb和grafana实现数据可视化
- 阿里云oss权限控制,上传下载测试
- starway(NOIP模拟测试24)
- 4.3 齐次线性方程组
- 使用enum代替Constants
- PMIC驱动之—PMIC硬件相关知识
- linux脚本:每天晚上 12 点,打包站点目录/var/www/html 备份到/data 目录下
- 2、基因树 与物种树的关系及建树
- 火狐3本月17日发布 与IE 8设计理念完全不同
- 中国求职者2亿私人简历泄露,APT情报资讯报告2018全球十大安全事件(10401字) (附PDF公号发“简历十大安全”下载)
- JSP中使用EL表达式
热门文章
- (附源码连接)利用AI将2D照片合成3D场景
- 计算促销最优组合方案
- 算法习题集 - Miscellaneous
- android 磨砂玻璃效果
- 计算机在维氏硬度的应用,华银显微维氏硬度计HVS-1000A使用方法
- 英语前缀 2011年8月16日15:55:43
- Python scrapy学习之爬取2k唯美壁纸详细过程笔记及讲解
- [EDI实施案例] 耐世特/Nexteer DESADV报文的业务解读
- 一篇评价牛顿的搞笑文章,作者老罗,但很有才
- 南昌理工学院计算机科学与技术排名,南昌理工学院排名2018年下滑5名 在民办大学中实力排全国第38名...