【Linux】进程控制

在这里插入图片描述

欢迎来到Cefler的博客
博客主页:那个传说中的man的主页
个人专栏:题目解析
推荐文章:题目大解析(3)


目录

  • 创建进程
    • fork函数
    • 写时拷贝
  • 进程退出
    • 进程常见退出方法
    • exit函数
    • _exit函数
    • exit函数和_exit函数对比
    • 错误码和退出码
  • 进程等待
    • 什么是进程等待?
    • 为什么要进行进程等待?
    • 如何进行进程等待?
    • 阻塞状态和非阻塞状态
    • 验证wait
    • 获取进程status
      • 细节补充
    • 父进程如何获得子进程的退出信息

创建进程

fork函数

在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。

#include 
pid_t fork(void);
返回值:子进程中返回0,父进程返回子进程pid,出错返回-1

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

  • 分配新的内存块内核数据结构PCB)给子进程
  • 将父进程部分数据结构内容拷贝至子进程
  • 添加子进程到系统进程列表当中
  • fork返回,开始调度器调度

fork常规用法

  • 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
  • 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数

fork调用失败的原因

  • 系统中有太多的进程
  • 实际用户的进程数超过了限制

这里我们有个问题
当父进程形成子进程之后,子进程写入,重新申请空间,写时拷贝,但何时拷贝?


父进程创建子进程的时候首先将自己的读写权限改成只读,然后再创建子进程。
此时当子进程进行写入修改时,页表转换会因为权限问题出错,操作系统就可以介入了!

为了更好理解,我们在稍微了解一下写时拷贝

写时拷贝

写时拷贝(Copy-on-Write,简称COW)是一种优化技术,用于减少在复制数据时的开销。它通常应用于操作系统内核、虚拟内存和文件系统等领域。

在写时拷贝技术中,当一个进程试图对某个共享资源进行写操作时,操作系统并不会立即复制整个资源,而是先将该资源标记为只读状态,并为该进程创建一个新的副本。然后,该进程对该资源进行写操作时,操作系统会将这些写操作转向到新的副本上,而原来的资源仍然保持只读状态,直到有其他进程需要进行写操作时才会真正地复制。

写时拷贝技术的好处在于,它能够避免对共享资源的不必要复制,从而节省了系统资源和时间。特别是在多进程环境下,如果多个进程同时访问同一份资源,使用写时拷贝技术可以避免出现竞态条件和数据不一致的问题。

写时拷贝技术在虚拟内存和文件系统中也有广泛的应用。例如,在虚拟内存中,当一个进程需要修改其中某个页面时,操作系统会将该页面标记为只读状态,并为该进程创建一个新的页面副本;在文件系统中,当一个进程需要修改某个文件时,操作系统会将该文件标记为只读状态,并为该进程创建一个新的文件副本。这些副本都是在需要时才会真正地复制,从而避免了不必要的复制和开销。

总之,写时拷贝技术是一种非常实用的优化技术,它能够在多进程或多线程环境下有效地减少资源的复制和开销,提高系统的性能和效率。

进程退出

进程常见退出方法

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

  1. 从main返回
  2. 调用exit
  3. _exit

异常退出:

  • ctrl + c,信号终止

exit函数

exit() 函数是 C 标准库中的一个函数,用于终止当前正在运行的程序。它接受一个整数参数作为程序的退出状态码,并将控制权返回给操作系统。

exit() 函数的原型如下:

void exit(int status);

其中,status 参数表示程序的退出状态码。一般来说,退出状态码为 0 表示程序正常终止,非零值则表示程序异常终止,可以用来传递程序执行的结果或错误信息。

当调用 exit() 函数时,它会执行以下操作:

  1. 执行任何已注册的 atexit() 函数,这些函数在程序终止前会被调用。
  2. 关闭所有打开的文件流(使用 fclose() 函数)。
  3. 刷新标准 I/O 缓冲区(使用 fflush() 函数)。
  4. 将退出状态码传递给操作系统,并终止程序的执行。

在程序终止后,操作系统会接收到退出状态码,并根据该状态码来判断程序的执行情况。通常情况下,可以使用 echo $? 命令来查看上一个程序的退出状态码。

