进程是操作系统中最基本、最重要的概念,可以说进程是操作系统结构的基础。操作系统上任何程序的执行都离不开进程。
1.进程的概念
传统的程序本是一组指令的集合,是一个静态实体,无法描述程序在内存中的执行情况,也就不能如实反映程序并发执行过程的特征,为了深刻描述程序动态执行过程的性质,人们就引入了“进程”的概念。 进程在系统中表现为一种数据结构,实现对正在运行的程序过程的抽象,其中进程由三个部分组成:
- 进程控制块(PCB)–进程控制块是描述和控制进程运行的一个数据结构,主要包含进程标识符,进程控制和调度信息等,这些信息使程序成为一个能与其他进程并发运行的进程,操作系统是通过进程控制块来控制进程的运行。
- 代码段–是存放程序代码的数据,假如系统中有多个进程运行相同的一个程序,则可以使用同一个代码段。
进程的主要特征:
- 动态性–进程的实质是程序的一次执行过程,进程是动态产生,动态消亡。
- 并发性–表现为可以和其他进程一起并发执行。
- 独立性–进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的基本单位。
- 异步性–由于进程间的相互制约,使进程具有执行的间断性,即进程按各自独立的,不可预知的速度向前推进。
由于进程执行时的时间间断性,运行中的进程可能有三种基本状态:
- 就绪状态–指进程已分配到处理器以外的所有必要资源,具备了可以执行的所有条件。
- 执行状态--指进程占用处理器,进程的程序正在执行
- 阻塞状态–指进程由于等待某种事件的发生而处于暂停执行的状态。
Linux调度器将进程分为三种不同类型,每种进程都有自己的特点和属性:
- 交互进程--由一个Shell启动的进程,此类进程有大量的人机交互,既可以在前台进行,也可以在后台运行。
- 批处理进程–这种进程不需要人机交互,运行于后台,需要大量的系统资源,是一个进程序列。
- 监控进程–也称为守护进程,Linux启动时启动的进程,并在后台一直进行,等待请求处理任务。
2.进程的操作
在Linux系统中定义了一系列的进程操作函数,包括进程的创建、调度、终止等。
1. 进程的创建
一个进程创建了子进程,其中创建子进程的进程我们称为父进程,除了创建与被创建之外,父进程与子进程之间的关系也是管理与被管理的关系。
Linux系统启动时处于核心模式,此时只有一个进程:初始化进程(init进程),其进程号为1,Linux系统中的所有进程都是由init进程直接或间接创建的。
程序可以通过getpid()
获得自身所运行的进程ID,也可以通过getppid()
获取父进程的ID。创建进程时,通过函数fork()
。
2.进程的调度 sched.h
Linux系统内核中有一个进程控制表,进程控制表每一项都是一个task_struct
结构,即代表了一个进程。Linux内虽然进程都是动态分配的,但还是需要考虑最大进程数。在系统中,默认的最大进程数是512,即表示系统中可同时容纳的进程个数为512个。
Linux管理进程的最好方法是使用命令来管理,主要涉及的命令有ps at top kill pstree nice renice
等。
3. 进程的终止
子进程一旦被创建,就与其父进程一起被系统进行调度,子进程运行完毕后,并不立即释放所占有的所有进程控制表项,而是作为僵尸进程存在于系统中,直到父进程正常退出或调用wait()
函数,子进程才彻底终止。
pid-t wait(int *status);
pdi_t waitpid(pid_t pid, int *status, int options);
options:
WNOHANG--要求如果没有子进程退出就立即返回
WUNTRACED--对已经停止但为报告状态的子进程
该调用也从等待中返回和报告状态,如果status不为空,调用将使status指向该信息。
调用了wait()
函数的父进程会马上阻塞自己,由wait()
函数自动分析是否当前进程的某一个子进程已经退出,如果找到了这样一个僵尸进程,wait()函数就会收集这个子进程的信息,将它彻底终止并返回子进程的结束状态,如果没有找到这样一个子进程,wait()
函数就会一直阻塞在这里,直到找到一个变成僵尸进程的子进程出现为止,但如果父进程还未调用wait()
函数就终止了,此时子进程就将被init
进程接管,它将控制子进程退出后必须的清除工作。
3.进程间的通讯 systemV posix
在同一台计算机中的进程相互通信的方式主要有:管道(pipe)、信号(signal)、信号量(semaphore)、消息队列(message)、共享内存(shared memory),其中信号量、消息队列、共享内存被称为IPC机制。不同机器之间的进程通讯可以使用套接字技术。
1.管道
管道是IPC机制中的最老形式,是进程直接进行数据交换的通道,并且所有UNIX系统都提供了这种通信机制,管道分为普通管道和命名管道。它们都是通过内核缓冲区按照先进先出的方式进行数据传输,其普通管道的特点为:
- 半双工
- 只能在具有共同祖先的进程之间使用
- 单独构成一种独立的文件系统
- 没有名字
- 管道的缓冲区是有限的
- 管道的传送数据是无格式的
- 写入管道的数据读完之后就从管道中消失
管道的创建和读写
无名管道
#include <unistd.h>
创建管道:int pipe(int pipefd[2]);
pipe[0]-->读数据
pipe[1]-->写数据
命名管道
创建管道:int mkfifo(const char *pathname, mode_t mode);
一般文件的I/O操作函数都可以用于管道。
close(fd);
int read(fd, buffer, len);
int write(fd, buffer, len);
2.信号 signal()
信号是的一种重要的进程方式,类似于中断机制,由某些错误条件而产生的事件,如果不指定,系统会调用默认操作来处理信号,大部分信号的默认操作将会终止进程。进程可以指定处理函数,由该函数处理,也可以忽略某个信号,对该信号不做任何处理,就像未发生过一样。
signal函数的原型
void (*signal(int signum, void (* handler) (int))) (int)
对于信号的操作,一般步骤为:安装信号,实现信号处理函数,发送信号,信号的生命周期从发送信号开始,到相应的处理函数执行完毕后结束。