APUE读书笔记-15进程内部通信(5)

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了APUE读书笔记-15进程内部通信(5)相关的知识,希望对你有一定的参考价值。

参考技术A

三种类型的XSI IPC:消息队列,信号量,和共享内存,有许多共同点。这里将讨论这些共同的特性,后面会依次讨论它们的各自的特性。

XSI的IPC函数基于System V的IPC函数。这三种类型的IPC起源于1970s的一个被称作"Columbus UNIX"的内部AT&T的UNIX版本。后来这三种类型的IPC被添加到System V中。它们不使用文件系统的命名空间而是使用自己的标准的命名空间。

我们应当知道(前面的表格中也说了):消息队列,信号量和共享内存被定义为Single UNIX Specification的XSI扩展。

内核中的每个IPC的结构(消息队列,信号量或者共享内存段),通过一个非负的整数来引用。例如当发送或者从一个消息队列中接收消息的时候,我们所知道的就只是这个队列的标识。和文件描述符号不同,IPC的标识不是一个小的整数。实际上,当创建并且移走一个IPC结构的时候,相应的标识符号会连续地增加,知道它到达一个最大的正整数值,然后又回归到0。

标识只是一个IPC对象的内部名字。协作进程需要一个外部名字策略以便能够使用IPC对象进行交互。为了实现这个目的,IPC对象和一个关键字进行关联,这个关键字的作用就像是一个外部的名字一样。

当创建一个IPC结构的时候(通过调用msgget, semget, 或 shmget),必须指定一个关键字。关键字的类型是key_t,它是在头文件<sys/types.h>中被定义的一个长整数类型。这个关键字在内核里面被转换成一个标识。

对于一个客户和服务,有许多种不同的方法来使用同一个IPC结构。

返回:如果成功返回关键字,如果失败返回(key_t)-1。

这里的path参数必须引用一个已经存在的文件名称,而参数id则只使用它的低8位来生成关键字。

ftok创建的关键字,一般是通过path对应的文件的stat结构的st_dev和st_ino成员结合project ID,来进行生成的。如果指定两个不同的path,那么ftok通常会返回两个不同的关键字。然而由于i-node成员和关键字都是通过长整型来存储的,所以在创建关键字的时候必然会丢失一些信息(关于索引节点的唯一性的信息),这意味着如果两个不同的路径名,如果指定的project id是相同的话,也是有可能会产生同样值的关键字的。

三个get函数(msgget,semget和shmget)都有两个类似的参数:一个关键字和一个整数标记。

当一个 新的IPC结构被创建 (通常是服务进程创建)的时候,通常是由如下情况导致:

如果想要引用一个已经存在的队列(通常是客户进程引用),关键字必须要和创建该队列时候的关键字一样并且没有指定IPC_CREAT标记。

需要注意的是,我们不可能通过IPC_PRIVATE关键字来引用一个已经存在的queue,因为这个特殊的关键字值会创建一个新的queue。为引用一个已经存在的使用关键字IPC_PRIVATE创建的queue,我们必须知道相应的表示,然后在其他的IPC调用中(例如msgsnd和msgrcv)使用那个标识,而不是通过get函数来获得标识。

如果我们想要创建一个新的IPC结构变量,那么我们需要确保我们不会引用一个使用同样表示的已经存在的IPC结构变量,我们必须同时设定IPC_CREAT和IPC_EXCL位的标记。使用这个方法,当已经存在了那个IPC结构的时候,会导致返回一个EEXIST错误(这个情况就类似使用指定了O_CREAT和O_EXCL标记的open函数)。

XSI的IPC结构和一个ipc_perm结构相关联,这个结构定义了权限和属主,并且至少包含了如下的成员:

每种实现都包含了额外的成员,可以通过查看你系统上面的<sys/ipc.h>文件来了解完整的定义。