需要注意的是,exit() 函数不会直接返回到调用它的地方,而是将控制权交还给操作系统。因此,在 exit() 函数之后的代码将不会被执行。

另外,如果希望在程序终止前执行一些清理操作,可以使用 atexit() 函数注册一个函数,在程序退出时自动调用该函数。

int atexit(void (*function)(void));

atexit() 函数接受一个函数指针作为参数,该函数在程序终止前会被调用。可以多次调用 atexit() 函数来注册多个清理函数,它们将按照相反的顺序执行。

总结起来,exit() 函数是用于终止程序并返回退出状态码的函数,它在程序终止前执行一些清理操作,并将退出状态码传递给操作系统。

_exit函数

_exit() 函数是一个系统调用,用于终止当前进程的执行,并立即返回操作系统。它不同于标准库中的 exit() 函数,因为它不会进行任何清理操作,也不会刷新缓冲区或关闭文件流。相反,它会直接终止进程的执行,释放所有已分配的资源,并将退出状态码传递给操作系统。

_exit() 函数的原型如下:

void _exit(int status);

其中,status 参数表示进程的退出状态码。一般来说,退出状态码为 0 表示进程正常终止,非零值则表示进程异常终止,可以用来传递进程执行的结果或错误信息。

当调用 _exit() 函数时,它会执行以下操作:

  1. 直接终止进程的执行,不会执行任何清理操作。
  2. 释放所有已分配的资源,包括打开的文件、内存等。
  3. 将退出状态码传递给操作系统,并终止进程的执行。

需要注意的是,_exit() 函数不会刷新标准 I/O 缓冲区,也不会关闭打开的文件流。如果需要在进程终止前执行这些操作,可以使用标准库中的 exit() 函数。

另外,_exit() 函数并不是线程安全的,如果在多线程环境下使用,可能会导致不可预测的结果。在多线程环境下应该使用 pthread_exit() 函数来终止线程的执行。

总结起来,_exit() 函数是用于终止进程并返回退出状态码的系统调用,它直接终止进程的执行,并释放所有已分配的资源。与标准库中的 exit() 函数不同,它不会进行任何清理操作,也不会刷新缓冲区或关闭文件流。

exit函数和_exit函数对比

_exit函数和exit函数都可以用于终止进程,但它们的行为和效果是略微不同的。

_exit函数是一个系统调用,它会立即结束进程,而不会进行任何清理工作(例如刷新缓冲区或关闭文件流)。这意味着,如果进程仍有未完成的I/O操作,则可能会导致数据丢失或损坏,因为退出进程将不会等待这些操作完成。此外,_exit函数不返回退出码,因为它假定进程终止是由于错误发生而不是正常完成

相比之下,exit函数是一个库函数,它在调用前会对进程进行清理工作。例如,它会刷新所有输出流和关闭所有打开的文件描述符。然后,它会返回给操作系统一个退出码,以表示进程的状态。这个退出码可以被其他程序所使用,例如shell脚本可以根据进程的退出码确定后续行动。

那么_exit函数存在的意义是什么呢?_exit函数通常用于在进程出现致命错误时,立即终止进程。例如,如果进程遇到了无法恢复的错误,如内存耗尽或无法访问关键资源,则可以使用_exit函数,以确保进程立即停止,而不会留下任何未完成的I/O操作或其他问题。

总之,_exit函数和exit函数在使用场景上有所不同。_exit函数适用于在进程遇到无法恢复的错误时,立即终止进程;而exit函数适用于在完成某些任务后退出进程,并提供状态码以供其他程序使用

printf打印的东西在缓冲区中,如果想要立马刷新打印要么加上\n或者使用flush刷新
而exit般结束程序后,会刷新缓冲区,_exit结束进程不会刷新

错误码和退出码

错误码和退出码都是用来表示程序执行结果的代码,但它们的含义和用途有所不同。

错误码通常是由函数或系统调用返回的,用于指示程序执行过程中发生的错误类型。例如,在 Linux 系统中,许多系统调用会返回一个整数类型的错误码,其中 0 表示调用成功,其他值则表示不同类型的错误。错误码通常是负数或非零整数,具体的取值和含义可以参考相关的文档或头文件。

