目录
1.进程创建
1.1字符串常量为什么不可以修改?
1.2代码段和数据段到底是什么?
1.3.fork函数初识
1.4.fork函数返回值
1.5.写时拷贝:
1.6写时拷贝按需进行的原理(与页表的权限有关)
1.7.fork常规用法
2.进程终止
2.1.进程退出场景
2.2.进程常见退出方法
2.3.exit()函数和_exit()函数辨析
2.4.辨析退出码、错误码、退出信号
2.4.1退出码转换为错误码的操作
2.5.普通函数的返回值
3.进程等待
3.1.进程等待必要性
3.2wait()和waitpid()函数
wait
功能:
waitpid
功能:
返回值:
3.3阻塞等待和非阻塞等待
3.3.1、阻塞等待
3.3.2非阻塞等待
3.4通过位操作获取子进程的退出码和退出信号
4. 进程程序替换
4.1. 概念与原理
概念:
原理:
4.2. exec*系列替换函数
函数解释
命名理解
4.3那我们具体如何进行进程替换呢?
4.4替换为什么没有影响父进程?
1.进程创建
1.1字符串常量为什么不可以修改?
这里为什么编译不通过?
因为字符串具有常量属性,字符常量不可被修改。这里的问题是字符串为什么会有常量属性呢?
这里是字符串常量,具有常性,所以存储在了代码段当中的常量区。
因为这里的字符串地址一定是虚拟地址,而改成字符H,是在物理空间上做修改,所以此时就需要页表进行映射,而这里就会有权限的限制,只读的权限,不可修改,所以才会有字符常量不可修改这样的限制,本质是操作系统的锅!
1.2代码段和数据段到底是什么?
代码段里面存储的是可执行代码和常量区;数据段存储的是全局变量和静态变量
1.3.fork函数初识
在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
#include <unistd.h>
pid_t fork(void);
返回值:自进程中返回0,父进程返回子进程id,出错返回-1
1.4.fork函数返回值
子进程返回0,
父进程返回的是子进程的pid
1.5.写时拷贝:
为什么要用拷贝的形式,父进程直接将资源给子进程不就行了吗?
我们通常的操作有增删改查,可能会直接修改了原来的内容,所以需要额外拷贝一份资源。
通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式拷贝副本。
1.6写时拷贝按需进行的原理(与页表的权限有关)
在进行拷贝的时候,会将数据段的页表权限改成只读权限!然后任何一方想要进行写入的时候,这个时候操作系统就会介入,将权限改回来可读可写,所以当我们的子进程进行写入的时候就会报错缺页中断。操作系统就会介入,这样就写时拷贝就可以按需进行!
页表不仅仅有将虚拟地址转换为物理内存,还会有权限位
1.7.fork常规用法
- 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
- 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数
2.进程终止
2.1.进程退出场景
- 进程代码运行完毕,结果正确
- 进程代码运行完毕,结果不正确
- 进程代码没用执行完,进程出异常了
2.2.进程常见退出方法
正常终止(可以通过 echo $? 查看进程退出码):
1. 从main返回
2. 调用exit
3. _exit
异常退出:
ctrl + c,信号终止
2.3.exit()函数和_exit()函数辨析
exit函数会支持刷新缓冲区,_exit函数不支持。exit()底层封装了_exit(),两者是上下层关系
2.4.辨析退出码、错误码、退出信号
退出码包含错误码,当退出码是0的时候,表示程序正常退出;如果退出码!=0,这个退出码就表示错误码。然后利用sterror函数将其进行转换。
进程如果在执行的时候异常了,os会发送信号终止它,这个就是退出信号。非0就代表程序出异常,0代表程序正常执行。
任何进程最终的执行情况,我们可以使用两个数字表明具体的执行情况,一个是退出码,另一个就是退出信号
2.4.1退出码转换为错误码的操作
使用语言或者系统自带的方法进行转化,例如:在linux中,使用strerror()函数。
char* strerror(int errnum);
#include<stdio.h>
#include<string.h>
int main()
{
for(int i = 0; i < 100; i++)
printf("%d:%s\n", i, strerror(i));
return 0;
}
2.5.普通函数的返回值
- 普通函数退出,仅仅表示函数调用完毕。
- 函数也被称为子程序,与进程退出时返回退出码类似,函数执行完毕也会返回一个值,这个值通常用于表示函数的执行结果或状态。
- 调用函数,我们通常想看到两种结果:a.函数的执行结果(函数的返回值);b.函数的执行情况(函数是否成功执行了预期的任务),例如:fopen()函数的执行情况是通过其执行结果来间接表示。
fopen函数举例:返回了非空的FILE*指针,则可认为函数执行成功;返回了NULL,则可认为函数执行失败,需要进一步检查错误的原因(errno变量或调用perror()函数)。
3.进程等待
3.1.进程等待必要性
- 子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
- 进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
- 父进程派给子进程的任务完成的如何,我们需要知道。如子进程运行完成,结果对还是不对,或者是否正常退出。
- 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
3.2wait()和waitpid()函数
wait
pid_t wait(int* status);
功能:
等待任意一个子进程结束,并回收其资源。
返回值:调用成功,返回已经结束进程的PID,同时获取到了子进程的退出状态码;调用失败,返回-1,并设置错误码以指示错误的原因。
参数status:输出型参数,用于存储子进程的退出状态,由OS填充,如果不需要这个信息,可以传递NULL,否则,OS会根据该参数,将子进程的信息反馈给父进程。
waitpid
pid_t waitpid(pid_t pid, int* status, int options);
功能:
等待任意一个子进程或者指定的子进程结束,并回收其资源。
参数pid:如果pid = -1,等待任意一个子进程,与wait等效;如果pid > 0,等待其进程的PID与pid相等的子进程。
参数option:如果option = 0,则为阻塞等待;如果option = WNOHANG,则为非阻塞等待。
返回值:
调用成功,返回收集到的子进程的PID,同时获取到了子进程的退出状态码;调用失败,返回-1,并设置错误码以指示错误的原因;如果为非阻塞等待,waitpid调用成功且没有收集到已结束的子进程,则返回0。
3.3阻塞等待和非阻塞等待
3.3.1、阻塞等待
定义:进程在发出某个请求(如:I/O操作、等待某个条件成立等)后,如果请求不能立即得到满足(如:数据未准备好、资源被占用等),进程会被挂起,在此期间无法继续执行其他任务,直到等待条件满足或被唤醒。
一心一意,专心做一件事!
特点:
a.行为 -> 进程在等待期间无法执行其他任务。
b.触发方式 -> 等待由外部条件触发(如:数据到达、资源释放等)。
c.管理层面:由操作系统或者底层系统资源管理。
d.效率与并发性:效率低。
应用场景:实时性要求不高,等待时间相对比较短的情况,如:简单文件的读写操作。
#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 is running, id:%d, ppid:%d\n", getpid(), getppid());
sleep(1);
cnt--;
}
exit(1); //子进程退出
}
int status = 0; //存储子进程退出状态
pid_t rid = waitpid(id, &status, 0); //父进程等待 —— 阻塞等待
if(rid > 0) //等待成功
printf("wait success, status:%d\n", status);
else if(rid == -1) //调用失败
perror("wait error!\n");
return 0;
}
3.3.2非阻塞等待
定义:进程在发出某个请求后,不会被立即挂起已等待请求的完成,即使请求不能立即得到满足,进程在等待期间可以继续执行其他任务,同时可能会以某种方式(轮询访问、回调等)定期检查请求状态或者等待结果的通知。
特点:
a.行为 -> 进程在等待期间可以执行其他任务;
b.触发方式 -> 可能通过编程的方式实现,如:轮询、回调等。
c.管理层面:在应用层通过编程实现。
d.效率与并发性:效率高,提高并发性和响应能力。
应用场景:需要高并发和响应能力的场景,如:在网络编程中,服务器同时处理多个客户端的请求。
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#define SIZE 5
int main()
{
Inittask();
pid_t id = fork();
if(id == 0) //子进程
{
int cnt = 2;
while(cnt)
{
printf("I am a process, id:%d, ppid:%d\n", getpid(), getppid()); sleep(1);
cnt--;
}
exit(1); //子进程退出
}
int status = 0; //存储子进程退出状态
while(1) //基于非阻塞轮询的访问
{
pid_t rid = waitpid(id, &status, WNOHANG); //非阻塞等待
if(rid > 0) //调用成功,收集到了已经结束的子进程 {
printf("wait success, status:%d\n", status);
break;
}
else if(rid == 0) //调用成功,未收集到已经结束的子进程
{
printf("child is running, father do other thing!\n");
printf("------------ Task begin ----------------\n");
executeTask(); //等待期间,执行其他任务
printf("------------ Task end ----------------\n");
}
else //调用失败
{
perror("wait error\n");
break;
}
sleep(1);
}
return 0;
}
3.4通过位操作获取子进程的退出码和退出信号
我们这里只讲解16位的情况下
#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 is running, id:%d, ppid:%d\n", getpid(), getppid());
sleep(1);
cnt--;
}
exit(1); //子进程退出
}
int status = 0; //存储子进程退出状态
pid_t rid = waitpid(id, &status, 0);
if(rid > 0) //等待成功
printf("wait success, status:%d, exit code:%d, exit sign:%d\n",
status, (status>>8)&0xff, status&0x7f); //位操作获取子进程的退出码、退出信号
return 0;
}
4. 进程程序替换
4.1. 概念与原理
概念:
它允许一个进程在执行期间,用一个新的程序来替换当前正常执行的程序,即:用全新的程序替换原有的程序。
这意味着进程在调用一种exec函数,当前进程的用户空间代码和数据被新程序的代码和数据完全替换(覆盖),从新程序的启动例程开始执行。
注意:调用exec函数,并不会创建新的进程,而是对原有进程的资源进行替换,因此调用exec前后该进程的pid并未发生改变。
原理:
加载新程序 -> 替换当前程序 -> 更新页表 -> 执行新程序。
- 加载新程序:当进程决定进行程序替换时(调用exec函数),它会请求OS将全新程序(代码和数据)从磁盘中加载到内存。
- 更新页表:为了实现替换,OS需要更新页表,将原来指向旧程序代码的虚拟地址映射到新程序代码的物理地址上,这样,就会执行新程序的代码。
4.2. exec*系列替换函数
有六种以exec开头的函数,统称exec函数:
#include <unistd.h>`
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[]);
函数解释
这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
如果调用出错则返回-1
所以exec函数只有出错的返回值而没有成功的返回值。
命名理解
这些函数原型看起来很容易混,但只要掌握了规律就很好记。
l(list) : 表示参数采用列表
v(vector) : 参数用数组
p(path) : 有p自动搜索环境变量PATH
e(env) : 表示自己维护环境变量
事实上,只有execve是真正的系统调用,其它五个函数最终都调用 execve,所以execve在man手册 第2节,其它函数在man手册第3节。这些函数之间的关系如下图所示
4.3那我们具体如何进行进程替换呢?
我们要知道当我们把一个程序进行./,那么这个程序就变成了一个进程,而在我们的这个进程中执行了关于进程替换的函数,那么该进程就会被替换,执行另一个进程!
我们不一定要让一个进程直接进行替换,可以创建子进程,让子进程进行替换,让父进程等待我们的结果就可以.
4.4替换为什么没有影响父进程?
因为进程具有独立性,我们将子进程进行替换,发生写时拷贝,不会影响父进程
一次想生成两个可执行文件,就需要这么写,不然makefile默认值生成第一条指令!