目录
1、进程创建
1.1、fork函数内部完成的功能
创建子进程,子进程拷贝父进程的PCB。
①分配新的内存和内核数据结构(task_struct)给子进程。
②将父进程部分数据结构拷贝至子进程。
③添加子进程到系统列表中,添加到双向链表中。
④ork返回,开始调度器调度(操作系统开始调度)
1.2、用户空间&内核空间
内核空间 :
Linux操作系统和驱动程序运行在内核空间。系统调用函数都是在内核空间运行的,因为是操作系统通过的函数。
用户空间:
应用程序都是运行在用户空间的。若程序员写的代码调用了系统调用函数,则会切换到内核空间执行,系统调用函数执行完毕后,再返回到用户空间继续执行用户代码。
1.3、写时拷贝
父进程 创建出来子进程,子进程的PCB拷贝父进程,页表也是拷贝父进程的。起初,操作系统并没有给子进程当中的变量分配空间进行存储,子进程当中的变量还是原来父进程物理地址当中的内容。如果不改变变量值,父子进程共享一个数据。如果改变变量值,才以写实拷贝的方式拷贝一份。此时父子进程通过各自的页表,指向不同的物理地址。
图解如下:
2、进程终止
2.1、进程终止的场景
场景一:代码运行结束,结果正确
场景二:代码运行结束,结果不正确。
场景三:代码异常终止
2.2正常终止
可以通过echo $?来查看进程的退出码。
①从main函数的ruturn(并不是任何函数的return语句都可以结束进程,必须是mian函数的return语句)
②调用exit函数(c标准库函数)
#include<stdlib.h>
void exit(int status);
参数:进程退出时的退出码。 作用:谁调用终止谁。
③调用_exit函数(系统调用函数)
#include<unistd.h>
void _exit(int status)
2.3、异常终止
程序崩溃(内存访问越界,访问空指针)
Ctrl + c命令
2.4、exit和_exit函数的区别
exit函数比_exit函数多执行了两个步骤:
1、执行用户自定义的清理函数
#include<stdlib.h>
int atexit(void(*function)(void))
功能:注册一个函数,在进程终止的时候调用,被调用的函数只能是返回值类型为void的无参函数
2、冲刷缓冲区,关闭流等
缓冲区:c标准库定义的,而非内核。
建立缓冲区的目的:减少IO次数,以内IO操作比较耗费时间。
当触发刷新缓冲区的条件后,缓冲区的内容才会继续进行IO操作
关闭流:标准输入、标准输出、标准错误
2.5、缓冲区
冲刷缓冲区的方式
①exit() ②main函数中的return ③fflush ④\n
缓冲方式
全缓冲:当缓冲区写满了才进行IO
行缓冲:当在输入输出中遇到换行符的时候
不缓冲:不带缓冲,标准IO库不对字符进行缓冲存储
3、进程等待
3.1:为什么要进程等待
已知子进程先于父进程退出,父进程如果不管不顾,子进程就会变成僵尸进程,进而造成内存泄漏。 进程一旦进入僵尸状态,就会刀枪不入,“杀人狂魔”的kill - 9也无能为力,因为谁也没有办法杀死一个死去的进程。 但是,父进程给子进程的任务他完成的如何,我们需要知道。 父进程通过进程等待的方式,回收子进程资源,获取子进程退出状态信息。
总之,父进程进行进程等待,等待子进程退出之后。回收子进程的退出状态信息,防止子进程变成僵尸进程。
3,2、进程等待的方法
wait方法:
#include<sys/wait.h>
#include<sys/types.h>
pid_t wait(int* status);
返回值:返回被等待进程的pid,失败则返回-1
函数参数:输出型参数,获取子进程状态,不关心则可以设置为 NULL
函数特性:
阻塞的:谁调用,谁等待。直到等待的子程序退出。
两种情况:
①发起阻塞,资源存在,无需等待,直接执行函数功能后返回。
②发起阻塞,资源不在,等待资源到来后,执行函数功能返回。
waitpid函数:
pid_t waitpid(pid_t pid,int* status,int options);
返回值:返回收集到的pid。设置了WNOHANG选项,调用过程中waitpid发现没有已退出的子程序可以收集,则返回0;如果调用出错,返回-1,这时errno会被设置为相应的值以指示错误所在。
pid:pid = -1,等待任意一个子进程,与wait等效;pid > 0,等待进程id与pid相等的子程序。
status:后面总结,这两个函数的status含义一样。
options:WNOHANG:若pid指定的进程没有结束,则waitpid函数返回0;不予等待(并没有完成函数功能,若正常结束,则返回该子进程的pid)
函数特性:
参数options被设置为WNOHANG后,为非阻塞。
非阻塞:当调用一个非阻塞的函数时候,函数会判断资源是否准备好。准备好:执行函数功能返回;没准备好:函数报错返回(注:函数功能并没有完成)
要点:非阻塞要搭配循环来使用。
参数status的含义:
wait和waitpid都有一个参数status,该参数为输出型参数,由操作系统填充;如果传递NULL,表示不关心子进程退出状态信息;否则,操作系统会修改该参数,将子进程的退出信息返回给父进程;status不能简单的当作整形来看,我们只有四字节的低两个字节。
如何判断子进程正常退出还是异常退出?
正常退出:返回值>0&&退出信号没有被设置(==0)
异常退出:返回值>0&&退出信号被设置(>0)
代码验证
1、wait函数
①子进程还是僵尸进程吗?
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/wait.h>
4 #include<stdlib.h>
5 int main(){
6 pid_t ret = fork();
7 if(ret == -1){
8 return -1;
9 }else if(ret == 0){
10 //child
11 printf("I am child,pid = %d ppid= %d\n",getpid(),getppid());
12 }else{
13 //father
14 printf("I am father,pid = %d,ppid = %d\n",getpid(),getppid());
15 wait(NULL);
16 while(1){
17 sleep(1);
18 }
19 }
②正常情况下获取status值
1 #include<stdio.h>
2 #include<uni d.h>
3 #include<sys/wait.h>
4 #include<stdlib.h>
5 int main(){
6 pid_t ret = fork();
7 if(ret == -1){
8 return -1;
9 }else if(ret == 0){
10 //child
11 printf("I am child,pid = %d ppid= %d\n",getpid(),getppid());
12 exit(99);
13 }else{
14 //father
15 printf("I am father,pid = %d,ppid = %d\n",getpid(),getppid());
16 int status = 0;
17 int ret = wait(&status);
18 if(ret == -1){
19 return -1;
20 }else if (ret > 0 && ((status & 0x7f) == 0)){
21 //子进程正常退出
22 printf("child process return code is %d\n",(status >> 8)&0xff);
23 }else{
24 //异常退出
25 printf("child process receive signal is %d,coredump flag is %d\n",status&0x7f,(status>>7)&0x1);
26 }
27 }
28 return 0;
29 }
③异常退出
为什么这里的coredump标志位还是0?
原因为没有设置coredump文件。
修改:
2、waitpid函数
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/wait.h>
4 #include<stdlib.h>
5 int main(){
6 pid_t ret = fork();
7 if(ret == -1){
8 return -1;
9 }else if(ret == 0){
10 //child
11 printf("I am child,pid = %d ppid= %d\n",getpid(),getppid());
12 exit(99);
13 }else{
14 //father pid:这个含义就是刚刚创建出来的子进程的PID
15 printf("I am father,pid = %d,ppid = %d\n",getpid(),getppid());
16 waitpid(ret,NULL,WNOHANG);
17 while(1){
18 sleep(1);
19 }
20 }
可以看到子进程是一个僵尸进程。原因是非阻塞要搭配循环来使用。
1 #include<stdio.h>
2 #include<unistd.h>
3 nclude<sys/wait.h>
4 #include<stdlib.h>
5 int main(){
6 pid_t ret = fork();
7 if(ret == -1){
8 return -1;
9 }else if(ret == 0){
10 //child
11 printf("I am child,pid = %d ppid= %d\n",getpid(),getppid());
12 exit(99);
13 }else{
14 //father pid:这个含义就是刚刚创建出来的子进程的PID
15 printf("I am father,pid = %d,ppid = %d\n",getpid(),getppid());
16 int re = 0;
17 do{
18 re = waitpid(ret,NULL,WNOHANG);
19 }while(re == 0);
20 while(1){
21 sleep(1);
22 }
23 }
24 return 0;
25 }
可以看到搭配循环后子进程等待到了子进程。
4、进程程序替换
4.1、为什么要有进程程序替换
想让程序执行不一样的代码。用为父进程创建出来的子进程和父进程拥有相同的代码段,所以,子进程看到的代码和父进程是一样的。当我们想要让子进程去执行不同的代码段的时候,就需要让子进程调用进程程序替换的接口,从而让子程序执行不一样的代码。
4.2、替换原理
替换进程的数据段和代码段并更新堆栈。
4.3、exec函数簇
这些函数包含的头文件都为<unistg,h>
返回值:如果调用成功,加载新的程序从启动代码(main)开始执行,不再返回;如果调用失败,则返回-1.
1、execl函数
int execl(const char* file,const char* arg , ...)
path:带路径的可执行程序(需要路径)。
arg:传递给可执行程序的命令行参数,第一个参数必须是可执行程序本身。如果要传递多个参数,则用‘,’将其隔开,最后用NULL结尾。
1 #include <stdio.h>
2 #include <unistd.h>
3 int main(){
4 printf("i am main..., process start...\n");
5 int ret = execl("/bin/pwd", "pwd", NULL);
6 printf("if run here, execl failed : %d\n", ret);
7 return 0;
8 }
~
2、execlp函数
int execlp(const char* file,const char* arg , ...)
file:可执行程序,可以不带路径,也可以带路径。execlp函数会去搜索PATH这个环境变量。若可执行程序在PATH中,则正常替换,执行替换后的程序;若不在,则报错返回,替换失败。
1 #include <stdio.h>
2 #include <unistd.h>
3 int main(){
4 printf("i am main..., process start...\n");
5 int ret = execlp("pwd", "pwd", NULL);
6 printf("if run here, execl failed : %d\n", ret);
7 return 0;
8 }
3、execle函数
int execle(const char *path, const char *arg, ..., char * const envp[]);
参数:相交于execl,增加了一个envp[],剩下的完全一致。envp:程序员传递的环境变量,程序员在调用该函数的时候,需要自己组织环境变量传递给函数。
1 #include <stdio.h>
2 #include <unistd.h>
3 int main(){
4 printf("i am main..., process start...\n");
5 extern char** environ;
6 int ret = execle("/home/DL/linux/process/pro_exec/execle/mygetenv/mygetenv", "mygetenv", NULL, environ);
7 printf("if run here, execlp failed : %d\n", ret);
8 return 0;
9 }
#include <stdio.h>
#include <stdlib.h>
int main(){
printf("%s:%d\n", __FILE__, __LINE__);
printf("%s\n", getenv("PATH"));
return 0;
}
4、execv函数
int execv(const char* path, char* const argv[])
argv:也是传递给可执行程序的命令行参数,但是必须以指针数组的方式进行传递。剩下的与execl一样。
#include <stdio.h>
#include <unistd.h>
int main(){
printf("i am main..., process start...\n");
char* argv[10] = {NULL};
argv[0] ="ls";
argv[1] = "-l";
int ret = execv("/bin/ls", argv);
printf("if run here, execl failed : %d\n", ret);
return 0;
}
5、execvp函数
int execvp(const char* path, char* const argv[])
argv必须以指针数组的方式传递,其余与enecvp一样
6、execve函数
int execve(const char* path, char* const argv[], char* const envp[])
除了要以针织数组的方式传数据,其余与execle一样。
拓展
函数之间的关系和区别:
execve是系统调用函数,其他5个都是库函数
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
/*
* * 1.创建子进程
* * 2.父子进程执行不同的逻辑
* * 子进程进行进程程序替换
* * 父进程进行等待
* * */
int main(){
pid_t pid = fork();
if(pid < 0){
printf("fork failed\n");
return 0;
}else if(pid == 0)
{
//child
printf("i am child, start exec...\n");
execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
printf("if you see the msg, exec failed\n");
}else{
//father
printf("i am father, i prepare wait child process\n");
wait(NULL);
}
return 0;
}
埋点:父进程是死等子进程的,在子进程不退出之前,父进程是什么事情都不能干的。
问:有没有可能让父进程在执行自己代码的情况下,当子进程退出了,父进程还能及时调用wait进行等待。
答:当前的知识储备是解决不了的,等到学过进程信号之后,就有办法来解决了。