【Linux】探索进程控制奥秘,解锁高效实战技巧

可涵不会debug 2024-09-18 12:07:02 阅读 60

目录

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);

<code>#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.效率与并发性:效率低。

应用场景:实时性要求不高,等待时间相对比较短的情况,如:简单文件的读写操作。

<code>#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位的情况下

<code>#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默认值生成第一条指令!



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。