进程的基本控制

前言:整个操作系统都在围绕进程这一概念具体展开,所以对于进程的控制就显得十分重要,这篇文章主要讲述以下几点:

1. 进程创建
2. 进程退出
3. 进程等待
4. 进程程序替换

进程创建

在操作系统中,对于父子进程的概念非常重要,必要linux自带的bash,对于你在命令行输入的一些指令,它是不会自己去处理你这些请求的,而是通过创建子进程去处理,它只需要知道子进程返回的消息就好了。

为什么要基于这样的父子进程关系呢?试想一下,我们和操作系统打交道是通过shell内建的bash,如果用户的什么请求都经由bash去亲自执行,那么一个bash也不够用啊,其次,如果一旦请求中出了问题,那么bash挂掉的话,谁来帮我们向操作系统传达我们的请求呢?

基于上面提出的种种问题,就引出进程创建子进程的必要性了。

进程的创建方式:

  1. pid_t fork(void);
  2. pid_t vfork(void);
认识fork函数
pid_t fork(void)
返回值:pid_t其实就是一个整型,typedef成pid_t只是为了一眼看上去知道这是一个进程号
子进程中返回值为0.
父进程中返回操作系统给子进程分配的pid号。
fork失败返回-1

进程调用fork,当控制权限转移到内核中的fork代码后:

  • 分配新的内存块和数据结构给子进程
  • 将父进程的大多数数据结构拷贝至子进程
  • 添加子进程到系统进程列表
  • fork返回,操作系统进行进程调度

当一个进程fork出一个子进程后,就有两个二进制代码相同的进程,并且运行到相同的地方,但每个进程都将开始执行自己的代码。

如下:

int main()
{
    printf("Before fork: pid is:%d\n",getpid())//getpid函数为获取进程pid
    pid_t id = fork();
    if(id < 0)
    {
        perror("fork error");
        exit(1);
    }
    printf("After fork:pid is %d\n",getpid());

    return 0;
}

这里写图片描述

这里需要注意的是,先执行子进程还是父进程完全取决于操作系统的进程调度器决定。

fork失败的原因:

  • 系统中的进程数达到了上限
  • 系统的内存不足
  • 系统不支持,如Windows不支持fork
认识vfork函数

对于vfork来说,其他的都是fork函数用法一样,只要记住最重要的两个特性就好。

  1. 子进程一定先于父进程执行。
  2. 子进程调用exec或者exit之后父进程才能执行

进程终止

进程退出的场景:

  1. 代码运行完,结果正确
  2. 代码运行完,结果不正确
  3. 代码异常终止

常见进程退出:

正常终止:
1. main函数返回
2. 调用exit函数
3. 调用_exit函数

exit函数和_exit函数的区别:

  1. exit会进行清理工作,如刷新缓冲区等,而exit直接退出程序
  2. _exit是系统调用,exit最终也会调用_exit。
    异常终止
    CTRL+C/kill -9

具体的程序退出部分,可见博客尾部的链接。

进程等待

进程等待是非常重要的,如果父进程对子进程不管不顾的话,那么可能会产生僵尸进程,从而造成内存泄漏。
并且作为父进程,创建子进程是让它完成一些任务的,总要知道它返回的结果,完成的怎么样。

wait函数
pid_t wait(int *status)//阻塞式等待
返回值:成功返回等待进程的pid,失败返回-1
参数:输出型参数,获取子进程的退出状态,不关心可以为NULL,该参数由操作系统初始化

status
这里写图片描述

所以查看的话,先查看低七位是否为0 ,如果是0代表程序正常退出,可以查看高八位具体的退出码。
如果低七为不为0,则代表信号终止,高八位就没有意义了,可以查看低七位的具体信号。
core dump是指进程终止时所记录的现场。

以下面的代码为实例:

#include<stdio.h>
#include<wait.h>

int main()
{
    pid_t id = fork();
    if(id > 0)
    {
        //father
        int status = 0;
        int ret  = wait(&status);
        if(ret > 0 && (status&0x7f) == 0)
        {
            //success
            printf("child exit code:%d\n",(status>>8));
        }
        else
        {
            //signal exit
            printf("signal code:%d\n",(status>>8)&0xff);
        }
    }
    else if(id == 0)
    {
        //child
        sleep(3);
        exit(5);//子进程的退出码
    }
    else
    {
        perror("fork");
    }
    return 0;
}

