进程创建
fork函数初识
fork函数:从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程
#include <unistd.h>
pid_t fork(void);
返回值:子进程中返回0,父进程返回子进程id,出错返回-1
4 int main()
5 {
6 printf("Before: pid is %d\n", getpid());
7 pid_t id = fork();
8
9 if(id==-1)
10 {
11 perror("fork()");
12 exit(1);
13 }
14
15 printf("After:pid is %d, fork return %d\n", getpid(), id);
16 sleep(1);
17 return 0;
18 }
写时拷贝

当父进程形成子进程之后,子进程写入,就要发生写时拷贝(重新申请空间,进行拷贝,修改页表(由OS完成)),问题:子进程正在写入,怎样完成写时拷贝的操作?
原因:父进程创建子进程的时候,首先将自己的页表中的读写权限改成只读,然后再创建子进程
以上这一点,用户不知道,用户可能对某一批数据进行写入,那么对只读权限的数据进行写入操作,此时页表的虚拟地址向物理地址转换过程,会因为权限问题而出错,操作系统OS就可以介入了,出错分两类:
1 真的出错 如对代码区进行写入操作
2 不是真的出错 会触发我们进行重新申请内存,拷贝内容的策略机制(写时拷贝)
fork常规用法
fork调用失败的原因
系统中有太多的进程
5 #define N 10
6
7 void Worker()
8 {
9 int cnt = 10;
10 while(cnt)
11 {
12 printf("I am child process, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
13 sleep(1);
14 cnt--;
15 }
16 }
17
18 int main()
19 {
20 for(int i = 0;i<N;i++)
21 {
22 sleep(1);
23 pid_t id = fork();
24 if(id==0)
25 {
26 printf("create child process success: %d\n", i);
27 // child
28 Worker();
29 exit(0);
30 }
31 }
32
33 //只有父进程走到这里
34 sleep(100);
35
36 return 0;
37 }
监控:
改进以上代码:
8 #define N 10
9
10 typedef void (*callback_t)();
11
12 void Worker()
13 {
14 int cnt = 10;
15 while(cnt)
16 {
17 printf("I am child process, pid: %d, ppid: %d, cnt: %d\ n", getpid(), getppid(), cnt);
18 sleep(1);
19 cnt--;
20 }
21 }
22
23 void createSubPorcess(int n, callback_t cb)
24 {
25 for(int i = 0; i < n; i++)
26 {
27 sleep(1);
28 pid_t id = fork();
29 if(id == 0)
30 {
31 printf("create child process success: %d\n", i);
32 // child
33 cb();
34 exit(0);
35 }
36 }
37 }
38
39
40 int main()
41 {
42 createSubPorcess(N, Worker);
43 //只有父进程走到这里
44 sleep(100);
45 return 0;
46 }
进程终止
main函数也是一个函数,由返回值,我们平常main函数返回的都是0,可以返回其他值吗?当然是可以的,我们可以用echo $?来查看main函数的返回值
?:保存的是最近一个子进程执行完毕时的退出码
main函数的返回值,叫做进程的退出码,0表示成功,非0表示失败
进程退出场景
1 代码运行完毕,结果正确
2 代码运行完毕,结果不正确
3 代码异常终止
在多进程环境中,父进程创建子进程的目的是,帮它办事
进程常见退出方法
正常终止(可以通过 echo $? 查看进程退出码):
1. 从main返回
2. 调用exit
3. _exit
异常退出:ctrl + c,信号终止
退出码
父进程可以通过获得子进程的退出码即main函数的返回值,用来判断子进程的运行结果(来得知子进程把事情办得怎么样)
当某进程的退出码为非0,我们需要知道这个进程是因为什么原因失败的(如退出码为1,2,3,4)我们就可用不同的数字表示不同的原因
纯数字能够表示错误原因,但是不直观,所以我们需要将退出码的纯数字形式转换成退出码的字符串形式,strerror就具有这种功能(内置的转换),当然我们也可以自定义将纯数字的退出码定义成我们想要表达的字符串形式
42 for(int i = 0;i<200;i++)
43 {
44 printf("%d: %s\n",i,strerror(i));
45 }
40 int main()
41 {
42 for(int i = 0;i<200;i++)
43 {
44 printf("%d: %s\n",i,strerror(i));
45 }
46 return 10;
47 }
现在最近执行完成的子进程是echo $?命令,它是成功执行的,所以退出码是0
错误码errno
当某个库函数/系统调用接口出错的时候会被自动设置
42 printf("before: %d\n", errno);
43 FILE *fp = fopen("./log.txt", "r");//没有这个文件
44 printf("after: %d, error string : %s\n", errno, strerror(errno));
退出码vs错误码
错误码:通常是衡量一个库函数或者是一个系统调用接口的调用情况
当失败的时候,用来衡量函数的详细出错原因
退出码:通常是一个进程退出的时候,它的退出结果
当失败的时候,用来衡量进程出错的详细出错原因
我们可以将退出码与错误码设置成一样的:
40 int main()
41 {
42 int ret = 0;
43 printf("before: %d\n", errno);
44 FILE *fp = fopen("./log.txt", "r");//没有这个文件
45 if(fp == NULL)
46 {
47 printf("after: %d, error string : %s\n", errno, strerror( errno));
48 ret = errno;
49 }
50 return ret;
51 }
进程异常
进程的退出场景中还有一条是:代码异常终止
程序运行起来就是一个进程了,程序崩溃,其实就是进程异常,一旦进程异常,就不会再继续运行了,即被操作系统杀掉(也只能被OS杀掉),(因为操作系统是进程的管理者)
操作系统通过信号的方式来杀掉进程
除零错误:
int a = 10;
a/=0;
其实也就是8号信号:SIGFPE
信号:
当进程出现异常,异常信息会被操作系统检测到,进而被操作系统转换成信号把这个进程杀掉
我们可以给正常的进程发送某些信号:
while(1)
{
printf("I am a normal process: %d\n", getpid());
sleep(1);
}
进程出异常,本质是进程收到了对应的信号,自己终止了
所以一个进程是否出现异常,我们只要看有没有收到信号即可
信号与退出码都是数字,信号没有0号,所以判断是否为信号,只需要看它是否为0
exit函数
参数是进程的退出码,类似于main函数的return
42 printf("I am a process, pid: %d, ppid: %d\n",getpid(), getppid());
43 exit(10);
任意地点调用exit,表示进程退出,不进行后续执行
39 void func()
40 {
41 printf("call func()\n");
42 exit(6);
43 }
44
45 int main()
46 {
47 func();
48 printf("I am a process, pid: %d, ppid: %d\n",getpid(), getppid());
49 exit(10);
50 }
exit最后也会调用_exit
_exit函数
#include <unistd.h>
void _exit(int status);
参数:status 定义了进程的终止状态,父进程通过wait来获取该值
39 void func()
40 {
41 printf("call func()\n");
42 _exit(6);
43 }
44
45 int main()
46 {
47 func();
48 printf("I am a process, pid: %d, ppid: %d\n",getpid(), getppid());
49 _exit(10);
50 }
exit与_exit都能用来终止进程
47 printf("hello\n");
48 sleep(3);
49 exit(1);
现象:hello字符串一开始就显示
去掉上面代码中的\n
47 printf("hello");
48 sleep(3);
49 exit(1);
现象:一开始hello字符串并没有显示出来,当进程退出后才显示
printf向显示器打印是行刷新,因为没有\n所以不会立即显示
结论:exit在终止进程时会刷新缓冲区
47 printf("hello");
48 sleep(3);
49 _exit(1);
现象:进程结束后没有看到字符串hello
exit vs _exit
1 exit是库函数 _exit是系统调用
2 exit终止进程的时候,会自动刷新缓冲区 _exit终止进程的时候,不会自动刷新缓冲区
我们目前知道的缓冲区,绝对不再操作系统内部(否则exit()能刷新,_exit()也应该要刷新),这个缓冲区在C标准库里
所以当我们调用exit()时,会先把对应的数据写到操作系统里,然后再刷新
但是调用_exit()时,数据是在C缓冲区的,数据不在操作系统里,所以终止进程时不会刷新
进程等待
什么是进程等待
通过wait/waitpid的方式,让父进程(一般是父进程)对子进程进行资源回收的等待过程
为什么要进行等待
1 解决子进程僵尸问题带来的内存泄露问题(必须)
2 父进程创建子进程的目的是让子进程完成任务,子进程将任务完成得如何
父进程需要知道--->通过进程等待的方式获取子进程的退出信息--两个数字(信号编码,退出码)
(退出码看运行结果的正确与否,信号编码看进程异常与否)
(不是必须,但是系统需要提供这样的基础功能)
如何进行等待
wait:
一次等待任意一个子进程的退出
14 void Worker()
15 {
16 int cnt = 5;
17 while(cnt)
18 {
19 printf("I am child process, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt--);
20 sleep(1);
21 }
22 }
23
24
25 int main()
26 {
27 pid_t id = fork();
28 if(id==0)
29 {
30 //child
31 Worker();
32 exit(0);
33 }
34 else
35 {
36 sleep(10);
37 //father
38 pid_t rid = wait(NULL);
39 if(rid==id)
40 {
41 printf("wait success, pid: %d\n",getpid());
42 }
43
44 sleep(10);
45 }
46 return 0;
47 }
所以进程等待能够回收子进程的僵尸状态
下一个问题:子进程运行期间,父进程有没有调用wait呢?父进程在干什么?
14 void Worker()
15 {
16 int cnt = 5;
17 while(cnt)
18 {
19 printf("I am child process, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt--);
20 sleep(1);
21 }
22 }
23
24
25 int main()
26 {
27 pid_t id = fork();
28 if(id==0)
29 {
30 //child
31 Worker();
32 exit(0);
33 }
34 else
35 {
37 //father
printf("wait before\n");
38 pid_t rid = wait(NULL);
printf("wait after\n");
39 if(rid==id)
40 {
41 printf("wait success, pid: %d\n",getpid());
42 }
43
44 sleep(10);
45 }
46 return 0;
47 }
如果子进程根本没有退出,父进程必须在wait上进行阻塞等待,直到子进程僵尸,wait自动回收,返回
一般而言,父子进程谁先运行不知道,由调度器决定,但是一般都是父进程最后退出
waitpid:
waitpid有wait一样的功能,此外还可获取退出信息
获取子进程status
wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充
如果传递NULL,表示不关心子进程的退出状态信息
否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程
其中status是一个输出型参数
14 void Worker()
15 {
16 int cnt = 5;
17 while(cnt)
18 {
19 printf("I am child process, pid: %d, ppid: %d, cnt: %d\ n", getpid(), getppid(), cnt--);
20 sleep(1);
21 }
22 }
25 int main()
26 {
27 pid_t id = fork();
28 if(id==0)
29 {
30 //child
31 Worker();
32 exit(10);
33 }
34 else
35 {
36 printf("wait before\n");
37 //father
38 int status = 0;
39 pid_t rid = waitpid(id,&status,0);
40 printf("wait after\n");
41 if(rid==id)
42 {
43 printf("wait success, pid: %d, status: %d\n",getpid(),status);
44 }
45
46 sleep(10);
47 }
48 return 0;
49 }
子进程的退出码是10,status为什么为2560?
所以要想获得退出信息(信号编码与退出码) 就不能对status整体使用
进程异常的场景:子进程执行的代码除0错误
当一个进程异常(收到了信号)退出码exit code就无意义了
判断一个进程是否异常,只需要看信号编码exit sig是否为0(0表示正常,非0表示异常,因为信号编码都是非0的)
父进程是如何通过wait/waitpid获取子进程的退出信息(退出码与退出信号)呢?
当然啦,以上方式获取status中的退出信号与退出码优点麻烦,这里有更轻松的方式:
14 void Worker()
15 {
16 int cnt = 5;
17 while(cnt)
18 {
19 printf("I am child process, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt--);
20 sleep(1);
21 int a = 10;//除0错误,进程异常
22 a/=0;
23 }
24 }
27 int main()
28 {
29 pid_t id = fork();
30 if(id==0)
31 {
32 //child
33 Worker();
34 exit(0);
35 }
36 else
37 {
38 printf("wait before\n");
39 //father
40 int status = 0;
41 pid_t rid = waitpid(id,&status,0);
42 printf("wait after\n");
43 if(rid==id)
44 {
45 if(WIFEXITED(status))
46 {
47 printf("child process normal quit, exit code : %d\n", WEXITSTATUS(status));
48 }
49 else
50 {
51 printf("child process quit except!\n");
52 }
53 }
54
55 sleep(10);
56 }
57 return 0;

将以上代码稍加改动,去掉除0的代码,恢复成正常代码,让进程正常终止
等待多个进程:
6 const int n = 10;
7
8 void Worker(int number)
9 {
10 int cnt = 10;
11 while(cnt)
12 {
13 printf("I am child process, pid: %d, ppid: %d, cnt: %d, number: %d\n", getpid(), getppid(), cnt--, number);
14 sleep(1);
15 }
16 }
18 int main()
19 {
20 for(int i = 0;i<n;i++)
21 {
22 pid_t id = fork();
23 if(id==0)
24 {
25 Worker(i);
26 exit(i);
27 }
28 }
29
30 //等待多个子进程
31 for(int i = 0;i<n;i++)
32 {
33 int status = 0;
34 pid_t rid = waitpid(-1,&status,0);
35 if(rid>0)
36 {
37 printf("wait child %d success, exit code: %d\n", rid, WEXITSTATUS(status));
38 }
39 }
40 return 0;
41 }
父进程调用wait/waitpid 为阻塞式调用时:子进程不退出,wait/waitpid不返回(阻塞等待)
父进程什么都干不了
为非阻塞式调用时: 若是等待的条件不满足,wait/waitpid不阻塞,立即返回(非阻塞等待)
往往要重复调用
我们通常采用:轮询+非阻塞式方案,进行进程等待(即将wait放在while循环中,一直去检测)
优点在于:在进行等待的过程中,可以顺便做一下占据时间并不多的某些事情
waitpid的返回值:
返回值>0: 等待成功
返回值==0:等待是成功的,但是对方还没有退出
返回值<0:等待失败
一个简单的非阻塞式等待代码:
#define TASK_NUM
typedef void (*task_t)();
8 void worker(int cnt)
9 {
10 printf("I am child,pid: %d,ppid: %d\n,cnt: %d\n",getpid(),getppid(),cnt);
11 }
void download()
{
printf("this is a download task is running!\n");
}
void printLog()
{
printf("this is a write log task is running!\n");
}
void show()
{
printf("this is a show info task is running!\n");
}
void executeTask(task_t tasks[],int num)
{
for(int i = 0;i<num;i++)
{
if(tasks[i])
{
tasks[i]();
}
}
}
int addTask(task_t tasks[],task_t t)
{
int i = 0;
for(; i<TASK_NUM;i++)
{
if(tasks[i]==NULL)
{
tasks[i] = t;
return 1;
}
}
return 0;
}
void InitTasks(task_t tasks[],int num)
{
for(int i = 0;i<num;i++)
{
tasks[i] = NULL;
}
}
12
13 int main()
14 {
task_t tasks[TASK_NUM];
InitTasks(tasks,TASK_NUM);
addTask(tasks,download);
addTask(tasks,printLog);
addTask(tasks,show);
15 pid_t id = fork();
16 if(id==0)
17 {
18 //child
19 int cnt = 5;
20 while(cnt)
21 {
22 worker(cnt);
23 sleep(2);
24 cnt--;
25 }
26
27 exit(0);
28 }
30 while(1)
31 {
32 //father
33 int status = 0;
34 pid_t rid = waitpid(id,&status,WNOHANG);
35 if(rid>0)
36 {
37 //wait success,child quit now
38 printf("child quit success, exit code: %d, exit signal: %d\n",(status>>8)&0xFF,status&0x7F);
39 break;
40 }
41 else if(rid==0)
42 {
43 //wait success,but child not quit
printf("########################\n");
44 printf("child is alive,wait again,father do other thing...\n");
executeTask(tasks,TASK_NUM);
printf("########################\n");
45 }
46 else
47 {
48 //wait failed,child unknow
49 printf("wait failed!\n");
50 break;
51 }
52
53 sleep(1);
54 }
55 return 0;
56 }
非阻塞等待,可以让等待方在wait返回时,顺便做做自己的事情
进程程序替换
我们所创建的所有子进程,它们执行的代码,都是父进程代码的一部分,只不过采用了if else进行分流,如果我们想要子进程执行新的程序呢?执行全新的代码,访问全新的数据,不再与父进程有瓜葛?答案:程序替换
下图为父子进程有瓜葛:
单进程版的程序替换代码(无子进程)
--见见程序替换
1 #include<unistd.h>
2 #include<stdio.h>
3
4 int main()
5 {
6 printf("pid: %d,exec command begin\n",getpid());
7 execl("/usr/bin/ls","ls","-a","-l",NULL);
8 printf("pid: %d,exec command end\n",getpid());
9 return 0;
10 }
现象:我们可以用“语言”调用其他程序
怎么不显示以下这一条语句??后面回答
程序替换的原理
每个进程运行起来就要有自己的task_struct,mm_struct(地址空间) 页表
程序替换就是:用新程序的代码和数据替换调用exec系列函数的程序的代码与数据
其中并没有创建新进程
多进程版的程序替换代码:
5 int main()
6 {
7 pid_t id = fork();
8 if(id==0)
9 {
10 //child
11 printf("pid: %d,exec command begin\n",getpid());
12 sleep(3);
13 execl("/usr/bin/ls","ls","-a","-l",NULL);
14 printf("pid: %d,exec command end\n",getpid());
15 }
16 else
17 {
18 //father
19 pid_t rid = waitpid(-1,NULL,0);
20 if(rid>0)
21 {
22 printf("wait success,rid: %d\n",rid);
23 }
24 }
25 return 0;
26 }
进程具有独立性,父进程fork创建出子进程,代码和数据共享,当新程序的代码与数据要替换子进程的代码和数据时,会发生写时拷贝,让父子进程的代码和数据各自私有一份,那么子进程的代码和数据被替换则不会影响父进程
子进程怎么知道要从新程序的最开始执行?它怎么知道最开始的地方在哪里?
exce系列的函数会读取新程序(磁盘中的文件,EIF格式)头部字段entry的内容:可执行程序的入口地址,将其写到调用exec系列函数的程序的eip(CPU内的寄存器,记录该程序执行到哪里了)中,这样原程序就执行了新程序的内容
exec系列的函数,如果当前进程调用执行成功,那么后续代码没有机会执行了,因为被替换掉了
exec系列的函数只有失败的返回值,无成功的返回值
所以对于execl系列的函数,不用判断,只要执行exec系列函数后续的代码就是出错
正确写法:
程序替换的方法
程序替换的要解决的本源问题:
1 必须先找到这个可执行程序
2 必须告诉exec系列的函数怎么执行
#include <unistd.h>`
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ...,char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);

execl:
execlp:
6 int main()
7 {
8 pid_t id = fork();
9 if(id==0)
10 {
11 //child
12 printf("pid: %d,exec command begin\n",getpid());
13 sleep(1);
14 execlp("ls","ls","-a","-l",NULL);
15 printf("pid: %d, exec command end\n",getpid());
16 exit(1);
17 }
18 else
19 {
20 //father
21 pid_t rid = waitpid(-1,NULL,0);
22 if(rid>0)
23 {
24 printf("wait success,rid: %d\n",rid);
25 }
26 }
27 return 0;
28 }
execv:
6 int main()
7 {
8 pid_t id = fork();
9 if(id==0)
10 {
11 char* const argv[] = {
12 "ls",
13 "-a",
14 "-l",
15 NULL
16 };
17 //child
18 printf("pid: %d,exec command begin\n",getpid());
19 sleep(3);
20 execv("/usr/bin/ls",argv);
21 printf("pid: %d, exec command end\n",getpid());
22 exit(1);
23 }
24 else
25 {
26 //father
27 pid_t rid = waitpid(-1,NULL,0);
28 if(rid>0)
29 {
30 printf("wait success,rid: %d\n",rid);
31 }
32 }
33 return 0;
34 }
execvp:
6 int main()
7 {
8 pid_t id = fork();
9 if(id==0)
10 {
11 char* const argv[] = {
12 "ls",
13 "-a",
14 "-l",
15 NULL
16 };
17 //child
18 printf("pid: %d,exec command begin\n",getpid());
19 sleep(3);
20 execvp(argv[0],argv);
21 printf("pid: %d, exec command end\n",getpid());
22 exit(1);
23 }
24 else
25 {
26 //father
27 pid_t rid = waitpid(-1,NULL,0);
28 if(rid>0)
29 {
30 printf("wait success,rid: %d\n",rid);
31 }
32 }
33 return 0;
34 }
程序替换能替换系统的指令程序,当然也能替换我们自己写的程序
mytest2.cc
Makefile:一次形成两个可执行程序的做法
现在用我们自己写的c程序将c++程序调用起来:
mytest.c:
6 int main()
7 {
8 pid_t id = fork();
9 if(id==0)
10 {
11 //child
12 printf("pid: %d,exec command begin\n",getpid());
13 sleep(3);
14 execl("./mytest2","mytest2",NULL);
15 printf("pid: %d, exec command end\n",getpid());
16 exit(1);
17 }
18 else
19 {
20 //father
21 pid_t rid = waitpid(-1,NULL,0);
22 if(rid>0)
23 {
24 printf("wait success,rid: %d\n",rid);
25 }
26 }
27 return 0;
28 }
exec系列的函数能够执行程序的进程替换,所以除了上面用c程序调用c++程序外,我们还可以用c程序调用脚本程序
1 当我们进行程序替换的时候,子进程对应的环境变量,是可以直接从父进程来的
儿子进程的环境变量从父进程继承下来,孙子进程的环境变量从儿子进程继承下来
验证:
在儿子进程中导入环境变量:使用putenv
儿子进程新增的环境变量不会影响父进程:
bash中没有MYVAL2
2 环境变量被子进程继承下去是一种默认行为,不受程序替换的影响
因为通过地址空间可以让子进程继承父进程的环境变量数据
疑问:环境变量与命令行参数也是数据,exec函数会替换进程的代码与数据,为什么环境变量不受程序替换的影响?
答:程序替换,环境变量不会被替换
让子进程执行的时候,获得的环境变量:
1 将父进程的环境变量原封不动传给子进程
a 直接用 b 直接传
execle:
其中传递的envp不是新增式传递,而是覆盖式传递
mytest.c
extern char**environ;
6 int main()
7 {
8 pid_t id = fork();
9 if(id==0)
10 {
11 //child
12 printf("pid: %d,exec command begin\n",getpid());
13 execle("./mytest2","mytest2",NULL,environ);
14 printf("pid: %d, exec command end\n",getpid());
15 exit(1);
16 }
17 else
18 {
19 //father
20 pid_t rid = waitpid(-1,NULL,0);
21 if(rid>0)
22 {
23 printf("wait success,rid: %d\n",rid);
24 }
25 }
26 return 0;
27 }
2 想要传递自己的环境变量
我们可以直接构造环境变量表,给子进程传递
mytest.c
9 int main()
10 {
11 char *const myenv[] ={
12 "MYVAL1=11111111111111",
13 "MYVAL2=11111111111111",
14 "MYVAL3=11111111111111",
15 "MYVAL4=11111111111111",
16 NULL
17 };
18
19 pid_t id = fork();
20 if(id==0)
21 {
22 //child
23 printf("pid: %d,exec command begin\n",getpid());
24 execle("./mytest2","mytest2",NULL, myenv);
25 printf("pid: %d, exec command end\n",getpid());
26 exit(1);
27 }
28 else
29 {
30 //father
31 pid_t rid = waitpid(-1,NULL,0);
32 if(rid>0)
33 {
34 printf("wait success,rid: %d\n",rid);
35 }
36 }
37 return 0;
38 }
3 想要新增式传递环境变量
使用putenv在当前进程导入环境变量,可以传给它的子进程
总结:
程序替换可以将命令行参数和环境变量通过自己的参数传递给被替换的程序的main函数中
尤其是环境变量,采用覆盖式传递
execve:
设计简易shell

要写一个shell,需要循环以下过程:
5. 父进程等待子进程退出(wait/waitpid)
Makefile:
1 mybash:myshell.c
2 gcc -o $@ $^ -std=c99
3 .PHONY:clean
4 clean:
5 rm -f mybash
myshell.c
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <string.h>
4 #include <unistd.h>
5 #include <sys/types.h>
6 #include <sys/wait.h>
7
8
9 #define NUM 1024
10 #define SIZE 64
11 #define SEP " "
12 //#define Debug 1
13
14 int lastcode = 0;//最近一个进程的退出码
15 char enval[1024];//设置环境变量
16 char cwd[1024];//设置环境变量PWD的值
17
18 const char *getUsername()
19 {
20 const char *name = getenv("USER");
21 if(name) return name;
22 else return "none";
23 }
24 const char *getHostname()
25 {
26 const char *hostname = getenv("HOSTNAME");
27 if(hostname) return hostname;
28 else return "none";
29 }
30 const char *getCwd()
31 {
32 const char *cwd = getenv("PWD");
33 if(cwd) return cwd;
34 else return "none";
35 }
36
37 int getUserCommand(char *command, int num)
38 {
39 printf("[%s@%s %s]# ", getUsername(), getHostname(), getCwd());
40 char *r = fgets(command, num, stdin); // 最终你还是会输入\n
41 if(r == NULL) return -1;
42 // "abcd\n" 处理'\n'
43 command[strlen(command) - 1] = '\0'; //不会越界,至少会有\n
44 return strlen(command);
45 }
46
47 void commandSplit(char *in, char *out[])
48 {
49 int argc = 0;
50 out[argc++] = strtok(in, SEP);
51 while( out[argc++] = strtok(NULL, SEP));
52
53 #ifdef Debug
54 for(int i = 0; out[i]; i++)
55 {
56 printf("%d:%s\n", i, out[i]);
57 }
58 #endif
59 }
60
61 int execute(char *argv[])
62 {
63 pid_t id = fork();
64 if(id < 0) return -1;
65 else if(id == 0) //child
66 {
67 // exec command
68 execvp(argv[0], argv); // cd ..
69 exit(1);
70 }
71 else // father
72 {
73 int status = 0;
74 pid_t rid = waitpid(id, &status, 0);
75 if(rid > 0){
76 lastcode = WEXITSTATUS(status);
77 }
78 }
79
80 return 0;
81 }
82
83
84 char *homepath()
85 {
86 char *home = getenv("HOME");
87 if(home) return home;
88 else return (char*)".";
89 }
90
91
92 void cd(const char *path)
93 {
94 chdir(path);
95 char tmp[1024];
96 getcwd(tmp, sizeof(tmp));
97 sprintf(cwd, "PWD=%s", tmp);
98 putenv(cwd);
99 }
100
101 //bash自己执行内建命令,类似于bash内部的一个函数
102 int doBuildin(char *argv[])
103 {
104 if(strcmp(argv[0], "cd") == 0)
105 {
106 char *path = NULL;
107 if(argv[1] == NULL) path=homepath();
108 else path = argv[1];
109 cd(path);
110 return 1;
111 }
112 else if(strcmp(argv[0], "export") == 0)
113 {
114 if(argv[1] == NULL) return 1;
115 strcpy(enval, argv[1]);
116 putenv(enval);
117 return 1;
118 }
119 else if(strcmp(argv[0], "echo") == 0)
120 {
121 if(argv[1] == NULL)//为空
122 {
123 printf("\n");
124 return 1;
125 }
126 if(*(argv[1]) == '$' && strlen(argv[1]) > 1)//不为空,且紧跟$
127 {
128 char *val = argv[1]+1; //1. $PATH(环境变量) $?(退出码)
129 if(strcmp(val, "?") == 0)
130 {
131 printf("%d\n", lastcode);
132 lastcode = 0;
133 }
134 else
135 {
136 const char *enval = getenv(val);
137 if(enval) printf("%s\n", enval);
138 else printf("\n");
139 }
140 return 1;
141 }
142 else
143 {
144 printf("%s\n", argv[1]);
145 return 1;
146 }
147 }
148 //.......其他内建命令
149
150 return 0;
151 }
152
153
154
155 int main()
156 {
157 while(1)
158 {
159 // 1. 打印提示符&&获取用户命令字符串获取成功
160 char usercommand[NUM];
161 int n = getUserCommand(usercommand, sizeof(usercommand));
162 if(n <= 0) continue;
163 // 2. 分割字符串
164 // "ls -a -l" -> "ls" "-a" "-l"
165 char *argv[SIZE];
166 commandSplit(usercommand, argv);
167 // 3. check build-in command 检查并让mybash执行内建命令
168 n = doBuildin(argv);
169 if(n) continue;
170 // 4. 子进程执行对应的命令
171 execute(argv);
172 }
173 }