- 1 进程创建的三种方式
- fork
- vfrok
- clone
- 2 进程终止
- 进程正常退出
- return
- exit
- _exit
- 进程异常退出
- 进程收到某个信号,而该信号使进程终止
- abort
- 3 进程等待
- 进程等待的方法
- wait
- waitpid
- 4 进程替换
- 替换原理
- 替换函数
- 制作一个简单的shell
参考文章:
https://zhuanlan.zhihu.com/p/498427466?utm_source=wechat_session&utm_medium=social&utm_oi=977698418977746944&utm_campaign=shareopn
https://blog.csdn.net/gogokongyin/article/details/51178257
在linux中主要提供了fork、vfork、clone三个进程创建方法。在Linux源码中,这三个调用的执行过程是执行fork()、vfork()、clone()时,通过一个系统调用表映射到sys_fork()、sys_vfork()和sys_clone(),再在这三个函数中去调用do_fork()去做具体的创建进程工作。
forkfork创建一个进程时,复制出来的子进程有自己的task_struct结构体和pid,然后复制父进程其他所有的资源。
例如,要是父进程打开了五个文件,那么子进程也有五个打开的文件,而且这些文件的当前读写指针也停在相同的地方。
这样得到的子进程独立于父进程,具有良好的并发性。但是子进程需要复制父进程很多资源,所以fork是一个开销很大的系统调用,这些开销并不是所有的情况下都是必须的,比如某进程fork出一个子进程,其子进程仅仅是为了调用exec执行另一个可执行文件,那么fork过程对于虚拟空间的复制将是一个多余的过程。
但由于现在Linux采取了copy-on-write(写时复制)技术,fork最初不会真的产生两个不同的拷贝。写时复制是在推迟真正的数据拷贝,若后来确实发生了写入,那意味着父进程和子进程的数据不一致了,就需要产生复制动作,每个进程拿到属于自己的那一份。所以有了写时复制后,vfork其实现意义就不大了。
fork调用一次,返回两个值,对于父进程,返回的是子进程的pid值,对于子进程,返回的是0 。 在fork之后,子进程和fork都会继续执行fork调用之后的指令。
#include#include #include #include int main(void) { int a=5,b=2; pid_t pid; pid = fork(); if(pid==0) { a = a-4; printf("child process PID = %d,a=%d,b=%dn",getpid(),a,b); }else if(pid >0) { printf("parent process PID = %d, a=%d,b=%dn",getpid(),a,b); }else{ perror("fork error"); exit(1); } return 0; }
可见,子进程中将变量a的值该为1,而进程中则保持不变。
vfork系统调用不同于fork,用vfork创建的子进程与父进程共享地址空间,也就是说子进程完全运行在父进程的地址空间上,如果这时子进程修改了某个变量,这将影响父进程。
因此,如果fork的例程改用vfork的话,那么两次打印a、b的值是相同的,所在地址也是相同的。
#include#include #include #include int main(void) { int a=5,b=2; pid_t pid; pid = vfork(); if(pid==0) { a = a-4; printf("child process PID = %d,a=%d,b=%dn",getpid(),a,b); exit(0); }else if(pid >0) { printf("parent process PID = %d, a=%d,b=%dn",getpid(),a,b); }else{ perror("fork error"); exit(1); } return 0; }
但此处有一点要注意的是,用vfork创建的子进程必须先调用exit()来结束,否则子进程将不能结束,fork则不存在这个情况。
vfork也是在父进程中返回子进程的进程号,在子进程中返回0,用vfork创建子进程后,父进程会被阻塞直到子进程调用exec(exec将一个新的可执行文件载入到地址空间并执行)或exit。vfork的好处是在子进程被创建后往往仅仅是为了调用exec执行另一个程序,因为它就不会对父进程的地址空间由任何引用,因此通过vfork共享内存可以减少不必要的开销。
#include#include #include #include int main(void) { int a=5,b=2; pid_t pid; pid = fork(); if(pid==0) { if(execl("./vfork_example","example",NULL)<0) { perror("exec error"); exit(1); } }else if(pid >0) { printf("parent process a=%d,b=%d,the address a = %p ,b=%pn",a,b,&a,&b); }else{ perror("vfork error"); exit(1); } return 0; }
vfork_example.c
#include#include int main(void) { int a=1,b=2; sleep(3); printf("child process,a=%d,b=%d,the address a =%p,b =%pn",a,b,&a,&b); return 0; }
子进程调用了exec,父进程会继续执行,子进程sleep(3),所以父进程会提前结束。
系统调用fork()和vfork()是无参数的,而clone()则带有参数。fork()是全部复制,vfork()是共享内存,而clone是可以将父进程资源有选择地复制给子进程,而没有复制的数据结构则通过指针的复制让子进程共享,具体要复制那些资源给子进程,由参数列表中的clone_flags来决定。
int clone(int (*fn)(void *), void *child_stack,
int flags, void *arg, ...
);
fn为函数指针,此指针指向一个函数体,即想要创建进程的静态程序(我们知道进程的4要素,这个就是指向程序的指针,就是所谓的“剧本", ); child_stack为给子进程分配系统堆栈的指针(在linux下系统堆栈空间是2页面,就是8K的内存,其中在这块内存中,低地址上放入了值,这个值就是进程控制块task_struct的值); arg就是传给子进程的参数一般为(0); flags为要复制资源的标志,描述你需要从父进程继承那些资源(是资源复制还是共享,在这里设置参数:
下面是flags可以取的值
| 标志 | 含义 |
|---|---|
| CLONE_PARENT | 创建的子进程的父进程是调用者的父进程,新进程与创建它的进程成了“兄弟”而不是“父子” |
| CLONE_FS | 子进程与父进程共享相同的文件系统,包括root、当前目录、umask |
| CLONE_FILES | 子进程与父进程共享相同的文件描述符(file descriptor)表 |
| CLONE_NEWNS | 在新的namespace启动子进程,namespace描述了进程的文件hierarchy |
| CLONE_SIGHAND | 子进程与父进程共享相同的信号处理(signal handler)表 |
| CLONE_PTRACE | 若父进程被trace,子进程也被trace |
| CLONE_VFORK | 父进程被挂起,直至子进程释放虚拟内存资源 |
| CLONE_VM | 子进程与父进程运行于相同的内存空间 |
| CLONE_PID | 子进程在创建时PID与父进程一致 |
| CLONE_THREAD | Linux 2.4中增加以支持POSIX线程标准,子进程与父进程共享相同的线程群 |
#define _GNU_SOURCE #include#include #include #include #include #include #include int variable,fd; int do_something(void*arg) { variable = 42; printf("in child processn"); close(fd); return 0; } int main(void) { void *child_stack; char tempch; variable = 9; fd = open("./test.txt",O_RDONLY); child_stack=(void *)malloc(16384); printf("The varibale is %dn",variable); clone(do_something,child_stack,CLONE_VM|CLONE_FILES,NULL); sleep(3); printf("The variable is now %dn",variable); if(read(fd,&tempch,1)<1) { perror("file read error"); exit(1); } printf("we could read from the filen"); return 0; }
我们在clone指定了CLONE_VM和CLONE_FILES,所以子进程与父进程共享相同的文件描述符(file descriptor)表以及子进程与父进程运行于相同的内存空间,所以会出现上述情况。
参考文章:
https://zhuanlan.zhihu.com/p/435709371
https://zhuanlan.zhihu.com/p/63424197
进程正常退出 return在main函数中使用return退出进程。return num等同于exit(num),所做的事可以看下面的exit介绍。
exitexit函数可以在代码中任何位置使进程退出,并且exit在退出进程前还会做一系列工作:
- 调用用户通过atexit或on_exit定义的函数
- 关闭所有打开的流,所有的缓存数据均被刷新
- 调用_exit函数终止进程。
#include#include void show() { printf("hello world"); exit(1); } int main(void) { show(); return 0; }
终止进程前会将缓冲区当中的数据输出。
_exit函数也可以在代码中的任何地方退出进程,但是_exit函数会直接终止进程,并不会在退出进程前会做任何收尾工作。
我们将上面代码中的exit函数改成_exit函数,运行会没有输出。
例如,在进程运行过程中向进程发生kill -9信号使得进程异常退出,或是使用Ctrl+C使得进程异常退出等。
abort调用abort()函数,会使进程异常终止。
3 进程等待https://zhuanlan.zhihu.com/p/435709371
进程等待的方法 waitpid_t wait(int* status);
等待任意子进程退出,status保存子进程的退出码。所以父进程会被阻塞,直到子进程退出。WEXITSTATUS(status)宏可以获取子进程的退出值。
on success, returns the process ID of the terminated child; on error, -1 is returned.
#includewaitpid#include #include #include #include int main(void) { pid_t pid = fork(); if(pid==0) { int count = 10; while(count--) { printf("Child process : PID = %d; PPID : %dn",getpid(),getppid()); sleep(1); } } else if(pid>0) { int status; pid_t ret = wait(&status); if(ret>0) { printf("wati child success n"); printf("child process pid = %d,return status=%dn",ret,WEXITSTATUS(status)); } } else{ printf("fork errorn"); exit(1); } exit(0); }
函数原型:
pid_t waitpid(pid_t pid, int *wstatus, int options);
参数含义:
pid:
< -1 meaning wait for any child process whose process group ID is equal to the absolute value of pid.
-1 meaning wait for any child process.
0 meaning wait for any child process whose process group ID is equal to that of the calling process.
> 0 meaning wait for the child whose process ID is equal to the value of pid.
options的值是下面0个或多个或(OR)值
- WNOHANG (wait no hung): 即使没有子进程退出,它也会立即返回,直接返回0,不会像wait那样永远等下去。
- WUNTRACED :用于调试。
如果孩子已经停止(但没有通过 ptrace(2) 跟踪),也会返回。 即使未指定此选项,也会提供已停止的跟踪子项的状态。
state和wait一样。
#include#include #include #include #include int main(void) { pid_t pid = fork(); if(pid==0) { int count = 10; while(count--) { printf("Child process : PID = %d; PPID : %dn",getpid(),getppid()); sleep(1); } } else if(pid>0) { int status; pid_t ret = waitpid(pid,&status,0); if(ret>0) { printf("wati child success n"); printf("child process pid = %d,return status=%dn",ret,WEXITSTATUS(status)); } } else{ printf("fork errorn"); exit(1); } exit(0); }
运行的结果和wait一样。
4 进程替换原文链接:
https://zhuanlan.zhihu.com/p/435709371
用fork创建子进程后,子进程执行的是和父进程相同的程序(但有可能执行不同的代码分支),如想让子进程执行另一个程序,往往需要调用一种exec函数。
当进程调用exec函数时,该进程的用户空间代码和数据完全被新程序替换,并从新程序的启动代码开始执行。
-
当进程程序被替换后,有没有创建新的进程?
进程程序被替换之后,该进程对应的PCB、进程地址空间 以及页表等数据结构都没法发生改变,只是进程在物理内存当中的数据和代码发生了改变,所有并没有创建新的进程,而且进程程序替换前后该进程的pid并没发生改变。 -
子进程进行进程程序替换后,会影响父进程的代码和数据吗?
子进程刚被创建时,与父进程共享代码和数据,但当子进程需要进行进程程序替换时,也就意味着子进程需要对其数据和代码进行写入操作,这时便需要将父子进程共享的代码和数据进行写时拷贝,此后父子进程的代码和数据也就分离了,因此子进程进行程序替换后不会影响父进程的代码和数据。
替换函数有六种以exec开头的函数,它们统称为exec函数。
int execl(const char *path, const char *arg, ...
);
int execlp(const char *file, const char *arg, ...
);
int execle(const char *path, const char *arg, ...
);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *file, char *const argv[],
char *const envp[]);
exec函数的后缀含义如下:
- l(list):表示参数采用列表的形式
- v(vector):表示参数采用数组的形式
- p(path):表示能自动搜素环境变量PATH,进行程序查找
- e(env):表示可以传入自己设置的环境变量。
事实上,只有execve才是真正的系统调用,其它五个函数最终都是调用的execve,所以execve在man手册的第2节,而其它五个函数在man手册的第3节,也就是说其他五个函数实际上是对系统调用execve进行了封装,以满足不同用户的不同调用场景的。
shell也就是命令行解释器,其运行原理就是:当有命令需要执行时,shell创建子进程,让子进程执行命令,而shell只需等待子进程退出即可。
其实shell需要执行的逻辑非常简单,其只需循环执行以下步骤:
- 获取命令行。
- 解析命令行。
- 创建子进程。
- 替换子进程。
- 等待子进程退出。
其中,创建子进程使用fork函数,替换子进程使用exec系列函数,等待子进程使用wait或者waitpid函数。
#include#include #include #include #include #include #include #define LEN 1024 //命令最大长度 #define NUM 32 //命令拆分后的最大个数 int main() { char cmd[LEN]; //存储命令 char* myargv[NUM]; //存储命令拆分后的结果 char hostname[32]; //主机名 char pwd[128]; //当前目录 while (1){ //获取命令提示信息 struct passwd* pass = getpwuid(getuid()); gethostname(hostname, sizeof(hostname)-1); getcwd(pwd, sizeof(pwd)-1); int len = strlen(pwd); char* p = pwd + len - 1; while (*p != '/'){ p--; } p++; //打印命令提示信息 printf("[%s@%s %s]$ ", pass->pw_name, hostname, p); //读取命令 fgets(cmd, LEN, stdin); cmd[strlen(cmd) - 1] = ' '; //拆分命令 myargv[0] = strtok(cmd, " "); int i = 1; while (myargv[i] = strtok(NULL, " ")){ i++; } pid_t id = fork(); //创建子进程执行命令 if (id == 0){ //child execvp(myargv[0], myargv); //child进行程序替换 exit(1); //替换失败的退出码设置为1 } //shell int status = 0; pid_t ret = waitpid(id, &status, 0); //shell等待child退出 if (ret > 0){ printf("exit code:%dn", WEXITSTATUS(status)); //打印child的退出码 } } return 0; }
说明:
当执行./myshell命令后,便是我们自己实现的shell在进行命令行解释,我们自己实现的shell在子进程退出后都打印了子进程的退出码,我们可以根据这一点来区分我们当前使用的是Linux操作系统的shell还是我们自己实现的shell



