进程控制总结
- 1 进程创建的三种方式
- fork
- vfrok
- clone
- 2 进程终止
- 进程正常退出
- return
- exit
- _exit
- 进程异常退出
- 进程收到某个信号,而该信号使进程终止
- abort
- 3 进程等待
- 进程等待的方法
- wait
- waitpid
- 4 进程替换
- 替换原理
- 替换函数
- 制作一个简单的shell
1 进程创建的三种方式
参考文章:
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()去做具体的创建进程工作。
fork
fork创建一个进程时,复制出来的子进程有自己的task_struct结构体和pid,然后复制父进程其他所有的资源。
例如,要是父进程打开了五个文件,那么子进程也有五个打开的文件,而且这些文件的当前读写指针也停在相同的地方。
这样得到的子进程独立于父进程,具有良好的并发性。但是子进程需要复制父进程很多资源,所以fork是一个开销很大的系统调用,这些开销并不是所有的情况下都是必须的,比如某进程fork出一个子进程,其子进程仅仅是为了调用exec执行另一个可执行文件,那么fork过程对于虚拟空间的复制将是一个多余的过程。
但由于现在Linux采取了copy-on-write(写时复制)技术,fork最初不会真的产生两个不同的拷贝。写时复制是在推迟真正的数据拷贝,若后来确实发生了写入,那意味着父进程和子进程的数据不一致了,就需要产生复制动作,每个进程拿到属于自己的那一份。所以有了写时复制后,vfork其实现意义就不大了。
fork调用一次,返回两个值,对于父进程,返回的是子进程的pid值,对于子进程,返回的是0 。 在fork之后,子进程和fork都会继续执行fork调用之后的指令。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>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=%d\n",getpid(),a,b);}else if(pid >0){/* 这是父进程 */printf("parent process PID = %d, a=%d,b=%d\n",getpid(),a,b);}else{perror("fork error");exit(1);}return 0;
}
可见,子进程中将变量a的值该为1,而进程中则保持不变。
vfrok
vfork系统调用不同于fork,用vfork创建的子进程与父进程共享地址空间,也就是说子进程完全运行在父进程的地址空间上,如果这时子进程修改了某个变量,这将影响父进程。
因此,如果fork的例程改用vfork的话,那么两次打印a、b的值是相同的,所在地址也是相同的。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>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=%d\n",getpid(),a,b);exit(0);}else if(pid >0){/* 这是父进程 */printf("parent process PID = %d, a=%d,b=%d\n",getpid(),a,b);}else{perror("fork error");exit(1);}return 0;
}
但此处有一点要注意的是,用vfork创建的子进程必须先调用exit()来结束,否则子进程将不能结束,fork则不存在这个情况。
vfork也是在父进程中返回子进程的进程号,在子进程中返回0,用vfork创建子进程后,父进程会被阻塞直到子进程调用exec(exec将一个新的可执行文件载入到地址空间并执行)或exit。vfork的好处是在子进程被创建后往往仅仅是为了调用exec执行另一个程序,因为它就不会对父进程的地址空间由任何引用,因此通过vfork共享内存可以减少不必要的开销。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>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=%p\n",a,b,&a,&b);}else{perror("vfork error");exit(1);}return 0;
}
vfork_example.c
#include <stdio.h>
#include <unistd.h>
int main(void)
{int a=1,b=2;sleep(3);printf("child process,a=%d,b=%d,the address a =%p,b =%p\n",a,b,&a,&b);return 0;
}
子进程调用了exec,父进程会继续执行,子进程sleep(3),所以父进程会提前结束。
clone
系统调用fork()和vfork()是无参数的,而clone()则带有参数。fork()是全部复制,vfork()是共享内存,而clone是可以将父进程资源有选择地复制给子进程,而没有复制的数据结构则通过指针的复制让子进程共享,具体要复制那些资源给子进程,由参数列表中的clone_flags来决定。
int clone(int (*fn)(void *), void *child_stack,int flags, void *arg, .../* pid_t *ptid, void *newtls, pid_t *ctid */ );
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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sched.h>int variable,fd;int do_something(void*arg)
{variable = 42;printf("in child process\n");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 %d\n",variable);clone(do_something,child_stack,CLONE_VM|CLONE_FILES,NULL);sleep(3);printf("The variable is now %d\n",variable);if(read(fd,&tempch,1)<1){perror("file read error");exit(1);}printf("we could read from the file\n");return 0;
}
我们在clone指定了CLONE_VM和CLONE_FILES,所以子进程与父进程共享相同的文件描述符(file descriptor)表以及子进程与父进程运行于相同的内存空间,所以会出现上述情况。
2 进程终止
参考文章:
https://zhuanlan.zhihu.com/p/435709371
https://zhuanlan.zhihu.com/p/63424197
进程正常退出
return
在main函数中使用return退出进程。return num等同于exit(num),所做的事可以看下面的exit介绍。
exit
exit函数可以在代码中任何位置使进程退出,并且exit在退出进程前还会做一系列工作:
- 调用用户通过atexit或on_exit定义的函数
- 关闭所有打开的流,所有的缓存数据均被刷新
- 调用_exit函数终止进程。
#include <stdio.h>
#include <stdlib.h>void show()
{printf("hello world");exit(1);
}
int main(void)
{show();return 0;
}
终止进程前会将缓冲区当中的数据输出。
_exit
_exit函数也可以在代码中的任何地方退出进程,但是_exit函数会直接终止进程,并不会在退出进程前会做任何收尾工作。
我们将上面代码中的exit函数改成_exit函数,运行会没有输出。
进程异常退出
进程收到某个信号,而该信号使进程终止
例如,在进程运行过程中向进程发生kill -9信号使得进程异常退出,或是使用Ctrl+C使得进程异常退出等。
abort
调用abort()函数,会使进程异常终止。
3 进程等待
https://zhuanlan.zhihu.com/p/435709371
进程等待的方法
wait
pid_t wait(int* status);
等待任意子进程退出,status保存子进程的退出码。所以父进程会被阻塞,直到子进程退出。WEXITSTATUS(status)宏可以获取子进程的退出值。
on success, returns the process ID of the terminated child; on error, -1 is returned.
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>int main(void)
{pid_t pid = fork();if(pid==0){int count = 10;while(count--){printf("Child process : PID = %d; PPID : %d\n",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=%d\n",ret,WEXITSTATUS(status));}}else{printf("fork error\n");exit(1);}exit(0);
}
waitpid
函数原型:
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 <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>int main(void)
{pid_t pid = fork();if(pid==0){int count = 10;while(count--){printf("Child process : PID = %d; PPID : %d\n",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=%d\n",ret,WEXITSTATUS(status));}}else{printf("fork error\n");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, .../* (char *) NULL */);int execlp(const char *file, const char *arg, .../* (char *) NULL */);int execle(const char *path, const char *arg, .../*, (char *) NULL, char * const envp[] */);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只需等待子进程退出即可。
其实shell需要执行的逻辑非常简单,其只需循环执行以下步骤:
- 获取命令行。
- 解析命令行。
- 创建子进程。
- 替换子进程。
- 等待子进程退出。
其中,创建子进程使用fork函数,替换子进程使用exec系列函数,等待子进程使用wait或者waitpid函数。
#include <stdio.h>
#include <pwd.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#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] = '\0';//拆分命令myargv[0] = strtok(cmd, " ");int i = 1;while (myargv[i] = strtok(NULL, " ")){i++;}pid_t id = fork(); //创建子进程执行命令if (id == 0){//childexecvp(myargv[0], myargv); //child进行程序替换exit(1); //替换失败的退出码设置为1}//shellint status = 0;pid_t ret = waitpid(id, &status, 0); //shell等待child退出if (ret > 0){printf("exit code:%d\n", WEXITSTATUS(status)); //打印child的退出码}}return 0;
}
说明:
当执行./myshell命令后,便是我们自己实现的shell在进行命令行解释,我们自己实现的shell在子进程退出后都打印了子进程的退出码,我们可以根据这一点来区分我们当前使用的是Linux操作系统的shell还是我们自己实现的shell