期中之后的第一个lab 就是实现一个简单的Shell 程序,程序的大部分已经写好,只需要实现 eval 函数和处理信号的sigchld_handle, sigint_handle, sigtstp_handle这三个函数。 这个lab 主要要求处理好各个信号,因为上课的时候一直听得很糊涂,就拖着没有写,直到这两天deadline逼近才动手。同样是时间紧迫,debug的时候出了很多问题,在网上搜了很多解答,但是因为题目版本不一样,并不完全适用,比如之前的不需要重定向。因此把自己写的代码也贴出来,最后是一些自己的心得。

        这里还有shell lab的所有文件压缩包提供下载
 

一些心得:

  • 测试的时候的一些奇葩函数, mytstpp 向shell 发出一个SIGTSTP,而mytstps 向自己的进程发出SIGTSTP信号。前者shell 会调用sigtstp_handle 函数,而后者会使子进程stop,然后向shell 发出SIGCHLD信号,shell调用sigchld_handle 函数。这就要求我们分清楚每一个信号会由哪个进程(函数)处理。
  • shell收到的每个SIGTDTP,SIGINT信号都要发给前台进程,而这个前台进程是由自己的job_list 列表维护的,而实际上每个子进程的停止,终止都是由信号操作。
  • 调试的时候可以用GDB,但是涉及到信号和线程,可以在网上搜一下有很好的教程。
  • 通过运行tshref 可以找到每个命令的标准输出,这个稍微注意一下就可以了。
就写到这里了,如果有错误,烦请指正,同时欢迎交流!
 
