1.进程创建
fork()函数为什么要给子进程返回0,给父进程返回子进程的pid
父子进程立场:父进程不需要标识,子进程需要标识,因为父进程可以有多个子进程,而子进程只有一个父进程。子进程是要执行任务的,父进程需要通过子进程的pid来区分子进程,而子进程不需要
如何理解fork有两个返回值?
在fork()还没有return pid的时候,子进程就已经创建好了,return pid被执行了两次。
进程调用fork,当控制转移到内核中的fork代码后,内核做:
- 分配新的内存块和内核数据结构给子进程
- 将父进程部分数据结构内容拷贝至子进程
- 添加子进程到系统进程列表当中(也就是fork()还没有返回子进程就已经被创建出来了)
- fork返回,开始调度器调度
fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器决定。
写时拷贝
通常,父子代码共享,父子不在写入时,数据也是共享的。当任意一方试图写入,便以写时拷贝的方式各自一份副本。这里所说的共享,是指父子进程对应的页表指向的是同一块物理内存。
为什么要写实拷贝呢?
-
进程具有独立性
-
那么为什么不在创建子进程的时候就分开呢?
因为子进程未必使用父进程的所有数据,直接将父进程的数据给子进程拷贝一份,而子进程又不用,这岂不是资源浪费。所以,在子进程需要修改数据的时候,再进行写时拷贝。这叫做按需分配。另外,写实拷贝也做到了延时分配,要知道,一个子进程创建出来未必要立刻调度,等进程调度的时候在把空间给他即可,可以高效使用任何内存空间
2.进程终止
进程退出的场景有如下三种:
- 代码运行完毕,结果正确
- 代码运行完毕,结果不正确
- 代码异常终止
为什么main函数要有返回值,返回值给了谁?
- 当你运行程序形成进程的时候,你是想让该进程完成某种任务,并且你需要知道任务完成的结果。main函数的返回值是进程退出码。0代表成功。!0代表失败,这是人为定义的。这个返回值是给操作系统。我们通过进程退出码来判断任务的完成情况。
我们可以使用echo $? 指令打印出最近一次进程退出时的退出码
如果退出码不是0,那么失败的原因有多种。每一种退出码都有对应的字符串含义,帮助用户确认,任务失败的原因。下面我们利用下面代码测试一下Linux下不同退出码代表的含义
#include <stdio.h>
2 #include <string.h>
3 int main()
4 {
5 int i = 0;
6 for(i = 0;i < 150; i++)
7 {
8 printf("%d: %s\n", i, strerror(i));
9 }
10 return 0;
11 }
运行结果如下,共134条错误码
进程退出的常见方法:
-
从main返回
只有main函数中的return代表进程退出
-
调用exit
-
_exit
对于exit和_exit,在代码的任何地方,调用exit都代表进程退出!他们的差别是,exit会释放进程曾经占用的资源,比如缓冲区。而_exit直接终止进程,不会做任何的收尾工作!
为了更好地了解这一点,我们来用下面这段代码测试一下
#include <stdio.h> #include <string.h> #include <unistd.h> #include <stdlib.h> int main() { printf("hello world"); sleep(3); // exit(0); _exit(0); return 0; }
这段代码如果使用exit,在进程终止之后会将缓冲区中的hello world打印到屏幕上,而_exit不会
进程如果是异常退出了,那么退出码将没有任何意义。
进程终止了,操作系统做了什么呢?
- 释放曾经申请的数据结构,释放曾经申请的内存,从各种队列等数据结构中移除
3.进程等待
-
进程等待通常是由父进程完成!为什么要有进程等待呢?原因如下:
- 子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
- 父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
- 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
进程等待的方法
-
wait方法
#include<sys/types.h> #include<sys/wait.h> pid_t wait(int*status); 返回值: 成功返回被等待进程pid,失败返回-1。 参数: 输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
1 #include <stdio.h> 2 #include <string.h> 3 #include <unistd.h> 4 #include <stdlib.h> 5 #include <sys/types.h> 6 #include <sys/wait.h> 7 8 int main() 9 { 10 pid_t id = fork(); 11 if(id == 0) 12 { 13 //child 14 int count = 0; 15 while(count < 10) 16 { 17 printf("I am child, pid: %d, ppid: %d\n", getpid(), getppid()); 18 count++; 19 sleep(1); 20 } 21 exit(0); 22 } 23 else 24 { 25 //father 26 printf("I am father, pid: %d, ppid: %d\n", getpid(), getppid()); 27 pid_t ret = wait(NULL); 28 if(ret >= 0) 29 { 30 printf("wait child success! %d\n", ret); 31 } 32 printf("father runing...\n"); 33 sleep(10); 34 } 35 return 0; 36 }
子进程运行期间,父进程wait的时候,父进程就是在单纯的等子进程退出——阻塞等待
关于阻塞和非阻塞:
阻塞就是单纯的一直等,非阻塞也是等,不过不会因为条件不满足而卡住。
父子进程谁先运行不确定,但是wait之后,大部分都是子进程先退出,父进程读取子进程退出信息,父进程才退出
进程等待成功,不意味着子进程运行成功。想要知道子进程是否运行成功,需要看子进程的退出码
-
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: WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出) WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码) options: WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进 程的ID。
status
-
是一个整数,我们只关注该整数的低16位
进程异常的时候,本质是进程运行的时候出现了某种错误,导致进程收到信号
-
4.进程程序替换
-
替换原理
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
-
六种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, ...,char *const envp[]); int execv(const char *path, char *const argv[]); int execvp(const char *file, char *const argv[]); int execve(const char *path, char *const argv[], char *const envp[]);
l – list v – vector p:有p自己动搜索环境变量PATH e:表示自己维护环境变量
- 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
- 如果调用出错则返回-1
- 所以exec函数只有出错的返回值而没有成功的返回值。
下面这段代码是对前5个函数的测试
其实前五个函数的底层都是第六个函数
#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> #include <stdlib.h> int main() { pid_t id = fork(); if(id == 0) { printf("I am a process\n"); sleep(2); // execl("/usr/bin/ls", "ls", "-a", "-i", "-l", NULL); // execlp("ls", "ls", "-a", "-i", "-l", NULL); // char* myargv[] = { // "ls", // "-a", // "-i", // "-l", // NULL // }; execv("/usr/bin/ls", myargv); // execvp("ls", myargv); char* myenv[] = { "MYENV=HAHAHA", NULL }; execle("./mycmd", "mycmd", NULL, myenv); exit(11); } int status = 0; pid_t ret = waitpid(id, &status, 0); if(ret > 0) { printf("wait success\n"); printf("signal: %d\n", status & 0x7F); printf("exit code: %d\n", WEXITSTATUS(status)); } return 0; }
5.简易shell
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <string.h>
#define LEN 1024
#define NUM 32
int main()
{
char* myarg[NUM];
char cmd[LEN];
while(1)
{
printf("[ls@VM-20-7-centos shell]$ ");
fgets(cmd, LEN, stdin);
//把字符串拆开 得到一个一个的子串
cmd[strlen(cmd)-1] = '\0';
myarg[0] = strtok(cmd, " ");
int i = 1;
while(myarg[i] = strtok(NULL, " "))
{
i++;
}
pid_t id = fork();
if(id == 0)
{
//child
execvp(myarg[0], myarg);
exit(11);
}
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if(ret > 0)
{
printf("exit code: %d\n", WEXITSTATUS(status));
}
}
return 0;
}