更正:

第一题中,哲学家就餐问题中的哲学家的状态state[i]应属于临界区变量,是可能会产生读写冲突的,所以对其进行读写的时候均需要加一把互斥锁。

非常感谢不听不听不听的指正。

--------------------------------分割线--------------------------------

大家好!今天给大家带来的是操作系统概念第十版的期中大作业。

题目概述

总体要求

完成以下三个题目。

1. 哲学家就餐(dinning philosophers)问题:dph.c

2. 生产者消费者(producer-consumer)问题:prod.c和cons.c

3. Linux内核实验:(c)complete fair scheduler(CFS)内核代码阅读报告(b)添加系统调用:用户层测试程序mycall.c

该项目目录下,手工编写Makefile,通过命令行下的make all命令,可以同时产生上述文件对应的可执行文件:dph,prod, cons, mycall;通过单独的make 目标,可以产生单独的bin文件。比如make dph可以单独生成dph。

该目录下,同时存放1个word文件,按上述顺序包含实验内容报告,每个题目安排一大节。

题目占分:题目1:20分,题目2:30分,题目3(a):30分,题目3(b):20分. 对于编程题,结果正确和代码质量(逻辑流程清晰,适当添加注释)分别占70%和30%的分数。

1. 哲学家就餐问题

参考课本(第十版)第7章project 3的要求和提示

1. 使用POSIX实现

5. 要求通过make,能输出dph文件,输出哲学家们的状态。打印结果,截屏放到作业报告中。

知识点:POSIX信号量、POSIX条件变量、互斥、循环队列

2. 生产者消费者问题

1. 需要创建生产者和消费者两个进程(注意:不是线程),一个prod,一个cons,每个进程有3个线程。它们之间的缓冲最多容纳20个数据。

2. 每个生产者随机产生一个数据,打印出来自己的id(进程、线程)以及该数据;每个消费者取出一个数据,然后打印自己的id和数据。

3. 生产者和消费者这两个进程之间通过共享内存来通信,通过信号量来同步。

4. 生产者生成数据的间隔和消费者消费数据的间隔,按照负指数分布来控制,各有一个控制参数

5. 运行的时候,开两个窗口,一个./prod

,另一个./cons
,要求测试不同的参数组合,打印结果,截屏放到作业报告里。

知识点:具名信号量、进程间通信、共享内存

3. Linux内核实验(Linux 4.0或以上)

a) 以CFS的主要文件Fair.c为起点,浏览相关联的文件。理解Linux进程的基本结构、状态设置,CPU的调度基本架构,理解CFS调度算法的基本流程和主要数据结构。摘取关键代码片段,用自己的的方式描述出来。不要求理解每一条语句,但需要陈述主要脉络。此外,单独回答以下问题:

1) 简述进程优先级、nice值和权重之间的关系

2) CFS调度器中的vruntime的基本思想是什么?是如何计算的?何时得到更新?其中的min_vruntime有什么作用?

b) 添加一个内核系统调用,重新编译内核,启动后运行screenfetch命令(可能需要安装),截屏显示结果,需要显示出运行主机的内核版本、CPU等信息(注意:每个同学在自己的机器上编译,这些信息会有所差异,以此作为同学们的作业区分。)

编写用户层程序mycall.c调用该调用,要求打印出当前进程的调度信息(如下图所示),通过dmesg可以查看。实现时,可以通过current访问sched_entity的数据成员。

dmesg应输出的信息

知识点:Linux内核代码分析及修改、系统调用表、系统调用实现及调度实体

题目解析

1.

第一道题其实是作业题,题目要求用POSIX的信号量和条件变量解决哲学家就餐问题。哲学家就餐问题的描述请自行查阅。

首先我们要明确一件事情,在防止死锁的解决条件下哲学家就餐问题有多种解法,其中有两种解法为:

1.单数的哲学家先拿起左边的筷子,再拿起右边的筷子。吃完饭时先放下右边的筷子,再放下左边的筷子;双数的哲学家先拿起右边的筷子,再拿起左边的筷子。吃完饭时先放下左边的筷子,再放下右边的筷子。这样可以防止一种死锁情况的发生:所有哲学家都先拿起左边的筷子。

2.哲学家当且仅当它的邻居们(注意这个们)都没有在吃饭的时候才会从HUNGRY状态变成EATING状态。

在这里我们讨论第二种解法。

首先我们定义哲学家的三种基本状态,分别是THINKING,HUNGRY和EATING。

