一.进程创建
1.fork函数
(1)为什么要给子进程返回0,给父进程返回子进程的pid?
父子进程立场:父进程不需要标识,子进程需要标识;子进程是要执行程序的,父进程需要区分子进程,子进程不需要(就是父亲要辨别是哪一个孩子,而父亲只有一个,不用分辨)
(2)如何理解fork有两个返回值的问题?
fork只是代码的集合,在fork内部就已经实现分流,在return之前执行两次,则返回两个值
2.写时拷贝
通常,父子进程代码共享(共享:即父子进程对应的页表指向的是同一块物理内存),父子不再写入时,数据也是共享的,当任意一方试图写入时,便以写时拷贝的方式各自一份副本。
写时拷贝的过程:
为何要写时拷贝:
①进程具有独立性
②不在创建时分开:子进程不一定会使用父进程的所有数据,写入的本质是:需要的时候,按需分配
延时分配:需要的时候再给你,本质是可以高效可以使用任何内存空间
代码会不会写时拷贝:90%不会,只读的不会
3.fork常规用法
①父进程复制自己,使父子进程同时执行不同的代码
②一个进程要执行一个不同的程序
4.fork调用失败原因:
①系统中有太多的进程
②实际用户的进程数超过了限制
二、进程终止
1.退出场景
①代码运行完毕,结果正确
②代码运行完毕,结果不正确
③代码异常终止
2.进程退出码
(1)echo $? :显示打印最近一次进程的退出码(默认正确退出,退出码为0)
(2)进程退出码每个数字代表的含义不一样,能帮助用户确认失败的原因,类似与计网网页出现的错误码,不同错误码代表出现的错误不一样
程序代码:
#include<stdio.h>
#include<string.h>
int main()
{
for(int i = 0; i < 100; i++)
{
printf("%d: %s\n",i,strerror(i));
}
return 0;
}
(3)main返回值给了谁?—— OS
为什么main函数要有返回值?
运行程序,加载,形成进程
3.进程退出方法
① 从main返回,只有main函数中的return代表进程退出
② 调用exit,在代码中的任何地方,调用exit都代表进程退出
③_exit函数,也代表进程的终止
②与③的差别:exit会释放进程曾经占用的资源,比如:缓冲区,_exit直接终止进程,不会做任何收尾工作!
注意:进程异常退出,退出码没有了意义
进程终止后,OS做了什么?
释放曾经申请的数据结构,释放曾经申请的内存,从各种队列等数据结构中移除
三、进程等待
1.进程等待的原因
进程等待:通常由父进程完成
原因:(1)回收子进程资源 (2)获取子进程退出信息
2.进程等待的方法
(1)wait方法
返回值:
成功返回被等待进程pid,失败返回-1。
参数:
输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include <unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
//child
int count = 0;
while(count < 10)
{
printf("I am child:pid:%d,ppid:%d\n",getpid(),getppid());
count++;
sleep(1);
}
exit(0);
}
else{
//father
printf("I am father:pid:%d,ppid:%d\n",getpid(),getppid());
pid_t ret = wait(NULL);
if(ret > 0)
{
printf("wait child success!,%d\n",ret);
}
printf("father run......\n");
sleep(10);
}
return 0;
}
结论:
①在子进程运行期间,父进程wait的时候,父进程在阻塞等待子进程退出
②父子谁先运行不确定,但是wait之后,大部分情况都是子进程先退出,父进程读取子进程退出信息,父进程才退出
(2)waitpid方法
pid_ t waitpid(pid_t pid, int *status, int options);
返回值:
a.当正常返回的时候waitpid返回收集到的子进程的进程ID;
b.如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
c.如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
参数:
pid:
Pid=-1,等待任一个子进程。与wait等效。
Pid>0.等待其进程ID与pid相等的子进程。
父进程等待成功,并不意味着子进程运行成功,只是子进程退出,要判定子进程是否运行成功要看status判定
status:
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出),本质是检查信号
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
利用宏定义获取status,而不是移位:
if( WIFEXITED(status) && ret == pid ){
printf("wait child 5s success, child return code is :%d.\n",WEXITSTATUS(status));
}
else{
printf("wait child failed, return.\n");
return 1;
}
options:
WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include <unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
//child
int count = 0;
while(count < 10)
{
printf("I am child:pid:%d,ppid:%d\n",getpid(),getppid());
count++;
sleep(1);
}
exit(0);
}
else{
//father
printf("I am father:pid:%d,ppid:%d\n",getpid(),getppid());
// pid_t ret = wait(NULL);
int status = 0;
pid_t ret = waitpid(id,&status,0);
if(ret > 0)
{
printf("wait child success!,%d\n",ret);
}
printf("father run......\n");
sleep(10);
}
return 0;
}
进程等待成功,是否意味着子进程运行成功?
不是,进程等待成功只意味着子进程退出了,是否运行成功要看status状态
(3)获取子进程status
需要右移8位并与0xFF,得到status状态信息
(status >> 8)& 0xFF
把exit函数的退出码改变,获取退出码,说明子进程运行成功
注意:进程异常的时候,本质是进程运行的时候出现了某种错误,导致进程收到信号!
查看进程收到的信号:
第一种:运行程序,发送kill -2 进程号(2为终止进程信号),程序退出获得child get signal:2
第二种:程序本身有错
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include <unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
//child
int count = 0;
while(count < 10)
{
printf("I am child:pid:%d,ppid:%d\n",getpid(),getppid());
count++;
if(count == 5)
{
int a = 1/0;
}
sleep(1);
}
exit(0);
}
else{
//father
printf("I am father:pid:%d,ppid:%d\n",getpid(),getppid());
// pid_t ret = wait(NULL);
int status = 0;
pid_t ret = waitpid(id,&status,0);
if(ret >= 0)
{
printf("wait child success!,%d\n",ret);
printf("status:%d\n",status);
printf("child xeit code:%d\n",(status>>8)&0xFF);
printf("child get signal:%d\n",status&0x7F);
}
printf("father run......\n");
sleep(10);
}
return 0;
}
8为SIGFPE信号,浮点数报错
查看不同信号值所代表的意义:1-31:普通信号;34-64:实时信号
(4)基于非阻塞接口的轮询
即:父进程在等待子进程退出的过程中,不会傻傻地等待,还会干自己的事!
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include <unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
//child
int count = 0;
while(count < 10)
{
printf("I am child:pid:%d,ppid:%d\n",getpid(),getppid());
// count++;
sleep(1);
count++;
}
exit(1);
}
while(1){
int status = 0;
pid_t ret = waitpid(id,&status,WNOHANG); //WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
if(ret > 0)
{
printf("wait child success!\n");
printf("exit code:%d\n",WEXITSTATUS(status));
break; //若等待成功,需要退出
// printf("status:%d\n",status);
// printf("child exit code:%d\n",(status>>8)&0xFF);
// printf("child get signal:%d\n",(status) & 0x7F);
}
else if(ret == 0)
{
//child not quit,waitpid success
printf("father do other thing!\n");
sleep(1);
}
else{
printf("waitpid error!\n");
break; //等待失败,也要跳出循环
}
// printf("father run......\n");
// sleep(10);
}
return 0;
}
四、进程程序替换
1.替换原理
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
结论:
(1)当前进程在进行程序替换的时候,没有创建新的进程
(2)进程程序替换,一经替换,后续代码不会执行,绝不返回!
(3)程序替换,可能失败(如:文件不存在),程序后续代码运行不会受到影响
execl系列函数,不需要判断返回值,只要返回就是失败
(4)进程替换,不会影响进程打开的文件信息(只会影响其映射关系,不会影响其数据结构),子进程会继承父进程打开的文件信息
2.替换函数
l(list) : 表示参数采集列表,要带路径
v(vector) : 参数用数组,要带路径,用一个数组先表示出来
p(path) : 有p自动搜索环境变量PATH,不用带路径
e(env) : 表示自己维护环境变量
(1)execl函数
#include<stdio.h>
#include<unistd.h>
int main()
{
printf("I am a process!\n");
sleep(5);
execl("/usr/bin/ls","ls","-a","-i","-l",NULL); //NULL结尾表示结束
return 0;
}
父进程让子进程(子进程去查找路径发现错误,不会执行execl函数中的内容)去执行其它的:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
#include<sys/types.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
printf("I am a process!\n");
sleep(5);
execl("/usr/bio/ls","ls","-a","-i","-l",NULL); //NULL结尾表示结束
exit(10);
}
int status = 0;
pid_t ret = waitpid(id,&status,0);
if(ret > 0)
{
printf("signal:%d\n",status & 0x7F);
printf("exit code:%d\n",(status>>8) & 0xFF);
}
return 0;
}
路径改回来后:
(2)execlp函数
//与execl函数不同的是,不用写路径,p会自动去找路径,要执行的是谁+怎么执行即可
execlp("ls","ls","-a","-i","-l",NULL);
(3)execv函数
char *myargv[] = {"ls","-a","-i","-l",NULL};
execv("usr/bin/ls",myargv);
(4)execvp函数
char *myargv[] = {"ls","-a","-i","-l",NULL};
execvp("ls",myargv);
3.makefile一次生成多个可执行文件
(1)exec.c文件里面去执行cmd.c文件里的内容(也可以调用脚本文件、python文件、c++文件等)
a.makefile文件:
.PHONY:all
all:myexec mycmd
myexec:exec.c
gcc -std=c99 -o $@ $^
mycmd:cmd.c
gcc -o $@ $^
.PHONY:clean
clean:
rm -f myexec mycmd
b.cmd.c文件
#include<stdio.h>
int main()
{
printf("I am a new exe,my cmd!\n");
return 0;
}
c.exec.c文件
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
#include<sys/types.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
printf("I am a process!\n");
sleep(5);
// execl("/usr/bin/ls","ls","-a","-i","-l",NULL); //NULL结尾表示结束
// execlp("ls","ls","-a","-i","-l",NULL);
char *myargv[] = {"ls","-a","-i","-l",NULL};
//execv("/usr/bin/ls",myargv);
// execvp("ls",myargv);
execl("./mycmd","mycmd");
exit(10);
}
int status = 0;
pid_t ret = waitpid(id,&status,0);
if(ret > 0)
{
printf("signal:%d\n",status & 0x7F);
printf("exit code:%d\n",(status>>8) & 0xFF);
}
return 0;
}
(2)调用环境变量
调用系统的环境变量:
#include<stdio.h>
#include<stdlib.h>
int main()
{
printf("I am a new exe,my cmd!\n");
printf("my env:%s\n",getenv("MYENV"));
printf("OS env:%s\n",getenv("PATH"));
return 0;
}
execl("./mycmd","mycmd",NULL);
调用自定义的环境变量:
char *myenv[]= {"MYENV=helloworld",NULL};
execle("./mycmd","mycmd",NULL,myenv);
可看到自己修改的环境变量改变,系统发环境变量为null,这种环境变量修改是覆盖式的