代码附下:
 /*
* tsh - A tiny shell program with job control
*
* 2014.12.1
*
*/
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <ctype.h>
#include <signal.h>
#include <sys/types.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <errno.h> /* Misc manifest constants */
#define MAXLINE 1024 /* max line size */
#define MAXARGS 128 /* max args on a command line */
#define MAXJOBS 16 /* max jobs at any point in time */
#define MAXJID 1<<16 /* max job ID */ /* Job states */
#define UNDEF 0 /* undefined */
#define FG 1 /* running in foreground */
#define BG 2 /* running in background */
#define ST 3 /* stopped */ /*
* Jobs states: FG (foreground), BG (background), ST (stopped)
* Job state transitions and enabling actions:
* FG -> ST : ctrl-z
* ST -> FG : fg command
* ST -> BG : bg command
* BG -> FG : fg command
* At most 1 job can be in the FG state.
*/ /* Parsing states */
#define ST_NORMAL 0x0 /* next token is an argument */
#define ST_INFILE 0x1 /* next token is the input file */
#define ST_OUTFILE 0x2 /* next token is the output file */ /* Global variables */
extern char **environ; /* defined in libc */
char prompt[] = "tsh> "; /* command line prompt (DO NOT CHANGE) */
int verbose = ; /* if true, print additional output */
int nextjid = ; /* next job ID to allocate */
char sbuf[MAXLINE]; /* for composing sprintf messages */ 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 job_list[MAXJOBS]; /* The job list */ struct cmdline_tokens {
int argc; /* Number of arguments */
char *argv[MAXARGS]; /* The arguments list */
char *infile; /* The input file */
char *outfile; /* The output file */
enum builtins_t { /* Indicates if argv[0] is a builtin command */
BUILTIN_NONE, BUILTIN_QUIT, BUILTIN_JOBS, BUILTIN_BG, BUILTIN_FG
} builtins;
};
/* End global variables */ /* Function prototypes */
void eval(char *cmdline); void sigchld_handler(int sig);
void sigtstp_handler(int sig);
void sigint_handler(int sig); /* Here are helper routines that we've provided for you */
int parseline(const char *cmdline, struct cmdline_tokens *tok);
void sigquit_handler(int sig); void clearjob(struct job_t *job);
void initjobs(struct job_t *job_list);
int maxjid(struct job_t *job_list);
int addjob(struct job_t *job_list, pid_t pid, int state, char *cmdline);
int deletejob(struct job_t *job_list, pid_t pid);
pid_t fgpid(struct job_t *job_list);
struct job_t *getjobpid(struct job_t *job_list, pid_t pid);
struct job_t *getjobjid(struct job_t *job_list, int jid);
int pid2jid(pid_t pid);
void listjobs(struct job_t *job_list, int output_fd); void usage(void);
void unix_error(char *msg);
void app_error(char *msg);
typedef void handler_t(int);
handler_t *Signal(int signum, handler_t *handler); /*
* main - The shell's main routine
*/
int main(int argc, char **argv) {
char c;
char cmdline[MAXLINE]; /* cmdline for fgets */
int emit_prompt = ; /* emit prompt (default) */ /* Redirect stderr to stdout (so that driver will get all output
* on the pipe connected to stdout) */
dup2(, ); /* Parse the command line */
while ((c = getopt(argc, argv, "hvp")) != EOF) {
switch (c) {
case 'h': /* print help message */
usage();
break;
case 'v': /* emit additional diagnostic info */
verbose = ;
break;
case 'p': /* don't print a prompt */
emit_prompt = ; /* handy for automatic testing */
break;
default:
usage();
}
} /* Install the signal handlers */ /* These are the ones you will need to implement */
Signal(SIGINT, sigint_handler); /* ctrl-c */
Signal(SIGTSTP, sigtstp_handler); /* ctrl-z */
Signal(SIGCHLD, sigchld_handler); /* Terminated or stopped child */
Signal(SIGTTIN, SIG_IGN);
Signal(SIGTTOU, SIG_IGN); /* This one provides a clean way to kill the shell */
Signal(SIGQUIT, sigquit_handler); /* Initialize the job list */
initjobs(job_list); /* Execute the shell's read/eval loop */
while () { if (emit_prompt) {
printf("%s", prompt);
fflush(stdout);
}
if ((fgets(cmdline, MAXLINE, stdin) == NULL) && ferror(stdin))
app_error("fgets error");
if (feof(stdin)) {
/* End of file (ctrl-d) */
printf("\n");
fflush(stdout);
fflush(stderr);
exit();
} /* Remove the trailing newline */
cmdline[strlen(cmdline) - ] = '\0'; /* Evaluate the command line */
eval(cmdline); fflush(stdout);
fflush(stdout);
} exit(); /* control never reaches here */
} /*
* eval - Evaluate the command line that the user has just typed in
*
* If the user has requested a built-in command (quit, jobs, bg or fg)
* then execute it immediately. Otherwise, fork a child process and
* run the job in the context of the child. If the job is running in
* the foreground, wait for it to terminate and then return. Note:
* each child process must have a unique process group ID so that our
* background children don't receive SIGINT (SIGTSTP) from the kernel
* when we type ctrl-c (ctrl-z) at the keyboard.
*/
void eval(char *cmdline) {
int bg; /* should the job run in bg or fg? */
struct cmdline_tokens tok; /* Parse command line */
bg = parseline(cmdline, &tok); if (bg == -)
return; /* parsing error */
if (tok.argv[] == NULL)
return; /* ignore empty lines */ int stdi,stdo;
stdi=dup(STDIN_FILENO);
stdo=dup(STDOUT_FILENO); int infg, outfg;
infg = -;
outfg = -;
if (tok.infile != NULL) {
infg = open(tok.infile, O_RDONLY, );
dup2(infg, STDIN_FILENO);
}
if (tok.outfile != NULL) {
outfg = open(tok.outfile, O_RDWR, );
dup2(outfg, STDOUT_FILENO);
} pid_t pid;
struct job_t *job;
sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask, SIGCHLD);
sigaddset(&mask, SIGINT);
sigaddset(&mask, SIGTSTP);
if (tok.builtins == BUILTIN_NONE) {
sigprocmask(SIG_BLOCK, &mask, NULL); if ((pid = fork()) == ) {
sigprocmask(SIG_UNBLOCK, &mask, NULL);
setpgid(, ); execve(tok.argv[], tok.argv, environ); if(infg!=-)
close(infg);
if(outfg!=-)
close(outfg); } else { addjob(job_list, pid, bg + , cmdline);
job = getjobpid(job_list, pid);
sigprocmask(SIG_UNBLOCK, &mask, NULL); sigemptyset(&mask);
if (!bg) {
while(pid==fgpid(job_list))
sleep();
} else {
printf("[%d] (%d) %s\n", job->jid, pid, job->cmdline);
}
} } else {
if(tok.builtins==BUILTIN_QUIT)
exit();
else if(tok.builtins==BUILTIN_JOBS) {
listjobs(job_list,STDOUT_FILENO);
}
else{
int jid;
if(tok.argv[][]=='%')
jid=atoi((tok.argv[])+sizeof(char));
else
jid=pid2jid(atoi(tok.argv[]));
job=getjobjid(job_list,jid);
if(tok.builtins==BUILTIN_BG) {
printf("[%d] (%d) %s\n",job->jid,job->pid,job->cmdline);
job->state=BG;
kill(-(job->pid),SIGCONT);
} else {
job->state=FG;
kill(-(job->pid),SIGCONT);
}
}
} dup2(stdi, STDIN_FILENO);
dup2(stdo, STDOUT_FILENO);
if(infg!=-)
close(infg);
if(outfg!=-)
close(outfg);
return;
} /*
* parseline - Parse the command line and build the argv array.
*
* Parameters:
* cmdline: The command line, in the form:
*
* command [arguments...] [< infile] [> oufile] [&]
*
* tok: Pointer to a cmdline_tokens structure. The elements of this
* structure will be populated with the parsed tokens. Characters
* enclosed in single or double quotes are treated as a single
* argument.
* Returns:
* 1: if the user has requested a BG job
* 0: if the user has requested a FG job
* -1: if cmdline is incorrectly formatted
*
* Note: The string elements of tok (e.g., argv[], infile, outfile)
* are statically allocated inside parseline() and will be
* overwritten the next time this function is invoked.
*/
int parseline(const char *cmdline, struct cmdline_tokens *tok) { static char array[MAXLINE]; /* holds local copy of command line */
const char delims[] = " \t\r\n"; /* argument delimiters (white-space) */
char *buf = array; /* ptr that traverses command line */
char *next; /* ptr to the end of the current arg */
char *endbuf; /* ptr to the end of the cmdline string */
int is_bg; /* background job? */ int parsing_state; /* indicates if the next token is the
input or output file */ if (cmdline == NULL) {
(void) fprintf(stderr, "Error: command line is NULL\n");
return -;
} (void) strncpy(buf, cmdline, MAXLINE);
endbuf = buf + strlen(buf); tok->infile = NULL;
tok->outfile = NULL; /* Build the argv list */
parsing_state = ST_NORMAL;
tok->argc = ; while (buf < endbuf) {
/* Skip the white-spaces */
buf += strspn(buf, delims);
if (buf >= endbuf)
break; /* Check for I/O redirection specifiers */
if (*buf == '<') {
if (tok->infile) {
(void) fprintf(stderr, "Error: Ambiguous I/O redirection\n");
return -;
}
parsing_state |= ST_INFILE;
buf++;
continue;
}
if (*buf == '>') {
if (tok->outfile) {
(void) fprintf(stderr, "Error: Ambiguous I/O redirection\n");
return -;
}
parsing_state |= ST_OUTFILE;
buf++;
continue;
} if (*buf == '\'' || *buf == '\"') {
/* Detect quoted tokens */
buf++;
next = strchr(buf, *(buf - ));
} else {
/* Find next delimiter */
next = buf + strcspn(buf, delims);
} if (next == NULL) {
/* Returned by strchr(); this means that the closing
quote was not found. */
(void) fprintf(stderr, "Error: unmatched %c.\n", *(buf - ));
return -;
} /* Terminate the token */
*next = '\0'; /* Record the token as either the next argument or the input/output file */
switch (parsing_state) {
case ST_NORMAL:
tok->argv[tok->argc++] = buf;
break;
case ST_INFILE:
tok->infile = buf;
break;
case ST_OUTFILE:
tok->outfile = buf;
break;
default:
(void) fprintf(stderr, "Error: Ambiguous I/O redirection\n");
return -;
}
parsing_state = ST_NORMAL; /* Check if argv is full */
if (tok->argc >= MAXARGS - )
break; buf = next + ;
} if (parsing_state != ST_NORMAL) {
(void) fprintf(stderr,
"Error: must provide file name for redirection\n");
return -;
} /* The argument list must end with a NULL pointer */
tok->argv[tok->argc] = NULL; if (tok->argc == ) /* ignore blank line */
return ; if (!strcmp(tok->argv[], "quit")) { /* quit command */
tok->builtins = BUILTIN_QUIT;
} else if (!strcmp(tok->argv[], "jobs")) { /* jobs command */
tok->builtins = BUILTIN_JOBS;
} else if (!strcmp(tok->argv[], "bg")) { /* bg command */
tok->builtins = BUILTIN_BG;
} else if (!strcmp(tok->argv[], "fg")) { /* fg command */
tok->builtins = BUILTIN_FG;
} else {
tok->builtins = BUILTIN_NONE;
} /* Should the job run in the background? */
if ((is_bg = (*tok->argv[tok->argc - ] == '&')) != )
tok->argv[--tok->argc] = NULL; return is_bg;
} /*****************
* Signal handlers
*****************/ /*
* sigchld_handler - The kernel sends a SIGCHLD to the shell whenever
* a child job terminates (becomes a zombie), or stops because it
* received a SIGSTOP, SIGTSTP, SIGTTIN or SIGTTOU signal. The
* handler reaps all available zombie children, but doesn't wait
* for any other currently running children to terminate.
*/
void sigchld_handler(int sig) {
pid_t pid;
int status;
while ((pid = waitpid(-, &status, WUNTRACED | WNOHANG)) > ) {
if (WIFSTOPPED(status)) {
int jid=pid2jid(pid);
if(jid!=) {
printf("Job [%d] (%d) stopped by signal %d\n",jid,pid,WSTOPSIG(status));
(getjobpid(job_list,pid))->state=ST;
}
} else if (WIFSIGNALED(status)) {
if (WTERMSIG(status) == SIGINT) {
int jid=pid2jid(pid);
if(jid!=) {
printf("Job [%d] (%d) terminated by signal %d\n",jid,pid,SIGINT);
deletejob(job_list,pid);
}
}
else
deletejob(job_list, pid);
} else
deletejob(job_list, pid);
}
return;
} /*
* sigint_handler - The kernel sends a SIGINT to the shell whenver the
* user types ctrl-c at the keyboard. Catch it and send it along
* to the foreground job.
*/
void sigint_handler(int sig) {
pid_t pid = fgpid(job_list);
if (pid != ) {
kill(-pid, sig);
}
return;
} /*
* sigtstp_handler - The kernel sends a SIGTSTP to the shell whenever
* the user types ctrl-z at the keyboard. Catch it and suspend the
* foreground job by sending it a SIGTSTP.
*/
void sigtstp_handler(int sig) {
pid_t pid = fgpid(job_list);
if (pid != ) {
kill(-pid, sig);
}
return;
} /*********************
* End signal handlers
*********************/ /***********************************************
* Helper routines that manipulate the job list
**********************************************/ /* clearjob - Clear the entries in a job struct */
void clearjob(struct job_t *job) {
job->pid = ;
job->jid = ;
job->state = UNDEF;
job->cmdline[] = '\0';
} /* initjobs - Initialize the job list */
void initjobs(struct job_t *job_list) {
int i; for (i = ; i < MAXJOBS; i++)
clearjob(&job_list[i]);
} /* maxjid - Returns largest allocated job ID */
int maxjid(struct job_t *job_list) {
int i, max = ; for (i = ; i < MAXJOBS; i++)
if (job_list[i].jid > max)
max = job_list[i].jid;
return max;
} /* addjob - Add a job to the job list */
int addjob(struct job_t *job_list, pid_t pid, int state, char *cmdline) {
int i; if (pid < )
return ; for (i = ; i < MAXJOBS; i++) {
if (job_list[i].pid == ) {
job_list[i].pid = pid;
job_list[i].state = state;
job_list[i].jid = nextjid++;
if (nextjid > MAXJOBS)
nextjid = ;
strcpy(job_list[i].cmdline, cmdline);
if (verbose) {
printf("Added job [%d] %d %s\n", job_list[i].jid,
job_list[i].pid, job_list[i].cmdline);
}
return ;
}
}
printf("Tried to create too many jobs\n");
return ;
} /* deletejob - Delete a job whose PID=pid from the job list */
int deletejob(struct job_t *job_list, pid_t pid) {
int i; if (pid < )
return ; for (i = ; i < MAXJOBS; i++) {
if (job_list[i].pid == pid) {
clearjob(&job_list[i]);
nextjid = maxjid(job_list) + ;
return ;
}
}
return ;
} /* fgpid - Return PID of current foreground job, 0 if no such job */
pid_t fgpid(struct job_t *job_list) {
int i; for (i = ; i < MAXJOBS; i++)
if (job_list[i].state == FG)
return job_list[i].pid;
return ;
} /* getjobpid - Find a job (by PID) on the job list */
struct job_t *getjobpid(struct job_t *job_list, pid_t pid) {
int i; if (pid < )
return NULL;
for (i = ; i < MAXJOBS; i++)
if (job_list[i].pid == pid)
return &job_list[i];
return NULL;
} /* getjobjid - Find a job (by JID) on the job list */
struct job_t *getjobjid(struct job_t *job_list, int jid) {
int i; if (jid < )
return NULL;
for (i = ; i < MAXJOBS; i++)
if (job_list[i].jid == jid)
return &job_list[i];
return NULL;
} /* pid2jid - Map process ID to job ID */
int pid2jid(pid_t pid) {
int i; if (pid < )
return ;
for (i = ; i < MAXJOBS; i++)
if (job_list[i].pid == pid) {
return job_list[i].jid;
}
return ;
} /* listjobs - Print the job list */
void listjobs(struct job_t *job_list, int output_fd) {
int i;
char buf[MAXLINE]; for (i = ; i < MAXJOBS; i++) {
memset(buf, '\0', MAXLINE);
if (job_list[i].pid != ) {
sprintf(buf, "[%d] (%d) ", job_list[i].jid, job_list[i].pid);
if (write(output_fd, buf, strlen(buf)) < ) {
fprintf(stderr, "Error writing to output file\n");
exit();
}
memset(buf, '\0', MAXLINE);
switch (job_list[i].state) {
case BG:
sprintf(buf, "Running ");
break;
case FG:
sprintf(buf, "Foreground ");
break;
case ST:
sprintf(buf, "Stopped ");
break;
default:
sprintf(buf, "listjobs: Internal error: job[%d].state=%d ", i,
job_list[i].state);
}
if (write(output_fd, buf, strlen(buf)) < ) {
fprintf(stderr, "Error writing to output file\n");
exit();
}
memset(buf, '\0', MAXLINE);
sprintf(buf, "%s\n", job_list[i].cmdline);
if (write(output_fd, buf, strlen(buf)) < ) {
fprintf(stderr, "Error writing to output file\n");
exit();
}
}
}
if (output_fd != STDOUT_FILENO)
close(output_fd);
}
/******************************
* end job list helper routines
******************************/ /***********************
* Other helper routines
***********************/ /*
* usage - print a help message
*/
void usage(void) {
printf("Usage: shell [-hvp]\n");
printf(" -h print this message\n");
printf(" -v print additional diagnostic information\n");
printf(" -p do not emit a command prompt\n");
exit();
} /*
* unix_error - unix-style error routine
*/
void unix_error(char *msg) {
fprintf(stdout, "%s: %s\n", msg, strerror(errno));
exit();
} /*
* app_error - application-style error routine
*/
void app_error(char *msg) {
fprintf(stdout, "%s\n", msg);
exit();
} /*
* Signal - wrapper for the sigaction function
*/
handler_t *Signal(int signum, handler_t *handler) {
struct sigaction action, old_action; action.sa_handler = handler;
sigemptyset(&action.sa_mask); /* block sigs of type being handled */
action.sa_flags = SA_RESTART; /* restart syscalls if possible */ if (sigaction(signum, &action, &old_action) < )
unix_error("Signal error");
return (old_action.sa_handler);
} /*
* sigquit_handler - The driver program can gracefully terminate the
* child shell by sending it a SIGQUIT signal.
*/
void sigquit_handler(int sig) {
printf("Terminating after receipt of SIGQUIT signal\n");
exit();
}

