Linux:进程控制

1.进程创建

1-1 fork()

cpp 复制代码
#include <unistd.h>

pid_t fork(void);

返回值:子进程中返回0,⽗进程返回⼦进程id,出错返回-1

创建fork()时:

• 分配新的内存块和内核数据结构给⼦进程

• 将⽗进程部分数据结构内容拷⻉⾄⼦进程

• 添加⼦进程到系统进程列表当中

• fork返回,开始调度器调度, 给父进程返回pid, 给子进程返回0

写时拷贝:父子独立性

例子:

cpp 复制代码
int main( void )
{
 pid_t pid;
 printf("Before: pid is %d\n", getpid());
 if ( (pid=fork()) == -1 )perror("fork()"),exit(1);
 printf("After:pid is %d, fork return %d\n", getpid(), pid);
 sleep(1);
 return 0;
} 

运⾏结果:

root@localhost linux\]# ./a.out Before: pid is 43676 After:pid is 43676, fork return 43677 After:pid is 43677, fork return 0

fork之前⽗进程独⽴执⾏,fork之后,⽗⼦两个执⾏流分别执⾏。注意,fork之后,谁先执⾏完 全由调度器决定。

1-2 写时拷贝

通常,⽗⼦ 代码共享,⽗⼦ 再不写⼊时,数据也是共享的,当任意⼀⽅试图写⼊,便以写时拷**⻉** 的**⽅** 式各**⾃**备⼀份副本。

父进程如果没子进程,那么数据段是可写的,一旦有了子进程,那么就会被修改为只读。

子进程对数据段进行写的时候,会报错,但意识到是数据段, 就会开启写时拷贝。

浅拷贝 写时拷贝

为什么要写时拷贝?

(1)只拷贝自己想改的,

(2)减少创建时间,减少内存浪费。

解释:因为有写时拷⻉技术的存在,所以⽗⼦进程得以彻底分离离!完成了进程独⽴性的技术保证!写时拷⻉,是⼀种延时申请技术,可以提⾼整机内存的使⽤率。

2.进程终止

本质就是释放系统资源,就是释放进程申请的相关内核数据节后和对应的数据和代码。

2-1进程退出场景

三种退出场景

1.代码完毕->结果正确

  1. ->结果错误,没有按预想进行

3.代码异常终止

2-2进程常⻅退出⽅法

正常终⽌(可以通过 echo $? 查看进程退出码):

  1. 从main返回

  2. 调⽤exit

  3. _exit

异常退出:

• ctrl + c,信号终⽌

2-2-1:退出码

什么是退出码?

退出码(退出状态)可以告诉我们最后⼀次执⾏的命令的状态。在命令结束以后,我们可以知道命令 是成功完成的还是以错误结束的。其基本思想是,程序返回退出代码 0 时表⽰执⾏成功,没有问题。 代码 1 或 0 以外的任何代码都被视为不成功

问题:int main(),返回0,返回给谁?

答:返回给父进程bash.main函数返回值,通常表名程序的执行情况属于进程退出场景的前两种情况。

当返回0的时候,表示代码完毕,结果正确。

当返回非0的时候,不同的返回值来表示不同的出错原因。

如:

一些指令:

"echo $?",一条指令:表示打印最近一个程序退出时的退出码 -> 进程退出码 -> main函数返回值。

"strerror(i)":i表示任意数字,整个strror(i)可以表示各个数字对应的错误信息

"return errno":如果出现错误,但不知道是哪一种,可以用来查看错误信息

代码终止异常:退出码无意义。一旦出现异常,一般是进程收到了信号。

2-3 exit和_exit

2-3-1 _exit

cpp 复制代码
#include <unistd.h>

void _exit(int status);

参数:status 定义了进程的终⽌状态,⽗进程通过wait来获取该值

2-3-2 exit(数字)

cpp 复制代码
#include <unistd.h>

void exit(int status);

:使得一个进程终止。等价与return

任何地方调用exit()都表示进程结束。后续代码一律不执行。并返回给父进程bash,子进程退出码

exit最后也会调⽤_exit,但在调⽤_exit之前,还做了其他⼯作:

  1. 执⾏⽤⼾通过atexit或on_exit定义的清理函数。

  2. 关闭所有打开的流,所有的缓存数据均被写⼊(清空缓冲区)

  3. 调⽤_exit

_exit() (系统调用)vs exit((库函数))

:进程调用exit 退出的时候,会进行缓冲区的刷新

调用**_exit()** ,不会进行缓冲区的刷新

库函数会调用相关的系统调用来完成自己的功能。

库缓冲区:语言层会刷新缓冲区,系统层不会刷新缓冲区

3.进程等待

3-1进程等待的必要性

1.解决僵尸进程造成的内存泄漏问题

2.回收子进程资源(最重要的), 获取子进程退出信息(可选)

3-2 进程等待方法

3-2-1 wait方法

cpp 复制代码
#include<sys/types.h>

#include<sys/wait.h>

pid_t wait(int* status);

返回值:

 成功返回被等待进程pid,失败返回-1。
参数:

 输出型参数,获取⼦进程退出状态,不关⼼则可以设置成为NULL

子进程退出:返回对应pid;

子进程没退出:父进程会阻塞在wait调用处。

waitpid:成功:返回子进程pid

失败:>0,子进程pid

-1:等待任意个子进程。

3-2-2 waitpid方法

