Linux 系统编程 —— 进程编程

什么是进程 -> 运行中的程序

进程是资源分配的基本单位
线程是CPU调度的基本单位

进程并行,线程并发

  • 你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行。
  • 你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。
  • 你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。

并发的关键是你有处理多个任务的能力,不一定要同时。并行的关键是你有同时处理多个任务的能力。所以我认为它们最关键的点就是:是否是『同时』。

程序是代码和数据的集合性的静态概念,而进程是程序执行一次执行的实体,是动态的概念

程序代码编译的过程: 预处理 -> 编译 -> 汇编 -> 链接

进程的特性:动态性,独立性,并发性,异步性,结构性

进程的三种基本状态:

  • 就绪态:当进程已分配到除CPU以外的所有必要资源后,只要再获得CPU,便可立即执行。
  • 运行态:进程已获得CPU,其程序正在执行。
  • 阻塞态:正在执行的进程,由于等待某个事件发生而无法执行时,便放弃处理机而处于阻塞状态。
    在这里插入图片描述

PCB (进程管理块):为了描述控制进程的运行,系统中存放进程的管理和控制信息的数据结构称为进程控制块(PCB Process Control Block),它是进程实体的一部分,是操作系统中最重要的记录性数据结构。它是进程管理和控制的最重要的数据结构,每一个进程均有一个PCB,在创建进程时,建立PCB,伴随进程运行的全过程,直到进程撤消而撤消。

PCB进程控制块是进程存在的唯一的标志

PID (进程标识符):操作系统里每打开一个程序都会创建一个进程ID,即PID(Process Identifier)。PID是各进程的代号,每个进程有唯一的PID编号。它是进程运行时系统分配的,并不代表专门的进程。在运行时PID是不会改变标识符的,但是进程终止后PID标识符就会被系统回收,就可能会被继续分配给新运行的程序。

Linux 系统中进程号由0开始,进程号为0及1的进程由内核创建。进程号为0的进程通常是调度进程,进程号为1的进程通常是init进程。除调度进程外,Linux 下面所有的进程都是由init进程直接或间接创建。

  • 0号进程(swapper进程):调度进程,内核启动后的第一个进程,涉及到对于系统和硬件CPU交互的核心进程。0号进程是1号和2号进程的父进程。
  • 1号进程(init进程):涉及系统的初始化,创建新的进程。1号进程是所有用户态进程的父进程。
  • 2号进程(kthreadd进程):内核线程管理,创建所有的内核态进程。2号进程是所有内核线程的父进程。

1. getpid,getppid,getpgid:获取当前进程号,父进程号,组进程号

#include <sys/types.h>
#include <unistd.h>

pid_t getpid(void);
pid_t getppid(void);

返回值为进程ID,进程ID一般为一非负整数值。

#include <unistd.h>

pid_t getpgid(pid_t pid);

用来取得参数pid 指定进程所属的组识别码。如果参数pid 为0, 则会取得目前进程的组识别码。
执行成功则返回组识别码,如果有错误则返回-1。

2. fork,vfork函数:创建一个新进程

#include <unistd.h>

pid_t fork(void);
#include <sys/types.h>
#include <unistd.h>

pid_t vfork(void);

返回值为-1时,创建子进程失败。返回0时,子进程开始执行。返回 >0时(fork返回新创建子进程的进程ID),父进程开始执行。

fork

  • 操作系统会复制一个与父进程完全相同的子进程。
  • 新进程和原有进程共享代码空间, 可执行程序是同一个程序。
  • Linux操作系统会为新进程产生一个ID和进程控制块。
  • 当子进程或者父进程不进行写操作时,父子进程共享同一分数据,当有写操作 时,数据会分离(称为写时复制Copy-On-Write),互不干涉。
  • 子进程/父进程对数据所做的任何修改,都不会影响另一方。
  • 父子进程都是从fork之后开始执行。到底是父进程还是子进程先开始执行,看操作系统的调度算法。

vfork