enum {THINKING,HUNGRY,EATING}state[5];

然后我们使用条件变量来使得哲学家们在还不满足吃饭条件的时候进行等待,并给每一个哲学家的条件变量配上一把互斥锁。

pthread_cond_t self[5];// Behalf on each philosophers
pthread_mutex_t mutex[5]; // For conditional variables

然后我们定义五个线程的id,以及给线程函数传递的线程索引数组:

pthread_t tid[5];// For pthread_create()
int id[5];// For philo function

接下来,我们先摆出哲学家函数,并加以分析:

#define TRUE 1
void *philo(void *param){do{int id = *( (int *)param);/* Try to pickup a chopstick*/pickup_forks(id);printf("The philosopher %d is eating...n",id);/* Eat a while*/srand((unsigned)time(NULL));int sec = (rand()%((3-1)+1)) +1;// make sec in [1,3]sleep(sec);/* Return a chopstick */return_forks(id);printf("The philosopher %d is thinking...n",id);/* Think a while */srand((unsigned)time(NULL));sec = (rand()%((3-1)+1)) +1;// make sec in [1,3]sleep(sec);}while(TRUE);pthread_exit(NULL);
}

首先,我们传递一个索引给线程函数,使其知道自己是第几号哲学家。然后哲学家试图拿起两只筷子,如果成功拿起两只筷子,就随机睡眠一段时间(模拟吃饭)。然后放下筷子,并随机睡眠一段时间(模拟思考),重复上述过程。

接下来我们来研究拿起筷子的函数pickup_forks()和放下筷子的函数return_forks():

void pickup_forks(int i){state[i] = HUNGRY;// Wants to eattest(i);// Check can eat or notpthread_mutex_lock(&mutex[i]);while (state[i] != EATING){pthread_cond_wait(&self[i],&mutex[i]);//Wait his neighbors ate}pthread_mutex_unlock(&mutex[i]);
}

pickup_forks()函数接受一个参数i,代表哲学家的索引。拿起筷子前首先要把哲学家的状态设为饥饿(这样他才有机会拿起筷子),然后调用test函数判断是否满足吃饭条件(判断他的邻居们是否都不在吃饭)。如果不满足上述条件,该哲学家(基于条件变量)将会被挂起(相当于他在等待有饭吃),等待别人通知他可以吃饭(基于条件变量的唤醒)。

void return_forks(int i){state[i] = THINKING;//Notify his neighbor that I was ate.test((i+4)%5);test((i+1)%5);
}

return_forks()就显得比较简单,哲学家吃完之后会陷入思考阶段。并通知他的邻居们“我吃完饭了”,test函数的逻辑为:如果我的邻居们都没在吃饭,并且我饿了,我就进入吃饭状态,并解除我的挂起状态(如果我饿了,并且我未能切到吃饭状态前那我一定是被挂起的)。

void test(int i){// A philosopher can eat when he wants to eat and his neighbors aren't eating.if ( (state[(i+4)%5] != EATING)&&(state[i] == HUNGRY) &&(state[(i+1)%5] != EATING)){pthread_mutex_lock(&mutex[i]);state[i] = EATING;pthread_cond_signal(&self[i]);pthread_mutex_unlock(&mutex[i]);}}

那test就是,判断我是否能进入吃饭状态,如果能,进入吃饭状态并解除挂起状态。

所以该问题的整体解答如上所示。

附:如何在摁下Ctrl-C的时候可以正常退出程序呢?

当我们摁下Ctrl-C的时候,Linux内核捕获该特殊信息,并给进程发送一个SIGINT信号。如果有线程处于被挂起的状态(且不可被中断,就是state==TASK_UNINTERRUPTED),那么SIGINT信号将会被忽略。那么我们可以通过重写对SIGINT的处理函数,使得我们可以取消线程(因为线程有pthread_exit()函数,所以还是可以被取消的)。

信号处理重写函数signal()定义位于signal.h

在main()函数中,写下:

signal(SIGINT,func);

其中func为一个类型为函数指针的对象,该函数指针定义为:

void (*ptr)(int);

所以func是一个没有返回值,参数为一个int的函数。

func定义如下:

void func(int signum){printf("nKilling the philosophers...n");for (int i=0;i<5;i++){pthread_cancel(tid[i]);}for (int i=0;i<5;i++){pthread_cond_destroy(&self[i]);pthread_mutex_destroy(&mutex[i]);}printf("Killed philosophers.n");
}

在这里,我们试图“杀死”哲学家(通过pthread_cancel)。当线程全部被关闭后,销毁条件变量及其配套的互斥锁。

2.

第二道题属于经典的生产者-消费者问题,我们不对该题目做出描述,而是直接提供解决方案。

我们要明确,生产者可以为消费者生产满的缓冲区,消费者可以为生产者生产空的缓冲区。所以我们要定义两个信号量,以表示有多少个满的缓冲区,有多少个空的缓冲区(一个数据放在一个缓冲区内):

sem_t *full;
sem_t *empty;

为什么使用指针,后面将会解释说明。

那么每次生产/消费缓冲区的时候,都应该对临界区变量buffer上锁,那么我们可以使用pthread_mutex_t(POSIX提供的互斥锁)或者sem_t(人为规定的二值信号量)对buffer上锁。前者不会存在编程上的逻辑问题,后者若编程不当,会破坏这个互斥锁的二值性。

本文中使用后者来完成互斥锁的定义,互斥锁的定义如下:

sem_t *s_mutex;//mutex for struct

然后我们来定义缓冲区数据结构,我们使用循环队列来完成缓冲区的定义:

typedef int buffer_item;
#define BUFFER_SIZE 20
struct buf{int rear;int front;buffer_item buffer[BUFFER_SIZE];
};

首先,我们声明一个缓冲区对象,并进行清零。

struct buf sm;
memset(&sm,0,sizeof(struct buf));

然后我们读取

,并转化为数字:
int lambda_p = atof(argv[1]);

然后我们打开具名信号量(named semaphore),以方便两个进程之间共享信号量,并在生产者这边对其进行初始化:

full = sem_open("full",O_CREAT,0666,0);//sem_open returns a sem_t pointer
empty = sem_open("empty",O_CREAT,0666,0);
s_mutex = sem_open("mutex",O_CREAT,0666,0);
sem_init(full,1,0);
sem_init(empty,1,BUFFER_SIZE);
sem_init(s_mutex,1,1);

我们如何在两个进程之间传递数据呢?我们在之前的博文中讲过,父进程和子进程之间是可以通过共享内存(shared memory)进行通信的,那两个独立的进程治建能否使用共享内存进行数据共享呢?一样是可以的。

int shm_fd = shm_open("buffer",O_CREAT | O_RDWR,0666); //Create the shared memory
ftruncate(shm_fd,sizeof(struct buf));
ptr = mmap(0,sizeof(struct buf),PROT_WRITE,MAP_SHARED,shm_fd,0);//Mapped it into file, which can shared by different process

同样地,我们开辟一块共享内存,并将其截断到一个struct buf的长度(保证写越界的时候会有segmentation fault错误),然后将其映射到文件中以方便其他进程访问。

然后就是创建三个线程,在这里不赘述三个线程的创建方法,我们关心的应该是线程函数如何构建。

同样地,我们先摆出生产者函数,然后再逐条分析:

void *producer(void *param){int idx = *(int *)param;do{double interval_time = atof(lambda_p);usleep((unsigned int)(produce_time(interval_time)*1e6)); // sleep the time by the distribution of negative exponetial.buffer_item item = rand() % 255;struct buf *shm_ptr = ((struct buf *)ptr);// read the round queue's information from shared memory.sem_wait(empty); // If there is no empty buffer, block.sem_wait(s_mutex);//lock the binary-mutex and execute the code in critical area.printf("Producing the data %d to buffer[%d] by id %ldn",item,shm_ptr->rear,tid[idx]);shm_ptr->buffer[shm_ptr->rear] = item; // Put the data to round queue.shm_ptr->rear = (shm_ptr->rear+1) % BUFFER_SIZE;sem_post(s_mutex);//Unlock the binary-mutexsem_post(full);// Add a full buffer}while(1);pthread_exit(0);
}

首先,我们让生产者按照负指数分布睡眠一段时间(题目要求)。然后随机生产一个数字(个人习惯把它卡在[0,255]的范围内)。然后将ptr转化为struct buf类型的指针,以准备向共享内存中写入数据。接下来通过信号量empty查询空的缓冲区有多少个,如果该值大于0,就“拿走一个缓冲区”(通过sem_wait使empty减一),然后对缓冲区上锁(s_mutex),并根据循环队列的特性写入缓冲区的指定位置。然后去锁,并使信号量full+1(sem_post),代表着多了一个满的缓冲区。

如何返回一个符合负指数分布的时间呢?我们先来回顾负指数分布的公式,根据概率论,设

为随机变量,
为参数,则令
符合负指数分布的概率密度函数为:

对概率密度函数做积分,得到概率分布函数:

因此我们要求满足负指数分布的

,也就是求
的关系。

所以我们有:

我们可以认为

是一个在
上分布的随机变量,令其通过rand()函数生成。因为rand()函数生成的随机数范围为
,所以

所以返回一个符合负指数分布的随机变量的函数如下:

double produce_time(double lambda_p){double z;do{z = ((double)rand() / RAND_MAX);}while((z==0) || (z == 1));return (-1/lambda_p * log(z));
}

在消费者这边,我们要通过名字打开具名信号量,这样才可以通过具名信号量与生产者进行通信,具名信号量和共享内存的打开方式如下:

//In cons.c
sem_t *full;
sem_t *empty;
sem_t *s_mutex;
void *ptr;
full = sem_open("full",O_CREAT);//link the semaphore from producer
empty = sem_open("empty",O_CREAT);
s_mutex = sem_open("mutex",O_CREAT);
int shm_fd = shm_open("buffer",O_RDWR,0666);
ptr = mmap(0,sizeof(struct buf),PROT_READ | PROT_WRITE,MAP_SHARED,shm_fd,0); // read the shared memory from producer

当试图创建一个已经存在的具名信号量时,如果只指定了O_CREAT位,不会产生任何错误,只会直接返回该信号量在内存中的地址。

消费者的函数也是类似的,流程就是:先睡眠一段时间,然后试图获取满的缓冲区的信号量(如果没有就挂起),获得成功后对临界区变量shm_ptr上锁,并通过循环队列的特性读取数据。最后到达剩余区,并sem_post一个empty,表示产生了一个空的缓冲区。

void *consumer(void *param){int idx = *(int *)param;do{double interval_time = atof(lambda_c);sleep((unsigned int)produce_time(interval_time));struct shm_mutex *shm_ptr = ((struct shm_mutex *)ptr);sem_wait(full);//Wait for a full buffersem_wait(s_mutex);buffer_item item = shm_ptr->buffer[shm_ptr->front];shm_ptr->front = (shm_ptr->front+1) % BUFFER_SIZE;sem_post(s_mutex);sem_post(empty);//Add a empty bufferprintf("Consuming the data %d by pid %ldn",item,tid[idx]);}while (1);pthread_exit(0);
}

至于题目要求的打印进程的ID……我觉得进程就一边一个呀有啥好打印的……就没打印,如果要打印,获取当前进程的pid就好了。

3 a).

简述CFS的工作原理这个大家见仁见智,就自己回答吧。此处仅回答一下单独回答问题的我的答案:

1)nice值越大(代表着它对别的进程越友好),进程优先级越低,权重则越小。nice值与进程权重有一一对应的关系,可以通过数组prio_to_weight进行转换。

2)vruntime,全称virtual runtime,是一个记录当前进程运行时间的一个量,但是它不是记录真实的进程运行时间,而是根据权重来对进程运行时间对runtime进行放大/缩小,其基本思想就在于,在相同权重下,运行时间短的进程理应被优先选择。但是权重大的进程有着更高的优先级,理应有优先权,所以它的vruntime被放大的倍数就会小,从而可以保证高优先级的进程有更多的运行时间。

3 b).