CSAPP2e:Shell lab 解答的更多相关文章

  1. CSAPP shell Lab 详细解答

    Shell Lab的任务为实现一个带有作业控制的简单Shell,需要对异常控制流特别是信号有比较好的理解才能完成.需要详细阅读CS:APP第八章异常控制流并理解所有例程. Slides下载:https ...

  2. 深入理解计算机系统项目之 Shell Lab

    博客中的文章均为meelo原创,请务必以链接形式注明本文地址 Shell Lab是CMU计算机系统入门课程的一个实验.在这个实验里你需要实现一个shell,shell是用户与计算机的交互界面.普通意义 ...

  3. 【CSAPP】Shell Lab 实验笔记

    shlab这节是要求写个支持任务(job)功能的简易shell,主要考察了linux信号机制的相关内容.难度上如果熟读了<CSAPP>的"异常控制流"一章,应该是可以不 ...

  4. Spider-Scrapy css选择器提取数据

    首先我们来说说css选择器:其实在上面的概述:和scrapy相关的函数就这么三个而已:response.css("css表达式").extract().extract_first( ...

  5. CS基础课不完全自学指南

    本文讲的是计算机学生怎么自学专业课,说长点就是该如何借助网络上已有的高质量学习资源(主要是公开课)来系统性的来点亮自己的CS技能树.这篇文章完全就是一篇自学性质的指南,需要对编程充满热情,起码觉得编程 ...

  6. RH033读书笔记(5)-Lab 6 Exploring the Bash Shell

    Lab 6 Exploring the Bash Shell Sequence 1: Directory and file organization 1. Log in as user student ...

  7. RH033读书笔记(11)-Lab 12 Configuring the bash Shell

    Sequence 1: Configuring the bash Shell Deliverable: A system with new aliases that clear the screen, ...

  8. 企业shell面试题及解答

    1.面试题:使用for循环在/tmp目录下批量创建10个html文件,其中每个文件需要包含10个随机小写字母加固定字符串template,示例如下 aaesdffbnv_template.html 方 ...

  9. Lab: Web shell upload via Content-Type restriction bypass

    首先上传一个正常头像. 之后,上传木马文件,并抓包 POST /my-account/avatar HTTP/1.1 Host: ac4f1f7d1eaa6cd2c0d80622001b00f9.we ...

随机推荐

  1. (剑指Offer)面试题23:从上到下打印二叉树

    题目: 从上往下打印出二叉树的每个节点,同层节点从左至右打印. 思路: 很明显,这是一个广度优先遍历. 需要一个队列容器来保存结点,具体操作: 1.将根结点压入队列中,并打印根结点:如果根结点有子结点 ...

  2. Gson 和 Fastjson 你不知道的事

    背景 目前在公司负责的业务, 主要是跟JSON数据打交道, fastjson .gson都用, 他们适用于不同场景.fastjson号称是业界处理json效率最高的框架, 没有之一.但在某些场景下, ...

  3. listView divider marginLeft marginRight

    要实现这样的效果: 新建drawable  用inset 进行实现.代码如下: <?xml version="1.0" encoding="utf-8"? ...

  4. Hadoop on Mac with IntelliJ IDEA - 7 解决failed to report status for 600 seconds. Killing!问题

    本文讲述作业在Hadoop 1.2.1完成map后ruduce阶段遇到failed to report status for 600 seconds. Killing!问题的解决过程. 环境:Mac ...

  5. 【java开发系列】— JDOM创建、改动、删除、读取XML文件

    有非常多中操作XML文件的方法,这里介绍一下JDOM的用法和技巧. JDOM下载地址 创建XML文档 XML文件是一种典型的树形文件,每一个文档元素都是一个document元素的子节点. 而每一个子元 ...

  6. map的实现

    1.map的实现是使用平衡树,AVL树或者红黑树. 2.在无序的情况下,查找为常数时间.有序的时候,查找为对数时间.二叉排序树(BST)就是为了解决这个问题. 3.但是,极端情况下,BST的查找效率退 ...

  7. js设置控件的隐藏与显示的两种方法

    js设置控件的隐藏与显示,设置控件style的display和visibility属性就可以了,下面有个示例,需要的朋友可以参考下用JavaScript隐藏控件的方法有两种,分别是通过设置控件的sty ...

  8. Codeforces Educational Codeforces Round 3 B. The Best Gift 水题

    B. The Best Gift 题目连接: http://www.codeforces.com/contest/609/problem/B Description Emily's birthday ...

  9. UTF-8 BOM(EF BB BF)

    原标题:link标签和script标签跑到body下面,网页顶部有空白,出现“锘匡豢”乱码,UTF-8 BOM,EF BB BF 来自:http://tunps.com/link-and-script ...

  10. Android学习之路

    Android基础 整理下个人认为新手们必须要掌握的知识点,顺便也会附带相应觉得不错的讲解博客地址. 两分钟彻底让你明白Android Activity生命周期(图文)! Activity实际开发中使 ...