【Linux】进程控制

目录

1.进程创建

1.1. fork创建子进程

1.2. 写时拷贝

2.进程退出

2.1. 进程退出方式

2.1 退出码

2.3 exit and _exit

3. 进程等待

3.1. 概念

3.2. wait and waitpid

3.2.1. wait

3.2.2. waitpid

4.进程程序替换

4.1. 替换原理

4.2. 如何替换

4.2.1. execl

4.2.2. execv

4.2.3. execlp、execvp

4.2.4. execle、execve

5. 命令行解释器


1.进程创建

1.1. fork创建子进程

fork创建子进程前面已经见过,现在来详细学习它的使用。

#include <unistd.h>

pid_t fork(void);

返回值:自进程中返回0,父进程返回子进程id,出错返回-1

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

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

  • 将父进程部分数据结构内容拷贝至子进程

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

  • fork返回,开始调度器调度

所以,现在我们已经知道,进程不仅仅是将代码和数据加载到内存上,还需要操作系统维护它的PCB、mm_struct、页表等等。

子进程和父进程的所有代码都是共享的,只是fork之前的代码子进程不会执行。

1.2. 写时拷贝

子进程和父进程的数据采用写时拷贝,子进程不改变数据时,与父进程使用同一块空间,需要改变时操作系统会为子进程单独开辟一块空间。

如图:

2.进程退出

程序退出的三种情况:

代码运行完毕,结果正确;

代码运行完毕,结果错误;

代码异常终止。

2.1. 进程退出方式

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

  1. 从main返回

  2. 调用exit

  3. _exit

exit和return 本身会要求进行缓冲区刷新!

例如:使用main函数返回

#include<stdio.h>

int main()
{
    printf("hello world!\n");
    return 66;
}

echo $?:输出最近一次进程退出时的退出码。

2.1 退出码

任何进程退出时,都会留下退出码,保存在PCB里面,操作系统根据退出码可以知道进程是否正常运行。

linux下有134个退出码,通常0表示正常退出,其他数字表示不同的错误。

所以这就是为什么main函数中的返回值是0的原因。

查看linux系统下的错误码:

#include<stdio.h>
#include<string.h>

int main()
{
    int i = 0;
    for(i = 0; i < 134; ++i)
    {
      printf("%d: %s\n", i, strerror(i));
    }
    return 0;
}

 

程序崩溃时的退出码是没有意义的,因为进程没有执行结束就退出了。

2.3 exit and _exit

通过exit也可结束进程,这点其实在C/C++向对空间申请内存失败时使用过。

且exit函数在任何地方使用,都代表终止该进程,参数是退出码。

#include<stdio.h>
#include<string.h>
#include<stdlib.h>

int main()
{
    int i = 0;
    for(i = 0; i < 10; ++i)
    {
        printf("%d\n",i);
        if(i==5)
        {
            exit(1); // 退出码设为1
        }
    }
    return 0;
}

 

_exit:强制终止进程,不要进行进程的后续收尾工作,比如刷新缓冲区。

例如:

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

int main()
{
    printf("hello world!\n");
    sleep(4);
    _exit(12);
}

 

3. 进程等待

3.1. 概念

进程等待是什么?

例如创建父子进程,父进程需要子进程去完成任务,子进程退出时父进程需要知道子进程完成的怎么样。

所以父进程fork之后,需要通过wait/ waitpid等待子进程退出。

为什么要让父进程等待?

  1. 通过获取子进程退出的信息,能够知道子进程的执行结果。

  2. 可以确保子进程先退出,父进程后退出。

  3. 进程退出时会先进入僵尸状态,会造成内存泄漏问题,需要通过父进程wait,释放子进程占用的资源。

3.2. wait and waitpid

3.2.1. wait

wait:

#include<sys/types.h>

#include<sys/wait.h>

pid_t wait(int*status);

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

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

例如:

#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)
    {//child
        int cnt = 5;
        while(cnt--)
        {
            printf("child is running pid:%d , cnt is %d\n", getpid(), cnt);
            sleep(1);
        }
        exit(0);
    }
    sleep(10) //让子进程进入僵尸状态
    pid_t ret = wait(NULL);
    if(ret > 0)
    {
        printf("father wait :%d\n",ret);
    }
    else
    {
        printf("father wait fail\n");
    }
    return 0;
}

 子进程进入僵尸状态:

父进程成功等待子进程,最后父进程退出:  