如何添加一个内核系统调用?

首先我们要明白一点,系统调用是什么?

操作系统的主要功能是为管理硬件资源和为应用程序开发人员提供良好的环境来使应用程序具有更好的兼容性,为了达到这个目的,内核提供一系列具备预定功能的多内核函数,通过一组称为系统调用(system call)的接口呈现给用户。系统调用把应用程序的请求传给内核,调用相应的内核函数完成所需的处理,将处理结果返回给应用程序。 (via 百度百科)

在操作系统中,一般函数的执行分为用户态和内核态,内核态用于执行一些比较底层的,与硬件打交道的操作。这些操作是经过严密调试的,如果系统调用出问题可能会直接导致系统崩溃。当用户态程序调用系统调用时,用户态程序将会被挂起,并发起一个INT $0x80的中断,这个中断会导致系统调用陷入内核态,并完成指定的处理工作。

系统调用在操作系统中是固定的,但是我们可以通过修改内核源代码的方式来增加我们自己所需要的内核调用。

在Linux中,系统调用与以下三个文件相关:

source-code/arch/x86/entry/syscalls/syscall_64.tbl

source-code/include/linux/syscalls.h

source-code/kernel/sys.c

第一个是系统调用表,系统调用在内核加载的时候其实就已经固定下来,其查表的依据就是这张系统调用表。