当建立IPC的时候,所有的成员都会被初始化。之后,我们可以通过调用msgctl,semctl,或者shmctl来修改uid,gid,和mode成员。为了能够改变这些值,调用进程必须是IPC结构的创建者或者是超级用户。修改这些成员和给一个文件调用chown或者chmod类似。

mode成员的值和<sys/stat.h>中定义的文件访问权限很类似,但是对于任何的IPC结构没有相应的执行权限。同时,消息队列和共享内存使用read和write但是信号量使用read和alter。下表展示的就是每种IPC形式的六个权限:

有一些实现定义了一些常量来表示这些权限,但是这些都没有被Single UNIX Specification标准化。

我们可以看到,所有三种形式的XSI IPC都有内部的限制。所有这些限制可以通过内核来进行配置,我们后面讲到每一种IPC的时候,将会对这些限制进行描述。

每个平台提供了它自己的显示和修改特定限制的方法。FreeBSD 5.2.1,Linux 2.4.22,和Mac OS X 10.3提供了sysctl命令来查看和修改内核配置参数。Solaris 9可以通过修改文件/etc/system然后重新启动来修改内核的配置参数。

在Linux上面,呢可以通过运行"ipcs -l"来查看IPC相关的限制。在FreeBSD上,相应的命令是"ipcs -T"。在Solaris上,你可以运行sysdef -i来查看可以调节的参数。

(内容不会被及时释放)XSI IPC的一个基本问题就是IPC结构是系统范围内的,没有一个引用计数。例如,如果我们创建了一个消息队列,将一些消息放到这个队列上面,然后终止进程,那么消息队列和它的内容并不会被删除。它们会保留在系统中,直到其它进程调用msgrcv或者msgctl进行特定的读取或删除,或者通过执行ipcrm命令,或者通过系统的重新启动。与pipe相比,pipe会在最后一个引用它的进程结束的时候完全地被移除。对于FIFO,尽管管道的名字在文件系统中保留着(以管道文件的形式),但是FIFO中保留的任何数据会在最后一个引用该FIFO的进程结束的时候被移走。

(无法像文件那样直观)另外一个XSI IPC的问题就是,这些IPC结构不是通过在文件系统中的名字被知道的。我们无法通过前面的函数来访问和修改它们。有许多系统调用(msgget,semop,shmat,等等)被添加到内核中以便支持这些IPC对象。我们无法通过ls命令来查看IPC对象,我们无法通过rm命令来删除它们,我们也无法通过chmod命令来修改它们的权限。然而,有两个命令"ipcs"和"ipcrm"被加入了进来,可以实现类似的功能。

(无法进行高级的文件操作)因为这些IPC不能使用文件描述符号,所以我们不能构对它们使用多I/O函数(select和poll函数)进行操作。这使得我们同时使用多个IPC结构或者通过文件或者设备I/O使用任何这些IPC结构变得非常的困难。例如,我们无法有一个服务进程不经过忙等待的循环方式等待消息被放到两个消息队列中的一个上面。

还有更多关于有点和缺点的讨论或者争辩,这里就不一一列举了。同时有一个描述各种IPC特性的表格,这里也不列出了。具体参见参考资料。

后面将会对三种IPC分别进行详细的讲解。

参考: APUE2/ch15lev1sec6.html

读书笔记

第三章 进程管理

3.1进程

1.进程