3.2.2. waitpid

waitpid:

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:

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

 

例如:

#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)
    {//child
        int cnt = 5;
        while(cnt--)
        {
            printf("child is running pid:%d , cnt is %d\n", getpid(), cnt);
            sleep(1);
        }
        exit(0);
    }
    sleep(10);
    // pid_ret = waitpid(-1,NULL,0) 等待任意子进程
    pid_t ret = waitpid(id,NULL,0); // 等待指定子进程
    if(ret > 0)
    {
        printf("father wait :%d\n",ret);
    }
    else
    {
        printf("father wait fail\n");
    }
    sleep(5);
    return 0;
}

参数2:status:输出型参数,获取子进程退出信息

wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充 。

status不能简单的当作整形来看待,可以当作位图来看待 ,只研究status低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)
    {//child
        int cnt = 5;
        while(cnt--)
        {
            printf("child is running pid:%d , cnt is %d\n", getpid(), cnt);
            sleep(1);
        }
        exit(11); // 将子进程退出码设为11,看看status能否获取到
    }
    //sleep(10);
    int status = 0;
    pid_t ret = waitpid(id,&status,0);
    if(ret > 0)
    {
        printf("father wait :%d, status exit code: %d, status exit signal: %d\n",ret,(status>>8&0xFF),status&0x7F);
    }                                                                               // 获取status次低八位
    else                                                                                         // 获取status低八位
    {
        printf("father wait fail\n");
    }
    sleep(5);
    return 0;
}

bash 是命令行启动的所有进程的父进程!bash 一定是通过wait方式得到子进程的退出结果,所以我们能够通过 echo $? 查看到子进程的退出码!

但是系统为了避免这种获取退出码时使用麻烦的位操作,为我们提供了宏 WEXITSTATUS(status)

if(ret > 0)
{
	if(WEXITSTATUS(status)>0)
	{
		printf("%d\n", WEXITSTATUS(status)) // 正常退出获取退出码
	}
	else
	{
		printf("error") // 异常退出
	}
}

参数3:WNOHANG:设置父进程等待方式为非阻塞等待。设置为0表示为阻塞等待。

阻塞等待:父进程一直等待子进程PID,不被调度执行(PCB被放入等待队列,将进程状态改为S),直到子进程运行结束才运行。

非阻塞等待: 在父进程等待子进程中,子进程没有结束返回PID时,父进程收到0,来代表子进程没有退出,继续执行父进程代码。

例如:

#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)
    {//child
        int cnt = 5;
        while(cnt--)
        {
            printf("child is running pid:%d , cnt is %d\n", getpid(), cnt);
            sleep(1);
        }
        exit(11);
    }
    int status = 0;
    while(1) // 轮询等待子进程
    {
         pid_t ret = waitpid(id, &status, WNOHANG);
         if(ret==0)
         {
              // 子进程没有退出,但是waitpid等待成功,需要父进程重复等待
             printf("Do father things!\n"); // 子进程没有退出,父进程执行自己的代码
             sleep(1);
         }
         else if(ret>0)
         {
             // 子进程退出,等待成功
             printf("father wait :%d, status exit code: %d, status exit signal: %d\n",ret,WEXITSTATUS(status),status&0x7F);
             break;
         }
         else
         {
             // 等待失败
             perror("wwaitpid");
             break;
         }
    }
    return 0;
}

4.进程程序替换

当前我们创建子进程的目的:通过 if else 语句让子进程执行父进程的一部分代码。

那么能不能让子进程执行一个全新的程序呢?

4.1. 替换原理

进程不变,仅替换当前进程的代码和数据。

程序本质就是一个文件,文件 = 程序的代码 + 程序数据。所以进程替换就是将指定 文件加载到进程的数据段和代码段,不会创建新的进程。

问题:既然父子进程代码是共享的,那么为什么子进程的代码改变不会影响父进程?

进程具有独立性,进程替换会更改代码区的代码,也要发生写时拷贝。

那么是如何加载到内存中的指定进程中呢?是通过加载器,而加载器的底层是通过exec系列的系统接口实现的。

4.2. 如何替换

进程替换时需要使用到系统调用接口:

命名理解

  • l(list) : 表示参数采用列表

  • v(vector) : 参数用数组

  • p(path) : 有p自动搜索环境变量PATH

  • e(env) : 表示自己维护环境变量

4.2.1. execl

例如:

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


