Linux中的进程控制

本文详细介绍了Linux系统中的进程控制,包括进程标识、进程创建(fork、vfork)及其区别,进程终止的方式和退出码,以及进程等待函数wait和waitpid的使用。特别讨论了写时拷贝技术以及进程程序替换的概念,帮助读者深入理解Linux进程的生命周期和管理机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

进程标识

进程创建

fork()

fork用法:

  1. 父进程复制自己,使父子进程同时执行不同的代码段------在网络编程中最常见
  2. 一个进程要执行另一个不同的程序------shell的实现方式

一个现有的进程调用fork创建一个新进程,新进程为子进程。是父进程的一个副本,子进程拷贝父进程的数据段、堆和栈并独立使用,父子进程共享正文段(代码段)、内存映射。但是由于fork之后通常要调用exec,所以现在通常采用写时拷贝(Copy-On-Write,COW)技术

子进程继承父进程的属性:

用户、组、会话ID,终端,当前工作目录和根目录,文件模式屏蔽字,信号屏蔽、文件描述符、环境、共享内存、内存映射

子进程自己的属性:

fork返回值,进程ID,文件锁,未处理的闹钟被清除,未处理信号被置空

fork之后父子进程执行顺序不确定,取决于内核调度算法

Q:为什么父进程返回子进程ID,子进程返回0?

A:一个进程可以有多个子进程,没有一个函数可以使一个进程可以获得其所有子进程的ID,并且ID为0的进程是供内核交换进程使用,所以子进程ID不会为0,不会出现父子进程都返回0的情况;一个子进程只有一个父进程,所以子进程总是可以调用getppid获得其父进程的进程ID。

fork失败原因:

  1. 系统中已经拥有太多进程
  2. 该用户ID的进程总数超过了系统限制(由limits.h中POSIX.1的CHILD_MAX决定)
#include <stdio.h> 
#include <unistd.h> 
#include <sys/types.h> 
#include <stdlib.h> 
 
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; 
}

