CSAPP shelllab实验_csapp:shelllab实验-程序员宅基地

技术标签: c语言  CSAPP  linux  

一、准备工作

Hints
• 这是基于CSAPP教材第八章的配套实验。
• 使用跟踪文件来指导 shell 的开发。从 trace01.txt 开始,确保 shell 产生与reference shell 相同的输出。然后继续跟踪文件 trace02.txt,以此类推。
• waitpid、kill、fork、execve、setpgid 和 sigprocmaskfunctions将会被用到。waitpid 的WUNTRACED 和 WNOHANG 选项也会被用到。
• 当我们实现信号处理程序时,请确保向整个前台进程组发送 SIGINT 和 SIGTSTP 信号,在kill 函数的参数中使用“-pid”而不是“pid”。sdriver.pl 程序可以测试此错误。
• 这项任务的棘手部分之一是决定 waitfg和 sigchldhandler 函数之间的工作分配。我们建议采用以下方法:
– In waitfg, use a busy loop around the sleep function.
– In sigchld handler, use exactly one call to waitpid.
• 在 eval中,父进程必须在分叉子进程之前使用 sigprocmask来阻塞 SIGCHLD信号,然后取消阻塞这些信号,在通过调用 addjob 将子进程添加到作业列表之后,再次使用 sigprocmask。因为孩子继承了他们父母被阻止的向量,所以在执行新程序之前,孩子必须确保解除对SIGCHLD 信号的阻止。父进程需要以这种方式阻塞 SIGCHLD 信号,以避免子进程在父进程调用 addjob 之前被sigchldhandler 捕获(并因此从作业列表中删除)的竞争情况。
• more, less, vi 和 emacs等程序会对终端设置做一些奇怪的事情。不要从 shell 运行这些程序。坚持使用简单的基于文本的程序,如/bin/ls、/bin/ps 和/bin/echo。
• 当从标准的 Unix shell 运行您的 shell 时,我们的 shell 正在前台进程组中运行。如果我们的 shell创建了一个子进程,默认情况下,该子进程也是前台进程组的成员。由于键入 ctrl-c 会向前台组中的每个进程发送一个 SIGINT,因此键入 ctrl-c 会向我们的 shell 以及 shell 创建的每个进程发送一个 SIGINT,这显然是不正确的。
解决方法是:在 fork 之后,但在 execve 之前,子进程应该调用 set GID(0,0),这将子进程放在一个新的进程组中,该进程组的组标识与子进程的组标识相同。这确保前台进程组中只有一个进程,即 shell。当键入 ctrl-c 时,shell 应该捕获结果 SIGINT,然后将其转发到适当的前台作业(或者更准确地说,包含前台作业的进程组)。

实验介绍
首先输入命令tar xvf shlab-handout.tar来扩展目标文件。不要解压之后再上传到服务器,否则会出现下图所示的情况。
在这里插入图片描述
然后输入make 命令来编译tsh.c文件,如果程序被修改则需要先输入make clean指令。
在这里插入图片描述

  • eval:读取指令并产生子进程执行。
  • builtin_cmd:执行内置命令,bg、fg、quit、jobs。 do_bgfg:处理fg和bg操作。
  • waitfg:等待前台进程结束。
  • sigchld_handler:捕捉SIGCHILD信号,给子进程收尸,避免产生僵尸进程。
  • sigint_handler:捕捉SIGINT(ctrl-c)信号,将SIGINT信号发给前台进程组。
  • sigtstp_handler:捕捉SIGTSTP(ctrl-z)信号,将SIGTSTP信号发给前台进程组。
  • 使用make testXX和make rtestXX指令比较traceXX.txt文件在编写的shell和reference shell的运行结果;或者也可以使用”./sdriver.pl -t traceXX.txt -s ./tsh -a “-p”和”./sdriver.pl -t traceXX.txt -s ./tshref -a “-p”

跟系统任务相关的几个命令:fg、bg、jobs、&、ctrl+z。

  1. & :这个用在一个命令的最后,可以把这个命令放到后台执行;
  2. ctrl + z:可以将一个正在前台执行的命令放到后台,并且暂停;
  3. jobs:查看当前有多少在后台运行的命令,列举出后台作业信息([作业号] 运行状态 作业名称);
  4. fg:将后台中的命令调至前台继续运行,如果后台中有多个命令,可以用 fg %jobnumber将选中的命令调出,%jobnumber是通过jobs命令查到的后台正在执行的命令的序号(不是pid);
  5. bg:将一个在后台暂停的命令,变成继续执行,如果后台中有多个命令,可以用bg %jobnumber将选中的命令调出,%jobnumber是通过jobs命令查到的后台正在执行的命令的序号(不是pid)。