1)进程就是处于执行期的程序(目标码存放在某种存储介质上),但进程并不仅仅局限于一段可执行程序代码。通常进程还要包含其他资源,像打开的文件,挂起的信号,内核内部数据,处理器状态,一个或多个具有内存映射的内存地址空间及一个或多个执行线程。当然还包括用来存放全局变量的数据段等,实际上,进程就是正在执行的程序代码的实时结果,内核需要有效而又透明地管理所有细节。
2)执行线程,简称线程,是在进程中活动的对象,每个线程都拥有一个独立的程序计数器、进程栈和一组进程寄存器,内核调度的对象是线程,而不是进程,在传统的Linux系统中,一个进程只包含一个线程,但现在的系统中,包含多个线程的多线程程序司空见惯。Linux系统的线程实现非常特别:它对线程和进程并不特别区分,对Linux而言,线程只不过是一种特殊的进程罢了。
3)在现代操作系统中,进程提供两种虚拟机制:虚拟处理器和虚拟内存。有趣的是,注意在线程之间可以共享虚拟内存,但每个都拥有各自的虚拟处理器。 
PS.在现代Linux内核中,fork()实际上是由clone()系统调用实现的。
程序通富哦exit()系统调用退出执行,父进程可以通过wait4()系统调用查询子进程是否终结。进程退出执行后被设置为将死状态,知道它的父进程调用wait()或waitpid()为止。

2.线程

执行线程,简称线程,是在进程中活动的对象。内核调度的对象是线程而不是进程。Linux将线程定义为特殊的进程。

3.虚拟处理器和虚拟内存

在现代操作系统中,进程提供两种虚拟机制:虚拟处理器虚拟内存
包含在同一个进程中的线程可以共享虚拟内存,但是每个都拥有各自的虚拟处理器。

4.几个函数

fork():创建新进程
exec():创建新的地址空间并把新的程序载入其中
clone():fork实际由clone实现
exit():退出执行
wait4():父进程查询子进程是否终结
wait()、waitpid():程序退出执行后变为僵死状态,调用这两个消灭掉。

3.2 进程描述符及任务结构

内核把进程的列表存放在叫做任务队列的双向循环链表中。链表中的每一项都是进程描述符。
进程描述符中包含的数据能完整地描述一个正在执行的程序,它打开的文件,进程的地址空间、挂起的信号、进程的状态等。

进程描述符的类型为task_struct,里面包含的数据有:
技术分享

3.2.1 分配进程描述符

Linux通过slab分配器分配task_struct结构——能达到对象复用缓存着色的目的。
slab分配器——动态生成,只需在栈底或者栈顶创建一个新的结构struct thread_info。

技术分享
每个任务的thread_info结构在它的内核栈的尾端分配。
结构中task域中存放的是指向该任务实际task_struct的指针。

2.进程描述符的存放

内核通过一个唯一的进程标识值PID来标识每个进程。pid类型为pid_t,实际上就是一个int类型,最大值默认设置为32768,可以通过修改/proc/sys/kernel/pid_max来提高上限。pid存放在各自进程描述符中。

通过current宏查找到当前正在运行进程的进程描述符。x86中,在内核栈的尾端创建thread_info结构,通过计算偏移间接地查找task_struct结构。current通过current_thread_info()把栈指针的后13个有效位屏蔽掉,再从thread_info的task域中提取并返回task_struct的地址。

3.3.3 进程状态

进程描述符中的state域是用来描述进程当前状态的。共有五种状态,标志如下:

TASK_RUNNING(运行):进程是可执行的,或者正在执行,或者在运行队列中等待执行
TASK_INTERRUPTIBLE(可中断):进程正在睡眠/被阻塞
TASK_UNINTERRUPTIBLE(不可中断):睡眠/被阻塞进程不被信号唤醒
TASK_TRACED:被其他进程跟踪的进程
TASK_STOPPED(停止):进程停止执行;进程没有投入运行也不能投入运行。
接收到SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU等信号时,或者调试时收到任何信号,都可以进入这种状态。

技术分享

3.2.4 设置当前进程状态

用set_task_state(task,state)函数。

set_task_state(task,state); //将任务task的状态设置为state
set_current_state(state)等价于set_task_state(current,state)

3.2.5 进程上下文

内核“代表进程执行”并处于进程上下文时,上下文中current宏是有效的,除非在此间隙有更高优先级的进程需要执行并由调度器做出了相应调整,否则在内核退出的时候,陈谷恢复在用户空间会继续执行。
程序执行系统调用或者触发异常后,会陷入内核空间,这时候内核代表进程执行,并且处于进程上下文中。
进程对内核的访问必须通过接口:系统调用和异常处理程序。

