【说明:利用fork复制的新进程,并未所有都复制了父进程的东东,这个在结尾时还会说明,先有个了解】linux
关于fork进程,能够用下面这种的通俗方式来理解:shell
首先咱们先来理解一个概念,这个以后在最后还会进行总结的:数组
fork新建立的进程,会复制父进程的全部信息(代码段+数据段+堆栈段+PCB),可是“全部”并不是绝对,仍是有少部分信息是不同的,另外还能够理解,每个进程都有本身独立的4GB的地址空间(对于32位系统来讲)bash
子进程与父进程的区别在于: 一、父进程设置的锁,子进程不继承 对于排它锁,若是说子进程会共享父进程的锁的话,那就有矛盾了。数据结构
二、各自的进程ID和父进程ID不一样异步
三、子进程的未决告警被清除【了解】函数
四、子进程的未决信号集设置为空集【了解】学习
【说明:关于 fork函数,它有一个特征:调用一次,返回两次,就如上面返回值的说明】动画
如上图所说:fork()出来的子进程成功,对于子进程来讲则返回0;而对于父进程来讲返回子进程ID,这是有缘由的,对于进程中的PCB保存了pid和ppid,因此对于子进程来讲,有办法知道pid和ppid,而对于父进程来讲,若是不返回子进程的id,则就没法知道新建立的进程的号码,由于PCB中并不会保存子进程的ID列表,这样就会让PCB膨胀,因此这也是有缘由的。ui
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main(int argc, char *argv[])
{
printf("before fork pid = %d\n", getpid());//打印当前进程ID,也就是原始父进程
pid_t pid;
pid = fork();//产生一个新的进程,注意:它里会有两个进程执行
if (pid == -1)
ERR_EXIT("fork error");
if (pid > 0)
{//证实是父进程
printf("this is parent pid=%d childpid=%d\n", getpid(), pid);//getpid()为当前父进程的ID,而pid则为新建立的进程id,也就是子进程
}
else if (pid == 0)
{//证实是子进程
printf("this is child pid=%d parentpid=%d\n", getpid(), getppid());//getpid()为当前子进程的ID,getppid()为当前子进程所属父进程的ID
}
return 0;
}
复制代码
编译运行:
缘由是因为当执行父进程以后,它就退出了,这时子进程执行时,这时子进程的父进程就变为init进程,因此就变成了1,若是咱们让父进程输出延时一下,保证子进程执行时父进程没退出,就如咱们的预期了:
此次再看效果:
对于fork函数,可能有一点比较难以理解,为啥它一次调用会有两次返回呢?这里再来用文字来解释一下:fork成功意味着建立了一个进程副本,意味着也就有两个进程了,两个进程都要执行各自相应的动做,因此两个进程都得要返回,实际上在内核中,是在各自的进程地址空间中返回的:
关于上面的第二个注意点,,若是父进程退出了,子进程尚未退出,咱们将子进程称为孤儿进程,这时会将子进程托孤给init进程。 关于第三点,其中提到了“僵尸进程”,用程序来看下现象:
这时,查看一下当前的进程状态:
僵尸状态,咱们尽可能得避免它,避免它的方法之一,能够采用一个系统调用----signal(信号,关于它,以后会详述,这里只是先了解一下):
这时,编译运行,再看效果:
下面再来理解一来系统是如何实现fork()的.
实际系统实现时,并未真正把全部的数据(代码段+数据段+堆栈段+PCB)都复制一份,只是为了方便理解,咱们能够认为是数据(代码段+数据段+堆栈段+PCB)都复制了一份,实际上代码段是只读的,是能够被共享的,每一个进程只要保存一个指向这个资源的指针既可,这能够加快进程的建立速度,大大提升了效率
而对于须要修改的进程才会复制那份资源,对于linux而言,它是基于页的的方式进行复制的,并没将全部数据都进行复制,只是复制须要页,其它页是不会复制的,因此咱们得正确理解“每一个进程有本身独立的4GB(对于32位系统来讲)的地址空间”,实际上不被修改的数据是共享的,对于这个理论,大体了解下,也是为了加深对fork()函数的理解。
父进程打开两个文件:
这时经过fork()函数新建了一个子进程,这时它共享父进程的文件,其结构以下:
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main(int argc, char *argv[])
{
signal(SIGCHLD, SIG_IGN);
printf("before fork pid = %d\n", getpid());
int fd;
fd = open("test.txt", O_WRONLY);//父进程打开一个文件
if (fd == -1)
ERR_EXIT("open error");
pid_t pid;
pid = fork();
if (pid == -1)
ERR_EXIT("fork error");
if (pid > 0)
{
printf("this is parent pid=%d childpid=%d\n", getpid(), pid);
write(fd, "parent", 6);//父进程往文件中写入内容
sleep(1);//睡眠是为了不孤儿进程的产生,保证子进程执行的时候,父进程没有退出
}
else if (pid == 0)
{
printf("this is child pid=%d parentpid=%d\n", getpid(), getppid());
write(fd, "child", 5);//子进程往文件中写入内容
}
return 0;
}
复制代码
先建立一个"test.txt",里面是空内容:
也就是能够说明,子进程是共享父进程打开的文件表项的。
注意:有时候可能会test.txt的内容输出以下:
上面这种输出并无按照咱们的预想,可能的缘由是跟两个进程的静态问题形成的,这个问题比较复杂,能够这样理解:也就是还没等子进程执行,父进程就已经结束了,这时子进程的文件偏移量会从0开始,因此以前父进程写入了parent,因为它退出来,子进程从0的位置开始写,因此最终输出就如上图所示了,为了保证如咱们预期来输出,能够将睡眠时间加长上些,保证子进程执行时,父进程没有退出,以下:
上节中咱们知道,fork的拷贝机制是copy on write,图中所说的exec函数,是指加载一个新的程序来执行,这个下面会有介绍到,先大概了解下,若是说没有copy on write机制的话,那父子进程都有本身独立的进程空间,也就是子进程须要完彻底全的拷贝父进程的地址空间,而若是子进程中执行exec的话,等于它被一个新的程序替换掉了,它根本不须要拷贝父进程的数据,因此就会形成地址空间的浪费,这时才引入了vfork,也就是vfork以后子进程在执行exec以前,是不会拷贝父进程的地址空间的,无论子进程有没有改写数据,它是一个历史问题(说得有点抽象,下面会以具体代码来一一阐述的)。
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int gval = 100;
int main(int argc, char *argv[])
{
signal(SIGCHLD, SIG_IGN);
printf("before fork pid = %d\n", getpid());
pid_t pid;
pid = fork();//这里是用的copy on write机制
if (pid == -1)
ERR_EXIT("fork error");
if (pid > 0)
{
sleep(1);//它的目的是为了让子进程先对gval进行++操做,以便观察父进程是否会受影响
printf("this is parent pid=%d childpid=%d gval=%d\n", getpid(), pid, gval);
sleep(3);
}
else if (pid == 0)
{
gval++;//子进程来改写数据
printf("this is child pid=%d parentpid=%d gval=%d\n", getpid(), getppid(), gval);
}
return 0;
}
复制代码
编译运行:
其缘由也就是因为fork()是采用copy on write的机制,下面用图来解析一下上面的结果:
下面将其改成vfork来实现:
编译运行:
这个输出结果,能够代表,vfork产生子进程,当改写数据时也不会拷贝父进程的空间的,父子是共享一份空间,因此当子进程改写的数据会反映到父进程上。
另外这段程序中出现了一个“段错误”,这是由于:
这时,编译再运行,就不会有错误了:
另外执行exec函数也同样,关于它的使用,以后再来介绍。
提示:vfork是一个历史问题,了解一下既可,实际中不多用它!
在演示vfork时,提到“子进程必须马上执行_exit”,那若是用exit(0)退呢?
编译运行:
咱们一般会将return 0 与exit(0)划等号,但若是在vfork()中,仍是划等号么?
编译运行:
那exit与_exit有啥区别呢?下面来探讨下,在探讨以前,先来回顾一下进程的五种终止方式:
下面以一个图来讲明exit与_exit的区别:
区别一:exit是C库中的一个函数;而_exit是系统调用。
区别二:exit在调用内核以前,作了“调用终止处理程序、清除I/O缓冲”;而_exit是直接操做内核,不会作这两件事。
编译运行:
exit(0)至关于return 0;因此可想将上面return 0换为exit(0)也是同样的能在屏幕上打印出来,那若是换成_exit(0)呢?
这时编译运行:
这时就正常显示了:
另外对于exit来讲,它会调用“终止处理程序”,所谓“终止处理程序”,就是指在程序结束的时候会调用的函数代码段,这些代码段,须要咱们安装才能够,能够用以下函数:
其中传递的参数是函数指针。
编译运行:
若是换成是_exit()呢?
编译运行:
插一句:对于fork函数,有一个问题需进一步阐述一下,以便加深对它的理解:
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main(int argc, char *argv[])
{
signal(SIGCHLD, SIG_IGN);
printf("before fork pid = %d\n", getpid());
pid_t pid;
pid = fork();
if (pid == -1)
ERR_EXIT("fork error");
if (pid > 0)
{
printf("this is parent pid=%d childpid=%d\n", getpid(), pid);
sleep(3);
}
else if (pid == 0)
{
printf("this is child pid=%d parentpid=%d\n", getpid(), getppid());
}
return 0;
}
复制代码
输出:
对于上面这段程序,就是上节中学习过的,可是有个问题值得思考一下,为啥fork()以后,不是从"before fork"从main的第一行起输出,而是从fork()以后的代码中去输出,这时由于fork()以后,拷贝了“代码段+数据段+堆栈段+PCB”,也就是两个进程的信息几乎都是同样,而因为堆栈段+PCB几乎是同样的,因此它会维护当前运行的信息,因此每一个进程会从fork()以后的代码继续执行,这一点须要理解。
另外,再看一个跟fork()相关的程序:
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main(int argc, char *argv[])
{
fork();
fork();
fork();
printf("ok\n");
return 0;
}
复制代码
编译运行:
这是为何呢?由于第一个fork()时,会产生两个进程,这时这两个进程都会执行它后面的代码,也就是第二个fork(),这时就有四个进程执行第二个fork()了,一样的,这时四个进程就会执行它下面的代码,也就是第三个fork(),这时就再产生四个进程,总共也就是八个进程了,这个比较很差理解,好好想一下!
最后,咱们来讲明一下execve函数,这个在上面介绍vfork()函数时,已经提到过了,它的做用是:替换进程映像,这时对它进行使用说明:
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int gval = 100;
int main(int argc, char *argv[])
{
signal(SIGCHLD, SIG_IGN);
printf("before fork pid = %d\n", getpid());
pid_t pid;
pid = vfork();
if (pid == -1)
ERR_EXIT("fork error");
if (pid > 0)
{
printf("this is parent pid=%d childpid=%d gval=%d\n", getpid(), pid, gval);
}
else if (pid == 0)
{
char *const args[] = {"ps", NULL};
execve("/bin/ps", args, NULL);//将子进程彻底替换成/bin/ps中的ps进程命令,因此这句话以后的代码就不会执行了,由于是彻底被替换了
gval++;
printf("this is child pid=%d parentpid=%d gval=%d\n", getpid(), getppid(), gval);
}
return 0;
}
复制代码
编译运行:
对于fork()函数,它建立了一个新进程,新进程与原进程几乎是同样的,而对于shell命令,如:
对于shell命令,它自己就是一个进程,要想执行ls程序,则需去加载ls程序,这时shell命令进程则需fork()建立一个新进程,而咱们知道新建立的进程与原进程几乎是同样的,也就意味着新的进程的代码仍是跟shell程序自己是同样的,也就没法执行ls程序,因此,这时咱们只有将新进程用ls程序替换,也就是用exec系列函数来替换,这也就是它的意义所在。
编译运行:
那若是被execlp函数替换后的进程ID是否会发生变化呢?为了说明这个问题,咱们先编写一个打印进程ID的程序:
hello.c:
编译,会用execl替换咱们编写的程序,来论证咱们提出的问题:
再来用execl替换成咱们写的hello程序:
这时运行:
若是将程序作一点小改动,以下:
这时编译运行:
这时为何呢?这是由于execlp函数执行失败了,因此没有替换成功,能够打印一下错误信息:
编译运行:
这时由于:
其中linux的环境变量以下:
下面就具体对execlp系列的每一个函数进行研究,先从总体上来看一下这些函数:
下面用代码来演示一下execlp与execvp这两个函数用法的差异:
编译运行:
换成不带l的函数,看下它的使用方式:
其运行结果跟上面同样,这就是带l的函数与不带l函数的使用区别。
下面来讲明一下函数参数的意义:
下面,咱们来研究一下下面两个函数的区别:
编译运行:
这是由于execl中的程序名须要带上全路径,而execlp不须要定全路径,会自动在环境变量中去搜寻,这就是带p与不带p的区别,因而咱们看一下ls命令的路径:
因而,将这个路径替换一下:
再次编译运行:
因此,对于下面这两个函数也就明白啥区别了:
这里就不作实验了,对于exec系列的函数,最后还剩一个execle函数:
下面就以实际代码来解析下这个参数的含义:
hello.c仍是以前的代码,再贴出来:
编译运行:
下面咱们将hello.c来输出程序的环境变量,实际上有对应的shell命令可以输出,效果以下:
因而改装咱们的hello.c:
而对于environ的数据结构是这样的:
这时,编译一下执行hello:
这时,咱们再执行以前替换hello的函数,这时也会输出环境信息:
这时咱们将execl函数,改成execle,并传递咱们本身的环境信息:
编译运行:
至此,咱们已经把exec系列相关的函数的区别,就已经所有学完了,能够好好体会下,对于这些函数,下面再来讲明下:
execve咱们能够看一下帮助:
最后,再来补充一个知识,在以前咱们学过了fcntl函数,该函数功能很强大,其中还漏了一个没有学到,就是:
编译运行:
若是没有用fcntl设置,咱们是能看到./hello程序的输出结果的,这也就是FD_CLOEXEC标志的做用了,它会对exec系列的函数产生影响,记住这点就能够了。
其实,打开一个文件时,也能够带上FD_CLOEXEC:
【说明:关于信号,很快就会有一个专题来仔细研究它,如今能够简单认为:它是一种异步通知事件】
【说明:若是父进程没有查询子进程的退出状态,子进程是没有办法真正彻底退出的,这时子进程的状态就称为僵尸状态,该进程就叫僵尸进程】
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>//提供wait函数声明
#include <sys/types.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main(int argc, char *argv[])
{
pid_t pid;
pid = fork();
if (pid == -1)
ERR_EXIT("fork error");
if (pid == 0)
{
sleep(3);//子进程休眠,是为了看到父进程会等待子进程退出
printf("this is child\n");
exit(100);
}
printf("this is parent\n");
int status;
wait(&status);等待子进程退出
return 0;
}
复制代码
看一下编译运行效果,下面用动画来展示,以便能体现到wait的意义:
从图中能够感觉到,父进程虽然是已经输出了,可是一直是等到子进程退出了才退出,这也就是wait会让父进程去查子进程的退出状态,从而避免了僵尸进程的出现。
编译运行:
对于这些状态信息,能够经过调用系统宏来查询,下面具体来介绍下:
编译运行:
下面咱们能够用abort函数,来模拟子进程非法退出的状况:
这时再编译运行:
实际上,对于子进程非法退出的,还能够判断得再仔细一些,由于有好几种状况能够形成子进程非法退出,如上图所示,一是由于捕获信号而终止,二是被暂停了,具体用法以下:
编译运行:
【说明:上面的这些宏在sys/wait.h头文件里定义】
对于上面刚学完的wait,它是等待随意的进程退出,由于一个父进程能够有多个子进程,若是只要有一个子进程退出,父进程的wait就会返回;
而waitpid则能够等待特定的进程退出,这是二者的区别,下面就来具体学习下这个函数的用法:
对于waitpid的pid参数的解释与其值有关:
实际上能够经过查看man帮助获得这个信息:
能够将咱们以前的程序用waitpid替换一下,效果同样:
也一样,用它来改装咱们以前的程序,对于父进程,实际上只有一个子进程,就能够直接传子进程的id既可,只等待这个子进程:
效果同样:
好比:waitpid(-100,&status,0)的意思就是,等待进程组ID=100里面的任一一个子进程。
最后,关于wait和waitpid,进行一个总结:
另外,对于僵进程,已经被提到过好几回了,最后再来总结一下:
实际上,它就等于用代码去执行咱们在命令行中敲的那些shell命令,下面就以实际代码来认识这个函数的使用:
编译运行:
实际上,system()函数是调用"/bin/sh -c",以下:
对于这个函数的使用,其实没什么难的,可是这个函数颇有表明性,咱们能够经过它来综合运用咱们所学习东西,这样其实仍是挺有意义的,接下来,会本身实现一个跟system一样的功能,来达到理解system函数的实现原理:
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int my_system(const char *command);//本身实现有system函数声明
int main(int argc, char *argv[])
{
my_system("ls -l | wc -w");//这里改用本身实如今system
return 0;
}
int my_system(const char *command)
{
pid_t pid;
int status;
if (command == NULL)
return 1;
  if ((pid = fork()) < 0)
status = -1;//出现不能执行system调用的其余错误时返回-1
  else if (pid == 0)
{//子进程
execl("/bin/sh", "sh", "-c", command, NULL);//替换成sh进程
exit(127);//若是没法启动shell运行命令,system将返回127,由于若是成功替换了以后,是不会执行到这句来的
}
else
{//父进程会等到子进程执行完
while (waitpid(pid, &status, 0) < 0)
{
if (errno == EINTR)//若是是被信号打断的,则从新waitpid
continue;
status = -1;
break;
}
     //这时就顺利执行完了
}
return status;
}
复制代码
编译运行:
在描述它以前,首先得先了解两个概念:进程组、会话期:
而它里面有bash shell进程组,里面只有bash进程:
而一个会话期,实际上就对应一个终端,当咱们打开多个虚拟终端时,能够用tty来查看终端数:
而守护进程是跟控制终端无关的,而且是在后台执行的,若是想让咱们在shell中启动的进程变成守护进程,则应该将它放到会话期当中:
那这时,咱们须要一个建立新的会话期的函数,其实是系统函数,它为setsid(),经过man来查看一下它的说明:
这就意味着,咱们在建立一个新的会话期以前,须要准备一个进程,保证该进程不是一个进程组组长,那如何保证呢?因为咱们运行的shell命令的父进程多是进程组组长,因此须要让父进程退出,这样就能够保证fork出来的子进程不是进程组组长,从而能够建立一个新的会话期了,总结一下上面说的流程:
按照上面的步骤下面以具体代码来实现一个守护进程:
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int setup_daemon(void);
int main(int argc, char *argv[])
{
return 0;
}
int setup_daemon(void)
{
pid_t pid;
pid = fork();
if (pid == -1)
ERR_EXIT("fork error");
if (pid > 0)//将父进程退出,保证子进程不是进程组组长
exit(EXIT_SUCCESS);
setsid();//若是走到这,表明是子进程,因为它不是一个进程组组长,因此能够建立一个新的会话期
return 0;
}
复制代码
当咱们用setsid()建立一个新的会话期以后,会有一个什么样的影响呢,仍是接着看它的说明介绍:
也就是以下图所示:
其实上面的程序就已经实现了一个守护进程,咱们调用一下运行看下:
编译运行:
咱们来查看下进程:
守护进程一般是在系统运行而运行的,一般将当前目录改成根目录,由于有可能守护进程是在某个shell提示符下运行的, 那么当前目录就是shell提示符所在的目的, 就拿咱们建立的这个守护进程而言,它的当前目录为:
这样,系统管理员就没法umount这个目录,由于守护进程是学期在后期运行的,这个目录不该该做为它的环境,因此这就产生了建立守护进程的第四个步骤:
修改代码:
最后还有一个步骤:
【说明:/dev/null表示空设备,这里就是把日志记录到空设备里,就是不记录日志。】
这时再运行,若是咱们往屏幕输出内容,这时是看不到内容的,由于已经将标准输出重定向了空设备:
实际上linux上已经有现成的方法能够建立一个守护进程了,以下:
在运行它以前,咱们来看下如今应该有几个守护进程了:
先将其都杀掉,以便来观察调用系统的建立守护进程是否成功:
这时,再运行:
对于系统的这个函数,都是传递的0,若是传递1会怎样呢?
编译运行:
实际上,对于咱们写的守护进程,也能够模拟成跟系统调用方式同样,修改程序以下:
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int setup_daemon(int nochdir, int noclose);//模拟系统建立守护进程的函数声明
int main(int argc, char *argv[])
{
setup_daemon(1, 1);//这时改用跟调用系统建立守护进程的本身实现的函数
printf("test ...\n");
for (;;) ;
return 0;
}
int setup_daemon(int nochdir, int noclose)
{
pid_t pid;
pid = fork();
if (pid == -1)
ERR_EXIT("fork error");
if (pid > 0)
exit(EXIT_SUCCESS);
setsid();
if (nochdir == 0)//实现很简单,作下参数判断既可
chdir("/");
if (noclose == 0)
{
int i;
for (i=0; i<3; ++i)
close(i);
open("/dev/null", O_RDWR);
dup(0);
dup(0);
}
return 0;
}
复制代码
编译运行:
【提示:在建立守护进程时,不重定向至空设备其实对于开发期间便于调试,若是等程序发布了以后,就得重定向了!】