二、对实验的一些研究

Step1
输入make rtest01和make test01;因为trace01.txt中只有CLOSE和WAIT命令,在EOF上正常终止。
在这里插入图片描述
Step2
输入make rtest02和make test02;tshref能够正常退出,而tsh因为quit的内置命令还没有写,所以不能正常退出。
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210129081532266.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NTk3NTU3NQ==,size_16,color_FFFFFF,t_70

Step3
trace03.txt中tsh>quit是打开bin目录下的echo可执行文件,在foreground开启一个子进程运行它(如果末尾有&,则说明是在background运行)。
运行echo进程时,通过tsh>quit命令。调用tsh并执行内置命令quit,退出子进程。
最后在tsh中执行内置命令quit,退出tsh进程,回到终端。
在这里插入图片描述
Step4
了解tsh.c中作业表job struct和操作管理函数(如addjob())

(1)文件符号:
空格:用来分隔命令和参数或者参数与参数;
&:如果一个命令以&结尾,shell应该在后台运行它,否则在前台运行;
#:以 # 开头的行就是注释,会被解释器忽略。
2)命令:
包括内建命令和外部命令。内置命令包括quit、jobs、bg、fg等。可以使用type来确定一个命令是否是内置命令。

在这里插入图片描述
用户程序myspin:使用myspin 指令可将进程挂起n秒

int main(int argc, char **argv)  {
    
     int i, secs;
     pid_t pid; 
     //判断命令长度是否为2,若不为2则输出错误信息并终止程序
     if (argc != 2) {
     	fprintf(stderr, "Usage: %s <n>\n", argv[0]); 	exit(0);
     }
     secs = atoi(argv[1]); //将字符串转换为整型
     for (i=0; i < secs; i++) //将进程挂起secs秒
        sleep(1);
 	
     pid = getpid(); 
     if (kill(pid, SIGINT) < 0)
        fprintf(stderr, "kill (int) error");
     exit(0);
}

在这里插入图片描述

Step5
编程实现jobs内建命令,使用trace05验证。在原有builtin_cmd函数中添加一个判断函数,如果参数是jobs,则执行listjobs函数的功能。

if(!strcmp(argv[0],"jobs"))
{
    
     listjobs(jobs);
     return 1;
}
/* listjobs - Print the job list */
void listjobs(struct job_t *jobs) 
{
    
    int i;
    
    for (i = 0; i < MAXJOBS; i++) {
    
	if (jobs[i].pid != 0) {
    
	    printf("[%d] (%d) ", jobs[i].jid, jobs[i].pid);
	    switch (jobs[i].state) {
    
		case BG: 
		    printf("Running ");
		    break;
		case FG: 
		    printf("Foreground ");
		    break;
		case ST: 
		    printf("Stopped ");
		    break;
	    default:
		    printf("listjobs: Internal error: job[%d].state=%d ", 
			   i, jobs[i].state);
	    }
	    printf("%s", jobs[i].cmdline);
	}
    }
}

定义一个作业的结构体,该结构体具有的属性有作业的作业号、进程号、状态、命令行参数

struct job_t {
                  /* The job struct */
    pid_t pid;              /* job PID */
    int jid;                /* job ID [1, 2, ...] */
    int state;              /* UNDEF, BG, FG, or ST */
    char cmdline[MAXLINE];  /* command line */
};
struct job_t jobs[MAXJOBS]; /* The job list */

下列函数进行清空、初始化、最大作业号、添加、删除功能。

void clearjob(struct job_t *job);
void initjobs(struct job_t *jobs);
int maxjid(struct job_t *jobs); 
int addjob(struct job_t *jobs, pid_t pid, int state, char *cmdline);
int deletejob(struct job_t *jobs, pid_t pid);

在这里插入图片描述
Step6
编程实现sigint_handler()、waitfg(),sigchld_handler(),验证trace06~07.

