进程控制
基础概念
1.进程
进程是一个具有一定独立功能的程序的一次运行活动
进程是一个程序正在执行的实例。每个这样的实例都有自己的地址空间和执行状态
2.进程的地址空间
分配给每个进程的内存(memory)由一系列段(segments)组成: text segment(代码段)包含机器语言指令。通常是只读的、共享的。
initialized data segment(初始化数据段) 包含初始化的全局变量和静态(static)变量。
uninitialized data segment (bss segment)(未初始化数据段) 包含未初始化的全局变量和静态变量。程序运行之前,系统会把这些变量初始化为0。
heap(堆) 程序运行时,为变量动态分配内存。
stack(堆栈)包含堆栈帧(stack frames)。一个堆栈帧被分配给当前的被调函数(called function)。一帧保存的内容有:局部变量、参数(arguments)、返回值。
3.进程的执行状态
执行状态 进程正在占用CPU。
就绪状态 进程已具备一切条件,等待分配CPU。
等待状态 进程不能使用CPU,若等待的事件发生则可将其唤醒。
4.进程ID
每个进程都有一个ID(ID是一个正整数),唯一标识了系统中的这个进程。
每个进程都有一个创建它的进程,叫父进程(Parent Process)
进程ID(PID):标识进程的唯一数字
父进程ID(PPID)
启动进程的用户ID(UID)
5.进程的生命周期
创建 每个进程都由其父进程创建。父进程可以创建子进程,子进程又可以创建子进程的子进程。
运行 多个进程可以同时存在,进程之间可以进行通信。
终止 结束一个进程的运行。
6.进程特点
动态性、并发性、独立性、异步性
7.进程互斥
进程互斥是指当有若干进程都要使用某一共享资源时,任何时候最多允许一个进程使用,其他要使用该资源的进程必须等待,直到占用该资源者释放了该资源为止。
8.临届资源 操作系统中将一次只允许一个进程访问的资源称为临界资源。
9.临届区
进程中访问临界资源的那段程序代码称为临界区。为实现对临界资源的互斥访问,应保证诸进程互斥的进入各自的临界区。
10.进程同步
一组并发进程按一定的顺序执行的过程称为进程间的同步。具有同步关系的一组并发进程称为合作进程,合作进程间互相发送的信号称为消息或事件。
11.进程调度
按一定算法,从一组待运行的进程中选出一个来占有CPU运行。
调度方式:抢占式非抢占式
12.进程死锁
多个进程因竞争资源而形成一种僵局,若无外力作用,这些进程都将永远不能再向前推进。
进程控制编程
1.获取ID
#include <sys/types.h>
#include <unistd.h>
获取本进程ID
pid_t getpid(void)
获取父进程ID
pid_t getppid(void)
例子:#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{ printf (“PID = %d\n”, getpid());
printf (“PPID = %d\n”, getppid());
return 0;}
2.进程创建fork
#include <unistd.h>
pid_t fork(void)
功能:创建子进程
fork的奇妙之处在于它被调用一次,却返回两次,它可能有三种不同的返回值:
1)在父进程中,fork返回新创建的子进程的PID;
2)在子进程中,fork返回0;
3)如果出现错误,fork返回一个负值
3.关于fork的几点说明
当fork()顺利完成任务时,就会存在两个进程,每个进程都从fork()返回处开始继续执行。
两个进程执行相同的代码(text)段,但是有各自的堆栈(stack)段、数据(data)段以及堆(heap)。
子进程的stack、data、heap segments是从父进程拷贝过来的。
fork()之后,哪一个进程先执行(scheduled to use the CPU)不确定。
fork()之后,不能确定哪个进程先执行,会有什么隐患吗?
产生原因: fork()之后,不能确定是父进程还是子进程获得CPU。
危害:这种bugs很难被发现。
措施: 如果需要确保特定的执行顺序,需要采用某种同步(synchronization)技术(semaphores,file locks…)。
例子
4.进程的终止
通常由8种方式使进程终止(terminate)
5种正常终止: 从main函数返回 调用exit 调用_exit或_Exit 最后一个线程从其启动例程(start routine)返回最后一个线程调用pthread_exit
3种异常终止: 调用abort 接到一个信号并终止最后一个线程对取消请求做出响应
不管进程如何终止,最后都会执行内核中的同一段代码:用于关闭所有打开的文件描述符,释放内存等。
5.exit()
下面两个函数都用于正常终止一个进程:_exit立即进入内核,exit先执行一些清理处理(调用各终止处理程序、关闭所有标准I/O流等),然后进入内核。
#include <stdlib.h>
void exit(int status);
#include <unistd.h>
void _exit(int status);
这两个函数都有一个整型参数,称之为终止状态(exit status)。
6.exec函数族
exec用被执行的程序替换调用它的程序
区别:
fork 创建一个新的进程,产生一个新的PID
exec 启动一个新程序,替换原有的进程,因此进程的PID不会改变。
7.Execl
#include <unistd.h>
int execl(const char * path, const char* arg1,…)
参数:
path : 被执行程序名(含完整路径)。
arg1 – argn: 被执行程序所需的命令行参数,含程序名。以空指针(NULL)结束。
8.Execlp#include <unistd.h>
int execlp(const char * path, const char* arg1,…)
参数:
path : 被执行程序名(不含路径,将从path环境变量中查找该程序)。
arg1 – argn: 被执行程序所需的命令行参数,含程序名。以空指针(NULL)结束。
9.execv#include <unistd.h>
int execv(const char * path, const char* argv[])
参数:
path : 被执行程序名(含完整路径)。
argv[]: 被执行程序所需的命令行参数数组。
10.System
#include <stdlib.h>
int system(const char* string)
功能:
调用fork产生子进程,由子进程来调用 /bin/sh -c string来执行参数string所代表的命令
11.进程等待
父进程创建子进程后,如何知道子进程什么时候终止?如何知道子进程怎么终止(正常or异常)?
措施 wait()或waitpid()
12.wait与waitpid
#include <sys/wait.h>
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);
返回值:若成功返回进程ID,若出错返回-1。
调用wait或waitpid的进程可能发生的情况有: 如果所有子进程都还在运行,则阻塞(Block)。 如果一个子进程已终止,正等待父进程获取其终止状态,则取得该子进程的终止状态立即返回。 如果它没有任何子进程,则立即出错返回。 在一个子进程终止前,wait使其调用者阻塞,而waitpid有一个选项,可使调用者不阻塞。
waitpid并不等待在其调用之后的第一个终止的子进程。它有若干个选项,可以控制它所等待的进程。
如果一个子进程已经终止,并且是一个僵死进程,wait立即返回并取得该子进程的状态,否则wait使其调用者阻塞直到一个子进程终止。如果调用者阻塞并且它有多个子进程,则在其一个子进程终止时,wait就立即返回。因为wait返回终止子进程的ID,所以总能了解到是哪一个子进程终止了。
注:僵死进程(zombie),一个已经终止、但是其父进程尚未对其进行善后处理(获得终止子进程的有关信息,释放它仍占用的资)的进程被称为僵死进程。
终止状态 :有4个互斥的宏可以用来获取进程终止的原因:
WIFEXITED(status)若子进程正常终止,该宏返回true。 此时,可以通过WEXITSTATUS(status)获取子进程的退出状态(exit status)。
WIFSIGNALED(status)若子进程由信号杀死,该宏返回true。此时,可以通过WTERMSIG(status)获取使子进程终止的信号值。
WIFSTOPPED(status)若子进程被信号暂停(stopped),该宏返回true。 此时,可以通过WSTOPSIG(status)获取使子进程暂停的信号值。
WIFCONTINUED(status) 若子进程通过SIGCONT恢复,该宏返回true。