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

    }
相关推荐
花归去4 分钟前
echarts 柱状图曲线图
开发语言·前端·javascript
喝拿铁写前端4 分钟前
当 AI 会写代码之后,我们应该怎么“管”它?
前端·人工智能
老前端的功夫8 分钟前
TypeScript 类型魔术:模板字面量类型的深层解密与工程实践
前端·javascript·ubuntu·架构·typescript·前端框架
田里的水稻17 分钟前
C++_python_相互之间的包含调用方法
c++·chrome·python
Nan_Shu_61431 分钟前
学习: Threejs (2)
前端·javascript·学习
G_G#39 分钟前
纯前端js插件实现同一浏览器控制只允许打开一个标签,处理session变更问题
前端·javascript·浏览器标签页通信·只允许一个标签页
@大迁世界1 小时前
TypeScript 的本质并非类型,而是信任
开发语言·前端·javascript·typescript·ecmascript
GIS之路1 小时前
GDAL 实现矢量裁剪
前端·python·信息可视化
是一个Bug1 小时前
后端开发者视角的前端开发面试题清单(50道)
前端
Amumu121381 小时前
React面向组件编程
开发语言·前端·javascript