int main()
{
    if(fork()==0)
    {
        //child
        execl("/usr/bin/ls", "ls","-a","-l","-n",NULL); // 指定子进程执行ls命令
        printf("hello world!\n");
        printf("hello world!\n");
        printf("hello world!\n");
        printf("hello world!\n");
        exit(1);
    }
    //father
    printf("father wait begin~~~\n");
    waitpid(-1, NULL, 0); // 父进程等待子进程
    printf("wait success!\n");
    return 0;
}

运行结果:从运行结果中可以看到,子进程确实执行了ls命令,但是为什么后面的打印hello world 没有打印出来?

这是因为,只要进程的程序替换成功,就不会执行后续代码(替换所有代码)。意味着exec*函数执行成功的时候,不需要返回值!

如果该类函数返回了,就一定是因为调用失败了!!

例如:执行其他可执行程序

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


int main()
{
    if(fork()==0)
    {
        //child
        execl("./test","test",NULL);
        exit(1);
    }
    printf("father wait begin~~~\n");
    waitpid(-1, NULL, 0);
    printf("wait success!\n");
    return 0;
}

//test.c:
int main()
{
    int i = 0;
    for(i = 0; i < 10; ++i)
    {
        printf("%d ",i);
    }
    return 0;
}

4.2.2. execv

用法与execl非常类似,只不过是将可变参数列表中的参数放进了数组,然后第二个参数改成传数组即可。

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


int main()
{
    if(fork()==0)
    {
        //child
        char*argv[] = {"ls", "-l", "-a", "-n", NULL}; // 指针数组
        execv("/usr/bin/ls", argv);
        exit(1);
    }
    printf("father wait begin~~~\n");
    waitpid(-1, NULL, 0);
    printf("wait success!\n");
    return 0;
}

4.2.3. execlp、execvp

exec系列函数后面带p的函数的意思是,如果替换的程序在环境变量中能够被找到,则调用函数时第一个参数直接写该程序名即可,不用带路径,第二个参数还是原来的参数。

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

int main()
{
    if(fork()==0)
    {
        //child
        execlp("ls", "ls","-a","-l","-n"); // ls命令可在环境变量PATH中被找到
        //char*argv[] = {"ls", "-l", "-a", "-n", NULL};
	    //execvp("ls", argv);
        exit(1);
    }
    printf("father wait begin~~~\n");
    waitpid(-1, NULL, 0);
    printf("wait success!\n");
    return 0;
}

4.2.4. execle、execve

 

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


int main()
{
    if(fork()==0)
    {
        //child  
        char*env[] = {"hello world", "hello world", "hello world", NULL};
        execle("./test", "test", NULL, env);
        //char*argv = {"test", NULL};
        //execve("./test",argv,env); 
        exit(1);
    }
    printf("father wait begin~~~\n");
    waitpid(-1, NULL, 0);
    printf("wait success!\n");
    return 0;
}

//test.c

int main()
{
    extern char**environ;
    int i = 0;
    for(i=0; environ[i]; ++i)
    {
        printf("%s\n",environ[i]);
    }
	return 0;
}

5. 命令行解释器

为什么在命令行上输入命令就能执行,为什么不同的选项要有空格?

写一个简单的命令行解释器就明白其中的原理了。

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

#define NUM 128
#define CMD_NUM 64
int main()
{
    char command[NUM];
    while(1)
    {
        char*argv[CMD_NUM] = {NULL};
        // 1.打印提示符
        command[0] = 0;
        printf("[wt@myhostname mini_shell]# ");

        // 2.获取命令字符串
        fgets(command,NUM,stdin);
        command[strlen(command)-1] = '\0';

        // 3.解析命令字符串
        const char* sep = " ";
        argv[0] = strtok(command, sep);
        int i = 1;
        while(argv[i] = strtok(NULL, sep))
        {
            ++i;
        }
        // 4. 检测命令是否是需要shell本身执行的内建命令
        if(strcmp(argv[0], "cd")==0)
        {
          if(argv[1] != NULL)
            chdir(argv[1]); //chdir: g
          continue;
        }
        
        // 5.执行第三方命令
        if(fork() == 0)
        {
            //child
            execvp(argv[0],argv);
            exit(1);
        }

        waitpid(-1, NULL, 0);
        
        fflush(stdout);
    }
    return 0;
}

这样一个简易的命令行解释器就完成了。

  • 34
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 29
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

风继续吹TT

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值