退出码则是在进程正常或异常终止时返回的一个整数值,用于传递程序执行的结果或错误信息。在 Linux 系统中,退出码通常是一个非负整数,其中 0 表示程序正常终止,其他值则表示程序异常终止,可以用来传递程序执行的结果或错误信息。退出码可以通过 shell 命令 echo $? 来查看。

需要注意的是,错误码和退出码的取值范围和含义可能因操作系统、编程语言或应用场景而异,因此在使用时需要参考相关的文档或规范。另外,错误码和退出码的取值应该尽量避免重复或冲突,以免造成混淆或错误判断。

进程等待

什么是进程等待?

进程等待是指一个进程暂停其执行,直到某个特定条件满足为止。在操作系统中,进程等待通常用于协调多个进程之间的合作和同步,并确保进程按照特定的顺序执行。

进程等待的常见场景包括:

  1. 父进程等待子进程:父进程可能需要等待子进程完成某个任务,才能继续执行后续操作。通过wait/waitpid的方式,让父进程对子进程进行资源回收的等待过程,直到子进程退出或终止。

  2. 进程等待资源:当多个进程需要共享某个资源时,可能需要使用进程等待来避免资源竞争和冲突。一个进程等待另一个进程释放资源,以便自己可以获得该资源并进行操作。

  3. 进程等待事件:在事件驱动的编程模型中,进程可能需要等待某个事件的发生,然后再继续执行。这可以通过使用特定的系统调用或库函数来实现,例如select、poll或epoll等。

进程等待的实现方式通常涉及操作系统提供的相关系统调用或函数。这些调用或函数允许进程设置等待条件,并在等待期间将其挂起,直到满足条件为止。一旦条件满足,被等待的进程将被唤醒,并继续执行。

进程等待是操作系统中一种重要的同步机制,它可以确保多个进程之间的正确协作和资源管理。通过适当地使用进程等待,可以避免竞争条件和数据不一致等问题,提高系统的可靠性和性能。

为什么要进行进程等待?

进行进程等待的主要目的是确保多个进程之间的正确协调和同步,以避免可能产生的危害和问题。下面是进程等待的几个重要方面,以及它们解决的相关危害:

  1. 避免资源竞争:当多个进程需要共享某个资源时,如果没有适当的同步机制,可能会导致资源竞争问题。例如,如果两个进程同时试图修改同一个文件,可能会导致文件内容的混乱或损坏。通过进程等待,可以确保在一个进程使用资源时,其他进程等待,从而避免竞争条件和数据不一致的问题。

  2. 防止死锁:死锁是指一组进程相互等待彼此持有的资源,导致系统无法继续执行的情况。如果没有适当的进程等待机制,可能会发生死锁问题,导致系统崩溃或无法正常运行。通过合理地使用进程等待,可以有效地避免死锁的发生,提高系统的可靠性和稳定性。

  3. 合作和同步:在某些情况下,多个进程可能需要按照特定的顺序执行,或者需要等待其他进程完成某个任务后才能继续执行。例如,父进程可能需要等待子进程完成某个计算任务,然后再进行下一步操作。通过进程等待,可以实现进程之间的合作和同步,确保它们按照特定的顺序执行,并在需要时相互等待。

  4. 提高系统性能:适当地使用进程等待机制可以提高系统的性能和效率。通过让进程在必要时等待,可以避免无谓的忙等待和资源浪费。相反,进程可以在等待期间释放CPU资源,使其他进程有机会执行,从而提高整个系统的吞吐量和响应性能。

总之,进程等待是为了解决多个进程之间可能产生的资源竞争、死锁、协作和性能问题。通过适当地使用进程等待机制,可以确保进程之间的正确协调和同步,提高系统的可靠性、稳定性和性能。

解决子进程僵尸问题带来的内存泄漏问题

如何进行进程等待?

