进程控制(Linux)
fork函数初始
#include <unistd.h>
pid_t fork(void);
返回值:自进程中返回0,父进程返回子进程id,出错返回-1
进程调用fork,当控制转移到内核中的fork代码后,内核做:
- 分配新的内存块和内核数据结构给子进程
- 将父进程部分数据结构内容拷贝至子进程
- 添加子进程到系统进程列表当中
- fork返回,开始调度器调度
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{printf("begin\n");fork();printf("after\n");return 0;
}
fork之前,父进程运行;fork之后,创建子进程,调度器决定父子进程谁先被调用。
写时拷贝细节
当fork之后,父子进程共享代码,但是对数据任意一方进行修改,都会触发写时拷贝。可是,是如何触发的呢?操作系统又是如何得知的呢?这和页表的权限位有关,读写和可执行。操作系统在创建子进程的时候,会把数据的写权限取消掉,保留读的权限;当你要对这部分数据进行写入的时候,操作系统就会感知到,由于这部分是数据,操作系统不会异常结束进程,而是会再给你开辟一块空间,写入数据,这就是写时拷贝。
虚拟内存,地址空间,进程地址空间,程序地址空间这些名字实际上都是一个东西,那就是进程地址空间,这也是最准确的叫法。
进程终止
进程退出场景:
- 代码运行完毕,结果正确
- 代码运行完毕,结果不正确
- 代码异常终止
进程常见退出方法:
- return结束
- exit
- _exit
查询最近一次退出码:
echo $?
exit
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{printf("Linxu");exit(11);return 0;
}
_exit
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{printf("Linxu");_exit(12);return 0;
}
return
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{printf("Linxu");return 0;
}
return 语句会在进程结束后,返回一个数字,叫退出码。告诉父进程,子进程的运行情况。C语言也会使用errno全局变量记录最近一次调用函数的情况。默认认为0是成功运行,其他值则是有异常。exit和_exit会设置退出码,发送信号,结束进程。
野指针,在操作系统上看就是,访问到了非写权限的地址,操作系统做出回应,终止进程。
因此,我们也可以使用指令kill搭配选项终止进程。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{while(1){printf("pid : %d\n",getpid());sleep(1);}return 0;
}
使用kill -11 终止上面进程,即使该进程代码没问题,我也能通过发送信号,告诉操作系统该进程不正常,从而异常结束。
所以,进程是否能正常退出,除了代码的语法和逻辑有关,还和进程的维护有直接关系。总而言之,如果你的进程被黑了,权限被改了,那么你的代码就形同虚设了。
exit和_exit函数都是设置退出码,结束进程。那两者有什么区别呢?exit是库函数,而_exit是系统调用接口。exit在清理掉缓冲流等之后,接着调用_exit函数结束进程;而_exit直接就结束进程。
进程等待
僵尸进程情况:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{pid_t id = fork();if(id < 0){perror("fork");return 1;}else if(id == 0){int cnt = 5;while(cnt){printf("i am child, pid:%d, ppid:%d, cnt:%d\n",getpid(),getppid(),cnt);cnt--;sleep(1);}exit(0);}else{while(1){printf("i am father, pid:%d, ppid:%d\n",getpid(),getppid());sleep(1);}}return 0;
}
查询指令:
while :; do ps ajx | head -1 && ps ajx | grep testwait | grep -v grep;sleep 1;echo "------------";done
wait结束进程
包含的头文件
#include <sys/types.h>
#include <sys/wait.h>
创建10个进程,使用wait回收进程
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#define N 10
void runChild()
{int cnt = 5;while(cnt){printf("i am child, pid:%d, ppid:%d, cnt:%d\n",getpid(),getppid(),cnt);cnt--;sleep(1);}
}
int main()
{for(int i = 0; i < N;i++){pid_t id = fork();if(id == 0){runChild();exit(0);}printf("create child process: %d success\n",id);}for(int i = 0; i < N; i++){pid_t id = wait(NULL);if(id > 0){printf("wait: %d success\n",id);}}sleep(5);return 0;
}
进程替换
execl代码 执行可执行程序
单进程
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{printf("begin: i am a process pid:%d, ppid:%d\n",getpid(),getppid());//这类方法的标准写法execl("/usr/bin/ls","ls","-a","-l",NULL);printf("after: i am a process pid:%d, ppid:%d\n",getpid(),getppid());return 0;
}
原理:旧程序的代码和数据被切换成新程序的代码和数据,不替换环境变量
多进程
#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("begin: i am a process pid:%d, ppid:%d\n",getpid(),getppid());//这类方法的标准写法execl("/usr/bin/ls","ls","-a","-l",NULL);printf("after: i am a process pid:%d, ppid:%d\n",getpid(),getppid());//exit(0);}//fatherelse{pid_t ret = waitpid(id,NULL,0);if(ret > 0){printf("wait success, father pid:%d, ret:%d\n",getpid(),ret);}}return 0;
}
程序替换接口
在Linux中一共有7个程序替换接口,6个库函数接口,1个系统接口execve;
7个接口没有本质区别,都是直接操作进程。不仅c和c++可执行程序可以被调用,像脚本语言,python,java等等语言都可以被调用,因为这些语言在操作系统看来就是一个个进程,没有区别。python也有类似execl接口的函数,只不过使用python语言进行包装,底层还是调用execl接口。
以ls指令为例,如下图解释:
这里参数的作用就是确定一个程序的路径,其次是执行方法,再次就是其他参数,如命令行参数
这6个库函数,带p的函数,指定环境变量PATH;带v的函数,使用字符数组传递后面的参数,不使用可变参数;带e的参数,可以传递环境变量;
上面我执行的ls指令是内部程序,外部程序也可以执行,如下面的代码:
otherExe可执行程序:
#include <iostream>
using namespace std;
int main()
{cout <<"helo Linux C++" <<endl;return 0;
}
mycommand可执行程序:
#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("begin: i am a process pid:%d, ppid:%d\n",getpid(),getppid());//这类方法的标准写法execl("./otherExe","otherExe",NULL);printf("after: i am a process pid:%d, ppid:%d\n",getpid(),getppid());exit(0);}//fatherelse{pid_t ret = waitpid(id,NULL,0);if(ret > 0){printf("wait success, father pid:%d, ret:%d\n",getpid(),ret);}}return 0;
}
运行结果:
在mycommand程序中调用execl函数,进行程序替换,运行otherExe函数;注意这里是多进程,子进程和父进程互不影响,并发运行。环境变量不会被替换,环境变量具有全局属性,进程在被创建时,就存在了。虽然环境变量也属于数据,但是进程替换并不会替换环境变量,否则父子关系无法保证,该进程无法回收。
vim工具一次编译多个文件
.PHONY:all
all:otherExe mycommand
mycommand:mycommand.cgcc -o $@ $^
otherExe:otherExe.ccg++ -o $@ $^ -std=c++11
.PHONY:clean
clean:rm -rf mycommand otherExe
上面execl替换外部程序的代码,我们可以使用简便的指令,实现一次编译多个文件,方法如上图的vim代码。
Linux形成的可执行程序是有格式,ELF,有表头,会存储进程入口地址,execl就是依靠这个地址实现进程替换。
如何上传环境变量:
- 新增环境变量:使用putenv,把环境变量上传到调用进程的上下文中。
- 覆盖环境变量:使用execl式的接口,要求末尾带e,如execle
覆盖环境变量
#include <iostream>
using namespace std;
int main(int argc,char* argv[],char* env[])
{cout <<"helo Linux C++" <<endl;int i = 0;cout <<"命令行参数" << endl;for(;argv[i]; i++){cout << i << " " << argv[i] << endl;}i = 0;for(; env[i]; i++){cout <<i << " " << env[i] <<endl;}return 0;
}
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{//putenv("MYENV=123456");pid_t id = fork();if(id == 0){ printf("begin: i am a process pid:%d, ppid:%d\n",getpid(),getppid());char* const myenv[] = {"MYVAL=123","MYPATH=/usr/bin/XXX",NULL};execle("./otherExe", "otherExe", "-a", "-b", NULL, myenv);printf("after: i am a process pid:%d, ppid:%d\n",getpid(),getppid());exit(0);}//fatherelse{pid_t ret = waitpid(id,NULL,0);if(ret > 0){printf("wait success, father pid:%d, ret:%d\n",getpid(),ret);}}return 0;
}
新增环境变量
#include <iostream>
using namespace std;
int main(int argc,char* argv[],char* env[])
{cout <<"helo Linux C++" <<endl;int i = 0;cout <<"命令行参数" << endl;for(;argv[i]; i++){cout << i << " " << argv[i] << endl;}i = 0;for(; env[i]; i++){cout <<i << " " << env[i] <<endl;}return 0;
}
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{putenv("MYENV=123456");pid_t id = fork();if(id == 0){ printf("begin: i am a process pid:%d, ppid:%d\n",getpid(),getppid());char* const myenv[] = {"MYVAL=123","MYPATH=/usr/bin/XXX",NULL};execl("./otherExe", "otherExe", "-a", "-b", NULL);printf("after: i am a process pid:%d, ppid:%d\n",getpid(),getppid());exit(0);}//fatherelse{pid_t ret = waitpid(id,NULL,0);if(ret > 0){printf("wait success, father pid:%d, ret:%d\n",getpid(),ret);}}return 0;
}
总结:
exec系列函数:
- l代表列表,参数使用列表一个个传参
- v代表vector,参数使用数组传参
- p代表PATH环境变量
- e代表env环境变量
6个库函数只在参数上有区别,本质一样,底层最终都会调用一个系统调用接口execve