Linux基础进程
Posted Ricky_0528
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux基础进程相关的知识,希望对你有一定的参考价值。
文章目录
9. 地址空间
9.1 程序地址空间
- C/C++的程序地址空间
-
修改了值,但是地址却是一样的
#include <iostream> #include <unistd.h> #include <cstdio> int g_val = 100; int main() pid_t id = fork(); if (id == 0) int cnt = 5; //child while (cnt--) printf("I am child, times: %d, g_val = %d, &g_val = %p\\n", cnt, g_val, &g_val); sleep(1); if (cnt == 3) printf("############child change val##############\\n"); g_val = 200; printf("##################done####################\\n"); else while (true) printf("I am parent, g_val = %d, &g_val = %p\\n", g_val, &g_val); sleep(1); return 0;
子进程修改变量后,父进程打印出来的变量值没变,但两个不同值的变量的地址居然是一样的
如果存储的是真正的物理地址,这种现象不可能发生
程序使用的地址是虚拟地址,而不是实际的物理地址,存放在进程的 struct mm_struct,称为进程地址空间
9.2 进程地址空间
让进程能够以统一的视角看待内存
地址空间本质是内核中的一种数据类型 struct mm_struct // 进程地址空间
,32为机器下大小为4G
每个进程都认为地址空间的划分是按照4G空间划分的,即每个进程都认为自己拥有4GB
地址空间上进行区域划分时,对应的先行位置称为虚拟地址
页表 + MMU:MMU通过查页表将虚拟地址转化为物理地址
const char* str = "hello world"
*str = 'H'
❌定义在字符常量区的变量无法被修改,本质是因为OS给你的权限只有 r 权限
为什么要有地址空间
-
通过添加一层软件层,完成有效的对进程操作内存进行风险管理(权限管理),本质目的是为了保护物理内存及各个进程的数据安全
-
将内存申请和内存使用的概念在时间上划分清除,通过虚拟地址空间来屏蔽底层申请内存的过程,达到进程读写内存和OS进行内存管理操作,进行软件上面的分离
-
站在CPU应用层的角度,进程可以看作统一使用4GB空间,而且每个空间区域的相对位置是比较确定的
OS这样设计的目的是为了达到一个目标:每个进程都认为自己是独占系统资源的(进程具有独立性)
解释了这种现象的原因
- 在未改变
g_val
的前,两个页表都指向同一块物理内存,父进程g_val
- 但当改变了之后就发生了写时拷贝,在物理内存重写开辟了一块地方,子进程页表指向了子进程
g_val
- 但虚拟地址并没有发生改变
子进程的创建时以父进程为模板的,父子进程一般的代码是共享的
#include <stdio.h>
int main()
char *p = "hello";
char *q = "hello";
printf("%p\\n%p\\n", p, q); // 两块地址是一样的
return 0;
所以,所有的只读数据一般就只有一份,因为操作系统维护一份的成本是最低的
9.3 地址空间的划分
10. 进程控制
10.1 进程创建fork()
fork函数初始
进程调用fork,当控制转移到内核中的fork代码后,内核做:
- 分配新的内存块和内核数据结构给子进程
- 将父进程部分数据结构内容拷贝至子进程
- 添加子进程到系统进程列表当中
- fork返回,开始调度器调度
注意,fork之后会从fork的下一条语句开始执行,之前的并不会被执行
所以,fork之前父进程独立执行,fork之后父子两个执行流分别执行。而且,fork之后,谁先执行完全由调度器决定
写时拷贝
通常,父子代码共享,父子在不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式复制一份副本
当进程试图写入时,系统会发生缺页中断,子进程会被暂停,进行写时拷贝
10.2 进程终止
进程退出场景
- 代码运行完毕,结果正确
- 代码运行完毕,结果不正确
- 代码异常终止
进程常见退出方法
正常终止(可以通过 echo $?
查看进程退出码)
- 从main函数返回(main函数的返回值是进程的退出码,0:结果正确、!0:结果不正确)
- 从main函数return代表进程退出
- 非main函数return代表函数返回
- 调用
stdlib.h
下的void exit(int status)
- 在任何地方调用都代表终止进程,参数为退出码status
- 使用
unistd.h
下的void _exit(int status)
- 之前提到printf不带’\\n’打印的数据就会被暂时保存在输出缓冲区中,exit和main return本身就会要求系统进行缓冲区刷新
- 而_exit为强制终止进程,不会进行进程后续的收尾工作,比如刷新用户缓冲区
C语言中将错误码转换为字符串描述的函数为string.h
下的strerror
异常退出
- 程序崩溃(除零错)
- Ctrl + C:信号终止
异常退出下,程序的退出码变得没有意义了
进程退出,在系统层面少了一个进程:free PCB,free mm_struct,free 页表和各种映射关系,代码 + 数据,申请的空间也要释放掉
10.3 进程等待
让父进程fork之后,需要通过wait/waitpid等待子进程退出
让父进程等待的原因:
- 通过获取子进程退出的信息,能够得知子进程的执行结果
- 可以保证时序问题,子进程先退出,父进程后退出
- 进程退出的时候会先进入僵尸状态,会造成内存泄露的问题,因此需要通过父进程wait释放该子进程占用的资源
- 另外进程一旦变成僵尸状态,那就刀枪不入,kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程
wait
方法
头文件:#include <sys/types.h>
和#include <sys/wait.h>
pid_t wait(int *status);
返回值:
- 成功返回被等待进程pid,失败返回-1。
参数:
- 输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
pid_t id = fork();
if (id == 0)
// 子进程
int cnt = 5;
while (cnt)
printf("child[%d] is running, cnt is: %d\\n", getpid(), cnt);
cnt--;
sleep(1);
exit(0);
sleep(10);
printf("father begin wait\\n");
// 父进程
pid_t ret = wait(NULL);
if (ret > 0)
printf("father wait: %d, success\\n", ret);
else
printf("father wait failed\\n");
sleep(10);
return 0;
父进程在开始等待之前一共睡眠10s,前5s子进程执行,后5s子进程退出,但父进程没有开始等待,因而子进程进入了僵尸状态,当父进程开始等待之后,子进程就被回收,回收之后父进程又睡眠10s,故只剩一共进程在运行,知道10s后进程全部结束运行
waitpid
方法
头文件:#include <sys/types.h>
和#include <sys/wait.h>
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(为一个输出型参数,与子进程如何退出的有关,让父进程得到子进程的执行结果)
- 一共是32个比特位,但只使用低16个比特位
- 如果传递NULL,表示不关心子进程的退出状态信息
- 代码正常退出下的退出码就是这里的退出状态
- 代码异常终止,其本质是这个进程因为异常问题,导致自己收到了某种信号,就是这里的终止信号
- core dump暂不关心
- WIFEXITED(status):判断进程是否正常退出
- WEXITSTATUS(status):提取出进程的退出码
-
options
-
0:默认行为,阻塞等待,即一定等到子进程退出才会返回,否则会一直停在这里
阻塞的本质:进程的PCB被放入了等待队列,并将进程的状态改为S状态
返回的本质:进程的PCB从等待队列拿到R队列,从而被CPU调度
-
WNOHANG:设置等待方式为非阻塞,可能需要多次检测——基于非阻塞等待的轮询方案
- 使用WNOHANG需要注意有一种情况是子进程根本还没退出,需要特殊处理一下
看到某些应用或者OS本身卡住了长时间不动,称做应用或程序hang住了
-
示例1——获取退出状态和终止信号:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
pid_t id = fork();
if (id == 0)
// 子进程
int cnt = 3;
while (cnt)
printf("child[%d] is running, cnt is: %d\\n", getpid(), cnt);
cnt--;
sleep(1);
exit(11);
printf("father begin wait\\n");
// 父进程
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if (ret > 0)
printf("father wait: %d, success, status exit code: %d, status terminate signal: %d\\n", ret, (status >> 8) & 0xff, status & 0x7f); // 获取退出状态和终止信号
else
printf("father wait failed\\n");
return 0;
-
正常退出的情况
-
异常退出的情况
获取退出码的简洁方式:
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if (WIFEXITED(status)) // 没有收到任何终止信号
// 即正常结束的,获取对应的退出码
printf("exit code: %d\\n", WEXITSTATUS(status));
else
printf("error, got a terminate signal\\n");
补充:bash是命令行启动的所有进程的父进程,而且bash一定是通过wait方式得到子进程的退出结果,所以用echo $?
能够查到子进程的退出码
实例2——非阻塞等待:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
pid_t id = fork();
if (id == 0)
int cnt = 5;
while (cnt)
printf("child[%d] is running, cnt: %d\\n", getpid(), cnt);
cnt--;
sleep(1);
exit(0);
int status = 0;
while (1) //轮询等待
pid_t ret = waitpid(id, &status, WNOHANG);
if (ret == 0)
// 子进程没有退出,但是waitpid是成功的,需要父进程重复进行等待
printf("Do father's things\\n");
else if (ret)
// 子进程退出了,waitpid也成功了,获取到了对应的结果
printf("fahter wait: %d, success, status exit code: %d, status terminate signal: %d\\n", ret, (status >> 8) & 0xff, status & 0x7f);
break;
else
// 等待失败
perror("waitpid");
break;
sleep(1);
return 0;
10.4 进程程序替换
目前创建子进程的目的是让子进程执行父进程代码的一部分,但如果需要子进程执行一个全新的程序,则使用进程程序替换
进程程序替换:进程不变,仅仅替换当前进程的代码和数据的技术
程序替换的本质就是把程序的进程代码+数据加载到特定的进程上下文中
C/C++程序要运行,必须先加载到内存中
如何加载:使用加载器(exec*)加载
进程程序替换会更改代码区的代码,也要发生写时拷贝
只要进程的程序替换成功,就不会执行后续代码,意味着exec*函数执行成功的时候,不需要返回值检测
只要exec*函数返回了,就一定是因为调用失败了
exec*函数命名理解
- l(list):表示参数采用列表
- v(vector):表示参数用数组
- p(path):表示自动搜索环境变量PATH
- e(env):表示自己维护环境变量
execl
方法
int execl(const char *path, const char *arg, ...);
path:要执行的目标程序的全路径,即 所在路径/文件名
…:可变参数列表
arg, …:要执行的目标程序在命令行上怎么执行,这里的参数就怎么一个一个的传递进去,必须以NULL作为参数传递的结束
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
pid_t id = fork();
if (id == 0)
// child
printf("I am child, pid: %d\\n", getpid());
sleep(5);
execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
printf("hahahahaha\\n");
printf("hahahahaha\\n");
printf("hahahahaha\\n");
printf("hahahahaha\\n");
printf("hahahahaha\\n");
printf("hahahahaha\\n");
printf("hahahahaha\\n");
printf("hahahahaha\\n");
printf("hahahahaha\\n");
printf("hahahahaha\\n");
exit(0);
while (1)
printf("I am father\\n");
sleep(1);
return 0;
execv
方法
int execv(const char *path, char *const argv[]);
就是将execl的参数列表放到了一个数组里,其余都一样
char* argv[] =
"ls",
"-a",
"-l",
NULL
;
execv("/usr/bin/ls", argv);
execlp
方法
int execlp(const char *file, const char *arg, ...);
第一个参数可能会和第二个参数一样,但这两个参数的含义完全不一样
- 第一个参数表示你要执行谁,即你要执行的文件名
- 第二个参数表示如何执行它
execlp("ls", "ls", "-a", "-l", NULL);
execle
方法
int execle(const char *path, const char *arg, ..., char * const envp[]);
model.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
if (fork() == 0)
// child
// exec*
char *envs[] =
"MYENV1=env test",
"MYENV2=haha",
"MYENV3=xixi",
NULL
;
execle("./myexe", "myexe", NULL, envs);
exit(1);
// parent
waitpid(-1, NULL, 0);
printf("wait success\\n");
return 0;
myexe.c
#include <stdio.h>
int main()
printf("this new process is printing envs\\n");
extern char** environ;
for (int i = 0; environ[i]; i++)
printf("%s\\n", environ[i]);
return 0;
execve
方法(系统调用)
int execvpe(const char *file, char *const argv[], char *const envp[]);
model.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
if (fork() == 0)
// child
// exec*
char* argv[] =
"myexe",
NULL
;
char *envs[] =
"MYENV1=env test",
"MYENV2=haha",
"MYENV3=xixi",
NULL
;
execle("./myexe", argv, envs);
exit(1);
// parent
waitpid(-1, NULL, 0);
printf("wait success\\n");
return 0;
myexe.c
#include <stdio.h>
int main()
printf("this new process is printing envs\\n");
extern char** environ;
for (int i = 0; environ[i]; i++)
printf("%s\\n", environ[i]);
return 0;
execvp
方法
int execvp(const char *file, char *const argv[]);
char* argv[] =
"ls",
"-a",
"-l",
NULL
;
execvp("ls", argv);
execvpe
方法
int execvpe(const char *file, char *const argv[], char *const envp[]);
总结
除了execve
为系统调用,其余的都是库函数,最终都是由execve
实现的
编写一个简单的shell
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#define NUM 128
#define CMD_NUM 64
int main()
char command[NUM];
for (; ; 以上是关于Linux基础进程的主要内容,如果未能解决你的问题,请参考以下文章