技术: 探究 Linux 进程

linux process? fork, vfork, wait, waitpid? far more than that.

(本以为花一周时间能写好, 结果反复修改了几次; 以后还会不断修改完善, 如果有时间)

我看到网上有人写的《单刷APUE系列》中对于进程环境&进程控制总结的还不错,但是探究进程却不说虚拟地址空间,不说进程控制块儿,是不是缺点儿什么?

为什么直接说Linux进程?

1.我们写的代码,除了操作系统内核&驱动,其他都是跑在用户态的程序。
2.进程是除了文件(系统)之外的,最大的抽象。
3.其实我是想上来就说符合posix标准的线程的,但是Linux下的线程模型其实是LWP。


引言

在开发中,多线程(pthread, Boost线程)用的比较多(可能是由于服务器端处理并发问题的演变),更偏向实战一些,往往进程谈的就比较少了。但是进程本身也是设计优良的(linux线程在内核看来就是轻量级进程–但是用户通过pthread_create()创建的用户级线程和内核直接调度的线程或者说轻量级进程还是有一点儿区别的,下面再说),个人觉得还是蛮有意思的,那么就从pcb开始一点儿点儿絮叨一下。

Linux进程模型

进程控制块(pcb)

Process Control Block

1
grep -rn "struct task_struct {" /usr/src

显示结果:

1
2
3
merlin@thinkpad-u16:~$ grep -rn "struct task_struct {" /usr/src
/usr/src/linux-headers-4.8.0-41/include/linux/sched.h:1460:struct task_struct {
/usr/src/linux-headers-4.8.0-22/include/linux/sched.h:1460:struct task_struct {

之后就能看到一个非常长的结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct task_struct {
volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
void *stack;
atomic_t usage;
unsigned int flags; /* per process flags, defined below */
unsigned int ptrace;

#ifdef CONFIG_SMP
struct llist_node wake_entry;
int on_cpu;
unsigned int wakee_flips;
unsigned long wakee_flip_decay_ts;
struct task_struct *last_wakee;

n int wake_cpu;
#endif
int on_rq;

int prio, static_prio, normal_prio;

//...

为什么需要维护这样一个结构体?
系统通过对PCB进行维护,从而管理和调度进程,最终达到对进程访问的资源的控制.

PCB中都维护了哪些数据成员?(不用记忆,理解如何控制进程及其访问的资源就能记住)

  • 进程id (pcb对应的进程id,libc或者glibc中的pid_t, 非负整数)
  • 进程的状态(ps aux | more查看STAT)
    ps aux | more
  • 寄存器的值(进程切换&恢复)
  • 虚拟地址空间的信息(可能是一张实际物理地址的映射二维表, 内核进程没有)
  • 终端的信息(pts或者tty, 执行的进程有的是需要控制终端的)
  • 当前工作目录(shell也是根据你当前的目录信息来解析你输入的命令)
  • umask掩码(控制文件修改/创建的默认权限)
  • 文件描述符表 (进程维护一张二维表,其中表项目(item)指向系统的打开的目录文件表,之后该表中的文件指针才去指向i-node;整形,其实是hash值)
  • 信号相关的信息
  • 进行凭证(gid, uid, suid, euid …)
  • 会话session相关信息
  • 进程组相关的信息
  • 进程可以使用的资源上限(resource limit)
    (例如最大线程的数量(根据实际硬件情况),栈的大小(8K,8192),最多能打开的文件数量(1024))
    ulimit -a
  • 时钟(定时器)

  • …(等等)

程序&进程内存模型

程序和进程:

  • 二进制代码(指令集合)是程序(就像剧本),加载进入内存,使用系统资源的一个运行实例就是进程(上演的话剧)。实际上扣住本质,加载进入内存,使用系统资源(比如文件资源,保存临时数据的堆栈,挂起的信号等)即可, 更专业的术语:an executing address space and the required system resources including cpu and file descriptors
  • 程序转换为进程:内核将程序读入内存,内核为该进程分配进程pid&其他资源,内核保存pid&进程状态,放入就绪队列等待执行(等待被调度)
  • 程序和进程是一对多的关系

进程和线程:

  • 内核并不区分线程和进程,也就是说没有特殊的数据结构或者字段去表示线程,线程就当做轻量级进程(LWP),同样用PCB来描述
  • Linux中内核调度的实体是LWP,而用户用posix pthread创建的是用户级线程,两者通过posix线程实现库件关联起来
  • posix pthread只是定义了线程库的标准,具体实现交给了NPTL(Native Posix Thread Library),其中另外两种实现LinuxThreads没有内核支持被废弃,NGPT效率不高也被废弃
  • 一般情况下getpid()系统调用返回的是当前进程控制块(pcb)的tgid(线程组id), 而不是current->pid (线程组的概念由NPTL提出); 就是说,如果一个进程是有多个线程,那么第一个主线程的tgid就是它的pid;但是其他的线程,即轻量级进程的pid并不等于tgid.(getpid拿到的其实是首线程的pid,而不是其他线程的pid;但是一定拿到的是所有线程的tgid)
  • 内核中不谈线程(线程多用于用户级或者linux系统编程),只谈进程管理,调度…

进程状态:

  • TASK_RUNNING 可运行或者正在运行状态
  • TASK_STOPPED 暂停(停止)状态(SIGSTOP 19, SIGSTSTP 20, SIGTTIN 21, SIGTTOU 22)
  • TASK_TRACED 跟踪状态
  • TASK_INTERRUPTIBLE 可中断&可唤醒状态(中断,或者信号都可以让正在挂起(睡眠)的它进入就绪)
  • TASK_UNINTERRUPTIBLE 不可中断状态(比如进程读取设备的时候)
  • TASK_NONINTERACTIVE 和上面两个一起用,不提供任何信息(防止使用管道的进程或者更多交互)—用于管道

进程模型(32位x86):
了解内存布局,并不是真的闲的无聊,而是了解进程虚拟地址空间对于以后分析和解决诸如可重入函数(reentrancy)返回静态指向静态分配内存的指针等问题有帮助。

先看一下size命令(Displays the sizes of sections inside binary files)

size [elf_file]

大概如下:(暂时没有考虑共享内存的分配以及共享库的加载位置
简单内存结构图
(特别注意heap和stack中间是有保存的)
(临时返回值到底是保存在函数调用前就开辟的栈空间中,还是寄存器中,具体平台具体分析)

补充:

  1. section可以在编译的时候用GNU扩展的__attribute__指定一下
  2. .data段只读,可共享;.bss段运行时才初始化为0;但两个区域都是存储的全局变量静态变量
  3. 该进程3G-4G是内核进程部分,主要维护pcb相关信息。

还没说完:

  1. 共享内存在进程中如何映射
  2. 共享库呢?
    在堆区的某一位置,设置了一个标志,从此标志以上的某一段区间就是给共享内存或者共享库使用的。

进程凭证

其实就是进程标识符:
比如:

pid, tgid; uid, gid; euid, egid; suid, sgid; fsuid, fsgid;
  • 其中, 虽然pid是32位unsigned int, 但是为了兼容16位Unix硬件平台,当前的最大pid数量依旧是2^15-1即32767; 超过了就要从头开始使用闲置的(300以内系统保留给daemon)

    1
    2
    cat /proc/sys/kernel/pid_max #结果是32768,其实就是内核常量PID_MAX+1
    #注意64位的的及其可能是 2^22
  • euid, egid是程序要访问特权代码/数据时需要检查的运行时权限, 由setuid程序(通常是些服务程序,如网络访问程序)运行时产生的euid, egid。注意只能用于可执行文件。
    (owner, group: x + s = S)也就是说运行时提升到了owner的权限

  • sticky bit,用于目录,显示在other用户的可执行位置(如果owner创建的目录,除了owner子集和root其他人即使有权限,也不能删除。
    chmod o+t dir; (x + t =T)
  • suid, sgid是当进程的euid, egid被改变的时候, 来保存原来的euid,egid
  • fsuid, fsgid访问特定文件系统时需要检查的运行时权限(可以看做是euid和egid的补充)

/proc文件系统

本质是查看当前系统有哪些进程,状态如何,关系如何以及使用了哪些文件/网络/锁这种,并且/proc的方式避免了直接窥视内核维护的数据结构可能带来的问题。(更简单的方式访问内核信息
其中比较重要的是/proc/[pid]/status, /proc/[pid]/task/proc/sys/kernel/pid_max

/proc虚拟文件(pseudo-filesystem)系统**
虚拟的意思是,其所包含的文件/子目录并不真实存储在磁盘上,而是由内核在进程访问此类信息的时候动态生成。例如: cat /proc/1/status

  1. /proc/pid/
    下其他文件节点: (基本都是只读的)—并且变化不定

    • status (各种信息:pid,tgid,内存使用量,信号)
    • mem 进程虚拟内存
    • maps 内存映射
    • fd 进程所打开文件的符号链接(目录)
    • cwd 当前进程的工作目录的符号链接
    • cmdline 以\0分隔的命令行参数
    • Environ 以\0分隔的环境列表
    • task目录下以每个线程的tid命名的文件夹可以查看线程的status节点,例如
      1
      cat /proc/1/task/1/status
  2. /proc/*
    主要有: (可能需要特权权限)

    • /proc/net 网络和套接字相关信息
      ls /proc/net
    • /proc/sys/* 系统配置相关信息 (例如上面,查看最大的进程数字 cat /proc/sys/kernel/pid_max)
      1
      echo 1000 > /proc/sys/kernel/pid_max
  3. /proc/[pid]/ns
    进程可以规划到命名空间, 每次新创建fork/exec*新进程的时候,也会设置namespace.
    (具体可以等到了解容器相关技术的时候在说—–我当前不太了解,不再多说)proc_pid_ns

uname()系统调用
写一个小程序就知道了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#define _GNU_SOURCE
#include <sys/utsname.h>
#include <stdio.h> //printf
#include <stdlib.h> //exit
#include <unistd.h>

int main(void)
{
struct utsname uts;
if(uname(&uts) == -1){
perror("shit");
exit(-1);
}

printf("Node name: (DNS prefix Name) %s\n", uts.nodename);
printf("System name: %s\n", uts.sysname);
printf("Release: %s\n", uts.sysname);
printf("Version: %s\n", uts.version);
printf("Machine: %s\n", uts.machine);

#ifdef _GNU_SOURCE
printf("Domain name: %s\n", uts.domainname);
#endif
exit(EXIT_SUCCESS);

return 0;
}

上面的uts结构体struct utsname来源于”UNIX Time-Sharing System”。


Linux进程管理(控制)

创建

系统进程(操作系统创建)
(管理和分配系统资源的进程,一般是由系统创建)

  1. 产生shell进程的过程如下:(注意这里并不去具体的谈论内核初始化或者内核的引导过程)
    • 根据系统的静态配置(init_task.h)产生0号进程(idle进程,只是执行cpu_idle()函数)
      (系统的空闲时间就是idle线程运行的时间;当没有其他进程运行时就运行它)
    • –> 通过高kernel_thread创建一个和0号进程共享地址空间的1号内核线程pid=1
    • –> init内核线程exec产生1号用户进程(此过程并没有调用do_fork只是根据inittab加载可执行程序init, 所以进程号是不变的)
    • –> fork出getty()进程
    • –> exec*() login进程
    • –> 登录成功的话产生shell进程
  2. shell进程如何产生别的进程?
    大致步骤如下:
  • 用户在shell中输入执行命令
  • shell进程搜索命令的可执行文件
  • 利用fork/clone创建子进程
  • shell创建的子进程载入可执行文件,并开始执行
  • shell等待子进程的结束(或者子进程自己退出)

非系统进程(用户或者用户的程序创建)
这个就是普通的创建问题,谈论一下:

1. fork, clone, exec, vfork(可以不使用)
2. do_fork, clone, copy_process问题
3. fork之后和父进程共享哪些,不共享哪些

其实从上面,shell创建子进程的过程,明显的可以发现,创建过程是可以细分为两个阶段的:

  1. fork/clone/vfork阶段—–复制父进程的资源(到底是否复制页表或者真正进行写入,要由具体的系统决定,比如不复制页表的vfork,比如有的用copy-on-write技术)
  2. exec*阶段—-儿子一般是要和爸爸有些区别的,或者虽然共享了代码段,但不一定执行,所以用exec相关系统调用去读取/载入命令的可执行文件

fork,clone,vfork: 这三个一般,fork用的最多,最近的fork一般都采用了cow技术,虽然也复制了页表,但是仅仅是在修改数据的时候才真正进行写入,也正是由于这种原因导致vfork基本可以到了废弃的地步。但是vfork出生的初衷是好的,既然子进程出来就是要执行另外的一段代码,那还不如在它执行之前,让父子进程共享地址空间(页表), 先让父进程阻塞住,等到子进程加载完自己的可执行文件(需要执行自己的代码),再让父进程恢复。—-也就是说,vfork和fork的区别就是在复制父进程资源的是时候是否复制页表--是否共享地址空间(因为此而导致的父进程挂起(且子进程只能运行exec或者exit)只能说是副作用,或者说是一种解决由此产生的问题的一种手段,方法)

(fork的代码太简单了,我就不写示例程序了,但是注意cow特性以及多进程fork的时候,子进程break出去—不能让子进程复制子进程(容易乱),只能让最原始的父进程统一fork)

而clone就是个定制化的fork。你可以指定在复制父进程资源的时候,哪些要复制&哪些不用复制—-以此来精确提升复制效率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* Prototype for the glibc wrapper function */

#define _GNU_SOURCE
#include <sched.h>

int clone(int (*fn)(void *), void *child_stack,
int flags, void *arg, ...
/* pid_t *ptid, struct user_desc *tls, pid_t *ctid */ );

/* Prototype for the raw system call */

long clone(unsigned long flags, void *child_stack,
void *ptid, void *ctid,
struct pt_regs *regs);

其中的flags用于标志需要从父进程复制的资源,举个例子(可以简单的认为): vfork = clone + CLONE_VM + CLONE_VFORK + SIGCHLD(具体可以参考一下kernel/process.c的源码),其中的CLONE_VM表示就表示共享地址空间,就好像是在fork轻量级进程(LWP)一样,实际上pthread_create()也是通过调用clone实现的。

fn是新进程需要执行的代码函数; child_stack指定为新进程分配的栈地址(有时候为了父子共享,你需要传递指针在的指针),arg可变参数就是传递给新进程的参数.

这个函数的调用不是很容易,有很多注意的地方,这个要给一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#define _GNU_SOURCE
#include <stdio.h>
#include <sched.h> //clone
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int data = 10;
void **stack = 0;
int child_process(){
printf("Child Process %d, %d data %d\n", getpid(), getppid(), data);

data = 20;
printf("Child Process %d, data %d\n", getpid(), data);
exit(2);
//while(1);
}

int main(void)
{
stack = (void**) malloc(8192);

if(stack == NULL){
perror("error:");
exit(-1);
}

//share the address, file systems, signal action, fd
int clone_flag = CLONE_VM | CLONE_SIGHAND | CLONE_FS | CLONE_FILES;
clone(child_process, stack, clone_flag, NULL);

sleep(1);
printf("Parent process %d, data %d\n", getppid(), data);

while(1);
return 0;
}

运行结果可能是由于核心转储机制的原因(给出当前页表的情况),所以应该用gdb去看(否则直接setmentation fault了)
clone_process

注意: CLONE_VM这个参数并不好用,去关注一下pthread_create()是怎么用的。

clone虽然详细给出了复制时可以选择性复制的便利,但是并没有解决如何复制(具体过程是怎么样的呢?pcb, 栈,资源等的处理),以及复制具体对应到页表(虚拟地址)的哪一段,故而又生出来了do_fork系统调用,在内核中是以do_fork()函数封装的系统调用(kernel/fork.c里面),就不去贴代码了,总之,大概是这样的关系:

fork_relationship
其中CLONE_KERNEL是一个宏,大概是这样定义的:

#define CLONE_KERNEL (CLONE_FS | CLONE_FILES | CLONE_SIGHAND)

相关代码,请参考具体的内核实现。

fork什么时候失败?

CHILD_MAX限制 或者没有足够内存创建页表(虚拟内存不足)
(fork失败时 error会被设置为EAGAIN)

system(const char *str)

启动新进程: 库函数system(const char *str);  等同于 sh -c str;  无法启动shell返回127, 其他错误返回-1, 正常返回退出码;调用成功返回0;
但是system往往是fork, exec*()出来的shell先返回, 而原来的进程后返回, 打印效果不是很理想, 需要更细力度的控制-----用系统调用fork,exec等,而不是库函数
如果fcntl没有设置 close on exec flag, 那么父进程中打开的文件资源, 在子进程中依旧保持打开状态

进程表

维护进程pid信息, 同时运行的最大pid数量只与用来建立进程表项的内存容量有关,不在存在限制(早期的Unix限制为256)32657

其他补充:

内核中获取进程标识是用的current宏,而平时系统编程用的是getpid,getppid
内核的进程调用,并不仅仅是根据nice值来进行进程调度的,它也会参考进程的起始时间等
用户进程-->内核进程: 中断 & 系统调用

销毁

正常死亡,被信号杀死, SIGCHILD

这里大致涉及3个问题:

1. exit(), _exit()问题
2. do_exit()和父子关系更新问题
3. 退出和信号问题(abort异常终止问题,return返回问题)

关于第一点:
exit()和_exit()差别还是挺大的,主要的有定义的头文件,,以及调用系统调用的封装函数前是否调用相关的资源处理函数,以及是否会自动刷新一下缓冲区(如果是采用缓冲室IO读写文件资源的话)

关于第二点:
其实子进程,如果退出了,一定会通知父进程/或者同组的其他进程,大致关系如下:

其中sys_exit_group()会把进程组内的所有进程全部终止掉。而do_exit()则是删除所有对当前进程的引用。
你去看do_exit()就能发现进程推出前会发送exit_signal设置退出状态;如果已经成了孤儿,那么退出时状态为’EXIT_DEAD’,否则(并且父进程没有wait())设置为EXIT_ZOMBIE

关于第三点:
异常退出的情况一般是两种:

  1. 收到内核发送过来的信号(kill或者raise)
  2. abort()
    之后执行内核中的一段代码,清理相关的资源。(这一点可以看一下信号中内核是如何实现信号的处理的;凡是进入内核态,总是要找点儿理由的,你说呢?)

return语句是把当前的流程控制权返还给调用函数/调用者;
exit执行完毕是把控制权交给系统。

等待(回收子进程)

wait, waitpid;我觉得这个里面稍稍有意思的地方有两个:

  1. 同步阻塞还是异步(询问之后,直接返回)
  2. waitpid可以通过传递不同的option参数来指定不同的行为(pid>0, =0,pid<-1,=-1相当于wait(NULL))

可能还要注意一下返回值,或者那个传出参数status相关状态的判断(系统定义了一些列的宏)但是不管哪一种,都是用来避免僵尸进程的(先阻塞自己,然后看看有没有已经结束的子进程)。

下面直接写俩demo:

第一个代码wait, WEXITSTATUS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <stdio.h>
#include <unistd.h> //Need exit()
#include <sys/types.h> //Need wait()
#include <sys/wait.h> //Need wait()
#include <stdlib.h> //Need sleep()

int main(void)
{
pid_t pid = fork();
if(pid < 0){
perror("fork error");
exit(-1);
}else if(pid==0){
sleep(2);
return 1;
}else if(pid>0){

int status;
waitpid(pid, &status,0); //status = (result_of_child % 256)*256;
if(WIFEXITED(status)){
printf("the status of child : %d %d\n", status, WEXITSTATUS(status));
}
}

return 0;
}

第二个代码waitpid以及获取相关的状态:
参数:

WNOHANG     异步,没有子进程结束,就立即返回
WUNTRACED   子线程暂停立即返回,但是结束不予理会

(可以连在一起使用 WNOHANG | WUNTRACED , 还是想像wait一样阻塞使用, 请直接传0)

返回值:

  1. 正常如果回收到了,就返回收集到的子进程的id
  2. 如果返回0,多半是因为设置了WNOHANG,然后你需要轮寻
  3. 出错返回-1设置errno (特别的当子进程不存在,或者不是该进程的子进程,errno一般设置为ECHLD)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>



int main(void){
pid_t pid, wait_result;
pid = fork();

if(pid<0){
perror("fork error");
exit(-1);
}else if(pid ==0){
sleep(10);
exit(0);
}else if(pid >0 ){
do{
wait_result = waitpid(pid, NULL, WNOHANG);
if(wait_result == 0 ){
printf("No Child exited\n");
sleep(1);
}
}while(wait_result==0);
if(pid == wait_result){
printf("finally, u are done %d\n", wait_result);
}else{
printf("must sth wrong..\n");
}
}

return 0;
}

Linux特殊进程

孤儿

父进程如果先死亡了,子进程自然成了孤儿,这个时候一般系统会寻找其兄弟进程或者1号进程作为其父进程。
(当然后爹会为这些孤儿收尸的;其实成为孤儿不一定都是坏的,为什么?这相当于收尸的代价转移)

僵尸进程

理论上,只要子进程死亡了,那么他就是僵尸;如果有人为他收尸(不管他亲爹还是后爹),那么他就不再是僵尸;如果没有,那么内核就会保存其pcb维护的相关数据结构,但是不进行清理也不进行调度,就在内存中放着(如此一来系统资源就被长期耗着);你可以简单理解称,子进程已经退出,而父进程没有退出。僵尸进程是杀不死的,kill -9也是杀不死的,一般处理方式是,让父进程处理SIGCHILD信号;父进程显示回收,比如使用wait()相关的函数;或者比较野蛮的,直接杀死它的parent进程,让init进程去收拾它(但是init进程收拾它的时间可能不确定)

一般处理僵尸进程的方法和技巧:
1.杀死父进程
2.处理SIGCHILD函数使用wait
3.设置忽略该信号SIGCHLD, SIGIGN
4.fork两次,子进程的回收自己来处理(设置忽略即可), 把孙进程的回收代价交给init进程

但是如果你能避免,最好要避免。

守护进程

守护进程的特殊的地方在于它脱离了控制终端, 有了这一点,它就能在后台长期存在,周期性地执行某些任务。

关键的是守护进程的创建的一般步骤:

  1. 创建子进程作为守护进程,然后父进程退出(目的是让1号进程领养守护进程)
  2. 在子进程中创建新会话(从而脱离父进程的进程组,会话,以及终端)—这一步是脱离控制的关键,setsid() - run a program in a new session
  3. 改变当前目录为其他目录(比如根目录)—避免后期要进入有些目录有麻烦–chdir();
  4. 重新设置文件权限掩码(mask(0))
  5. 关闭文件描述符(至少和终端关联的0,1,2文件描述符是没有存在的必要了)

其中setsid的作用:

  1. 让进程摆脱原来会话的控制
  2. 让进程摆脱原进程组的控制
  3. 让进城摆脱原控制终端的控制

补充概念:

终端
与用户交互的界面。
终端的概念, pst, tty问题, 终端的实现原理

进程组
进程组是进程和集合。组长的ID就是进程组组ID,并且组长的退出并不会影响进程组ID。

多个进程组是一个或者多个进程组的集合。
(简单说,从用户登录到用户退出,期间所有的进程都属于该会话时期)


Linux进程通信

Unix IPC: Signals, Pipes
Sytem V IPC: Shared Memory, Semaphore, Message Queues.

管道(Pipe)及有名管道(named pipe):管道可用于具有亲缘关系进程间的通信,有名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信;
信号(Signal):信号是比较复杂的通信方式,用于通知接受进程有某种事件发生,除了用于进程间通信外,进程还可以发送信号给进程本身;linux除了支持Unix早期信号语义函数sigal外,还支持语义符合Posix.1标准的信号函数sigaction(实际上,该函数是基于BSD的,BSD为了实现可靠信号机制,又能够统一对外接口,用sigaction函数重新实现了signal函数);
消息队列(Message):消息队列是消息的链接表,包括Posix消息队列system V消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。
共享内存:使得多个进程可以访问同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。往往与其它通信机制,如信号量结合使用,来达到进程间的同步及互斥。
信号量(semaphore):主要作为进程间以及同一进程不同线程之间的同步手段。
套接口(Socket):更为一般的进程间通信机制,可用于不同机器之间的进程间通信。起初是由Unix系统的BSD分支开发出来的,但现在一般可以移植到其它类Unix系统上:Linux和System V的变种都支持套接字。

由于IPC本身是一个复杂的内容,以至于就专门为此写了一本书详细探索,我下面只是精华过一遍,加上一些核心代码。

进程件通信也是很重要的内容, 但是由于我的计划安排的时间已经到了,所以还是下次有时间在来补上。估计是一个月后。

IPC的优缺点:

管道:速度慢,容量有限,只有父子进程能通讯
有名管道(named pipe):任何进程间都能通讯,但速度慢
消息队列:容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题
信号量:不能传递复杂消息,只能用来同步
共享内存:能够很容易控制容量,速度快,但要保持同步,比如一个进程在写的时候,另一个进程要注意读写的问题,相当于线程中的线程安全,当然,共享内存区同样可以用作线程间通讯,不过没这个必要,线程间本来就已经共享了同一进程内的一块内存

信号

管道

共享内存

文件(mmap相关)

消息队列

信号量

semaphore对象的计数值大于0,为signaled状态;
计数值等于0,为nonsignaled状态. semaphore对象适用于控制一个仅支持有限个用户的共享资源。(同时控制多个事物的竞争和掠夺; 俗称PV原语)


Linux进程调度

优先级

实时性

小结

理论+实践,估计能说一本书。

从使用的角度来看, 貌似内容也不是太多, 创建/销毁,以及IPC。大概就是这三部分内容。

创建销毁相关

1.fork, exec, exit,vfork(可以不使用)
2.fork之后和父进程共享哪些,不共享哪些
3.wait回收问题,僵尸进程问题
4.多进程如何fork问题
5.do_fork, clone, copy_process问题
6.僵尸进程如何避免,如何处理(包括技巧)

IPC相关

  1. 信号,管道,共享内存,消息队列,信号量,或者文件的方式(包括mmap手动处理), socket。各自的优缺点,适用范围。

参考资料

  1. 《后台开发核心技术与应用实践》 chapter 10, 11
  2. 《Linux/Unix系统编程手册(上,下)》 chapter 6, 9, 12, 24, 25, 26, 27, 28, 35
  3. 《Unix环境高级编程(第3版)》 chapter 7, 8, 9, 13, 15, 17
  4. 《Linux系统编程》 chapter 5, 6 (author: ROBERT LOVE)
  5. 《Unix networking programming volume 2, 2e》 chapter (author: W.R.Stevens)
  6. 《Linux高级程序设计(第3版)》 chapter 3, 8, 9 ,10, 11
  7. 《Linux内核修炼之道》 chapter 7,8

Merlin
Sunday, 19. March 2016 11:24 PM

文章目录
  1. 1. 引言
  2. 2. Linux进程模型
    1. 2.1. 进程控制块(pcb)
    2. 2.2. 程序&进程内存模型
    3. 2.3. 进程凭证
    4. 2.4. /proc文件系统
  3. 3. Linux进程管理(控制)
    1. 3.1. 创建
    2. 3.2. 销毁
    3. 3.3. 等待(回收子进程)
  4. 4. Linux特殊进程
    1. 4.1. 孤儿
    2. 4.2. 僵尸进程
    3. 4.3. 守护进程
  5. 5. Linux进程通信
    1. 5.1. 信号
    2. 5.2. 管道
    3. 5.3. 共享内存
    4. 5.4. 文件(mmap相关)
    5. 5.5. 消息队列
    6. 5.6. 信号量
  6. 6. Linux进程调度
    1. 6.1. 优先级
    2. 6.2. 实时性
  7. 7. 小结
    1. 7.1. 创建销毁相关
    2. 7.2. IPC相关
  8. 8. 参考资料
|