第二个是系统调用的头文件,用于存放系统调用的声明。

第三个是系统调用的源文件,用于存放系统调用的实现。

首先我们看系统调用表的开头:

因为我们是64位系统,所以我们要在64位系统调用表的最后加上自己的系统调用:

系统调用表(64位)

最后一行就是我们自己做的系统调用,其入口为sys_mycall函数,系统调用号为326。

然后我们在syscalls.h声明自己的系统调用函数,注意这个函数的名字要和entry point相一致,并且要加上asmlinkage标识符,以通知编译器调用函数时要在堆栈里找参数而不要在寄存器中找参数。

syscalls.h

最后我们在sys.c中实现sys_mycall:

因为我们要输出内核信息,所以要使用printk函数以及KERN_INFO宏,为了美观我们可以使用%-40和%26等格式化符号,current是一个指向当前进程的task_struct的指针,而se是task_struct中sched_entity结构体的一个对象,用于存储当前进程的调度实体信息的。

我们完成系统调用的编写之后,重新编译内核。

重新编译内核有四个步骤:

make menuconfig

make

make modules_install

make install

重编译内核需要较大的磁盘空间,所以必须对主分区进行扩容。

第一步是生成.config文件,用于编译的,在这一步可以删除掉不必要的驱动(理论上可以把所有可选的驱动全部删掉,因为虚拟机只需要最基本的命令行界面)。

