进程创建
fork函数
进程调用fork,当控制转移到内核中的fork代码后,内核做:
分配新的内存块和内核数据结构给子进程
将父进程部分数据结构内容拷贝至子进程
添加子进程到系统进程列表当中
fork返回,开始调度器调度
在fork结束之后,父进程和子进程都进行了调度,之后先运行谁是取决于调度器的。
当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。
fork函数返回值
子进程返回0,
父进程返回的是子进程的pid
fork常规用法
一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子
进程来处理请求。
一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。
fork调用失败的原因
系统中有太多的进程
(子进程的创建是要占用资源的)
实际用户的进程数超过了限制
写时拷贝
操作系统是不允许任何不高效,和浪费的情况出现的,所以当fork之后,子进程并不会
傻乎乎
的将父进程的所有数据的代码都拷贝过来,当一些子进程完全不需要
的东西就是不会拷贝过来了。
所以写时拷贝是一种按需申请资源的策略
。
现在我所理解的就是进程数据可以进行写时拷贝,代码虽然不能写时拷贝但是能进行整体替换。
进程终止
进程退出的场景(情况)
代码运行完毕,结果正确
代码运行完毕,结果不正确
代码异常终止,【信号】(崩溃的本质:进程因为某些原因,导致收到了操作系统的信号(kill -9))
退出码
进程终止的状态可以由进程的退出码看出运行完毕是否正确,在代码异常终止时时因为什么原因。
echo $?
是用来查看进程结束的退出码的,它查看的退出码只有第一次查看是有效的,重复查看一个进程的退出码它会变化的。
上面的退出码错误表是C语言的,
下面的图片是查看它的退出码,
进程常见退出方法
进程退出:就是将该进程所对应的内核数据结构和相关数据和代码进行释放
一、main() return
在其他函数中调用并不是进程退出,只有在main()当中才是
二、exit()
,C标准库函数,它是在任何地方都是进程结束并且exit后面的代码并不会继续执行。它是等价main() returm
三、_exit()
它是直接进行退出不做任何处理的,而exit
是进行缓冲区刷新,执行用户定义的清理函数
在这里也对return,exit进行了测试都是具有刷新缓冲区的功能的。
1 #include<stdio.h>
2 #include<stdlib.h>
3 #include<unistd.h>
4
5 void add()
6 {
7 printf("exit前");
8 //exit(123);
9 printf("exit后");
10 }
11
12 int main()
13 {
14 //add();
15 //
16 printf("hello world");
17 sleep(2);
18 //exit(1);
19 //return 0;
20 //return 0;
21 _exit(11);
22 }
~
进程等待
子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法
杀死一个已经死去的进程。
最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,
或者是否正常退出。
父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
进程等待的原因就是:防止内存泄漏,和获取子进程退出信息(在必要的情况下)
等待:就是通过系统调用,获取子进程退出码或者退出信号的方式,并顺便释放内存
下面的代码在子进程代码走完会等待5秒,
10-5
那5秒当中子进程是属于僵尸进程,等待父进程获取子进程信息,最后的等待5秒是子进程已经退出,只剩下父进程,最后wait返回的是子进程的pid
。
wait方法
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status);
返回值:
成功返回被等待进程pid,失败返回-1。
参数:
输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
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:
status并不要把它当作一个整数指针,而是将他看作成位图
(由一个比特位或多个比特位组成表示某些状态的数据结构)下面的这两个是
宏
,这两个宏的目的就是不用在我们自己算出退出码了
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
options:
WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进
程的ID。
退出码是数字,可以输入kill -l来查看信号(1~31称为普通信号,也是会用到的)
当信号为零时表示进程正常退出,之后在查看退出码是看因为什么原因退出的。
1 #include<stdio.h>
2 #include<stdlib.h>
3 #include<unistd.h>
4
5 int main()
6 {
7 pid_t id =fork();
8 if(id==0)
9 {
10 int cnt=5;
11 while(cnt)
12 {
13 printf("我还活着!!%d,pid=%d,ppid=%d\n",cnt--,getpid(),getppid());
14 sleep(1);
15 }
16 exit(1);
17 }
18 //sleep(10);
19 int status=0;
E> 20 pid_t ret=waitpid(id,&status,0);
21 printf("pid=%d,ppid=%d,ret=%d,status=%d,child code=%d,child signal=%d",getpid(),getppid(),ret,status,status>>8&0xFF,status&0x7F);
22 //sleep(5);
23
24 }
如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退
出信息。
如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
如果不存在该子进程,则立即出错返回。
父进程是如何获取子进程的推出信息的
读取子进程的内核数据结构
父进程在wait时,如果子进程没有退出,则父进程在干什么
在子进程一直没有退出,父进程则是一直调用waitpid进行等待–阻塞等待
当子进程结束时,会有个指针指向父进程,然后父进程从阻塞的队列当中移到正在运行的队列当中
如果并不想要父进程是阻塞状态呢
WNOHANG
waitpid当中的参数,代表夯住了,也是非阻塞状态
举个例子就是,当我想要找朋友出去玩,就打电话叫他,当朋友有事情,说让我等他一会,
我有两种选择就是
一、我给他打电话不挂断,一直等到他结束(属于阻塞等待)
二、我给他打电话挂断,过一会就打一个,在不打电话的期间我可以去做自己的事情(属于非阻塞状态)
1 #include<stdio.h>
2 #include<stdlib.h>
3 #include<unistd.h>
4
5 int main()
6 {
7 pid_t id =fork();
8 if(id==0)
9 {
10 int cnt=5;
11 while(cnt)
12 {
13 printf("我还活着!!%d,pid=%d,ppid=%d\n",cnt--,getpid(),getppid());
14 sleep(1);
15 }
16 exit(1);
17 }
18 //sleep(10);
19 while(1)
20 {
21 int status=0;
E> 22 pid_t ret=waitpid(id,&status,WNOHANG);
23
24 if(ret<0)
25 {
26 perror("waitpid err");
27 exit(1);
28 }
29 else if(ret==0)
30 {
31 printf("处于非阻塞状态\n");
32 sleep(1);
33 continue;
34 }
35 else{
36 printf("pid=%d,ppid=%d,ret=%d,status=%d,child code=%d,child signal=%d",getpid(),getppid(),ret,status,status>>8&0xFF,status&0x7F);
37 break;
38 }
39 } //sleep(5);
40
41 }
进程程序替换
替换原理
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数
以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动
例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
从下面的代码和输出结果可以看出,当我们进行程序替换之后,后面的代码就不会在调用了, 程序替换换的是整体,并不是局部替换,在子进程中进行程序替换并不会影响父进程,因为进程具有独立性, 子进程进行发生写时拷贝
#include<stdio.h>
2 #include<unistd.h>
3
4 int main()
5 {
6 printf("hello world\n");
7 printf("hello world\n");
8 printf("hello world\n");
9 printf("hello world\n");
10 execl("/bin/ls","ls","-a","-l",NULL);
11 printf("hi world\n");
12 printf("hi world\n");
13 printf("hi world\n");
14 }
execl
的返回值失败返回-1成功不返回值,只要有返回值就是失败了,所以execl
是不需要进行返回值判断的,只要继续向后运行就是失败了。
当程序替换失败后则会继续运行后面的代码,成功则不会。
在下面的代码中子进程的退出码是由execl
当中的ls
决定的,hello.txt
不存在则退出码是2,并不是1
#include <sys/wait.h>
int main()
{
extern char **environ;
pid_t id = fork();
if(id == 0)
{
//child
printf("我是子进程: %d\n", getpid());
//execl: 如果替换成功,不会有返回值,如果替换失败,一定有返回值 ==》如果失败了,必定返回 ==》 只要有返回值,就失败了
//不用对该函数进行返回值判断,只要继续向后运行一定是失败的!
execl("/bin/ls", "ls","hello。txt", NULL);
exit(1);
}
sleep(1);
//father
int status = 0;
printf("我是父进程: %d\n", getpid());
waitpid(id, &status, 0);
printf("child exit code: %d\n", WEXITSTATUS(status));
return 0;
}
execl的所有接口
execl
int execl(const char *path,const char *arg,...);
从下面可以看出,程序替换execl和在命令行执行的结果是一样的,
execl
的最后一个字母可以理解成为list
execv
int execv(const char *path,char *const argv[]);
execv
和execl
的区别在于最后一个单词,这里的最后一个代表的是vector
,所以这两个并没有本质的区别,只是在传参的方式上有所不同。
execlp
int execlp(const char *file,const char *arg,...);
当我们在执行此程序时,只需要指定此程序名即可,系统会自动在环境变量PATH当中查找,
execlp
的最后一个字母可以理解成PATH,在下面图片当中的两个ls
的意思是不一样的,第一个代表的意思是我要执行谁,第二个及其往后是代表我要怎么执行它,有时也是可以省略的。
execvp
int execvp(const char *file,char *const argv[]);
经过上面的程序替换,也就可以推算出
execvp
的用法了。
execle
int execle(const char *path, const char *arg, ..., char * const envp[]);
这里替换的程序也可以是自己写的程序,
execle
的前两个参数还是像之前一样,最后一个参数是传自定义环境变量的,otherfile
单独使用是没有MYENV的环境变量的,用过myfile
,使其获得了该环境变量
自定义环境变量会覆盖式的将PATH进行修改,也就是下面的argv将PATH进行了覆盖,只剩下了,MYENV you can see me
当我们想要使用当前系统的环境变量的话,之前用过char **environ,将该函数名替换自定义环境变量,就可以了
但是这样自定义就用不了了,所以及想要自定义,又想要系统给的不被覆盖就要使用putenv是将自定义环境变量添加到environ
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4 int main()
5 {//该代码就是将自定义环境变量添加到系统的环境变量当中。
6 extern char **environ;
12 // char *const argv[]={"MYENV=you can see me ",NULL};
19 putenv("MYENV=you can see me");//
20 execle("./otherfile","otherfile",NULL,environ);
21 }
execvpe
经过前面的介绍,
execvpe
和和前面的都大同小异。
int execvpe(const char *file, char *const argv[],
char *const envp[]);
execve
经过前面的介绍,
execvpe
和和前面的都大同小异。
int execve(const char *filename, char *const argv[],
char *const envp[]);
通过下面的图片知道,一共由七种接口,但是在3号表当中只有6个,还有一个`execve`在2号表当中,是因为在3号表当中的的6个接口都是将`execve`进行封装的 |