1.Linux进程的组成
Linux进程组成:由 正文段(text)、用户数据段(user segment)和系统数据段。
➢ 正文段:存放进程要执行的指令代码。Linux中正文段具有只读属性。
➢ 用户数据段:进程运行过程中处理数据的集合,它们是进程直接进行操作的所有数据,包括进程运行处理的数据段和进程使用的堆栈。
➢ 系统数据段:存放反映一个进程的状态和运行环境的所有数据。这些数据只能由内核访问和使用。 在系统数据段中包括进程控制块PCB.。
在Linux中,PCB是一个名为task_struct的结构体,称为任务结构体。
2.Linux进程在处理机上的执行状态
在Linux系统中,用户不能直接访问系统资源,如处理机、寄存器、存储器和各种外围设备。因此提供了两种不同指令:
➢ 一般指令:供用户和系统编程使用,不能直接访问系统资源;
➢ 特权指令:供操作系统使用,可以直接访问和控制系统资源;
为了区分处理机在执行那种指令,通常将处理机的执行状态又分为两种:
➢ 管态(内核态、系统)
➢ 目态(用户态)
由目态转变为管态的情况可能有:
➢ 进程通过系统调用向系统提出服务请求
➢ 进程执行某些不正常的操作时,如除数为0、超出权限的非法访问、用户堆栈溢出等。
➢ 进程使用设备进行I/O操作时,当操作完成或设备出现异常情况时。
4.进程上下文和系统上下文
进程上下文:进程的运行环境动态变化,在linux中把进程的动态变化的环境总和称为进程上下文。
➢ 当前进程
➢ 进程切换
➢ 上下文切换
➢ 进程通过系统调用执行内核代码时,内核运行是为进程服务,所以此时内核运行在进程上下文中。
系统上下文: 内核除了为进程服务,也需要完成操作系统本身任务,如响应外部设备的中断、更新有关定时器、重新计算进程优先级等。 故把系统在完成自身任务是的运行环境称为系统上下文(system context).
5.Linux进程的状态及转换
linux中进程状态分为5种。
每个进程在系统中所处的状态记录在它的任务结构体的成员项state中。进程的状态用符号常量表示,它们定义在/include/linux/sched.h下:
- #define TASK_RUNNING 0 可运行态(运行态、就绪态)
- #define TASK_INTERRUPTIBLE 1 可中断的等待态
- #define TASK_UNINTERRUPTIBLE 2 不可中断的等待态
- #define TASK_ZOMBLE 3 僵死态
- #define TASK_STOPPED 4 暂停态
Struct wait_queue {
Struct task_struct *task;/*指向一个等待态的进程的任务结构体*/
Struct wait_queue *next;/*指向下一个wait_queue结构体*/
}
注:
➢ 与可运行队列不同,等待队列不是直接由进程的任务结构体组成队列,而是由于任务结构体对应的wait_queue构成。
➢ 每个等待队列都有一个指向该队列的队首指针,它一般是个全局指针变量。
#define pid_hashfn(x) ((((x)>>8)^(x))&(PIDHASH_SZ-1))???
其中:参数x就是进程的标识PID,计算结果的哈希值用于检索对应的任务结构体。例如PID为228的哈希值是100,PID为27536的哈希值是123,PID为 27535的哈希值是100。
为了解决哈希值冲突的问题,Linux把具有相同哈希值的PID对应的进程组成一个个双向循环链表。在task_struct中的两个成员项:
Struct task_struct * pidhash_next;/指向后一个任务结构体的指针/
Struct task_struct * pidhash _prev;/指向前一个任务结构体的指针/
1.进程标识哈希表
Linux 使用一个称为pidhash[]的指针数组管理这些链表,称为进程标识哈希表。该表中记录各个链表首结点地址,数组元素的下标与链表的哈希值相同。在include/linux/sched.h中pidhash[]数组定义如下:
Struct task_struct * pidhash[PIDHASH_SZ];
从定义中可以看出,
➢ pidhash数组由PIDHASH_SZ个元素组成,每个元素是指向一个进程任务结构体的指针。
➢ 数组元素个数PIDHASH_SZ是系统中最多可容纳的进程数NR_TASKS除以4,定义如下:
#define PIDHASH_SZ(NR_TASKS>>2)???
图4.7Linux进程标识哈希表
2进程标识哈希表操作函数:
Hash_pid():进程创建时,将其任务结构体插入哈希链表
Unhash_pid():进程撤销时,将其任务结构体从哈希链表中删除。
Find_task_by_pid():根据PID相应进程的任务结构体。
#define SCHED_OTHER 0 普通进程的时间片轮转算法(根据优先权选择下一个进程)
#define SCHED_FIFO 1 实时进程的先进先出算法(适用于响应时间要求比较严格的短小进程)
#define SCHED_RR 2 实时进程的时间片轮转算法(适用于响应时间要求比较严格的较大进程)
因此在linux的可运行队列中,从调度策略来分SCHED_FIFO的实时进程具有最高优先级,其次是SCHED_RR的实时进程,而SCHED_OTHER的普通进程优先级最低。
8.Linux进程调度方法
实时进程的优先级大于普通进程的优先级,故只有当可运行队列的所有实时进程都运行完成后,普通进程才能得到运行。
linux普通进程的优先级由Priority和counter共同决定。在进程运行过程中Priority保持不变,体现了进程的静态优先级概念;而counter不断减少,表示了进程的动态优先级。采用动态优先级的方法,使得一个进程占用CPU的时间越长,counter的值越小。这样使得每个进程都可以公平地分配到CPU。
Current->state=state; /* 把当前进程状态设置为等待状态*/
Save_flags(flags);
_add_wait_queue(p,&wait); /*把当前进程加入等待队列*/
Sti();
Schedule(); /*执行进程调度*/
Cli();
2)运行态下的进程运行结束后
一般通过调用内核函数do_exit()终止运行进程并转入僵死状态。该函数部分源码:
……
Current->state = TASK_ZOMBIE; /*把当前进程设置为僵死状态*/
……
Schedule();/*执行调度程序*/
……
3)使用wake_up_process()将处于等待状态的进程唤醒,然后将它置于可运行状态。该函数部分源码:
Save_flags(flafs);
Cli();
p->state = TASK_RUNNING; /*把进程置为可运行态*/
if(!p—>next_run)
add_to_runqueue(p); /*加入到可运行队列*/
restore_flags(flags);
if(p->counter>current->counter+3)
need_resched =1; /*调度标志置位,执行进程调度*/
4)当一个进程的程序接受调试时。
调式进程向被调试进程发送SIGSTOP信号,被调试进程处理该信号时调用内核函数do_signal()。部分源码:
……
Current->state = TASK_STOPPED /*把当前进程置为暂停态*/
Notify_parent(current);
Schedule();/*执行进程调度*/
……
5)当被调试的进程接收到调试进程发送的SIGCONT信号时,执行send_sig(),其中使用wake_up_process()解除被调试进程的暂停态而重新进入可运行态。
If(sig==SOGKILL||sig==SIGCONT))
{
If(p->state==TASK_STOPPED) /*若进程为暂停态*/
wake_up_process(p);
2.当前进程时间片用完时
在进程时间片运行完时,需要将CPU重新分配给下一个被选中的进程,这个过程是在时钟中断中实现。在时钟中断处理程序中调用了内核函数update_process_times(),它用于更新进程的各个时间信息,其中包括下面语句:
p->counter -= ticks
if(p->counter<0)
{
P->counter=0;
Need_resched=1;
}
3.进程从系统调用返回用户态时
当进程从系统调用返回用户态时,需要执行内核的汇编例程ret_from_sys_call,其中包括对need_resched标志进行检测的指令。
Cmpl $0,need_resched
Jne reschedule
当need_resched=1时,就转移到reschedule。
Reschedule:
Call SYMBOL_NAME(schedule)
4.中断处理后,进程返回用户态时
同3,当中断处理结束后,也需要执行内核的汇编例程ret_from_sys_call
Struct task_struct *p_opptr /*指向祖先进程任务结构体指针*/
Struct task_struct *p_pptr /*指向父进程任务结构体指针*/
Struct task_struct *p_cptr /*指向子进程任务结构体指针*/
Struct task_struct *p_ysptr /*指向弟进程任务结构体指针*/
Struct task_struct *p_osptr /*指向兄进程任务结构体指针*/
说明:
在任务结构体中只有一个指向子进程指针,它指向该进程子进程中最年轻的一个,其他子进程通过兄弟关系指针与父进程的关系。
例:
#include
#include
Main()
{
Pid_t val;
Printf(“PID before fork():%d\n”,(int) getpid());
Val=fork();
If(val!=0)
Printf(“parent process PID:%d\n”,(int) getpid());
Else
Printf(“child process PID:%d\n”,(int) getpid());
}
程序执行结果:
PID before fork(): “此时父进程的PID”
parent process PID: “此时父进程的PID”
child process PID: “此时子进程的PID”
Main()
{
Pid_t val;
Val =fork();
If(val!=0)
{
…….. /*执行父进程的语句*/
}
Else
{
exec(…..); /*执行程序*/
}
……..
Exit(1);
}
Linux采用“写时拷贝”技术,所以子进程在创建时是共享父进程的正文段和数据段,但是在执行exec()时,子进程将建立自己的虚拟空间,并把参数arg0制定的可执行程序映像装入子进程的虚拟空间,这是就形成了子进程自己的正文段和数据段。
Kerneld_exit(); /*释放内核*/
_exit_mm(current); /*释放虚拟空间*/
_exit_files(current); /*释放打开的文件*/
_exit_fs(current); /*释放fs结构体*/
_exit_sighand(current); /*释放信号*/
Exit_thread(); /*释放线程*/
4)把进程的状态设置为僵死状态。
Current->state = TASK_ZOMBIE;
5)把退出码置入任务结构体。
Current->exit_code =code;/正常退出时,退出码为0;异常退出时,为非0值,通常是错误编号/
6)变更进程族亲关系。
Exit_notify();/把要撤销的进程的子进程的父进程赋予init进程/
7)执行进程调度,选择下一个使用CPU的进程。
Schedule();
2 . 进程的撤销 release()
一个进程终止后不能自己撤销自己,而只能由他的父进程或祖先进程来撤销它:
父进程在建立子进程后,通常要使用系统调用wait()等待子进程的终止。在一个进程终止时,把退出码置入任务结构体后,内核就把该进程终止的信号发给它的父进程。父进程就收到信号后,结束等待状态,继续执行wait()程序,检查到该子进程的状态为僵死态,则调用release()函数,撤销该子进程,将其任务结构体释放,则该子进程从此彻底消亡。
如果父进程在子进程之前终止,则为了不使其子进程成为孤立进程,则将它的子进程赋予init进程,使init进程成为这些进程的父进程,并由其负责撤销这些子进程。
管道分两种:无名管道、命名管道。它们的内部结构是一致的,但是用方式不同。
➢ 无名管道:只能在父子进程之间通信
➢ 命名管道:可以在任意进程间通信。
#include
Int pipe(int filedes[2]);
其中filedes[]是具有两个元素的int型数组。在调用pipe()建立一个无名管道后,使用两个文件标识号来表示管道的两端(一端写,一端读),并记入filedes[]中。其中filedes[0]是读取管道的文件标识号,filedes[1]是写入管道的文件标识号。
说明:
➢ 父子进程使用无名管道通信是建立在子进程继承父进程资源的基础上。父子进程通信时,必须先建立管道,再创建子进程。
➢ 使用管道时必须确定管道通信的方向,且一旦确定后不能改变。
➢ 父子进程中一个进程只能使用一个文件标识号,所以另一个不使用的标识号可以使用系统调用close()关闭它。
使用无名管道的进程通信
例:在该例中,父进程建立管道后创建一个子进程。子进程的任务是把一组字符串信息写入管道,父进程在子进程完成任务终止后,从管道中读取信息并显示在显示器上。
#include
Main()
{
Pid_t pid;
Int fds[2];
Char buf1[50],buf2[50];
Pipe(fds); /*建立无名管道*/
If((pid=fork())<0)/*创建子进程失败,程序终止*/
{
Printf(“Fork() Error\n”);
Exit(1);
}
Else if(pid==0) /*子进程*/
{
Close(fds[0]); /*关闭不使用的文件标识号*/
Sprintf(buf1,”these are transmitted data\n”); /*把信息写入缓冲区buf1*/
Write(fds[1],buf1,50); /*把缓冲区buf1中的信息写入管道*/
Exit(1) /*子进程终止*/
}
Else
{
Close(fds[1]); /*关闭不使用的文件标识号*/
Wait(0); /*等待子进程结束*/
Read(fds[0],buf2,50); /*读管道信息并把信息置入buf2*/
Printf(“%s\n”,buf2);/*显示buf2中的信息*/
}
}
继续说明:
➢ 无名管道与一般文件不同,它没有纳入文件系统的目录,不占用外存空间,仅使用内存作为数据传输的缓冲区。
➢ 缓冲区的大小决定每次写入管道的字节数。该值由全局符号常量PIPE BUF确定。 缺省值为一个物理页面。
➢ 由文件系统管理
#include
Int mkfifo(const char *path, mode_t mode);
其中:
path:指明要创建的命名管道的路径和名字;
mode:指明管道访问的权限。
创建成功返回0,否则为负数。
另一种方法:使用linux系统调用mknod()
mknod()可以建立任何类型的文件,在建立命名管道时使用的形式如下:
mknod(path,mode|S_FIFO,0);
参数含义与上同,其中S_FIFO表示建立FIFO文件。
这两种方法作用基本相同:建立命名管道的目录结构、inode节点、file文件结构体等。
2)打开管道
由于任何进程都可以通过命名管道进行通信,所以在使用命名管道时,必须先打开它,由系统调用open()实现。
open(char *path, int mode)
其中:
path:指明要使用的命名管道的路径和名字;
mode:指明管道访问的模式:O_RDONLY(只读)、O_WRONLY(只写)
创建成功返回文件标识号,否则为负数。
注:在使用文件操作对管道进行各种操作时,要使用文件标识号,而不是管道名。
例:有两个程序,其中wrfifo.c把一组信息写入管道,另一个程序rdfifo.c把管道中的信息读出后显示在屏幕上。
/*读管道程序rdfifo.c*/
#include
#include
#include
#include
#include
#include
Main(void)
{
Int fd,len;
Char buf[PIPE_BUF];
Mode_t mode=0666;
If(mkfifo(“fifo1”,mode)<0)
{
Printf(“mkfifo error\n”);
Exit(1);
}
If((fd=open“fifo1”,O_RDONLY))<0
{
Printf(“pipe open error\n”);
Exit(1);
}
While(len=(read(fd,buf,PIPE_BUF-1)>0)
Printf(“read fifo pipe: %s”,buf);
Close(fd);
}
/*写管道程序wdfifo.c*/
#include
#include
#include
#include
#include
#include
Main(void)
{
Int fd,len;
Char buf[PIPE_BUF];
Mode_t mode=0666;
If((fd=open(“fifo1”,O_WRONLY))<0
{
Printf(“pipe open error\n”);
Exit(1);
}
For(i=0;i<3;i++)
{
Len=sprintf(buf,”write fifo pipe from %d at %d times\n”,getpid(),i+1);
Write(fd,buf,len+1);
}
Close(fd);
}
在linux终端上运行这两个程序:
$./rdfifo&
$./wrfifo
结果:
Write fifo pipe from 945 at 1 times
Write fifo pipe from 945 at 2 times
Write fifo pipe from 945 at 3 times
Struct sem_queue
{
Struct sem_queue *next; /*指向队列后一个节点*/
Struct sem_queue *prev; /*指向队列前一个节点*/
Struct wait_queue *sleeper; /*指向被阻塞进程*/
Struct sem_undo *undo;
Int pid; /*实施操作的进程的PID*/
Int status;
Struct semid_ds *sma; /*指出对哪个信号量集合实施操作*/
Struct sembuf *sops; /*指向是进程阻塞的操作数组*/
Int nsops; /*指出操作数组中操作的个数*/
}
将信号量操作移出等待队列
上节我们介绍了,进程执行信号量操作时,出现进程阻塞,操作放入信号量操作等待队列的情况。下面我们介绍如何将进程唤醒、将操作冲等待队列移出的情况:
若某个进程在执行semop()时没有阻塞,函数将检查该信号量集的操作等待队列:
➢ 若无操作等待, 则返回;
➢ 若有操作等待, 则依次重新执行这些操作:
1)若进程仍需等待,则操作保留在等待队列中;
2)若进程可以继续执行,则通过该sem_queue结构体中的sleeper在进程等待队列中找到该进程,并将其唤醒;再将该sem_queue结构体从操作等待队列中删除。
struct msg
{
struct msg *msg_next; /*指向下一个消息*/
long msg_type; /*消息类型:大于0,是通信双方约定的消息标志*/
char *msg_spot; /*消息正文地址*/
time_t msg_time; /*消息发送时间*/
short msg_ts; /*消息正文大小,最大长度由MSGMAX决定,缺省为
4057字节*/
}
2.消息队列
Linux中可以根据进程的需要建立多个IPC消息队列,消息队列的最大数量由符号常量MSGMNI决定,缺省为128。系统对所有消息队列统一管理。每个消息队列有唯一的标识号。每个消息队列是由消息结构体构成的单向链表。
描述消息队列的数据结构struct msqid_ds,称为消息队列描述符。与消息队列一一对应。其定义如下:
struct msqid_ds
{
Struct ipc_perm msg_perm; /*访问权限*/
Struct msg *msg_first; /*指向消息队列头*/
Struct msg *msg_last; /*指向消息队列尾*/
Time_t msg_stime; /*最近发送消息的时间*/
Time_t msg_rtime; /*最近接收消息的时间*/
Time_t msg_ctime; /*最近修改消息的时间*/
Struct wait_queue *wait; /*等待接收消息的进程等待队列*/
Struct wait_queue *wait; /*等待发送消息的进程等待队列*/
Unshort msg_cbytes; /*队列中当前字节数*/
Unshort msg_qnum; /*队列中消息数*/
Unshort msg_qbytes; /*队列最大字节数,不能超过MSGMN(缺省16384)*/
Unshort msg_lspid; /*最近发送进程的PID*/
Unshort msg_lrpid; /*最近接收进程的PID*/
}
说明:IPC消息队列用于多个进程间的数据交换,其中包含多个进程发送来的消息,这些消息又要传递给不同的进程。故发送进程在发送的消息中需要设定一个类型msg_type(详见消息结构体),接收进程在消息队列中按照类型就可以找到对方进程发来的消息。
- 消息队列的生成与控制
1.建立及检索消息队列
在程序中使用msgget()建立一个消息队列或者检索消息队列的标识号。其定义为:
Int msgget(key_t key, int msgflg) /msgflg的值与semget()中的semflg完全相同/
调用成功则返回消息队列的标识号,否则返回-1;
2.消息队列的控制
对消息队列的控制操作由系统调用msgctl()实现,其定义如下:
Int msgctl(int msqid,int cmd,struct msqid_ds *buf)
Msqid:消息队列的标识号。
Cmd:操作类型。
Buf:用于向函数传递数据和从函数的得到的结果数据。
说明:cmd参数的种类与semctl()类似,但制定操作的意义不同。主要有:
MSG_STAT或IPC_STAT:把指定消息队列的描述符内容拷贝到buf指定的msqid_ds结构体中。
IPC_SET:允许修改制定消息队列描述符内容。该操作用于修改消息队列的访问权限,即msg_perm的成员项。超级用户可以修改msg_dbyte的值。变更后,msg_ctime的值自动更新。
IPC_RMID:删除消息队列及其数据结构,该操作只有超级用户或消息队列生成者进行。
MSG_INFO或IPC_INFO:把与消息队列有关的最大值数据输出到msginfo结构体中。在该结构体中记录着IPC消息队列的消息数、字节数和消息的字节最大数。
Struct shmid_ds
{
Struct ipc_perm shm_perm; / *访问权限*/
Int shm_segsz; /*共享内存大小(字节)*/
Time_t shm_atime; /*最近一次结合时间*/
Time_t shm_dtime; /*最近一次分离时间*/
Time_t shm_ctime; /*最近一次修改时间*/
Unsigned short shm_cpid; /*创建者pid*/
Unsigned short shm_lpid; /*最后一次对共享内存进行操作的进程的pid*/
Short shm_nattch; /*当前结合进程的数量*/
Unsigned short shm_npages; /*共享内存使用的页面数*/
Unsigned long shm_pages; /*共享内存页表指针*/
Struct vm_area_struct *attaches; /*供结合进程使用的虚拟内存描述符*/
}
3.共享内存的结合与分离
15.IPC对象
通常把Linux中使用的System V IPC 的3种通信机制称为IPC对象(IPC object)。
这三种机制有一定的统一性:
首先:它们有统一的管理方式,每一种机制都有描述符记录管理信息;
其次:有格式统一的操作接口,即系统调用。如生成函数***get()、控制函数***ctl();而且这些函数使用的符号常量也是一致的。
IPC对象的键值
IPC对象的另外一个重要特征:系统使用一个键值和一个标识号来识别每一个IPC对象。键值和标识号都是唯一的,且一一对应。为什么要使用两种识别方法呢?
因为:IPC的标识号是创建者进程运行过程中动态生成的,在其他进程中不可见,这样就无 法使用其IPC对象进行通信;而键值是一个静态的识别码,对所有进程都可见。
生成键值:调用c函数 ftok()。定义:
Key_t ftok(char * pathname, char proj);
Pathname:一个已经存在的路径和文件名;
Proj :任意字符
使用同一个IPC对象进行通信的相关进程可以使用该函数和相同参数的到该对象的键值。(为了使得相关进程能够得到生成键值的参数,一般的做法是把这些参数组织在一个头部文件中,然后将其包括到进程程序的头部文件中。
2.IPC对象的访问权限
为了限制进程对IPC对象的使用,防止出现混乱。IPC对其对象规定了一定的访问权限。结构体 ipc_perm记录着IPC对象的访问权限等静态属性。其定义为:
Struct ipc_perm
{
Key_t key; /*IPC对象键值*/
Unshort uid; /*所有者用户标识*/
Unshort gid; /*所有者组标识*/
Unshort cuid; /*创建者用户标识*/
Unshort cgid; /*创建者组标识*/
Unshort mode; /*访问权限和操作模式*/
Unshort seq /*序列号*/
}
进程访问IPC对象时,系统首先要根据该进程的用户标识和组标识,对照访问权限来确定本次访问是否合法。
3.IPC对象的终端命令
在linux中提供了终端上查看和删除IPC对象的命令。常用的有ipcs和ipcrm两条命令。
➢ Ipcs 查看命令
常用格式:ipcs [-asmq]
选择项
-a 显示所有IPC对象信息
-s 显示信号量机制信息
-m 显示共享内存信息
-q 显示消息队列信息
➢ Ipcrm 删除命令
常用格式:Ipcrm[shm|msg|sem] id /id 为IPC对象的标识号/