第二步是编译内核,这时候可以加上参数-j4或者-j8表示4核同时编译或8核同时编译。

第三步是安装模块

第四步是安装内核到系统中

然后reboot重启,并在开机的时候选择自己编译的内核。

然后我们完成mycall.c(用户级):

#include <unistd.h>
#include <sys/syscall.h>
#include <stdio.h>
#define _SYSCALL_MYCALL_ 326int main()
{syscall(_SYSCALL_MYCALL_);return 0;
}

就这么简单。

我也懒得打注释了,这个程序就是利用syscall函数来调用系统调用,指定系统调用号就可以了。

最后的输出效果大概如下吧:

screenfetch的结果,内核编译自Kernel Souce 4.4.194
mycall这个程序的调度信息

最后我们来讨论一下Makefile怎么写。

Makefile是make命令的配置文件,用于帮我们自动完成编译、安装、执行等一系列工作,在这里我们只需要编辑Makefile使其完成编译功能即可:

CC=gcc
CFLAGS=-Wall
RT=-lrt
POSIXT=-lpthread
MATH=-lm
all:$(CC) $(CFLAGS) prod.c -o prod $(RT) $(POSIXT) $(MATH)$(CC) $(CFLAGS) cons.c -o cons $(RT) $(POSIXT) $(MATH)$(CC) $(CFLAGS) dph.c -o dph $(RT) $(POSIXT)$(CC) $(CFLAGS) mycall.c -o mycallprod:$(CC) $(CFLAGS) prod.c -o prod $(RT) $(POSIXT) $(MATH)cons:$(CC) $(CFLAGS) cons.c -o cons $(RT) $(POSIXT) $(MATH)dph:$(CC) $(CFLAGS) dph.c -o dph $(RT) $(POSIXT)mycall:$(CC) $(CFLAGS) mycall.c -o mycallclean:rm -rf prodrm -rf consrm -rf dphrm -rf mycall

开头的几行是定义的一些宏,相当于C语言中的#define,使用的时候就$(MACRO_NAME)就可以了。然后每个参数后面接冒号,然后下面n行开头接Tab开头,并输入命令,这些命令其实就是平时在命令行编译的时候输入的命令,比如我们要编译dph,在命令行中应输入:

gcc -Wall dph.c -o dph -lrt -lpthread

通过宏替换就变成了:

$(CC) $(CFLAGS) dph.c -o dph $(RT) $(POSIXT)

这样在make dph的时候就会执行这个编译命令生成可执行文件dph了。

其他的选项也是相似的,clean一般来说是删除选项,使用暴力的rm就可以了。

觉得这篇文章好的朋友还麻烦点个赞同,谢谢啦~