了解接收信号、信号处理、信号阻塞概念

  1. 接收信号:当目的进程被内核强迫以方式对信号的发送做出反应时,目的进程就接收了信号。进程可以忽略这个信号,终止或者通过执行一个称为信号处理 程序的用户层函数捕获这个信号。
  2. 信号处理:signal函数可以通过下列三种方法之一来改变和信号signum相关联的行为:
    ①如果handler是SIG_IGN,那么忽略类型为signum的信号;
    ②如果handler是SIG_DFL,那么类型为signum的信号行为恢复为默认模式;
    ③否则,handler就是用户定义的函数的地址,这个函数称为信号处理程序;
  3. 当一个程序要捕获多个信号时,会有以下问题:
    待处理信号被阻塞;待处理信号不会排队等待;系统调用可以被中断。
  4. 信号阻塞:Unix信号处理程序通常会阻塞当前处理程序正在处理类型的待处理信号。

在这里插入图片描述在这里插入图片描述
Step7
编程实现sigtstp_handler捕获TSTP响应
在这里插入图片描述
Step8
编程实现内置命令bg和fg的do_bgfg()处理函数;比较trace09~10执行不同结果,在trace09中执行完bg %2命令后会将作业2放到后台运行,所以最终【2】后面会显示running;在trace10中执行fg %1会将后台的作业1停止之后添加到前台运行。
在这里插入图片描述
在这里插入图片描述
Step9
Trace11:./mysplit 4创建子进程并将其挂起 4 秒,而父进程在挂起 2 秒后发送 SIGINT 信号使子进程终止。
在这里插入图片描述

Trace12:./mysplit 4 创建子进程并将其挂起 4 秒,而父进程在挂起 2 秒后发送 SIGTSTP 信号使子进程停止直到下一个 SINCONT,因此执行 jobs 指令,可以看到子进程处 于 Stopped 状态,用 ps a 指令查看。
在这里插入图片描述

Trace13:./mysplit 4 创建子进程并将其挂起 4 秒,而父进程在挂起 2 秒后发送 SIGTSTP 信号使子进程停止直到接收一个 SINCONT 信号,因此执行 jobs 指令,可以看到子进程处于 Stopped 状态,用 ps a 指令查看;然后执行 fg %1 指令, 将后台停止的作业 1 切换至前台运行,再次使用 ps a 指令查看。
在这里插入图片描述
Step10

Trace14:处理输入未实现的命令,fg 和 bg 参数不正确等错误情况。
在这里插入图片描述
Step11
因为没有实现命令 bogus,所以执行./bogus 命令会报错。执行./myspin 10 命令,挂起10秒,但在挂起2秒后被SIGINT信号终止。./myspin 3 &和./myspin 4 &分别在后台执行./myspin 3 和./myspin 4 命令,且前者作业号为1,后者作业号为2。此时使用 jobs 命令查看,两者都在后台运行。使用 fg %1 命令将作业1切换至前台运行,挂起2秒后,发送 SIGTSTP 信号使其停止直到接收一个SINCONT 信号。此时再次使用 jobs 命令查 看,发现作业1已经处于Stopped状态,而作业2仍然处于 Running 状态。因为没有作业3,所以执行bg %3 命令会报错。然后执行 bg %1命令将已经在后台停止的作业1切换至运行状态,即重启作业1。此时使用jobs命令查看,作业1和作业2都处于后台运行状态。然后执行fg %1 命令将作业从后台运行状态切换至前台运行状态。
在这里插入图片描述在这里插入图片描述

三、代码

1、eval

void eval(char *cmdline) 
{
    
    char *argv[MAXARGS];
    char buf[MAXLINE];
    int bg;
    sigset_t prev;
    sigset_t mask;
    pid_t pid;
    sigprocmask(SIG_BLOCK,NULL,&mask);
    sigaddset(&mask,SIGCHLD);
    strcpy(buf,cmdline);
    bg=parseline(buf,argv);//内置函数,前台任务返回0,后台任务返回1
    if(argv[0]==NULL) return;
    if(!builtin_cmd(argv))//非内置指令时执行
    {
    
        sigprocmask(SIG_BLOCK,&mask,&prev);//屏蔽SIGCHLD
        if((pid=fork())==0)//子进程
        {
    
            sigprocmask(SIG_SETMASK,&prev,NULL);
            setpgid(0,0);//每个任务单独开一个进程组,方便信号处理中使用kill
            if(execve(argv[0],argv,environ)<0)
            {
    
                printf("%s:Command not found.\n",argv[0]);
                exit(0);
            }
        }
   
        else//父进程
        {
    
            if(bg)//后台任务
                addjob(jobs,pid,BG,cmdline);
            else//前台任务
                addjob(jobs,pid,FG,cmdline);
            sigprocmask(SIG_SETMASK,&prev,NULL);
            if(bg)//后台任务不等待任务结束
                printf("[%d](%d)%s",pid2jid(pid),pid,cmdline);
            else//前台任务,等待任务结束
                waitfg(pid);
        }
    }
    return;
}

