Linux—进程控制详解

一.进程概述

1.进程概念

Linux是一个多用户多任务操作系统,多用户是指多个用户可以同时操作计算机,多任务是指Linux可以同时执行多个任务。进程的话,简单的可以理解为运行中的程序,所以根据Linux的多任务特性,我们知道Linux可以同时启动多个进程。
进程是操作系统资源管理的最小单位,进程是一个动态的实体,是程序一次执行的过程(所以说进程可以简单理解为运行中的程序)。进程的出现是为了使多个程序并发执行,用以改善资源利用率,并且提高系统的吞吐量。

进程有如下一些特性:

  • 动态性:进程是程序的执行,是程序在处理机制上执行时的一个活动,所以进程具有动态性;
  • 并发性:多个进程可以在同一时间内运行在一个内存空间上,由此可知,进程具有并发行;
  • 独立性:虽然一个内存空间中可以有多个进程运行,但它们都在各自的虚拟内存空间中运行,相互间不干扰;
  • 异步性:各个进程都在按照自己的速度在运行,所以多个进程之间具有异步性。

2.进程与线程、程序的区别

进程与程序:区别在于进程是动态的,程序是静态的。可以认为进程是运行中的程序,而程序则是一些保存在硬盘上的可执行的代码
进程与线程:为了让计算机在同一时间能执行更多任务,在进程的内部又划分了许多线程。线程在进程内部,它是比进程更小的能够独立运行的基本单位。线程基本上不拥有独立的资源,同属一个进程下的所有线程共用全部资源,同一个进程内可以同时执行多个线程

3.进程标识

在Linux中每时每刻都会运行着大量的进程,如果我们要找一个特定的进程,或者查询一个特定进程的信息,怎么办?用进程的身份证就可以实现!进程的身份证就是ID,每一个进程都是通过唯一的ID来标识的,所以ID都是独一无二的,与进程一一对应。进程ID是一个非负数,每个进程除了ID还有一些其他的标识信息,都包含在unistd.h头文件中。可以通过如下的一些函数来获取进程ID:

函数声明 功能
pid_t getpid() 获取进程ID
pid_t getppid() 获取进程的父进程ID
pid_t getuid() 获取进程的实际用户ID
pid_t geteuid() 获取进程的有效用户ID
pid_t getgid() 获取进程的实际组ID
pid_t getegid() 获取进程的有效组ID

4.进程的结构

虽说进程可以理解为运行中的程序,但实际上Linux中的进程由三部分组成:代码段、数据段、堆栈段

  • 代码段存放程序的可执行代码
  • 数据端段存放程序的全局变量、常量、静态变量
  • 堆栈段中的堆用于存放动态分配的内存变量栈用于函数调用,存放着函数的参数、函数内部定义的局部变量

此处涉及到进程的内存映像,内存的映像是指内核在内存中如何存放可执行程序文件。在将程序转换为进程的过程中,操作系统将可执行程序由硬盘复制到内存中,从内存中的低地址到高地址依次如下:

  • 代码段:即二进制机器代码,代码段是只读的,可以被多个进程共享。比如一个父进程创建了一个子进程,则父子进程共享代码段,此外子进程还将获得父进程数据段、堆、栈的复制。
  • 数据段:储存已被初始化的变量,包括全局变量和已被初始化的静态变量。
  • 未初始化数据段:储存未被初始化的静态变量,也被称为bss段。
  • 堆:用于存放程序中动态分配的变量。
  • 栈:用于函数调用,保存函数的返回地址、函数的参数、函数内部定义的局部变量。
  • 高地址还储存了命令行参数和环境变量。

程序映像的布局图如下:
在这里插入图片描述

可执程序和内存映像的区别在于:

  • 可执行程序位于磁盘中而内存映像位于内存中
  • 可执行程序没有堆栈,因为程序被加载到内存中才会分配堆栈
  • 可执行程序虽然也有未初始化数据段但它并不储存在硬盘的可执行文件中;
  • 可执行文件是静态的、不变的,而内存映像是随着程序的执行在动态变化的。

5.进程的状态