cpp 复制代码
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: 输出型参数

 WIFEXITED(status): 若为正常终⽌⼦进程返回的状态,则为真。(查看进程
是否是正常退出)

 WEXITSTATUS(status): 若WIFEXITED⾮零,提取⼦进程退出码。(查看进程
的退出码)

 options:默认为0,表⽰阻塞等待

 WNOHANG: 若pid指定的⼦进程没有结束,则waitpid()函数返回0,不予以等
待。若正常结束,则返回该⼦进程的ID。

pid > 0可指定等待子进程。

pid = -1, 等待任意子进程

代码验证:

3-3退出码和终止信号

3-3-1 用status取出状态码

为什么不一样?

上面为退出状态:

下面为终止信号:前8位不用,core dump标志, 低7位表示终止信号(为什么被终止):

没有异常:低7各比特位全0

收到信号:低7各比特位不为0,表示位异常退出, 退出码无意义。
int整型变量一共32位,高16位我们不要,图中位低16位, 前8位位退出码,后8位如果属于征程退出,那么后面就都是0, 所以statux = 1 << 8 == 256,而不是 1.

也就是说正常终止,我们要获取退出码需要把status >> 8,才可以得到

附加问题

拿到子进程信息可以使用全局变量码?

不行, 存在写时拷贝, 要拿到子进程信息,只能通过系统调用来获取。

3-3-2 终止信号

使用kill -9 pid后,子进程被异常退出。

退出码为0, 退出信号为9,终止信号位9。

没有异常?

1.低7个比特位,全0

2.一旦不为0,那么就是异常退出的,退出码无意义。

3-4三个子问题

1.waitpid怎么拿到的status

父进程调用系统调用,操作系统取子进程拿到,返回给父进程

2.宏封装

封装获取退出码,用调用,我们也可以实现

1.等待成功 --- rid > 0

2.子进程是否退出异常 --- WIFEXITED

3.options 非阻塞调用

WNOHANG:wait no hang ,等的时候不要挂住 --- 非阻塞调用。

例子: 一直打电话:多次打,循环完成

非阻塞轮询:父进程不断请求调用子进程

返回值大于0:等待结束->回收成功

等于0:调用结束,子进程没有退出

小于0:失败

非阻塞调用:可以在等待对方的时候对方做自己的事情,效率高

No Block

非阻塞代码:

一直等待,直到成功:

验证父进程在等待子进程的时候,可以做自己的事情:

使用函数指针,循环调用:

cpp 复制代码
// 函数指针类型
typedef void (*func_t)();

#define NUM 5
func_t handlers[NUM+1];

// 如下是任务
void DownLoad()
{
    printf("我是一个下载的任务...\n");
}
void Flush()
{
    printf("我是一个刷新的任务...\n");
}
void Log()
{
    printf("我是一个记录日志的任务...\n");
}



// 注册

void registerHandler(func_t h[], func_t f)
{
    int i = 0;
    for(; i < NUM; i++)
    {
        if(h[i] == NULL)break;
    }
    if(i == NUM) return;
    h[i] = f;
    h[i+1] = NULL;
}

int main()
{
    registerHandler(handlers, DownLoad);
    registerHandler(handlers, Flush);
    registerHandler(handlers, Log);


    pid_t id = fork();
    if(id == 0)
    {
        //子进程
        int cnt = 3;
        while(1)
        {
            sleep(3);
            printf("我是一个子进程, pid : %d, ppid : %d\n", getpid(), getppid());
            sleep(1);
            cnt--;
            //int *p = 0;
            //*p= 100;
           // int a = 10;
           // a /= 0;
        }
        exit(10);
    }

    // 父进程
    while(1)
    {
        int status = 0;
        pid_t rid = waitpid(id, &status, WNOHANG);
        if(rid > 0)
        {
            printf("wait success, rid: %d, exit code: %d, exit signal: %d\n", rid, (status>>8)&0xFF, status&0x7F) ; // rid
            break;
        }
        //说明子进程好在调用,没有退出
        else if(rid == 0)
        {
            //父进程在运行
            // 函数指针进行回调处理
            int i = 0;
            for(; handlers[i]; i++)
            {
                handlers[i]();
            }
            printf("本轮调用结束,子进程没有退出\n");
            sleep(1);
        }
        else
        {
            printf("等待失败\n");
            break;
        }

    }
相关推荐
[seven]2 小时前
React Router TypeScript 路由详解:嵌套路由与导航钩子进阶指南
前端·react.js·typescript
无我Code2 小时前
前端-2025年末个人总结
前端·年终总结
文刀竹肃2 小时前
DVWA -SQL Injection-通关教程-完结
前端·数据库·sql·安全·网络安全·oracle
LYFlied3 小时前
【每日算法】LeetCode 84. 柱状图中最大的矩形
前端·算法·leetcode·面试·职场和发展
Bigger3 小时前
Tauri(21)——窗口缩放后的”失焦惊魂”,游戏控制权丢失了
前端·macos·app
Bigger3 小时前
Tauri (20)——为什么 NSPanel 窗口不能用官方 API 全屏?
前端·macos·app
bug总结3 小时前
前端开发中为什么要使用 URL().origin 提取接口根地址
开发语言·前端·javascript·vue.js·html
一招定胜负4 小时前
网络爬虫(第三部)
前端·javascript·爬虫
Shaneyxs4 小时前
从 0 到 1 实现CloudBase云开发 + 低代码全栈开发活动管理小程序(13)
前端