期中之后的第一个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. Combox和DropDownList控件的区别

    共同点:都是下拉框控件 不同点:Combox用在winform上,DropDownList用在网页上,且两者绑定方式略有不同 绑定数据例子如下—— 1.Combox绑定 DataTable dtBus ...

  2. 哈希表(Hash)的应用

    $hs=@() #定义数组 $hs=@{} #定义Hash表,使用哈希表的键可以直接访问对应的值,如 $hs["王五"] 或者 $hs.王五 的值为 75 $hs=@''@ #定义 ...

  3. Delphi Data Type to C# Data Type

    Delphi DataType C# datatype ansistring string boolean bool byte byte char char comp double currency ...

  4. Codeforces Good Bye 2015 D. New Year and Ancient Prophecy 后缀数组 树状数组 dp

    D. New Year and Ancient Prophecy 题目连接: http://www.codeforces.com/contest/611/problem/C Description L ...

  5. Codeforces Gym 100463D Evil DFS

    Evil Time Limit: 20 Sec Memory Limit: 256 MB 题目连接 http://codeforces.com/gym/100463/attachments Descr ...

  6. Codeforces Round #310 (Div. 1) C. Case of Chocolate set

    C. Case of Chocolate Time Limit: 20 Sec Memory Limit: 256 MB 题目连接 http://codeforces.com/contest/555/ ...

  7. ACdream 1115 Salmon And Cat (找规律&amp;&amp;打表)

    题目链接:传送门 题意: 一个数被觉得是一个完美的数,仅仅要须要满足下面的两个条件之中的一个 1)x = 1 or 3 2)x = 2 + a*b + 2*a + 2*b; a.b都是完美的数. 分析 ...

  8. ECLIPSE android 布局页面文件出错故障排除Exception raised during rendering: java.lang.System.arraycopy([CI[CII)V

    在布局添加控件手动添加还是拖的添加,添加edittext后布局就不好用,其他控件好用,然后就说下面这段话 Exception raised during rendering: java.lang.Sy ...

  9. Android Checkbox Example

    1. Custom String 打开 “res/values/strings.xml” 文件, File : res/values/strings.xml <?xml version=&quo ...

  10. 如何在 Visual Studio 2012 控制 TFS 版控時要忽略哪些檔案

    幾乎在任何一種版本控管的機制裡,都會遇到那些「不應該簽入到版本庫」的潛規則,以往我們在用 SVN 的時候,我就寫過幾篇文章要大家注意這點.最近都改用 TFS 做版控,因為大多使用 Visual Stu ...