写时拷贝(Copy-On-Write,COW

子进程从父进程中拷贝数据是采用写实拷贝,写时拷贝就是在子进程对父进程数据只读不写时,就不对数据进行拷贝,直接读取父进程的数据,只有当子进程需要对数据进行写操作时才会将需要写的数据另拷贝一份。这样做可以大大节约内存空间。

vfork()

与fork()区别:

  1. 调用方式返回值与fork()相同,但vfork目的是为了让创建出的进程exec一个新程序,所以并不完全复制父进程的地址空间
  2. vfork保证子进程先运行,子进程调用exec或exit后父进程才可以运行(调用exec、exit之前等待父进程动作会导致死锁)
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>

int value = 100;
int main()
{
    pid_t pid;
    if((pid = vfork()) == -1){
      perror("fork");
      exit(1);
    }
    if(pid == 0){
        //child
        sleep(1);
        value = 200;
        printf("child value = %d\n", value);
        exit(0);
    }
    else{
        //parent
        printf("parent value = %d\n", value);
    }
    return 0;
}

子进程中修改value的值,父进程中value也被修改

进程终止(process termination)

进程终止场景

  1. 代码运行完毕,结果正确(客观上的正确,即使结果与预期的不同)
  2. 代码运行完毕,结果错误
  3. 代码异常终止

进程终止方法

使进程终止共有8种方式

5种正常终止方式:

  1. 从main函数返回
        return n调用exit(n)
  2. 调用exit
        调用各种终止处理程序(如用户通过atexit或on_exit定义的),关闭所有标准I/O流
  3. 调用_exit或_Exit
        无需运行终止处理程序,无需用信号终止程序,不冲洗标准I/O流
  4. 最后一个线程从其启动例程退出
        进程的最后一个线程
  5. 最后一个线程调用pthread_exit

3种异常终止方式:

  1. 调用abort
  2. 收到信号
  3. 最后一个线程对取消请求做出相应

在这里我主要为大家讲一下进程方面的知识,线程方面先不做涉及:

查看进程退出码

echo $?

_exit

_exit(-1)进程退出码为255

exit

return

执行return n相当于执行exit(n),调用main函数运行时会将main函数返回值当做exit的参数

上述两个函数的关系如下:

进程等待

wait()

status:一个整型指针,指向的地址用来存放进程的终止状态,如不关心可设为空指针NULL

waitpid()

pid:

    pid == -1:等待任何一个子进程,此时waitpid与wait等效

    pid > 0:等待进程ID与pid相同的子进程

    pid == 0:等待组ID等于调用进程组ID的任何一个子进程

    pid < -1:等待组ID等于pid绝对值的任意一个子进程 

status:

    WIFEXITED(status):正常终止子进程,则为真(查看进程是否正常退出)

    WEXITSTATUS(status):若WIFEXITED不为0,则提取子进程的退出码(查看子进程的退出码)

    WIFSIGNALED(status):异常终止进程,则为真(查看进程是否异常退出)

    WTERMSIG(status):若WIFSIGNALED不为0,获取终止子进程的信号编号

    详细的介绍如下:

    

options:进一步控制waitpid

    0:什么都不做

    WNOHANG:若等待的子进程结束返回子进程ID,若等待的子进程没有结束waitpid()返回0(非阻塞等待)

    详细的介绍如下:

    

wait()与waitpid()作用

  1. 如果所有子进程都在运行,阻塞等待
  2. 如果子进程已经终止,等待父进程获取其终止状态,则父进程获得子进程终止状态立即返回
  3. 如果没有任何子进程,出错返回

wait()与waitpid()区别

  1. waitpid()可等待一个特定的进程,wait()只会等待任意一个进程
  2. waitpid()可以非阻塞等待,wait()只能阻塞等待
  3. waitpid()可以使用WUNTRACED和WCONTINUED来支持作业控制

waitid()

与waitpid类似但更加灵活,使用两个独立的参数idtype、id来表示子进程的类型、而不是将其与进程ID或进程组ID合二为一成为一个参数

idtype:

    

options:下列标志的按位或运算,表示等待的条件(WCONTINUED、WEXITED、WSTOPPED三个常量必须指定其一)

    

infop:指向siginfo结构的指针、siginfo中存放的是造成子进程状态改变信号的详细信息

下面通过两段代码给大家展示一下进程的阻塞等待和非阻塞等待:

阻塞等待:

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

int main()
{
    pid_t pid;
    pid = fork();
    if(pid < 0){
        printf("fork error!\n");
        return 1;
    } else if(pid == 0){ //child
        printf("child running..., pid is :%d\n", getpid());
        sleep(5);
        exit(1);
    } else{ //parent
        int status = 0;
        pid_t ret = waitpid(-1, &status, 0);
        printf("waiting for child...\n");
        if (WIFEXITED(status) && ret == pid){
            printf("wait child 5s, return code: %d\n", WEXITSTATUS(status));
        } else{
            printf("wait child failed!, return code: %d\n", WEXITSTATUS(status));
            return 1;
        }
    }
    return 0;
}

父进程阻塞等待子进程5秒后退出

非阻塞等待:

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

int main()
{
    pid_t pid;
    pid = fork();
    if(pid < 0){
        printf("fork error!\n");
        return 1;
    } 
    else if(pid == 0){ //child
      printf("child will run 5s..., pid is: %d\n", getpid());
      sleep(5);
      exit(1);
    } 
    else{
      int status = 0;
      pid_t ret = 0;
      while(ret == 0){
          ret = waitpid(-1, &status, WNOHANG);
          if(ret == 0){
            printf("child is running, wait...\n");
            sleep(1);
          } 
        }
        if(WIFEXITED(status) && ret == pid){
          printf("wait child 5s, return code: %d\n", WEXITSTATUS(status));
        } 
        else{
          printf("waiting failed, return code: %d\n", WEXITSTATUS(status));
          return 1;
        }
    }
    return 0;
}

父进程非阻塞等待子进程,每一秒进行轮询,一直在运行非阻塞等待子进程的代码段

status

子进程status是一个bitmap实现的位图,占一个整型,32个比特位,下十六个比特位又分成低八位和次低八位。次低八位(255)保存进程退出码,低八位(255)保存进程终止信号,这样就实现了仅用一个int空间就保存了多种信息 ,如图:

还记得一开始我提到的_exit(-1)的退出码是255吗?现在明白了没有,因为退出码只能存在0~255,而-1的unsigned值就是255

举个栗子:

#include <stdio.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main()
{
  pid_t pid;
  if ( (pid=fork()) == -1 )
    perror("fork"),exit(1);
  if ( pid == 0 ){
    sleep(20);
    exit(10);
  }
  else {
    int st;
    int ret = wait(&st);
    if ( ret > 0 && ( st & 0X7F ) == 0 ){ // 正常退出
      printf("child exit code:%d\n", (st>>8)&0XFF);
    } 
     else if( ret > 0 ) { // 异常退出
      printf("sig code : %d\n", st&0X7F );
    }
  }
}

正常退出和其他终端kill异常退出

进程程序替换

用fork()创建出子进程后,子进程往往要调用exec函数来执行另一个程序,当子进程调用exec函数时,该进程的用户空间代码何书记都会被新程序替换,从新程序的启动例程开始执行,但调用exec并不创建新进程,所以调用exec前后进程的ID不会改变。

举个例子:就像原来大家上课看小说,偷偷地把小说从书包里面拿出来藏在课本底下看,表面上老师看着你拿着课本,在用工学习,但实际你已经偷偷摸摸干别的事情去了。你在表面上在学习(进程ID)这个现象没有改变,但是你已经你从书包(硬盘)里拿出小说(新程序)去看(执行)了,看(执行)完小说你再把小说(新程序)放回书包(硬盘)里去,看看课本(返回当前进程),最后再合上课本,让老师看见你学完了(结束进程)。

如图:

替换函数

Linux中共有7种以exec开头的替换函数,统称exec函数

7个exec

前四个函数取路径名作为参数,后两个函数取文件名作为参数,最后一个取文件描述符作为参数

其实看起来多但是他们很好记忆

l(list):参数采用列表

v(vector):参数用数组

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

e(env):是否使用当前环境变量

f(fd):参数采用文件描述符

在这7个函数中只有execve是内核的系统调用,其余6个为库函数,他们的关系如下:

调用exec后,进程ID没有改变,但是新程序从调用进程中继承了以下属性:

一个C程序的启动终止过程

知道了上面这些内容,我们不难了解一个C程序是如何启动和终止的:

内核使程序执行的唯一方法就是调用exec函数,exec函数调用main函数,main函数再调用用户函数,然后用户函数exit或_exit、_Exit退出后返回main函数,同理main函数返回C启动历程,然后启动历程再调用exit退出并归还资源

my_shell

根据上面的知识我们不难实现一个简单的shell

  1. 获取命令行
  2. 解析命令行
  3. 建立一个子进程(fork)
  4. 替换子进程(execvp)
  5. 父进程阻塞等待子进程退出(wait)
#include <stdio.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <string.h>

char *argv[8];
int argc = 0;

void get_buf(char *buf)
{
    int i;
    int status = 0;
    for(argc = i = 0; buf[i]; i++){
        if(!isspace(buf[i]) && status == 0){
            argv[argc++] = buf + i;
            status = 1;
        } else if(isspace(buf[i])){
            status = 0;
            buf[i] = 0;
        }
    }
    argv[argc] = NULL;
}

void do_comm()
{
    pid_t pid = fork();

    switch(pid){
        case -1:
            perror("fork error");
            exit(0);
            break;
        case 0:
            execvp(argv[0], argv);
            perror("execvp error");
            exit(0);
        default:
            {
                int sstatus;
                while(wait(&sstatus) != pid)
                    ;
            }
    }

}

int main()
{
    char buf[1024] = {};
    while(1){
        printf("myshell> ");
        scanf("%[^\n]%*c", buf);
        get_buf(buf);
        do_comm();
    }
    return 0;
}

结果是不是很完美呢?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值