2、builtin_cmd

int builtin_cmd(char** argv)
{
    
	if (!strcmp(argv[0], "quit")) {
    
		exit(0);
	}
	if (!strcmp(argv[0], "fg") || !strcmp(argv[0], "bg")) {
    
		do_bgfg(argv);
		return 1;
	}
	if(!strcmp(argv[0], "jobs")) {
    
        //访问全局变量,阻塞所有信号
		//sigset_t mask_all, prev_mask;
		//sigfillset(&mask_all);
		//sigprocmask(SIG_SETMASK, &mask_all, &prev_mask);
		listjobs(jobs);
		//sigprocmask(SIG_SETMASK, &prev_mask, NULL);
		return 1;
	}
	if(!strcmp(argv[0], "&")){
    
		return 1;
	}
	return 0; /* not a builtin command */
}
char* argv[MAXARGS];
	char buf[MAXLINE];
	int bg;
	pid_t pid;

	strcpy(buf, cmdline);
	bg = parseline(buf, argv);
	if (argv[0] == NULL) {
    
		return;
	}
	if (!builtin_cmd(argv)) {
    
		sigset_t mask_chld, prev_mask, mask_all;
		sigemptyset(&mask_chld);
		sigaddset(&mask_chld, SIGCHLD);
		sigfillset(&mask_all);
        
		/*因为子进程可能在addjob前就结束并调用deleltejob,所以我们要先阻塞掉SIGCHLD,
		保证addjob操作成功*/
		sigprocmask(SIG_BLOCK, &mask_chld, &prev_mask);

		if ((pid = fork()) == 0) {
    
            //子进程默认继承父进程的mask,所以这里要恢复一下
			sigprocmask(SIG_SETMASK, &prev_mask, NULL);
			setpgid(0, 0); //令进程组号等于进程号
			if (execve(argv[0], argv, environ) <= 0) {
    
				printf("%s: Command not found\n", argv[0]);
				exit(0);
			}
		}

		// addjob涉及到全局变量的操作,需要保证操作的原子性,故这里阻塞掉所有信号
		sigprocmask(SIG_SETMASK, &mask_all, NULL);
		addjob(jobs, pid, bg?BG:FG, cmdline);
		sigprocmask(SIG_SETMASK, &prev_mask, NULL);
		
		// 在线程终止前需要打印些相关信息,所以addjob完还要阻塞一会儿SIGCHLD
		sigprocmask(SIG_BLOCK, &mask_chld, NULL);
		
		if (!bg) {
    
			waitfg(pid);
		} else {
    
            // 同上,操作全局变量时阻塞
			sigprocmask(SIG_SETMASK, &mask_all, NULL);
			printf("[%d] (%d) %s", pid2jid(pid), pid, cmdline);
		}
        // 操作结束后解除阻塞
		sigprocmask(SIG_SETMASK, &prev_mask, NULL);
	}

	return;

3、do_bgfg

void do_bgfg(char **argv) 
{
    
    sigset_t mask_all, prev_mask;
	sigfillset(&mask_all);
    // 访问全局变量jobs,阻塞所有信号
	sigprocmask(SIG_SETMASK, &mask_all, &prev_mask);

	struct job_t *job;
	int pid;
	if(argv[1] == NULL){
    
		printf("%s command requires PID or %%jobid argument\n", argv[0]);
		return;
	}
	else if(argv[1][0] == '%'){
    
		int jid = atoi(argv[1] + 1);
		job = getjobjid(jobs, jid);
		if(job == NULL) {
    
			printf("%%%d: No such job\n", jid);
			return;
		}
		pid = job->pid;
	}
	else {
    
		pid = atoi(argv[1]);
		if(pid <= 0){
    
			printf("%s: argument must be a PID or %%jobid\n", argv[0]);
			return;
		}
		job = getjobpid(jobs, pid);
		if(job == NULL){
    
			printf("(%d): No such process\n", pid);
			return;
		}
	}
	if(!strcmp(argv[0], "bg")){
    
		job->state = BG;
		printf("[%d] (%d) %s", job->jid, pid, job->cmdline);
		sigprocmask(SIG_SETMASK, &prev_mask, NULL);
		kill(-pid, SIGCONT); // 对子进程及其后代发送,故加负号
		return;
	}
	else if(!strcmp(argv[0], "fg")){
    
		job->state = FG;
		sigprocmask(SIG_SETMASK, &prev_mask, NULL);
		kill(-pid, SIGCONT); // 对子进程及其后代发送,故加负号
		waitfg(pid); // 子进程切换到了前台,故要等待它执行完
		return;
	}
	else if(!strcmp(argv[0], "kill")){
    
		sigprocmask(SIG_SETMASK, &prev_mask, NULL);
		kill(-pid,SIGQUIT); // 对子进程及其后代发送,故加负号
		return;
	}
	return;
}