当子进程正常退出时,会返回退出码。
运行结果:
这里写图片描述

接下来,我们直接kill -9掉该进程,结果应该返回9号信号,看如下运行结果:
这里写图片描述
我的Ubuntu是最新的,本应该返回9,但是操作系统将9这个数字,对应成第九个信号的名称显示出来。

下面是Linux下的信号:
这里写图片描述

waitpid函数
pid_t waitpid(pid_t pid,int *status,int option)//如果最后一个参数设置了WNOHANG就是非阻塞式等待

返回值:

  1. 正常返回收集子进程的进程ID
  2. 如果设置了选项WNOHANG,而调用waitpid发现没有已退出的子进程可以回收,则返回0,就是轮询等待的意思
  3. 如果调用中出错,返回-1,errno会被设置成相应的值以指示错误所在。

参数:
pid:

  • pid = -1,等待任意一个子进程,与wait等效
  • pid > 0,等待其进程ID与pid相等的进程

status:

  • WIFEXITED(status):若为正常终止子进程返回的状态,则为真(相当于上面的检查低7位为是否为0)
  • WEXITSTATUS(status):若WIFIXITED非零,提取子进程退出码(相当于上面的查看高8位的退出码)

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

需要注意的是:

  • 如果子进程已经结束,调用wait/waitpid时,函数会直接返回,并且释放资源,获得子进程退出信息。(对应的场景就是子进程退出时,父进程在沉睡,如果这时父进程不予以处理子进程则会产生僵尸进程)
  • 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞
  • 如果不存在该子进程,则立即出错返回

    如下代码示例:

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>


int main()
{
    pid_t id = fork();
    if(id > 0)
    {
        //father
        int status = 0;
        pid_t ret = 0;
        do
        {
            ret = waitpid(-1,&status,WNOHANG);//no-blocking
            if(ret == 0)
            {
                printf("child is running\n");
            }
            sleep(1);
        }
        while(ret == 0);

        if(WIFEXITED(status) && ret == id)
        {
            printf("wait child 3s success,child return code is:%d\n",WEXITSTATUS(status));
        }
        else
        {
            printf("wait child failed,return\n");
            return 1;
        }

    }
    else if(id == 0)
    {
        //child
        printf("child is run ,pid is:%d\n",getpid());
        sleep(3);
        exit(1);
    }
    else
    {
        printf("%s fork error\n",__FUNCTION__);
        return 1;
    }
    return 0;
}

进程程序替换

替换原理

用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程一般要调用一种exec函数以执行另一个程序。当进程调用exec函数族时,该进程的用户空间代码和数据完全被新的程序替换,从新程序的启动例程开始执行,调用exec并不创建新进程,所以调用exec前后该进程的pid并未改变。

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[]);
int execve(const char *path,char *const argv[],char *const envp[]);
函数解释
  • l(list):表示参数采用列表
  • v(vector):表示参数采用数组
  • p(path):带p的函数会自动搜索环境变量PATH
  • e(env):表示自己维护环境变量

需要特别注意的时,

  • 如果exec函数族调用成功,则从新程序的启动代码开始,所以没有返回值
  • 如果调用失败,则返回-1

代码示例:

#include<stdio.h>
#include<unistd.h>

int main()
{
    const *const argv[] = {"ls","-al",NULL};
    char *const envp[] = {"PATH=/bin:/usr/bin",NULL};//环境变量
execl("/bin/ls","ls","-al".NULL);

//带p的函数,不必再给出全路径
execlp("ls","ls","-al",NULL);

//带e的,需要自己配置环境变量
execle("ls","ls","-al".NULL,envp);

execv("/bin/ls",argv);

//带p的,不需要给出全路径
execvp("ls",argv);

//带e的,需要自己配置环境变量
execve("/bin/ls",argv,envp);

exit(0);
}

这里写图片描述

虽然exec函数族有六个函数,但是只有execve函数是系统调用,其他几个函数最终都会调用execve函数。

基于程序替换,可以实现一个简单的shell。


exit函数详解http://blog.csdn.net/qq_36528114/article/details/71321390
实现一个简单的shell:http://blog.csdn.net/qq_36528114/article/details/72582588

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值