目录
一、fork函数
1.进程:
2.fork函数:
3.写时拷贝
4.fork常规用法
5.fork调用失败的原因
二、进程终止
1.终止是在做这么?
2.进程终止的3种情况
3.如何终止
三、进程等待
四、进程程序替换
1.替换原理
2.原理
3.将代码改成多线程
4.认识这些函数
1.execl
2.execv
3.execlp/execvp
4.execle/execvpe
一、fork函数
1.进程:
内核的相关管理数据结构(task_struct + mm_struct + 页表) + 代码和数据
2.fork函数:
在linux中fork函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
进程调用fork,当控制转移到内核中的fork代码后,内核做:
分配新的内存块和内核数据结构给子进程
将父进程部分数据结构内容拷贝至子进程
添加子进程到系统进程列表当中
fork返回,开始调度器调度
fork函数返回值:子进程返回0,父进程返回的是子进程的pid。
为什么fork返回值是这样? 为了让父进程对子进程进行标识,知道子进程的pid,可以管理子进程。
3.写时拷贝
1.概念:
通常,父子代码共享,父子不再写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。
虚拟地址一样,物理地址却不一样。
4.fork常规用法
一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数
5.fork调用失败的原因
系统中有太多的进程
实际用户的进程数超过了限制
二、进程终止
1.终止是在做这么?
释放曾经的代码和数据所占据的空间
释放内核数据结构
2.进程终止的3种情况
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("i am a process! pid:%d, ppid:%d\n",getpid(), getppid());
sleep(3);
return 100;
}
~
echo:内建命令,打印的都是bash内部的变量数据
? : 父进程bash获取到的,最近一个子进程退出的退出码
退出码 : 告诉关心方(父进程),我把任务完成的怎么样了
0:成功 !0 : 失败
1,2,3,4,5....不同的非0值,一方面表示失败,一方面表示失败的原因
for(int errnum = 0; errnum < 240; ++errnum)
printf("%d -> %s\n",errnum, strerror(errnum)); //可以打印出每个数字代表的信息
为用户负责,可以了解错误信息
#include <stdio.h>
#include <unistd.h>
#include <string.h>
enum
{
success=0,
Div_zero,
Mod_zero,
};
const char* codeToerrstring(int code)
{
switch(code)
{
case success:
return "success";
break;
case Div_zero:
return "Div_zero";
break;
case Mod_zero:
return "Mod_zero";
break;
default:
return "unknow error";
break;
}
}
int exit_code = 0;
int Div(int x, int y)
{
if(x == 0)
{
exit_code = Div_zero;
return -1;
}
else
{
exit_code = success;
return x / y;
}
}
int main()
{
int result = Div(0,2);
printf("result = %d , %s\n",result, codeToerrstring(exit_code));
return exit_code;
}
代码跑完结果正不正确,可以通过进程的退出码来决定!
出错原因:系统 或者 自定义退出码。
代码执行时,出现异常,提前退出。
进程出异常,本质是进程收到OS发给进程的信号,提前终止
一旦出现异常,退出码便没有意义。
1.先确定是否异常
2.不是异常,就一定是代码跑完了,看退出码就行
衡量一个进程退出,我们只需两个数字:退出码,退出信号!
退出码 | 退出信号 | 释义 |
0 | 0 | 运行成功 |
!0 | 0 | 运行结果不对 |
!0 | !0 | 进程异常 |
0 | !0 |
3.如何终止
1> main函数return,表示进程终止(非main函数 - return,函数结束)。
2> exit() -- C库函数。 代码调用exit函数。注:我们代码的任意位置调用exit,都表示进程终止。
3> _exit() -- system call(系统调用) , exit会在进程退出时,冲刷缓冲区,而_exit不会。其它不变
注:exit底层调用_exit,目前我们所说的缓冲区,不是内核缓冲区。
三、进程等待
结论:任何子进程,在退出的时候,一般必须要被父进程进行等待。进程在退出的时候,如果父进程不管不顾,退出进程,子进程就会进行状态Z(僵尸进程),导致内存泄漏
why?
1.父进程通过等待,解决子进程退出的僵尸问题,回收系统资源(一定要考虑的)
2.获取子进程的退出信息,知道子进程是什么原因退出的。(可选的功能)
怎么办?
wait/waitpid
pid_t wait(int *status),等待父进程时,任意一个子进程退出,即等待成功。 等待成功时,返回子进程的pid。
如果子进程没有退出,父进程其实一直在进行阻塞等待!
pid_t waitpid(pid_t pid, int *status, int option)
返回值:当正常返回的时候waitpid返回手机的子进程的进程id
如果设置了选项WNOHANG,而调用中waitpid发现没有已退出
的子进程可收集,则返回0
如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在。
参数:pid: Pid = 1,等待人一个子进程。与wait等效
Pid = 0,等待其进程ID与pid相等的子进程。
int status (32位)
退出信息:1.输出型参数
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
void ChildRun()
{
int cnt = 5;
while(cnt--)
{
printf("i am child, cnt:%d pid:%d, ppid:%d\n", cnt, getpid(), getppid());
sleep(1);
}
}
int main()
{
printf("i am father, pid:%d, ppid:%d\n", getpid(), getppid());
pid_t id = fork();
if(id == -1) return 1;
if(id == 0)
{
//child
ChildRun();
printf("child quit...\n");
exit(1); //异常退出
}
sleep(7);
//parent
//pid_t rid = wait(NULL);
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid > 0)
{
printf("wait successful, rid:%d\n", rid);
}
else
{
printf("wait file!\n");
}
sleep(3);
printf("father quit,status:%d, child quit code:%d, child quit signal:%d\n",status, (status >> 8) & 0xFF, status & 0x7F);
return 0;
}
如果子进程是一个死循环,这时我们通过kill -9 3925发出一个信号终止子进程的话,就会得到
如果我们在子进程中加入一个野指针异常,比如:
void ChildRun()
{
int cnt = 5;
int* p =NULL;
while(1)
{
printf("i am child, cnt:%d pid:%d, ppid:%d\n", cnt, getpid(), getppid());
sleep(1);
*p =100; //野指针
}
}
上述status使用了为运算的方式,对于运用不是很友好
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
void ChildRun()
{
int cnt = 5;
//int* p =NULL;
while(cnt--)
{
printf("i am child, cnt:%d pid:%d, ppid:%d\n", cnt, getpid(), getppid());
sleep(1);
//*p =100;
}
}
int main()
{
printf("i am father, pid:%d, ppid:%d\n", getpid(), getppid());
pid_t id = fork();
if(id == -1) return 1;
if(id == 0)
{
//child
ChildRun();
printf("child quit...\n");
exit(123);
}
//father
while(1)
{
int status = 0;
pid_t rid = waitpid(id, &status, WNOHANG);//non block
if(rid == 0)
{
printf("child is running, father check next time!\n");
usleep(1000);
//DoOtherThing();
}
else if(rid > 0)
{
if(WIFEXITED(status))
{
printf("child quit success! child quit code:%d\n",WEXITSTATUS(status));
}
else
{
printf("child quit unsuccess! child quit code:%d\n",WEXITSTATUS(status));
}
break;
}
else
{
printf("wait filed!\n");
}
}
return 0;
}
WNOHANG:非阻塞式等待
四、进程程序替换
1.替换原理
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
#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 execvpe(const char *path, char *const argv[], char *const envp[]);
1.execl
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("begin execl...\n");
execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
printf("end execl...\n");
return 0;
}
2.原理
进程的程序替换
没有创建新的进程
站在被替换进程的角度,本质上就是这个程序被加载的内存中了
exec*系列的函数,执行完毕后,后续代码不见了,因为被替换了
execl函数的返回值不关心,因为替换成功,将不会向后继续运行,只要继续向后运行,则一定是运行失败了
类似“夺舍”。
3.将代码改成多线程
fork创建子进程,让子进程自己去替换 ,父进程wait
既可以让子进程完成了任务,父进程还不受影响。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
printf("begin execl...\n");
pid_t id = fork();
if(id == 0)
{
//child
sleep(2);
execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
exit(1);
}
int status = 0;
pid_t rid = waitpid(id, &status, 0); //block
if(rid > 0)
{
printf("father wait success! child exit code:%d\n", WEXITSTATUS(status));
}
printf("end execl...\n");
return 0;
}
父子进程具有独立性,当代码数据改变时,则子进程会发生写时拷贝。
4.认识这些函数
1.execl
l:list 列表。 path:我们要执行的程序,需要带路径。
例:ls -a -l execl(“/usr/bin/ls”,“ls”,“-a”,“-l”,NULL) ,结尾以NULL结束
2.execv
v:vector
int main()
{
printf("begin execl...\n");
pid_t id = fork();
if(id == 0)
{
//child
sleep(2);
char *const argv[] =
{
"ls",
"-a",
"-l",
"--color",
NULL,
};
execv("usr/bin/ls", argv);
exit(1);
}
int status = 0;
pid_t rid = waitpid(id, &status, 0); //block
if(rid > 0)
{
printf("father wait success! child exit code:%d\n", WEXITSTATUS(status));
}
printf("end execl...\n");
return 0;
}
3.execlp/execvp
p:用户可以不传要执行程序的路径(但是文件名要传),直接告诉它要执行谁就行。
查找这个程序,系统会自动在系统环境变量PATH中查找
例:execlp("ls", "ls", "-a", "-l", NULL); execvp("ls", argv);
4.execle/execvpe
e:environment
回到execl,我创建一个程序,并且要在testexec.c中运行它,并且打印它的pid
它们的pid均为22166,证明没有创建新的进程。
envp:整体替换所有的环境变量
1.用全新的自己写的给子进程
2.用老的环境变量给子进程,enverion
3.老的环境变量稍微修改,给子进程 putenv("");
5.真正的系统调用:execve
其它exec函数都是execve的封装,底层都是调用的这个函数