4、waitfg

void waitfg(pid_t pid)
{
    
    sigset_t m;
    sigemptyset(&m);
    while(pid==fgpid(jobs))
        sigsuspend(&m);//有信号时被唤醒检查前台进程pid是否变化,变化则说明前台进程结束。
    return;
}

5、sigchld_handler

void sigchld_handler(int sig) 
{
    
    pid_t pid;
    int status;
    while((pid=waitpid(-1,&status,WNOHANG|WUNTRACED))>0)
    {
    
        if(WIFEXITED(status))//正常结束
        {
    
            deletejob(jobs,pid);
        }
        if(WIFSTOPPED(status))//任务挂起时
        {
    
            struct job_t *job=getjobpid(jobs,pid);
            int jid=pid2jid(pid);
            printf("Job [%d] (%d) stopped by signal %d\n",jid,pid,WSTOPSIG(status));
            job->state=ST;
        }
        if(WIFSIGNALED(status))//任务被终止
        {
    
            int jid=pid2jid(pid);
            printf("Job [%d] (%d) terminated by signal %d\n",jid,pid,WTERMSIG(status));
            deletejob(jobs,pid);
        }

    }
    return;
}

6、sigint_handler

void sigint_handler(int sig) 
{
    
    pid_t pid=fgpid(jobs);
    if(pid!=0)
    {
    
        kill(-pid,sig);
    }
    return;
}

7、sigtstp_handler

void sigtstp_handler(int sig) 
{
    
    pid_t pid=fgpid(jobs);
    if(pid>0)
    {
    
        kill(-pid,sig);
    }
    return;
}
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_45975575/article/details/113363072

智能推荐

稀疏编码的数学基础与理论分析-程序员宅基地

文章浏览阅读290次,点赞8次,收藏10次。1.背景介绍稀疏编码是一种用于处理稀疏数据的编码技术,其主要应用于信息传输、存储和处理等领域。稀疏数据是指数据中大部分元素为零或近似于零的数据,例如文本、图像、音频、视频等。稀疏编码的核心思想是将稀疏数据表示为非零元素和它们对应的位置信息,从而减少存储空间和计算复杂度。稀疏编码的研究起源于1990年代,随着大数据时代的到来,稀疏编码技术的应用范围和影响力不断扩大。目前,稀疏编码已经成为计算...

EasyGBS国标流媒体服务器GB28181国标方案安装使用文档-程序员宅基地

文章浏览阅读217次。EasyGBS - GB28181 国标方案安装使用文档下载安装包下载,正式使用需商业授权, 功能一致在线演示在线API架构图EasySIPCMSSIP 中心信令服务, 单节点, 自带一个 Redis Server, 随 EasySIPCMS 自启动, 不需要手动运行EasySIPSMSSIP 流媒体服务, 根..._easygbs-windows-2.6.0-23042316使用文档

【Web】记录巅峰极客2023 BabyURL题目复现——Jackson原生链_原生jackson 反序列化链子-程序员宅基地

文章浏览阅读1.2k次,点赞27次,收藏7次。2023巅峰极客 BabyURL之前AliyunCTF Bypassit I这题考查了这样一条链子:其实就是Jackson的原生反序列化利用今天复现的这题也是大同小异,一起来整一下。_原生jackson 反序列化链子

一文搞懂SpringCloud,详解干货,做好笔记_spring cloud-程序员宅基地

文章浏览阅读734次,点赞9次,收藏7次。微服务架构简单的说就是将单体应用进一步拆分,拆分成更小的服务,每个服务都是一个可以独立运行的项目。这么多小服务,如何管理他们?(服务治理 注册中心[服务注册 发现 剔除])这么多小服务,他们之间如何通讯?这么多小服务,客户端怎么访问他们?(网关)这么多小服务,一旦出现问题了,应该如何自处理?(容错)这么多小服务,一旦出现问题了,应该如何排错?(链路追踪)对于上面的问题,是任何一个微服务设计者都不能绕过去的,因此大部分的微服务产品都针对每一个问题提供了相应的组件来解决它们。_spring cloud