在操作系统中,可以使用wait和waitpid两个系统调用来实现进程等待。

  1. wait系统调用:
    wait系统调用用于父进程等待子进程的终止,并获取子进程的退出状态。其语法如下:

    pid_t wait(int *status);
    
    • 参数status是一个指向整型变量的指针,用于存储子进程的退出状态
    • 返回值是子进程的进程ID(PID),如果出错则返回-1

    父进程在调用wait后会被阻塞,直到有子进程终止。当子进程终止时,父进程会解除阻塞并继续执行。父进程可以通过status指针获取子进程的退出状态,以进行后续处理。

  2. waitpid系统调用:
    waitpid系统调用也用于父进程等待子进程的终止,但相比wait,waitpid提供了更多的选项和灵活性。其语法如下:

    pid_t waitpid(pid_t pid, int *status, int options);
    
    • 参数pid表示要等待的子进程的PID:
      • 如果pid > 0,则等待具有该PID的子进程。
      • 如果pid == -1,则等待任何子进程(可以用于等待多进程)。
      • 如果pid == 0,则等待与调用进程属于同一个进程组的任何子进程。
      • 如果pid < -1,则等待进程组ID等于pid绝对值的任何子进程。
    • 参数status是一个指向整型变量的指针,用于存储子进程的退出状态。
    • 参数options用于指定一些额外的选项,如WNOHANG(非阻塞),WUNTRACED(追踪被停止的子进程)等。
    • 返回值同样是子进程的进程ID(PID),如果出错则返回-1。

    waitpid系统调用与wait类似,但它允许通过pid参数指定特定的子进程。此外,waitpid还可以选择非阻塞模式,即父进程不会被阻塞并立即返回。
    【Linux】进程控制_第1张图片

使用这两个系统调用,父进程可以合理地进行进程等待,并根据需要获取子进程的退出状态或进行其他处理。

使用wait和waitpid系统调用需要包含头文件。

阻塞状态和非阻塞状态

在Linux下,阻塞等待和非阻塞等待都是常用的进程/线程间通信方式。

  1. 阻塞等待(Blocking Wait)
    当一个进程或线程调用了阻塞式等待函数时,该进程或线程会被挂起,直到等待条件满足为止。阻塞等待是同步等待的一种形式,可以让流程顺序执行,但缺点是会让其他任务停滞,从而降低整个系统的并发性能。常见的阻塞等待函数包括read、write、accept、select、poll等。

    以read函数为例,在读取文件时,如果文件中没有数据可读,则read函数就会阻塞等待数据可用,直到有数据可读为止。示例如下:

    #include 
    #include 
    #include 
    #include 
    #include 
    
    int main() {
        int fd = open("example.txt", O_RDONLY);
        if (fd == -1) {
            perror("open");
            exit(EXIT_FAILURE);
        }
    
        char buffer[1024];
        ssize_t nbytes;
        
        // 阻塞等待文件描述符可读
        nbytes = read(fd, buffer, sizeof(buffer));
        if (nbytes == -1) {
            perror("read");
            exit(EXIT_FAILURE);
        }
    
        printf("Read %zd bytes: %s\n", nbytes, buffer);
    
        close(fd);
        return 0;
    }
    

    在上述示例中,read函数会阻塞等待文件描述符fd可读,并将读取到的数据存储在buffer中。

  2. 非阻塞等待(Non-Blocking Wait)
    当一个进程或线程调用了非阻塞式等待函数时,该函数立即返回,无论等待条件是否满足。非阻塞等待是异步等待的一种形式,它允许调用者在等待期间执行其他任务,并定期检查等待条件是否已满足。常见的非阻塞等待函数包括fcntl、select、poll等。

    以fcntl函数为例,在设置文件描述符为非阻塞模式时,可以使用fcntl函数实现。示例如下:

    #include 
    #include 
    #include 
    #include 
    #include 
    
    int main() {
        int fd = open("example.txt", O_RDONLY | O_NONBLOCK);
        if (fd == -1) {
            perror("open");
            exit(EXIT_FAILURE);
        }
    
        char buffer[1024];
        ssize_t nbytes;
    
        // 非阻塞等待文件描述符可读
        nbytes = read(fd, buffer, sizeof(buffer));
        if (nbytes == -1 && errno == EAGAIN) {
            printf("No data available yet. Continue with other tasks.\n");
        } else if (nbytes == -1) {
            perror("read");
            exit(EXIT_FAILURE);
        } else {
            printf("Read %zd bytes: %s\n", nbytes, buffer);
        }
    
        close(fd);
        return 0;
    }
    

    在上述示例中,open函数使用O_NONBLOCK标志打开文件,将文件描述符设置为非阻塞模式。当调用read函数读取文件时,如果文件中没有数据可读,则read函数会立即返回,并设置errno为EAGAIN。程序可以继续执行其他任务,等待条件满足后再次尝试读取数据。