3.2.6 进程家族树

所有的进程都是pid为1的init进程的后代。
内核在系统启动的最后阶段启动init进程。

系统中的每一个进程必有一个父进程,可以拥有0个或多个子进程,拥有同一个父进程的进程叫做兄弟。
这种关系存放在进程描述符中,parent指针指向父进程task_struct,children是子进程链表。

获得父进程的进程描述符:

struct task_struct *my_parent = current->parent;

访问子进程:

struct task_struct *task;
struct list_head *list;
list_for_each(list, &current->children){
    task = list_entry(list, struct task_struct, sibling);
    /* task现在指向当前的某个子进程 */
}

init进程的进程描述符是作为init_task静态分配的。

获取链表中的下一个进程:

list_entry(task->tasks.next, struct task_struct, tasks);

获取链表中的上一个进程:

list_entry(task->tasks.prev, struct task_struct, tasks);

以上依赖于next_task(task)和prev_task(task)这两个宏实现。
for_each_process(task)宏,依次访问整个任务队列,每次访问任务指针都指向链表中的下一个元素。

struct task_struct *task;
for_each_process(task){
    /* 它打印出每一个任务的名称和PID */
    printk("%s[%d]\n",task->comm, task->pid);
}

3.3 进程创建

  • 一般操作系统产生进程的机制:在新的地址空间创建进程、读入可执行文件、执行
  • Unix的机制:fork()和exec()。
fork():通过拷贝当前进程创建一个子进程。子进程与父进程的区别仅在于PID,PPID和某些资源和统计量
exec():读取可执行文件并将其载入地址空间开始运行。

 

3.3.1 写时拷贝

1.写时拷贝是一种可以推迟甚至免除拷贝数据的技术,内核不复制整个进程地址空间,而是让父进程和子进程共享一个拷贝。
2.资源的复制只有在需要写入时才会进行,在此之前以只读方式读取。
3.fork的实际开销就是复制父进程的页表以及给子进程创建唯一的进程描述符。

3.3.2 fork()

Linux通过clone()系统调用实现fork()。
创建进程的大概步骤如下:

fork()、vfork()、__clone()都根据各自需要的参数标志调用clone()。
由clone()去调用do_fork()。
do_fork()调用copy_process()函数,然后让进程开始运行。
返回do_fork()函数,如果copy_process()函数成功返回,新创建的子进程被唤醒并让其投入运行。

调用alloc_pid()为新进程分配一个有效的PID。

一般内核会选择子进程首先执行。但一般不能如愿。这是Linux的轻量级决定的。让子进程会马上调用exec()函数,能够避免写时拷贝的额外开销。

3.3.3vfork()

除了不拷贝父进程的页表项之外,vfork()系统调用和fork()的功能相同。理想情况下不要调用vfork()

注意:子进程作为父进程的一个单独的线程在它的地址空间里运行 ,父进程被阻塞,直到子进程退出或执行exec()。子进程不能向地址空间写入。

 

vfork()系统调用的实现是通过向clone()传递一个特殊标志来进行的。

调用copy_process()是,task_struct的vfor_done成员被设置为NULL。
执行do_fork()时,如果给定特定标志,则vfor_done会指向一个特定地址。
子进程先开始执行后,父进程不是马上恢复执行,而是一直等待,知道子进程通过vfor_done指针向它发送信号。
在调用mm_release()时,该函数用于进程退出内存地址空间,并且检查vfor_done是否为空,如果不为空,则会向父进程发送信号。
回到do_fork(),父进程醒来并返回。

3.4线程在Linux中的实现

线程机制是现代编程技术中常用的一种抽象概念,该机制提供了在同一程序内共享内存地址空间运行的一组线程,可以共享打开的文件和其他资源,支持并发程序设计,在多处理器系统上可以保证真正的并行处理。
Linux内核的角度来看并没有线程这个概念,它把所有线程都当做进程来实现,线程仅仅被视为一个与其他进程共享某些资源的进程。
对于Linux来说,线程只是一种进程间共享资源的手段。

 

