分类: LINUX
2012-04-28 10:38:06
进程简介
进程和线程是调度的基本单位 :
进程和线程的管理是操作系统中的核心部分。
线程描述进程内的执行 , 负责执行包含在进程的地址空间中的代码
进程的三个重要特性
独立性:进程是系统中独立存在的实体,它可以拥有自己独立的资源,比如文件和设备描述符等。未经进程本身允许,其他进程不能访问到这些资源。
动态性:程序只是一个静态的指令集合,而进程是一 个正在系统中活动的指令集合。在进程中加入了时间的概念。进程具有自己的
生命周期和各种不同的状态。
并发性:并发性由独立性和动态性衍生而来。若干个进程可以在单处理机状态上并发执行。
并发与并行:
并行 指在同一时刻内,有多条指令在多个处理机上同时执行。
并发 指在同一时刻内只能有一条指令执行,但多个进程的指令被快速轮
换执行,使得在宏观上具有多个进程同时执行的效果。
进程组成
作为申请系统资源的基本单位,进程必须有一个对应的物理内存空间,要对其进行高效的管理,首先要用数据结构对空间进行描述。
进程编号:进程以进程号 PID(process ID) 作为标识。任何对进程的操作都要有相应的 PID 号。每个进程都属于一个用户,进程要配备其所属的用户编号 UID 。 每个进程都属于一个用户组,所以进程还要配备其归属的用户组编号 GID 。
GNU/Linux 进程有两种基本形式,分别是内核线程 ( 由 kernel_thread创建 ) 和用户进程 ( 由 fork, clone 创建 ) 。
进程上下文: 运行进程的环境称为进程上下文 (context)
进程的上下文组成: 进程控制块 PCB 包括进程的编号、状态、优先级以及正文段和数据
段中数据分布的大概情况。
正文段 (text segment) 存放该进程的可执行代码。
数据段 (data segment) 存放进程静态产生的数据结构
用户堆栈 (stack)
进程函数
#include
#include
pid_t fork( void) ;
功能:创建一个新的进程。
返回:子进程中为 0 ,父进程中为子进程 ID ,出错为 -1
说明:
由 fork 创建的 新进程被称为 子进程( child process )。 该函数被调用一次,但返回两次。两次返回的区别是子进程的返回值是 0 ,而父进程的返回值则是子进程的进程 ID 。 一般来说,在 fork 之后是父进程先执行还是子进程先执行是不确定的。这取决于内核所使用的调度算法 。
fork 后父子进程的异同
不同点:
fork 的返回值;
进程 ID 、不同的父进程 ID ;
父进程设置的锁,子进程不继承;
子进程的未决告警被清除;
子进程的未决信号集设置为空集。
相同点:
使用 fork 函数得到的子进程从父进程处继承了整个进程的地址空间,包括:进程上下文 ( 一般不包含执行代码 ) 、进程 堆栈 、内存 信息、打开的文件描述符、 信号控制设 置、进程优先级、进程组号、当前工作目录 、根目录、资源 限制、控制 终端等。
#include
#include
pid_t vfork(v oid) ;
vfork 系统调用不同于 fork ,用 vfork 创建的子进程共享地址空间,也就是说子进程完全运行在父进程的地址空间上,子进程对虚拟地址空间任何数据的修改同样为父进程所见。但是用 vfork 创建子进程后,父进程会被阻塞直到子进程调用 exec 或 exit 。这样的好处是在子进程被创建后仅仅是为了调用 exec 执行另一个程序时,因为它就不会对父进程的地址空间有任何引用,所以对地址空间的复制是多余的,通过 v fork 可以减少不必要的开销。
#include
int clone(int ( *fn)( void *), v oid *child_stack, int flag, v oid *arg) ;
clone 可以让你有选择性的继承父进程的资源,你可以选择象vfork 一样和父进程共享一个虚存空间,你也可以不和父进程共享,你甚至可以选择创造出来的进程和父进程不再是父子关系,而是兄弟关系。
返回:成功返回子进程 ID ,失败返回 -1 。
参数fn 是函数指针,指向创建进程要执行的程序 child_stack 为要创建的进程分配系统 堆栈 空间(在 linux 下系统堆栈空间是2 页面,8KB 的内存,其中在这块内存中,低地址上放入了进程控制块 task_struct 的值)
flags 标志用来描述需要从父进程继承那些资源
arg 是传给子进程的参数
父子进程简单的同步
如果父进程没有在等待子进程退出,而子进程又退出了,这个子进程就会成为僵尸 (zom bie) 进程 ( 既不是活的,也不是死的 ) ,僵尸进程浪费了系统资源。如果父进程先于子进程退出,已经在运行的子进程会被视为继承自 init 进程。避免僵尸进程的方法是父进程调用 wait 函数等待子进程结束; 或是让父进程忽略子进程产生的退出信号。
#include
#include
pid_t wait(int * status) ;
pid_t waitpid(pid_t pid, int * status, int options) ;
功能:等待进程。若成功则返回子进程 ID 号,若出错则返回 -1 。
参数 status :用于存放进程结束状态。
pid :要等 待的进程 ID 。
pid == -1 等待任一子进程。于是在这一 功能方面 waitpid 与 w ait 等效。
pid > 0 等待其进程 ID 与 PID 相等的子进程。
pid == 0 等待其组 ID 等于调用进程的组 ID 的任一 子进程。
pid < -1 等待其组 ID 等于 PID 的绝对值的任一 子进程。
options :设 置等待方式。
0 :不设置。
WN OHA NG :如果没有任何已经结束的进程则马上返回,不等待。
WUNTRA CED :如果子进程进入暂停状态则马上返回。
调用 wait 或 waitpid 的进程可能会:
阻塞 ( 如果其所有子进程都还在运行 ) 。
带子进程的终止状态立即返回 ( 如果一个子进程已终止,正等待父进程存取其终止状态 ) 。
出错立即返回 ( 如果它没有任何子进程 ) 。
exec 函数簇
在用 v fork 函数创建子进程后,子进程往往要调用一个 exec 函数以执行另一个程序。
当进程调用一种 exec 函数时,该进程完全由新程序代换,而新 程序则从其 main 函数开始执行。因为调用 exec 并不创建新进程 ,所以前后的进程 ID 并未改变 。 exec 只是用另一个新程序替换了当前进程的正文、数据、堆和栈段。
#include
int execl(const char * pathname, const char * arg0, ... /* (char *) 0 */);
int execv (const char * pathname, char *const argv [] );
int execle(const char * pathname, const char * a rg0, ... /* (char *)0, char *const envp [] */);
int execv e(const char * pathname char *const a rgv [], char *const envp [] );
int execlp(const char * filename, const char * a rg0, ... /* (char * ) 0 */);
int execv p(const char * filename, char *const a rgv [] );
功能:实现代码替换
返回值:若出错则为 - 1 ,若成功替换新代码。
1. E: 指可以传递环境 变量表
2. L: 单独的参数传递 ,最后 要有一个 NULL
3. V : 传一个指 针数组名
4. P: 按照环境变量 来查找
应用举例
char * ps_argv[]={“ ps” ,”- ax ”, NULL };
char * ps_envp []={“P ATH= /b in: /usr/ bin ”, ”TER M =console ”, NULL}
execl(“/b in/ps ”, “ps ”, “-ax ”, NULL) ;
execv (“/bin/ps ”, ps_argv );
execle(“/bin/ps ”, “ps”, “- ax ”, NULL, ps_envp);
execve(“/b in/ps”, ps_arg v, ps_envp);
execlp(“ps”, “ps”, “- ax ”, NULL) ;
execvp(“ps ”, ps_argv);
信号
信号时 GNU/Linux 中进程的回调符号,可以为某个进程注册为在某
事件发生时接收信号,或是在某个默认操作退出时忽略信号。
信号是软件中断。信号 (signal) 机制是 Unix 系统中最为古老的进程之
间的通信机制。它用于在一个或多个进程之间传递异步信号。
产生信号:
当用户按某些终端键时,可以产生信号。例如:在终端上按 Ctrl - C 键
通常产生中断信号( SIGINT )。这是停止一个已失去控制程序的方
法。
硬件异常产生信号:除数为 0 、无效的存储访问等等。这些条件通常
由硬件检测到,并将其通知内核。然后内核为该条件发生时正在运行
的进程产生适当的信号。例如,对执行一个无效存储访问的进程产生
一个 SIGS EGV 。
SIGABRT:进程异常终止(调用 a bort 函数产生此信号)
SIGALRM:超时( alarm )
SIG FP E:算术运算异常(除以 0 , 浮点溢出等)
SIG HUP:连接断开
SIGILL:非法硬件指令
SIGINT:终端终端符 (Clt- C)
SIG KILL:终止(不能被捕捉或忽略)
SIGPIP E:向没有读进程的管道写数据
SIG QUIT:终端退出符 (Clt- \)
SIGTERM:终止(由 kill 命令发出的系统默认终止信号)
SIGUS R1:用户定义信号
SIGUS R2:用户定义信号
SIGS E GV:无效存储访问(段违例)
SIGC HLD:子进程停止或退出
SIGC ONT:使暂停进程继续
SIGST OP:停止(不能被捕捉或忽略
SIGTSTP:终端挂起符 (Clt- Z)
SIGTTIN:后台进程请求从控制终端读
SIGTTO UT:后台进程请求向控制终端写
信号处理
可以要求系统在某个信号出现时按照下列三种方式中的一种进行操作。
(1) 忽略此信号。大多数信号都可使用这种方式进行处理,但有两种信号却决不能被忽略。它们是: SIGKILL 和 SIGSTOP 。这两种信号不能被忽略的原因是:它们向超级用户提供一种使进程终止或停止的可靠方法。另外,如果忽略某些由硬件异常产生的信号(例如非法存储访问),则进程的行为是未定义的。
(2) 捕捉信号。为了做到这一点要通知内核在某种信号发生时,调用一个用户函数。在用户函数中,可执行用户希望对这种事件进行的处理。如果捕捉到 SIGCH LD 信号,则表示子进程已经终止,所以此信号的捕捉函数可以调用 waitpid 以取得该子进程的进程 ID以及它的终止状态。
(3) 执行系统默认动作。对大多数信号的系统默认动作是终止该进程。
#include
typedef void ( *sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler) ;
功能:为信号号为 signum 的信号 安装处理 函数
返回:成功返回之前的处理句柄,失败返回 SIG_ ERR
参数: handler 为用户指定的处理函数,或者是SIG_D EF 默认处理函数SIG_IGN 忽略
#include
#include
int kill(pid_t pid, int sig) ;
功能:发送信号到某进程
返回:成功为 0 ,失败为 -1
#include
int raise(int sig);
功能:发送信号到当前进程
返回:成功为 0 ,失败为 -1
#include
unsigned int alarm(unsigned int seconds) ;
功能:在 secends 秒后发送 SIGAL ARM 信号。若 secends 为 0 ,则
取消已设置闹铃。
返回:返回之前闹钟的剩余秒数,如果之前未设闹钟则返回 0 。
#include
int pause(void);
功能:挂起进程直到接收到一个信号
返回: pause 返回 -1 ,并将 errno 设置为 EINT R
进程间通信概述
进程是相互独立的,进程间的通信需要专门的机制。
进程之间的通信可以经由文件系统,但实际使用较为复杂(例如,需要锁机制)。
UNI X IPC (InterProcess Communication) 机制是各种进程通信方式的统称。
Linux 下的进程通信手段基本上是 从 Unix 平台上的进程通信手段继承而来的。
? 最初的 Unix IPC :信号、管道、 F I FO ;
? System VIPC : 消息队列 、 旗语(信号量)、共享内存区 ;
? P O SI X IPC : 消息队列、旗语(信号量)、共享内存区。
管道编程
管道是单向的、先进先出的、无结构的、固定大小的字节流,它把一个进程的标准输出和另一个进程的标准输入连接在一起。写进程在管道的尾端写入数据,读进程在管道的首端读出数据。数据读出后将从管道中移走,其它读进程都不能再读到这些数 据。管道提供了简单的流控制机制。进程试图读空管道时,在有数据写入管道前,进程将一直阻塞。同样,管道已经满时,进程再试图写管道,在其它进程从管道中移走数据之前,写进程将一直阻塞。
管道有一些固有的局限性:
1. 因为读数据的同时也将数据从管道移去,因此,管道不能用来对多个接收者广播数据。
2 . 管道中的数据被当作字节流,因此无法识别信息的边界。
3 . 如果一个管道有多个读进程, 那么写进程不能发送数据到指定的读进程。同样,如果有多个写进程,那么没有办法判断是它们中那一个发送的数据。
匿名管道提供了一个进程与其相同先祖子进程通信的方法。
#include
int pipe(int filedes [2 ]);
成功返回 0 ,失败返回 -1
filedes[0 ]: 为读句柄 , filedes [1]: 为写句柄
注:管道只不过是一对文件描述符,所以所有能操作文件描述符的函数都能用于管道,如 read, write, fnctl, select 等
写管道时,常数 PIP E_BU F 规定了内核中管道缓存器的大小
管道的一端关闭时:
写端关闭,读该管道在所有数据都被读取后,read 返回 0 ,表示达到了文件结束;读端关闭,写该管道产生信号 SIGPIPE
#include
int dup(int oldfd) ;
int dup2(int oldfd, int targetfd) ;
函数 dup 和 dup2 提供了复制文件描述符的功能,常用语 stdin, stdout, stderr 的重定向。
Int oldfd;
Oldfd = open(app_log”,(O_RDWR | O_CREATE),0644);
Dup2(oldfd,1);
Close(oldfd);
命名管道
#include
#include
int mkfifo(const char * pathname, mode_t mode) ;
函数 mkfifo 用于在文件系统中创建一个文件,提供 F I FO 功能。因为其在文件系统中可见,因此可以用于任何进程之间通信。成功返回 0 ,失败返回 -1 。
pathname 须为文件系统中不存在的文件,mode 可设为如 S_IS F I FO|0666
FI FO 的同步与读写
打开 F I FO 时的同步(如:open )
一般情况下(没有说明 O_NONBLOCK),只读打开要阻塞到某 个其它进程为写打开此 FIFO ;类似的,为写打开一个 FIFO 要阻塞到某个其它进程为读而打开它。
如果指定了 O_NONBLOCK,则只读打开立即返回;只写打开也立即返回,但如果没有进程已经为读而打开此FIFO,那么 open将出错返回 -1,errno 置为ENXIO 。
读写 FIFO 时的同步(如:read,write )同 pipe