Linux中的进程可以有以下几种状态:

  • 运行状态:进程正在运行或在运行队列中等待运行
  • 可中断等待状态:进程正在等待某个事件完成,等待过程中可以被信号或定时器唤醒
  • 不可中断等待状态:进程正在等待某个事件完成,在等待中不可以被信号或定时器唤醒,必须等待,直到等待的事件发生
  • 结束状态:进程收到信号后停止运作或者该进程正在被跟踪(在调试程序时,进程处于被跟踪状态)
  • 僵死状态:进程已经终止,但进程描述依然存在,直到父进程调用wait()函数后释放

使用ps命令可以查看当前进程的状态。

二.进程的基本操作

1.创建进程

创建进程有俩种方式:前台创建和后台创建,也可以分为:操作系统创建和父进程创建。
由操作系统创建的进程它们之间是平等的,不存在资源继承关系。在系统启动时,操作系统会创建一些进程,它们承担着管理和分配系统资源的任务,这些进程就是系统进程而通过已有的进程创建出来的进程叫做子进程,创建子进程的进程称为父进程(稍微有点绕),子进程和父进程存在隶属关系,子进程又可以创建进程,就这样子子孙孙无穷无尽…子进程可以继承父进程几乎所有的资源。
这里主要介绍后台创建的进程,即通过父进程创建的进程,一般可以通过三种渠道创建进程:

  • fork()函数创建
  • vfork()函数创建
  • exec()族函数创建

fork()创建

使用fork()需要引用"sys/types.h"和"unistd.h"头文件,函数的返回值类型为pid_t 为非负整数。若程序运行在父进程中,函数返回子进程的ID;若函数在子进程中,函数返回0;当函数返回值为负数时代表出错!

当要创建一个子进程时,一般是使用fork()函数来创建的(创建一个进程也常称为fork一个进程),fork的英文意思是分支,所以根据其英文意思我们就可以大概明白其函数的功能了——将当前进程分支出一个子进程,也可以说创建一个新进程。
创建一个子进程后,父进程会和子进程争夺CPU,谁先抢到CPU的使用权谁就先执行,另一个进程就会挂起等待。
下面说一下这个fork()函数与众不同的地方:调用一次,返回俩次!!!
以往的函数都是调用一次返回一次或者不返回,但为什么这个函数竟然可以返回俩次呢???原因就在于利用fork()函数在当前进程的基础上由创建了一个进程!进程是什么?是运行中的程序,也可以理解为一段代码的执行。所以,创建了子进程就相当于在当前进程中复制了一段代码在子进程中,复制在子进程中的代码就会再次执行一次,这样就造成了调用一次,返回俩次的错觉!!!
当前进程在代码中创建子进程之后,代码就相当于分为俩部分了:

  • 调用fork()之前的代码,只在当前进程中执行
  • 调用fork()之后的代码,在当前进程中会执行,但在子进程中也会同样执行一次。要注意的是调用fork()之前的代码所实现的操作和功能都会在子进程中保留,当前进程中的各种数据都会复制一份到子进程中,所以子进程和当前进程在执行后半段的代码的时候互不干扰
    下面用代码来说明:
    (因为在Linux环境下方便编译,所以就放了截图…凑活着看吧)
    在这里插入图片描述
    函数运行结果为:
    在这里插入图片描述我来解释一下代码,很明显,我把代码分为俩部分,分开比较好说:
    前半部分:前半部分主要定义了变量k和调用了fork()函数创建进程,k的作用是验证后半部分在当前进程和子进程中的执行互不干扰,而且子进程继承了父进程的资源。
    后半部分:后半部分主要是对fork()的返回值进行处理,通过条件语句来判断代码是在当前进程中执行的还是在子进程中执行的。如果返回0,代码在子进程中执行,使用getpid()获取子进程ID(当前进程为子进程),使用getppid()获取父进程ID;如果返回值大于0,代码在父进程中执行,而且返回的pid就是子进程的ID,所以再使用getpid()获取的就是父进程的ID。

vfork()创建

调用vfork()的本质还是调用fork()
在使用vfork()函数创建子进程时与fork()函数唯一不同的是:vfork()创建的子进程是和父进程共享内存空间的,就是在创建好子进程后,子进程上的内存空间和父进程共用,这样带来的影响就是,当变量在子进程中改变是,变量在父进程中的值也会随之改变!
使用vfork()后父子进程的执行顺序是固定的:先执行子进程后执行父进程。
这个代码和上面的差不多,改改就能用:
在这里插入图片描述

与上次的明显不同就是k的值,在子进程中k的值是2,在父进程中k的值是3.因为子进程先执行,子进程执行后k++变为2,而子进程和父进程共享变量,所以父进程中再次执行k++时,k的值就变成3了