vfork和fork一样都是创建一个子进程,但子进程不会从父进程复制任何东西。子进程完全和父进程共享数据和堆栈,子进程对数据的修改不会触发写时复制,也就是说子进程所作的修改会出现在父进程中。

vfork与fork的区别

  • vfork产生的子进程必须以exit或者exec返回,否则会出现未定义错误。
  • vfork一定保证子进程先运行,在子进程调用exit或者exec之前父进程是不可能运行的。
  • vfork的使用场合是子进程不会用到父进程任何资源的情况下。vfork只产生一个进程控制块,然后再通过exec产生子进程所需要的资源和代码。

僵尸进程,孤儿进程,守护进程简述

  • 僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。
  • 孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。
  • 守护进程(精灵进程): 守护进程是脱离于终端并且在后台运行的进程,脱离终端是为了避免在执行的过程中的信息在终端上显示,并且进程也不会被任何终端所产生的终端信息所打断。守护进程一般的生命周期是系统启动到系统停止运行,当然,也可以通过杀死进程的方式来结束进程的生命周期。

3. wait,waitpid函数:查询子进程的结束状态

#include <sys/types.h>
#include <sys/wait.h>

pid_t wait(int *status);

参数status:用来保存被收集进程退出时的一些状态,它是一个指向int类型的指针。但如果我们对这个子进程是如何死掉的毫不在意,只想把这个僵尸进程消灭掉,(事实上绝大多数情况下,我们都会这样想),我们就可以设定这个参数为NULL,就像这样:pid = wait(NULL);

如果成功,wait会返回被收集的子进程的进程ID,如果调用进程没有子进程,调用就会失败,此时wait返回-1,同时errno被置为ECHILD。

进程一旦调用了wait,就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。

#include <sys/types.h>
#include <sys/wait.h>

pid_t waitpid(pid_t pid, int *status, int options);

第一个参数pid:从参数的名字pid和类型pid_t中就可以看出,这里需要的是一个进程ID。但当pid取不同的值时,在这里有不同的意义。

  • pid>0时,只等待进程ID等于pid的子进程,不管其它已经有多少子进程运行结束退出了,只要指定的子进程还没有结束,waitpid就会一直等下去。

  • pid=-1时,等待任何一个子进程退出,没有任何限制,此时waitpid和wait的作用一模一样。

  • pid=0时,等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid不会对它做任何理睬。

  • pid<-1时,等待一个指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值。

第三个参数options:options提供了一些额外的选项来控制waitpid,目前在Linux中只支持WNOHANG和WUNTRACED两个选项,这是两个常数,可以用"|"运算符把它们连接起来使用,比如:

ret=waitpid(-1,NULL,WNOHANG | WUNTRACED);

如果我们不想使用它们,也可以把options设为0,如:

ret=waitpid(-1,NULL,0);

如果使用了WNOHANG参数调用waitpid,即使没有子进程退出,它也会立即返回,不会像wait那样永远等下去。

而WUNTRACED参数,由于涉及到一些跟踪调试方面的知识,加之极少用到,这里就不多费笔墨了,有兴趣的读者可以自行查阅相关材料。

waitpid的返回值比wait稍微复杂一些,一共有3种情况:

  • 当正常返回的时候,waitpid返回收集到的子进程的进程ID;
  • 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
  • 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;

当pid所指示的子进程不存在,或此进程存在,但不是调用进程的子进程,waitpid就会出错返回,这时errno被设置为ECHILD;

4. 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 execvpe(const char *file, char *const argv[],char *const envp[]);

1. execl

第一个参数path:表示要执行的文件名,包括路径名
第二个参数:代表执行该文件时传递过去的argv(0)、argv[1]……
第三个参数:必须用空指针(NULL)作结束

返回值:如果执行成功则函数不会返回,执行失败则直接返回-1,失败原因存于errno中。

与 system 函数的区别

  • execl 会代替调用它的程序,执行完成后不会回到主调程序中,直接用新的execl create的shell替代了原来的程序。
  • system 会fork一个子程序中,但他会在主调程序中等待 system的返回,相当于fork+execl+waitpid 三个函数的合体。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值