3.4.1 创建线程

新建的进程和它的父进程就是流行的所谓线程。

线程的创建和普通进程的创建类似,只不过在调用clone()时需要传递一些参数标志来指明需要共享的资源:

clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0);
共享地址空间、文件系统资源、文件描述符和信号处理程序。

普通的fork:

clone(SIGCHLD, 0);

vfork():

clone(CLONE_VFORK | CLONE_VM | SIGCHLD, 0);

传递给clone()的参数标志决定了新创建进程的行为方式和父子进程之间共享的资源种类。
技术分享
技术分享

3.4.2 内核线程

内核线程:独立运行在内核空间的标准进程。内核线程没有独立的地址空间只在内核空间运行,从来不切换到用户空间,可以被调度被抢占

内核线程只能由其他内核线程创建:

kthread_create()通过clone()系统调用创建内核线程后,处于不可运行状态,如果不通过wake_up_process()明确唤醒它,不会主动运行。
创建一个进程并让它运行起来 ,可以调用kthread_run()。
内核线程启动后就一直运行直到调用do_exit()退出,或者内核的其他部分调用kthread_stop()退出,传递给kthread_stop()的参数为kthread_create()函数返回的task_struct结构的地址。

3.5 进程终结

进程终结时,内核必须释放它所占有的资源并告知父进程。
进程终结的原因:一般是来自自身,发生在调用exit()系统调用时。
  • 显式的调用
  • 隐式的从某个程序的主函数返回

大部分依赖于do_exit()来完成。其中有几个重点:

……
给子进程重新找养父(线程组中的其他线程或者init进程)
调用schedule()切换到新的进程
……

进程不可运行并处于EXIT_ZONBIE退出状态,占用的所有内存就是内核栈、thread_info结构和task_struct结构。此时进程存在的唯一目的就是向它的父进程提供信息。

3.5.1 删除进程描述符

释放task_struct结构发生在父进程获得已终结的子进程信息并且通知内核不关注后,需要的系统调用是wait4():
挂起调用它的进程,直到其中的一个子进程退出,此时函数返回该子进程的PID。
释放进程描述符时,需要调用release_task()。

2.孤儿进程

如果父进程在子进程之前退出,必须有机制来保证子进程能找到一个新的父亲,否则这些成为孤儿的进程就会在退出时永远处于僵死状态。
可以用这两种方法解决:

在当前进程组找一个线程作为养父
让init成为它们的父进程。

 

操作步骤如下:

do_exit()中调用exit_notify()
exit_notify()调用forget_original_parent()
forget_original_parent()调用find_new_reaper()
遍历所有子进程并为它们设置新的父进程。
调用ptrace_exit_finish()同样进行新的寻父过程,给ptraced的子进程寻找父亲。
init进程调用wait()来检查其子进程,清除所有与其相关的僵死进程。

当一个进程被跟踪时,它的临时父亲设定为调试进程。如果此时它的父进程退出了,系统会为它和它的所有兄弟重新找一个新父亲。可以通过在一个单独的被ptrace跟踪的子进程链表中搜索相关的兄弟进程——用两个相对较小的链表减轻遍历带来的小号。

一旦系统为进程成功地找到和设置了心得父进程,就不会再有出现驻留僵死进程的危险了。init进程会例行调用wait()来检查其子进程,清楚所有与其相关的僵死进程。

以上是关于APUE读书笔记-15进程内部通信(5)的主要内容,如果未能解决你的问题,请参考以下文章

apue读书笔记之apue.h的设置

《Linux内核设计与实现》Chapter 5 读书笔记

APUE读书笔记-第四章 文件和目录

linux第二次读书笔记

Android开发艺术探索读书笔记——进程间通信

APUE读书笔记-05标准输入输出库(1)