java大作业私人管家系统_操作系统概念(Operating System Concepts)第十版期中大作业...相关推荐

  1. java大作业私人管家系统_重庆管家婆软件丨管家婆工贸PRO的E-MES管理详解

    其实,ERP和MES在制造操作中扮演着独立而又互补的角色.ERP能将企业所有方面的数据进行实时.可用的全面集成,为管理决策提供高效.准确的业务决策支持;MES则能加强MRP计划的执行,把MRP计划同车 ...

  2. [操作系统概念]Operating System Concepts 7th - Preface

    Preface - 前言(9 pages) Reading started at 2019/03/06 19:00 essential : absolutely necessary prevalent ...

  3. 《Operating System Concepts(操作系统概念)》课程学习(1)——Chapter 1 Introduction(第1章 绪论)

    操作系统概念 Operating System Concepts 说起操作系统,我想在坐的各位同学都不会陌生.因为无论我们想用计算机干什么,首先要做的就是启动操作系统,任何软件的运行都离不开操作系统的 ...

  4. 操作系统(Operating System)

    一.计算机系统概述 什么是操作系统(操作系统的概念) ※ 操作系统(Operating System, OS)是指控制和管理整个计算机系统的硬件和软件资源,并合理地组织调度计算机的工作和资源的分配:以 ...

  5. 【OS操作系统】Operating System 第五章:虚存技术

    OS操作系统系列文章目录 [OS操作系统]Operating System 第一章:操作系统的概述 [OS操作系统]Operating System 第二章:启动.中断.异常和系统调用 [OS操作系统 ...

  6. 关于细分到字段的权限系统_操作系统中的细分

    关于细分到字段的权限系统 为什么需要细分? (Why Segmentation is required?) In the Operating System, an important drawback ...

  7. 八字易经算法之用JAVA实现完整排盘系统_八字易經演算法之用JAVA實現完整排盤系統 | 學步園...

    去年一天,一個朋友去看望病人回來就驚奇的告訴我,他發現和他朋友一起住院(肝膽科)的病人無一例外都是屬相為虎的病人,不是大一輪就是小一輪的.這是為什麼呢? 這不是什麼偶然,也不是什麼巧合.也許通過八字能 ...

  8. 关于大数据系统及Hadoop系统中的概念

    什么是大数据 大数据(Big Data)姑且定义为无法被符合服务等级协议(service level agreement,SLA)的单台计算机处理或存储的任何数据集.理论上讲,单台计算机可以处理任意规 ...

  9. SI - 系统 - 操作系统简述 (Operating System)

    Unix 操作系统:System V.BSD Microsoft Windows Apple Mac OS Linux FreeBSD 安装 https://jingyan.baidu.com/art ...

最新文章

  1. 对分贝(dB)概念的理解
  2. Win API函数SetWindowOrgEx与SetViewportOrgEx
  3. java json格式字符串转为map_json格式的字符串序列化和反序列化的一些高级用法...
  4. 学习Java编程,英语对我们来说有多重要?
  5. 团队合作-需求分析之WBS
  6. 找出Java进程中大量消耗CPU
  7. android 模拟器 root
  8. HTML下拉菜单(超详细):
  9. 知乎周源微信_每周源代码3
  10. 2022全年度平板电视十大热门品牌销量榜单
  11. 卡尔曼滤波器之经典卡尔曼滤波
  12. 电脑PHP动画制作画板,html5教程制作简单画板代码分享
  13. (实测可用)STM32CubeMX教程-STM32L431RCT6开发板(定时器Timer2)
  14. 【ACL Findings 2021】Does Robustness Improve Fairness? Approaching Fairness with Word Substitution R
  15. 3D打印云平台在线显示
  16. 图书馆管理系统用户调研
  17. 用Python做一款自己的TK创建器
  18. 国外知名网站Stackoverflow 历时两年评选出11本对程序员最有影响力的书籍
  19. 2年Java开发需要具有什么水平?
  20. 什么是项目成本估算?项目成本的估算方法有哪些?

热门文章

  1. java两个线程循环打印_java循环打印 多线程
  2. php多线程模型,PHP进程模型、进程通讯方式、进程线程的区别分别有哪些?
  3. 大规模环境下基于语义直方图的多机器人实时全局定位图匹配
  4. 使用govendor灵活管理Go程序中的依赖包
  5. 在CentOS 6.8 x86_64上利用devtoolset搭建GCC 4.9.2和5.3.1开发环境
  6. 在Ubuntu 16.04.1 LTS上安装ATS 6.2.1 LTS实录
  7. Blender3.0电影级别CG场景制作视频教程
  8. Blender+Substance Painter全流程制作真实的机器人学习教程
  9. Comparator 和 Comparable
  10. Cacti安装详细步骤