exec()函数族

使用fork() 和vfork()函数创建子进程后,子进程执行的代码就是从父进程中复制过来的,这样同一段代码执行俩次也没啥意思,所以就有了exec()函数族,注意是函数族,exec()不是一个人在战斗!!!
言归正传,如果像让子进程执行另外一个程序,就要用到exec()函数族了。
但是,exec调用并没有生成新的进程,一个进程一旦调用exec,它本身就失去了价值,系统把原来的代码替换成新的程序的代码,废弃原有的数据段和堆栈段,并为新程序分配数据段与堆栈段,唯一保留的就是进程的ID,对系统而言:调用exec后的进程还是原来的进程,只不过其程序已经不是原来的程序了。可以理解为exec重新编写了程序。
Linux下exec函数族有6种不同的调用形式,它们的声明在头文件"unistd.h"中:

#include "unsitd.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, const char *argv[ ] );
int execve( const *char path,const char *argv[ ], char* const envp[ ] );
int execvp( const char *file, const char *argv[ ] );

这些函数都定义在系统函数库中,在使用前需要引用头文件"sys/types"和"unistd.h"并且必须在预定义是定义一个外部的全局变量,用来显示环境变量,比如:extern char **environ

环境变量:为了便于用户灵活地使用Shell,Linux引入了环境变量的概念,环境变量可以是用户的主目录、终端类型、当前目录等,它们定义了用户的工作环境,所以称之为环境变量。

environ是一个指向Linux系统全局变量的指针。定义了这个指针后就可以在当前工作的目录中执行系统程序,如同在shell中不输入路径就直接运行VIM、GCC等程序一样。

由于这些函数tmd名字都差不多,很容易让人弄错,所以这里归纳一下exec函数族的命名规律吧:(都是以exec为基础的)

-p
字符p是path的首字母,代表文件的绝对路径,当函数名中带有 p 时,函数的参数就可以不用写出文件的相对路径(就是很详细的路径),只需写出文件名即可,因为函数会自动搜索系统的path路径。

-l
字符 l 是 list 的首字母,表示需要将新程序的每个命令行参数都当作一个参数传给它,参数的个数不做规定,但在最后要加上 NULL 参数,表示参数输入结束。

-v
字符 v 是 vector 的首字母,表示该类函数支持使用参数数组,数组中的最后一个指针也要输入 NULL 参数,作为结束标志。

-e
字符 e 是 environment 的首字母,表示该函数可以接收一份新的环境变量。

首先介绍一下execve()函数的使用,因为其余5ge函数在执行的过程中都要最后调用一下execve()函数,execve()函数名中包含了v、e,下面是利用execve()函数,在execve.c程序中执行new.c的代码:

execve.c程序
在这里插入图片描述
new.c程序
在这里插入图片描述
结果:
在这里插入图片描述
所谓execve函数所实现的功能就是创建一个子进程,在子进程中执行另外一个文件,根据结果,在执行execve()后原代码中剩余部分就不执行了,那是因为调用了execve()函数,将进程中的代码段、数据段、和堆栈段都进行了修改,使得这个新创建的子进程只执行了这个程序的代码,此时父进程与子进程的代码不再有任何关系。执行了execve()
函数后,原来存在的代码都被释放了。

2.进程等待

进程等待就是为了同步父进程与子进程,通常调用wait()函数来使父进程等待子进程执行完毕。
wait()函数使父进程暂停执行,直到它的一个子进程结束为止。该函数的返回值是终止运行的子进程PID。
waitpid()函数可以指定等待特定的子进程。

3.结束进程

想要结束一个进程时,可以调用wait()_wait()函数来终止进程的运行。

exit()
该函数调用成功与失败都没有返回值,并且没有提示出错信息。

_exit()
该函数与exit()函数一样,调用成功与失败都没有返回值,并且没有提示出错信息。

需要注意的是 exit() 函数结束进程时会清空缓冲区,而 _exit() 函数不会清空缓冲区。所以对于 vfork() 函数创建的子进程,只能用 _exit() 函数来结束子进程,因为子进程和父进程的内存是共享的,如果使用 exit() 函数来结束子进程,会导致父进程内部数据丢失。

发布了62 篇原创文章 · 获赞 188 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_43743762/article/details/100857415