Js实现图片点击切换与轮播-程序员宅基地

文章浏览阅读5.9k次,点赞6次,收藏20次。Js实现图片点击切换与轮播图片点击切换<!DOCTYPE html><html> <head> <meta charset="UTF-8"> <title></title> <script type="text/ja..._点击图片进行轮播图切换

tensorflow-gpu版本安装教程(过程详细)_tensorflow gpu版本安装-程序员宅基地

文章浏览阅读10w+次,点赞245次,收藏1.5k次。在开始安装前,如果你的电脑装过tensorflow,请先把他们卸载干净,包括依赖的包(tensorflow-estimator、tensorboard、tensorflow、keras-applications、keras-preprocessing),不然后续安装了tensorflow-gpu可能会出现找不到cuda的问题。cuda、cudnn。..._tensorflow gpu版本安装

随便推点

物联网时代 权限滥用漏洞的攻击及防御-程序员宅基地

文章浏览阅读243次。0x00 简介权限滥用漏洞一般归类于逻辑问题,是指服务端功能开放过多或权限限制不严格,导致攻击者可以通过直接或间接调用的方式达到攻击效果。随着物联网时代的到来,这种漏洞已经屡见不鲜,各种漏洞组合利用也是千奇百怪、五花八门,这里总结漏洞是为了更好地应对和预防,如有不妥之处还请业内人士多多指教。0x01 背景2014年4月,在比特币飞涨的时代某网站曾经..._使用物联网漏洞的使用者

Visual Odometry and Depth Calculation--Epipolar Geometry--Direct Method--PnP_normalized plane coordinates-程序员宅基地

文章浏览阅读786次。A. Epipolar geometry and triangulationThe epipolar geometry mainly adopts the feature point method, such as SIFT, SURF and ORB, etc. to obtain the feature points corresponding to two frames of images. As shown in Figure 1, let the first image be ​ and th_normalized plane coordinates

开放信息抽取(OIE)系统(三)-- 第二代开放信息抽取系统(人工规则, rule-based, 先抽取关系)_语义角色增强的关系抽取-程序员宅基地

文章浏览阅读708次,点赞2次,收藏3次。开放信息抽取(OIE)系统(三)-- 第二代开放信息抽取系统(人工规则, rule-based, 先关系再实体)一.第二代开放信息抽取系统背景​ 第一代开放信息抽取系统(Open Information Extraction, OIE, learning-based, 自学习, 先抽取实体)通常抽取大量冗余信息,为了消除这些冗余信息,诞生了第二代开放信息抽取系统。二.第二代开放信息抽取系统历史第二代开放信息抽取系统着眼于解决第一代系统的三大问题: 大量非信息性提取(即省略关键信息的提取)、_语义角色增强的关系抽取

10个顶尖响应式HTML5网页_html欢迎页面-程序员宅基地

文章浏览阅读1.1w次,点赞6次,收藏51次。快速完成网页设计,10个顶尖响应式HTML5网页模板助你一臂之力为了寻找一个优质的网页模板,网页设计师和开发者往往可能会花上大半天的时间。不过幸运的是,现在的网页设计师和开发人员已经开始共享HTML5,Bootstrap和CSS3中的免费网页模板资源。鉴于网站模板的灵活性和强大的功能,现在广大设计师和开发者对html5网站的实际需求日益增长。为了造福大众,Mockplus的小伙伴整理了2018年最..._html欢迎页面

计算机二级 考试科目,2018全国计算机等级考试调整,一、二级都增加了考试科目...-程序员宅基地

文章浏览阅读282次。原标题:2018全国计算机等级考试调整,一、二级都增加了考试科目全国计算机等级考试将于9月15-17日举行。在备考的最后冲刺阶段,小编为大家整理了今年新公布的全国计算机等级考试调整方案,希望对备考的小伙伴有所帮助,快随小编往下看吧!从2018年3月开始,全国计算机等级考试实施2018版考试大纲,并按新体系开考各个考试级别。具体调整内容如下:一、考试级别及科目1.一级新增“网络安全素质教育”科目(代..._计算机二级增报科目什么意思

conan简单使用_apt install conan-程序员宅基地

文章浏览阅读240次。conan简单使用。_apt install conan