需要注意的是,在实际开发中,要根据不同的情况选择使用阻塞等待或非阻塞等待。阻塞等待适合于同步、顺序执行的场景,需要等待某个条件满足后再继续执行而非阻塞等待适合于异步、并发执行的场景,允许调用者在等待期间执行其他任务,并定期检查等待条件是否已满足

验证wait

#include                                                                            
    2 #include<stdlib.h>                                           
    3 #include<sys/wait.h>                                         
    4 #include<sys/types.h> //pit_t                                      
    5 int fun()                                                    
    6 {                                                            
E>  7   printf("call fun funciton done!\n");                       
    8   return 11;                                                 
    9 }                                                            
   10 void Woker()                                                 
   11 {                                                            
   12   int cnt = 5;                                               
   13   while(cnt--)                                               
   14   {                                                          
E> 15     printf("I am child process,pid: %d ppid: %d,cnt: %d\n",getpid(),getppid(),cnt);
   16     sleep(1);                                                                      
   17   }                                                                                
   18 }                                                                                  
   19 int main()                                                                         
   20 {                                                                                  
   21   pid_t id = fork();                                                               
   22   if(id == 0)                                                                      
   23   {                                                                                
   24     //child process                                                                
   25     Woker();                                                                       
   26     exit(0);                                                                       
   27   }                                                                                
   28   else{                                                                             
   29     //father process                                                               
   30     sleep(10);                                                                     
   31     pid_t rid = wait(NULL);                                                        
   32     if(rid==id)                                                                    
   33     {                                                                              
E> 34       printf("wait success,pid: %d\n",getpid());                                   
   35     }                                                                              
   36     sleep(10);                                                                     
   37   }                                                                                
   38   return 0;                                                                        
   39 }

命令行输入:

while :; do ps ajx|head -1&& ps ajx |grep myprocess | grep -v grep;sleep 1;echo "---------------------";done

观察现象
【Linux】进程控制_第2张图片

获取进程status

要获取进程的状态,可以使用waitpid系统调用或者wait系统调用。这两个系统调用都可以等待子进程的终止并获取其状态信息。

waitpid系统调用允许指定具体的进程ID来等待,而wait系统调用则等待任意子进程终止。这里以waitpid为例,下面是获取进程状态的一般步骤:

  1. 在父进程中,使用fork创建子进程。
  2. 在子进程中,执行相应的任务,并在任务完成后使用exit或_exit来退出,传递一个合适的状态码。
  3. 在父进程中,使用waitpid等待子进程的终止并获取其状态。

下面是一个示例代码,演示了如何使用waitpid来获取进程的状态:

#include 
#include 
#include 
#include 
#include 

int main() {
    pid_t child_pid;
    int status;

    child_pid = fork();

    if (child_pid == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    } else if (child_pid == 0) {
        // 子进程执行的任务
        printf("Child process: PID=%d\n", getpid());
        sleep(1);  // 模拟子进程执行一段时间
        exit(EXIT_SUCCESS);
    } else {
        // 父进程等待子进程终止并获取状态
        printf("Parent process: Child PID=%d\n", child_pid);
        waitpid(child_pid, &status, 0);

        if (WIFEXITED(status)) {
            int exit_status = WEXITSTATUS(status);
            printf("Child process exited with status %d\n", exit_status);
        } else if (WIFSIGNALED(status)) {
            int signal_num = WTERMSIG(status);
            printf("Child process terminated by signal %d\n", signal_num);
        }
    }

    return 0;
}

