目录
一,进程创建,fork/vfork
1,fork创建子进程,操作系统都做了什么
2,写时拷贝的做了什么
二,进程终止,echo $?
1,进程终止时,操作系统做了什么
2,进程终止的常见方式
3,如何正确终止一个程序
三,进程等待
1,为什么要进行,进程等待
2,如何等待,等待是什么
(1)进程等待必要性
(2)进程等待的方法
3,获取子进程status
四,进程替换
1,替换原理
2,替换函数
3,函数理解
4,命名理解
五,微型shell,重新认识shell运行原理
1,原理
2,实现微型shell
点个赞吧!!!666
一,进程创建,fork/vfork
1,fork创建子进程,操作系统都做了什么
fork创建子进程,是不是系统里多了一个进程?是的!
进程=内核数据结构+ 进程代码和数据!
进程代码和数据,一般从磁盘中来,也就是你的C/C++程序,加载之后的结果!
创建子进程,给子进程分配对应的内核结构,必须子进程自己独有了,因为进程具有独立性!
理论上,子进程也要有自己的代码和数据!
可是一般而言,我们没有加载的过程,也就是说,子进程没有自己的代码和数据!
所以,子进程只能”使用“父进程的代码和数据!
代码:都是不可被写的,只能读取,所以父子共享,没有问题!
数据:可能被修改的,所以,必须分离!
对于数据而言,什么时候分离?
如果,创建进程的时候,就直接拷贝分离。这杨样会导致,可能拷贝子进程根本不会用到数据空间,即使用到了,也可能只是读取。
而即使是OS,也不知道哪些空间可能会被写入,即使提前拷贝了,也不会立马使用。所以,OS选择了写时拷贝技术,将父子进程的数据进行分离。
OS为何要选择写时拷贝的技术,对父子进程进行分离?
用的时候再给你分配,是高效使用内存的一种表现,而且OS无法在代码执行前预知哪些空间会被访问。
所以,fork创建父子进程之后,代码是共享的,内核的数据会各进程写时拷贝一份。
2,写时拷贝的做了什么
进程调用fork,当控制转移到内核中的fork代码后,内核做了以下操作:
分配新的内存块和内核数据结构给子进程。
将父进程部分数据结构内容拷贝至子进程。
添加子进程到系统进程列表当中。
fork返回,开始调度器调度。
fork之后,父子进程代码共享是所有代码都共享的。
(1)我们的代码汇编之后会,会有很多行代码,而且每行代码加载到内存之后,都有对应的地址。
(2)因为进程随时可能被中断(可能并没有执行完),下次回来,还必须从之前的位置继续执行(不是最开始的位置),这就要求CPU必须随时记录下,当前进程执行的位置,所以,CPU内有对应的寄存器EIP(PC程序计数器),用来记录当前执行位置。
(3)寄存器在CPU内,只有一份,寄存器内的数据,是可以有多份的。进程的上下文数据,在fork创建子进程之后,对于子进程已经不重要了。虽然父子进程各自调度,各自都会修改EIP,但是已经不重要了,因为子进程已经认为自己的EIP起始值,就是fork之后的代码。
二,进程终止,echo $?
1,进程终止时,操作系统做了什么
当然是要释放进程申请的,相关内核数据结构和对应的数据与代码,本质就是释放系统资源。
2,进程终止的常见方式
(1)进程退出场景:
代码运行完毕,结果正确。
代码运行完毕,结果不正确。
代码异常终止,程序崩溃了。
(2)进程退出码:
查看退出码使用:echo $?
0,表示成功。
非0,表示失败,具体是几,要看退出的原因。
程序崩溃的时候,退出码无意义。一般而言,退出码对应的return语句,没有被执行。
[user@iZwz9eoohx59fs5a6ampomZ linux-52]$ cat exitcode.c
#include <stdio.h>
#include <unistd.h>
#include <string.h>int main()
{int number;for(number = 0; number < 100; number++){printf("%d: %s\n", number, strerror(number));}
}
[user@iZwz9eoohx59fs5a6ampomZ linux-52]$ ./exitcode
0: Success
1: Operation not permitted
2: No such file or directory
3: No such process
4: Interrupted system call
5: Input/output error
6: No such device or address
7: Argument list too long
8: Exec format error
9: Bad file descriptor
10: No child processes
。。。。。。
3,如何正确终止一个程序
使用 exit vs return 语句。
return语句,就是终止进程的,return+退出码。
exit语句,在代码的任何地方调用,都表示直接终止进程。
exit的头文件是stdlib.h,exit(int status)有一个参数,这个参数就是退出码。
exit最后也会调用exit, 但在调用exit之前,还做了其他工作:
1. 执行用户通过 atexit或on_exit定义的清理函数
2. 关闭所有打开的流,所有的缓存数据均被写入
3. 调用_exit
三,进程等待
1,为什么要进行,进程等待
(1)子进程退出,父进程不管子进程,子进程就要处于僵尸状态。
(2)父进程创建子进程,是要让子进程办事的,那么子进程把任务完成的怎么样,父进程关系吗?如果需要,如果得知?如果不需要,如何处理?
2,如何等待,等待是什么
(1)进程等待必要性
之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
最后,父进程派给子进程的任务完成的如何,我们需要知道。如果子进程运行完成,结果对还是不对,或者是否正常退出。
父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。
(2)进程等待的方法
wait方法
头文件:
#include<sys/types.h>
#include<sys/wait.h>
函数:
pid_t wait(int*status);
返回值:
成功返回被等待进程pid,失败返回-1。
参数:
输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
基本验证-等待僵尸进程
// 会话·1[user@iZwz9eoohx59fs5a6ampomZ linux-53]$ cat myproc.c
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>int main()
{pid_t id = fork();if(id < 0){perror("fork");exit(1); //标识进程运行完毕,结果不正确}else if(id == 0){//子进程int cnt = 5;while(cnt){printf("cnt: %d, 我是子进程, pid: %d, ppid: %d\n", cnt, getpid(), getppid());sleep(1);cnt--;}exit(0);}else{//父进程printf("我是父进程,pid: %d, ppid: %d\n", getpid(), getppid());sleep(7);pid_t ret = wait(NULL);//阻塞式等待if(ret > 0){printf("等待子进程成功,ret: %d\n", ret);}while(1){printf("cnt: %d, 我是父进程, pid: %d, ppid: %d\n", getpid(), getppid());sleep(1); }}
}// 会话·2[user@iZwz9eoohx59fs5a6ampomZ linux-53]$ ./myproc
我是父进程,pid: 31946, ppid: 30659
cnt: 5, 我是子进程, pid: 31947, ppid: 31946
cnt: 4, 我是子进程, pid: 31947, ppid: 31946
cnt: 3, 我是子进程, pid: 31947, ppid: 31946
cnt: 2, 我是子进程, pid: 31947, ppid: 31946
cnt: 1, 我是子进程, pid: 31947, ppid: 31946
等待子进程成功,ret: 31947
cnt: 31946, 我是父进程, pid: 30659, ppid: -386691177
cnt: 31946, 我是父进程, pid: 30659, ppid: -386691177
cnt: 31946, 我是父进程, pid: 30659, ppid: -386691177
^C
waitpid方法
函数:
pid_ t waitpid(pid_t pid, int *status, int options);
返回值:
当正常返回的时候waitpid返回收集到的子进程的进程ID;
如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
参数:
pid:
Pid=-1,等待任一个子进程。与wait等效。
Pid>0.等待其进程ID与pid相等的子进程。
status:
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真.(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
options:
WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
获取子进程退出的结果
如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
如果不存在该子进程,则立即出错返回。
3,获取子进程status
wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
如果传递NULL,表示不关心子进程的退出状态信息。
否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位)
// 会话1[user@iZwz9eoohx59fs5a6ampomZ linux-53]$ ./myproc
我是子进程: 5
我是子进程: 4
我是子进程: 3
我是子进程: 2
我是子进程: 1
子进程执行完毕,子进程的退出码:11
[user@iZwz9eoohx59fs5a6ampomZ linux-53]$ cat myproc.c
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>int main()
{pid_t id = fork();if(id == 0){// 子进程int cnt = 5;while(cnt){printf("我是子进程: %d\n", cnt);sleep(1);cnt--;}exit(11);}else{// 父进程int status = 0;// 只有子进程退出的时候,父进程才会waitpid函数,进行返回,此时父进程还活着// wait/waitpid 可以在目前的情况下,让进程退出具有一定的顺序性// 将来可以让父进程进行更多的收尾工作// id > 0 等待指定进程// id== 0 TODO// id== -1 等待任意一个子进程退出,等价于wait()pid_t result = waitpid(id, &status, 0);//阻塞状态下,等待子进程退出if(result > 0){// 可以不这么检测// printf("父进程等待成功,退出码:%d\n,退出信号:%d\n", (status>>8)&0xFF, status & 0x7F);if(WIFEXITED(status)){// 子进程是正常退出的printf("子进程执行完毕,子进程的退出码:%d\n", WEXITSTATUS(status));}else{ printf("子进程异常退出:%d\n", WIFEXITED(status));}}}
}// 会话2
[user@iZwz9eoohx59fs5a6ampomZ linux-53]$ while :; do ps axj | head -1 && ps axj | grep myproc |grep ; sleep 1; echo "-----------------------------------------";donePPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
-----------------------------------------PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
-----------------------------------------PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
-----------------------------------------PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
-----------------------------------------PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
-----------------------------------------PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
30659 17885 17885 30659 pts/1 17885 S+ 1001 0:00 ./myproc
17885 17886 17885 30659 pts/1 17885 S+ 1001 0:00 ./myproc
-----------------------------------------PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
30659 17885 17885 30659 pts/1 17885 S+ 1001 0:00 ./myproc
17885 17886 17885 30659 pts/1 17885 S+ 1001 0:00 ./myproc
-----------------------------------------PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
30659 17885 17885 30659 pts/1 17885 S+ 1001 0:00 ./myproc
17885 17886 17885 30659 pts/1 17885 S+ 1001 0:00 ./myproc
-----------------------------------------PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
30659 17885 17885 30659 pts/1 17885 S+ 1001 0:00 ./myproc
17885 17886 17885 30659 pts/1 17885 S+ 1001 0:00 ./myproc
-----------------------------------------PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
30659 17885 17885 30659 pts/1 17885 S+ 1001 0:00 ./myproc
17885 17886 17885 30659 pts/1 17885 S+ 1001 0:00 ./myproc
-----------------------------------------PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
-----------------------------------------PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
-----------------------------------------PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
^C
四,进程替换
1,替换原理
fork()之后,父子各自执行父进程代码的一部分,父子代码共享,数据写时拷贝各自一份。如果子进程就想有自己的代码,执行一个全新的程序呢?这时就使用到进程替换。
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
2,替换函数
其实有六种以exec开头的函数,统称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[]);
int execve(const char *path, char *const argv[], char *const envp[]);
3,函数理解
这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
如果调用出错则返回-1。
所以exec函数只有出错的返回值而没有成功的返回值。
最后一个参数,必须是NULL,要标识参数传递完毕。
案例:创建子进程,只使用最简单的exec函数。
printf("当前进程的结束代码!\n");为什么不打印呢?
因为,execl是程序替换,该函数成功调用之后,会将当前进程的所有代码数据都进行替换,包括已经执行和没有执行的,所以一旦调用成功,后续的所有代码都不会执行。
printf("当前进程的开始代码!\n");也被替换了,只是因为它在execl之前就打印了,才会显示出来"当前进程的开始代码!"。
[user@iZwz9eoohx59fs5a6ampomZ linux-54]$ cat myproc.c
#include <stdio.h>
#include <unistd.h>int main()
{printf("当前进程的开始代码!\n");printf("当前进程的结束代码!\n");return 0;
}
[user@iZwz9eoohx59fs5a6ampomZ linux-54]$ ./myproc
当前进程的开始代码!
当前进程的结束代码!
[user@iZwz9eoohx59fs5a6ampomZ linux-54]$ cat myproc.c
#include <stdio.h>
#include <unistd.h>int main()
{printf("当前进程的开始代码!\n");execl("/usr/bin/ls", "ls", "-l", "-a", "-i", NULL);printf("当前进程的结束代码!\n");return 0;
}
[user@iZwz9eoohx59fs5a6ampomZ linux-54]$ ./myproc
当前进程的开始代码!
total 28
1449872 drwxrwxr-x 2 user user 4096 Jul 3 13:46 .
1441793 drwxrwxr-x 11 user user 4096 Jul 3 13:34 ..
1449895 -rw-rw-r-- 1 user user 64 Jul 3 13:35 makefile
1449894 -rwxrwxr-x 1 user user 8536 Jul 3 13:46 myproc
1449896 -rw-rw-r-- 1 user user 222 Jul 3 13:45 myproc.c
4,命名理解
这些函数原型看起来很容易混,但只要掌握了规律就很好记
l(list) : 表示参数采用列表
v(vector) : 参数用数组
p(path) : 有p自动搜索环境变量PATH
e(env) : 表示自己维护环境变量
exec调用举例如下:
#include <unistd.h>
int main()
{char *const argv[] = {"ps", "-ef", NULL};char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};execl("/bin/ps", "ps", "-ef", NULL);// 带p的,可以使用环境变量PATH,无需写全路径execlp("ps", "ps", "-ef", NULL);// 带e的,需要自己组装环境变量execle("ps", "ps", "-ef", NULL, envp);execv("/bin/ps", argv);// 带p的,可以使用环境变量PATH,无需写全路径execvp("ps", argv);// 带e的,需要自己组装环境变量execve("/bin/ps", argv, envp);exit(0);
}
五,微型shell,重新认识shell运行原理
1,原理
用下图的时间轴来表示事件的发生次序。其中时间从左向右。shell由标识为sh的方块代表,它随着时间的流逝从左向右移动。shell从用户读入字符串"ls"。shell建立一个新的进程,然后在那个进程中运行ls程序并等待那个进程结束。
然后shell读取新的一行输入,建立一个新的进程,在这个进程中运行程序, 并等待这个进程结束。所以要写一个shell,需要循环以下过程:
1. 获取命令行
2. 解析命令行
3. 建立一个子进程(fork)
4. 替换子进程(execvp)
5. 父进程等待子进程退出(wait)
2,实现微型shell
根据这些思路,和我们前面的学的技术,就可以自己来实现一个shell了.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>#define NUM 1024
#define SIZE 32
#define SEP " "// 保存完整的命令行字符串
char cmd_line[NUM];
// 保存打散之后的命令行字符串
char *g_argv[SIZE];// shell 运行原理:通过子进程执行命令,父进程等待&&解析命令
int main()
{//0.命令行解释器,一定是一个常驻内存的进程,不退出while(1){//1.打印出提示信息 [root@localhost myshell]#printf("[root@localhost myshell]# ");fflush(stdout);memset(cmd_line, '\0', sizeof cmd_line);//2.获取用户的输入,输入的是各自指令和选型"ls -a -l -i"if(fgets(cmd_line, sizeof cmd_line, stdin) == NULL){continue; }cmd_line[strlen(cmd_line)-1] = '\0';//去掉空行,把\n变成\0,字符串长度下标就是\n,"ls -a -l -i\n\0 "//printf("echo: %s\n", cmd_line);//3.把输入的命令行字符串解析,从"ls -a -l -i",变成"ls","-a","-i","-l"//第一次调用,要传入原始字符串g_argv[0] = strtok(cmd_line, SEP);int index = 1;// 加颜色if(strcmp(g_argv[0], "ls") == 0){g_argv[index++] = "--color=auto"; }// 设置ll命令别名if(strcmp(g_argv[0], "ll") == 0){g_argv[0] = "ls";g_argv[index++] = "-l";g_argv[index++] = "--color=auto";}//第二次调用,如果还要解析原始字符串,传入NULLwhile(g_argv[index++] = strtok(NULL, SEP));//for(index = 0; g_argv[index]; index++)// printf("g_argv[%d]: %s\n", index, g_argv[index]);//4.执行命令,内置命令,让父进程(shell)自己执行的命令,就叫做内置(内键)命令 //内置命令,本质就是shell中的一个函数调用if(strcmp(g_argv[0], "cd") == 0) {if(g_argv[1] != NULL) chdir(g_argv[1]);continue;}//5.父进程调用子进程执行,fork()//子进程pid_t id = fork();if(id == 0){printf("下面的功能让是子进程执行的\n");execvp(g_argv[0], g_argv);exit(1);}//父进程int status = 0;pid_t ret = waitpid(id, &status, 0);if(ret > 0) printf("exit code: %d\n", WEXITSTATUS(status));}
}