在上述示例代码中,父进程调用fork创建子进程,并在子进程中进行一些任务。子进程执行完任务后使用exit退出,并传递一个合适的状态码。父进程调用waitpid等待子进程终止,并通过status参数获取子进程的状态信息。最后,通过WIFEXITED和WEXITSTATUS宏来判断子进程是否正常退出,并获取退出状态。

需要注意的是,waitpid和wait系统调用会阻塞父进程,直到子进程终止。如果不希望阻塞父进程,可以使用非阻塞方式(例如,使用waitpid的WNOHANG选项或使用信号处理机制)来获取进程状态。

总结起来,通过使用waitpid或wait系统调用以及相应的宏定义,可以获取进程的状态信息。

细节补充

  • wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
  • 如果传递NULL,表示不关心子进程的退出状态信息。
  • 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
  • status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位):

在wait系统调用中,参数status是一个指向整型变量的指针,用于存储子进程的退出状态。status变量是一个位图(bitmap),其中不同的位表示不同的状态信息。下面是status变量中各个位的含义:

  1. 低7位(0-6位):子进程退出状态的低7位,对应于exit或_exit系统调用的参数。
  2. 第8位(7位):子进程是否被信号杀死的标志位。如果子进程因为收到信号而终止,则该位被设置为1。否则,该位被设置为0。
  3. 第9位(8位):子进程是否产生过core dump(核心转储)的标志位。如果子进程因为收到SIGQUIT、SIGILL、SIGTRAP、SIGABRT、SIGBUS或SIGFPE信号而终止,并生成了core dump文件,则该位被设置为1。否则,该位被设置为0。
  4. 第10位(9位):子进程是否由于被Stopped信号而暂停执行的标志位。如果子进程当前处于Stopped状态,则该位被设置为1。否则,该位被设置为0。
  5. 第11位(10位):子进程是否由于被Continued信号而恢复执行的标志位。如果子进程当前处于Continued状态,则该位被设置为1。否则,该位被设置为0。

可以使用waitpid和WIFEXITED宏以及WEXITSTATUS宏来检查status变量中的不同位,以获取子进程的状态信息。例如,以下是一个使用waitpid和WIFEXITED宏的示例:

pid_t pid;
int status;

pid = waitpid(child_pid, &status, 0);  // 等待子进程结束
if (pid == -1) {
    perror("waitpid");
    exit(EXIT_FAILURE);
}

if (WIFEXITED(status)) {  // 子进程正常退出
    int exit_status = WEXITSTATUS(status);
    printf("Child process exited with status %d\n", exit_status);
} else if (WIFSIGNALED(status)) {  // 子进程被信号终止
    int signal_num = WTERMSIG(status);
    printf("Child process terminated by signal %d\n", signal_num);
} else if (WIFSTOPPED(status)) {  // 子进程被暂停
    int signal_num = WSTOPSIG(status);
    printf("Child process stopped by signal %d\n", signal_num);
} else if (WIFCONTINUED(status)) {  // 子进程被恢复执行
    printf("Child process resumed\n");
}

上述代码中,首先使用waitpid等待子进程结束,并将子进程的状态信息存储在status变量中。然后,使用WIFEXITED、WIFSIGNALED、WIFSTOPPED和WIFCONTINUED宏分别检查子进程的不同状态,并采取不同的处理方式。

总之,wait系统调用中指针整型变量status是一个位图,其中不同的位表示不同的状态信息。可以使用waitpid和一些宏定义来检查这些状态信息,并获取子进程的退出状态。
【Linux】进程控制_第3张图片
如下是一个进程status返回值:可以用位图解释
【Linux】进程控制_第4张图片
【Linux】进程控制_第5张图片

父进程如何获得子进程的退出信息

【Linux】进程控制_第6张图片


如上便是本期的所有内容了,如果喜欢并觉得有帮助的话,希望可以博个点赞+收藏+关注❤️ ,学海无涯苦作舟,愿与君一起共勉成长
【Linux】进程控制_第7张图片
在这里插入图片描述

你可能感兴趣的:(Linux,linux,进程控制)