Linux编程细节4-内核-进程管理

时间:2022-08-15 15:45:41

1 进程描述符

http://blog.chinaunix.net/uid-26497520-id-3608803.html

1.1 概述

一个进程就是处于执行期的程序,但进程不仅仅局限于一段可执行程序代码,通常进程还要包括其他资源,如 打开的文件、挂起的信号、内部数据、处理器状态、地址空间以及一个或多个执行线程(thread of execution)等,当然还包括用来 存储全局变量的数据段
执行线程,简称线程(thread),是在进程中的活动对象,每个线程拥有一个独立的程序计数器(即IP),进程栈和一组进程寄存器。 内核调度的对象是线程而不是进程
在现代操作系统中,进程提供两种虚拟机制:虚拟处理器和虚拟内存。虽然实际上可能是多进程正在分享一个处理器,但虚拟处理器给进程一种假象,让进程觉得自己在独享处理器;而虚拟内存让进程在获取和使用内存时感觉自己拥有整个系统所有的内存资源。
无疑,进程在它被创建的时刻开始存活,在Linux系统中,这通常是调用fork()系统调用的结果,该系统调用通过复制一个现有进程来创建一个全新的进程。在该调用结束时,在返回点这个相同位置上,父进程恢复执行,子进程开始执行。fork()系统调用从内核返回两次:一次回到父进程,另一次回到新诞生的子进程。通常新创建的进程都是为了立即执行新的、不同的程序,而接着调用exec*()家族函数就可以创建新的地址空间,将新程序载入。最终,程序通过exit()系统调用退出执行,这个函数会终结进程并将其占用的资源释放掉。父进程可以通过wait4()系统调用查询子进程是否终结,这其实使得进程拥有了等待特定进程执行完毕的能力。进程推出执行后被设置为僵死状态,直到父进程调用wait()或waitpid()为止。
Linux内核通常把进程也叫做 任务(task)

1.2 task_struct结构-进程描述符

内核把进程存放在叫做 任务队列的双向循环链表中,链表中的每一项都是类型为 task_struct、称为 进程描述符的结构,它包含一个具体进程的所有信息。在此我们将列出task_struct的部分成员进行分析和讲解。

struct task_struct {
volatile long state;
void *stack;
int prio, static_prio, normal_prio;
unsigned int policy;
int exit_state;
int exit_code, exit_signal;
pid_t pid;
pid_t tgid;
...

struct task_struct *parent;
struct task_struct *real_parent;
struct list_head tasks;
struct list_head children;
struct list_head sibling;
}


1.3 task_struct结构-各字段含义

http://blog.csdn.net/fdssdfdsf/article/details/7894211
假设当前进程为P:

real_parent:指向创建进程P的进程的描述符,如果P的父进程不存在,就指向进程1的描述符。
parent: 指向P的当前父进程,往往与real_parent一致。当出现Q进程向P发出跟踪调试ptrace()系统调用时,该字段指向Q进程描述符。
tasks: 将P连接到进程链表中。
children: P的子进程链表。
sibling: P的兄弟进程链表。
run_list:假设P状态为TASK_RUNNING,优先级为k,run_list将P连接到优先级为k的可运行进程链表中。

pid: P进程标识(PID)。
state: P进程状态,用set_task_state和set_current_state宏更改之,或直接赋值。
thread_info: 指向thread_info结构的指针。

group_leader: P所在进程组的领头进程的描述符指针
signal->pgrp: P所在进程组的领头进程的PID
tgid: P所在线程组的领头进程的PID
signal->session:P所在登录会话领头进程的PID
ptrace_children:一个链表头,链表中的所有元素是被调试器程序跟踪的P的子进程
ptrace_list: 当P被调试跟踪时,指向调试跟踪进程的父进程链表的前一个和下一个元素


1.4 task_struct结构-图例

linux通过slab分配器分配task_struct结构,这样能够达到对象复用和缓存着色(cache coloring)的目的,通过预先分配和重复使用task_struct,可以避免动态分配和释放所带来的资源消耗。在内核中,访问任务通常需要获得指向其task_struct的指针。实际上,内核中大部分处理进程的代码都是直接通过task_struct进行的。内核中current宏指向当前进程,因此通过current宏查找到当前正在运行的进程的进程描述符的速度就显得尤为重要。硬件体系结构不同哦,该宏的实现也不同,它必须针对专门的硬件体系结构做处理。有的硬件体系结构可以拿出一个专门寄存器来存放指向当前进程task_struct的指针,用于加快访问速度。而像x86这样的体系结构,其寄存器并不富余,就只能在内核栈(注意不是用户空间的栈)的尾端创建thread_info结构,通过计算偏移间接地查找task_struct结构。其中关系如图所示:

Linux编程细节4-内核-进程管理


2 内核栈

2.1 进程的堆栈

      每个进程都有自己的堆栈,内核在创建一个新的进程时,在创建进程控制块task_struct的同时,也为进程创建自己堆栈。一个进程有2个堆栈: 用户堆栈内核堆栈;用户堆栈的空间指向用户地址空间,内核堆栈的空间指向内核地址空间。当进程在用户态运行时,CPU堆栈指针寄存器指向用户堆栈地址,使用用户堆栈;当进程运行在内核态时,CPU堆栈指针寄存器指向的是内核栈空间地址,使用的是内核栈。

2.2 进程用户栈和内核栈之间的切换

1,用户态切换到内核态,如何获得栈顶指针?

答:当进程由于中断或系统调用从用户态转换到内核态时,进程所使用的栈也要从用户栈切换到内核栈。系统调用实质就是通过指令产生中断,称为软中断。进程因为中断(软中断或硬件产生中断),使得CPU切换到特权工作模式,此时进程陷入内核态,由于从用户态刚切换到内核态以后,进程的内核栈总是空的进程进入内核态后,首先把用户态的堆栈地址保存在内核堆栈中,然后设置内核栈地址(即空栈地址)为堆栈指针寄存器的地址,这样就完成了用户栈向内核栈的切换。

2内核态切换回用户态,如何获得栈顶指针?

答:当进程从内核态切换到用户态时,最后把保存在内核栈中的用户栈地址恢复到CPU栈指针寄存器即可,这样就完成了内核栈向用户栈的切换

2.3 thread_union结构-内核栈

以下union thread_union函数源码摘自2.6.34.14版本内核源码的include/linux/sched.h文件,这是对内核栈的定义。

union thread_union {
struct thread_info thread_info;
unsigned long stack[THREAD_SIZE/sizeof(long)];
};

注意永久分配给内核的内存区即内核栈(包括存储内核态的进程堆栈和线程描述符thread_info),占两个连续页框的8K空间,起始地址为2^13的倍数,大小为2^13即8K空间。这里相当于unsigned long stack[2408];

2.4 thread_union结构-thread_info

以下struct thread_info结构源码摘自2.6.34.14版本内核源码的arch/x86/include/asm/thread_info.h文件,thread_info的成员task指向当前进程。

struct thread_info {
struct task_struct *task; /* main task structure */
struct exec_domain *exec_domain; /* execution domain */
__u32 flags; /* low level flags */
__u32 status; /* thread synchronous flags */
__u32 cpu; /* current CPU */
int preempt_count; /* 0 => preemptable,<0 => BUG */
mm_segment_t addr_limit;
struct restart_block restart_block;
void __user *sysenter_return;
#ifdef CONFIG_X86_32
unsigned long previous_esp; /* ESP of the previous stack in case of nested (IRQ) stacks */
__u8 supervisor_stack[0];
#endif
int uaccess_err;
};

2.5 内核栈的产生

在陷入内核后,系统调用中也是存在函数调用和自动变量,这些都需要栈支持。用户空间的栈显然不安全,需要内核栈的支持。此外,内核栈同时用于保存一些系统调用前的应用层信息(如用户空间栈指针、系统调用参数)。

在进程被创建的时候,fork族的系统调用中会分别为内核栈和struct task_struct分配空间,调用过程是:
fork族的系统调用: fork--->do_fork--->copy_process--->dup_task_struct
在dup_task_struct函数中:

static struct task_struct *dup_task_struct(struct task_struct *orig)
{
struct task_struct *tsk;
struct thread_info *ti;

prepare_to_copy(orig);

tsk = alloc_task_struct(); //分配task_struct
if (!tsk)
return NULL;

ti = alloc_thread_info(tsk); //分配thread_info
if (!ti) {
free_task_struct(tsk);
return NULL;
}

*ti = *orig->thread_info;
*tsk = *orig;
tsk->thread_info = ti; //关联
ti->task = tsk; //关联

atomic_set(&tsk->usage,2);
return tsk;
}
其中:

alloc_task_struct使用内核的slab分配器去为所要创建的进程分配struct task_struct的空间
alloc_thread_info使用内核的伙伴系统去为所要创建的进程分配内核栈union thread_union 的空间

3 关联函数

3.1 获取当前thread_info-current_thread_info函数

以下current_thread_info函数源码摘自2.6.34.14版本内核源码的arch/x86/include/asm/thread_info.h文件,current_thread_info获得当前thread_info

/* how to get the current stack pointer from C */
register unsigned long current_stack_pointer asm("esp") __used;


/* how to get the thread information struct from C */
static inline struct thread_info *current_thread_info(void)
{
return (struct thread_info *)
(current_stack_pointer & ~(THREAD_SIZE - 1));
}

这里的THREAD_SIZE为2^13,因此表达式的含义即将栈顶esp的值0x015fa878将低13位清零(&0xffffe000),即为thread_info结构的起始地址0x015fa000。

3.2 获取当前task_struct-current宏

以下current宏源码摘自2.6.34.14版本内核源码的include/asm-generic/current.h文件,current调用current_thread_info从而获得指向当前进程描述符的指针

#define get_current() (current_thread_info()->task)
#define current get_current()

注意,程序一般在用户空间执行,当一个程序执行了系统调用或者触发了某个异常时,它就陷入了内核空间,此时,我们称内核“代表进程执行”并处于进程上下文中。只有在此上下文中current宏才是有效的。

3.3 pidhash表(通过pid查找对应的task_struct)

有的情况下,内核必须能从进程PID导出对应的进程描述符,比如当进程P1希望向进程P2发生一个信号时,P1调用kill(),其参数是P2的PID内核从这个PID到处其对应的进程描述符,然后从P2的进程描述符中取出记录挂起信号的数据结构指针。

顺序扫描进程描述符表并检查进程描述符的id字段是可行但相对低效的,为了加速查找,引入了4个散列表:

Linux编程细节4-内核-进程管理

内核初始化期间,动态地为4个散列表分配空间,并把它们的地址存入pid_hash数组。散列函数并不总能确保PID与表的索引一一对应,两个不同的PID散列到相同的表索引称为冲突。Linux利用链表来处理冲突的PID:每一个表项是由冲突的进程描述符组成的双向链表。

Linux编程细节4-内核-进程管理


4 进程的状态

内核中定义了如下几种进程状态(摘自2.6.34.14版本内核源码的include/linux/sched.h文件):

/*
* Task state bitmask. These bits are also
* encoded in fs/proc/array.c: get_task_state().
*
* We have two separate sets of flags: task->state
* is about runnability, while task->exit_state are
* about the task exiting. Confusing, but this way
* modifying one set can't modify the other one by
* mistake.
*/
#define TASK_RUNNING 0
#define TASK_INTERRUPTIBLE 1
#define TASK_UNINTERRUPTIBLE 2
#define TASK_STOPPED 4
#define TASK_TRACED 8
/* in tsk->exit_state */
#define EXIT_ZOMBIE 16
#define EXIT_DEAD 32
/* in tsk->state again */
#define TASK_DEAD 64
#define TASK_WAKEKILL 128
#define TASK_WAKING 256
#define TASK_STATE_MAX 512
由此可见,进程状态由一组标志组成,其中每个标志描述一种可能的状态,在当前Linux版本中,这些状态是 互斥的。进程描述符中的state字段和exit_state字段描述了进程当前所处的状态:

state可为TASK_RUNNING、TASK_INTERRUPTIBLE、TASK_UNINTERRUPTIBLE、TASK_STOPPED、TASK_TRACED等状态之一;

exit_state可为EXIT_ZOMBIE和EXIT_DEAD两种状态之一。只有当进程终止时,进程的状态才会变成这EXIT_ZOMBIE和EXIT_DEAD两种状态中的一种。

1 可运行状态(TASK_RUNNING)
——进程是可执行的;它或者正在执行,或者在运行队列中等待执行。
2 可中断的等待(TASK_INTERRUPTIBLE)
——进程正在休眠(也就是说它被阻塞);等待某些条件的达成。一旦这些条件达成,内核会把它设置为运行。处于此状态的进程也会因为收到信号而提前被唤醒
3 不可中断的等待(TASK_UNINTERRUPTIBLE)
——除了不会因为收到信号而被唤醒从而投入运行外,这个状态与TASK_INTERRUPTIBLE相同。这个状态通常在进程必须等待时不受干扰或等待事件很快就会发生时出现。由于处于此状态的任务对信号不作响应,所以较之TASK_INTERRUPTIBLE用得较少
4 暂停状态(TASK_STOPPED)
——进程停止执行;进程没有投入运行也不能投入运行。通常这种状态发生在接受到SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU等信号后。
5 跟踪状态(TASK_TRACED)
——进程的执行已由debugger程序暂停。当一个进程被另一个进程监控时(例如debugger执行ptrace系统调用监控一个测试程序),任何信号都可以把这个进程设置成这个状态。
6 僵死状态(EXIT_ZOMBIE)
——进程的执行被终止,但是父进程还没有调用wait4()或者waitpid()来获取该进程的信息。在父进程调用wait()类系统调用前,内核不能丢弃包含这死亡进程描述符中的数据,因为父进程可能还需要它。
7 僵死撤销状态(TASK_DEAD)
——最终状态;由于父进程刚发出wait4()或者waitpid()系统调用,因而进程由系统删除。为了防止其他执行线程在同一个进程上执行wait()类系统调用,而把进程的状态由EXIT_ZOMBIE改为EXIT_DEAD

Linux编程细节4-内核-进程管理

state字段的值通常使用一个简单的赋值语句设置,内核也使用set_task_state和set_current_state宏来设置指定进程和当前进程的状态,这些宏会设置内存屏障来保证编译程序或CPU控制单元不把赋值操作与其他指令混合,混合指令的顺序有时会导致灾难性的后果。


5 进程的组织

5.1 运行队列(TASK_RUNNING状态)

运行队列字段含义(包含在task_struct结构内):
struct task_struct  
{
...
struct list_head run_list; //构成运行队列
...
};

早期的Linux版本中,把所有TASK_RUNNING状态的进程放在一个运行队列中,这样,按照优先级排序该链表的开销比较大,早期的调度程序不得不遍历整个链表来选择最佳的进程。

Linux 2.6中的运行队列不同,系统中建立了多个可运行进程链表即运行队列中包含多个可运行进程链表。每个可运行进程链表对应一个优先级,优先级取值为0~139。假定某个进程优先级为k,那么该进程的task_struct结构中run_list字段就将其连接到优先级为k的可运行进程链表中(140个优先级队列)。另外,在多处理器系统中,每个CPU都有它自己的运行队列。这么多可运行进程链表由prio_array数据结构来管理。

#define MAX_PRIO140

struct prio_array {
unsigned int nr_active;
unsigned long bitmap[BITMAP_SIZE];
struct list_head queue[MAX_PRIO];
};

5.2 等待队列(TASK_INTERRUPTIBLE.TASK_UNINTERRUPTIBLE状态)

等待队列字段含义(在task_struct结构外):

//等待队列的数据结构
struct __wait_queue {
unsigned int flags;
#define WQ_FLAG_EXCLUSIVE0x01
void *private; //一般被赋值为task_struct类型的指针,等待队列就是通过此结构与一个进程相关联的
wait_queue_func_t func;
struct list_head task_list; //构成等待队列
};

typedef struct __wait_queue wait_queue_t;
typedef int (*wait_queue_func_t)(wait_queue_t *wait, unsigned mode, int flags, void *key);

//等待队列头
struct __wait_queue_head {
spinlock_t lock;
struct list_head task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;
flags标志进程是否互斥: flags为WQ_FLAG_EXCLUSIVE  时互斥,否则,非互斥。flags字段,决定了相关进程是互斥进程(flags = 1)还是非互斥进程(flags = 0)。这里解释下互斥进程与非互斥进程:非互斥进程总是由内核在事件发生时唤醒;互斥进程则是由内核在事件发生时有选择地唤醒,比如访问临界区的进程( 避免”惊群问题“ )。
func是一个函数指针,指向唤醒等待队列中进程的函数。prviate是传递给func的参数,用于指定所要唤醒的进程。

TASK_INTERRUPTIBLE、TASK_UNINTERRUPTIBLE状态的进程被分为很多类,每一类对应一个特定的事件。在这种状态下,进程状态无法提供足够的信息来快速的得到进程,因此引入额外的进程链表是必要的。这些链表称为“等待队列”。等待队列的用途很多,比如中断处理、进程同步、定时等。
等待队列头wait_queue_head_t的数据结构:该数据结构中有一个spinlock_t类型的lock变量,这是一个自旋锁,用来保证等待队列被互斥的访问和操作(因为等待队列不能被并行操作)。
等待队列元素wait_queue_t结构:该数据结构中有一个private字段,是一个进程描述符(task_struct)的指针;有一个func字段,是一个函数指针,表示进程的如何唤醒(即唤醒时调用该函数);

Linux编程细节4-内核-进程管理

5.3 其他状态(EXIT_ZOMBIE等)

对于其他状态的进程,Linux做如下处理:
TASK_STOPPED、EXIT_ZOMBIE、EXIT_DEAD状态的进程,Linux并没有为它们建立专门的链表,因为访问简单

5.4 tasks成员

首先要谈到的是tasks成员,它是struct list_head类型的,它将进程组织成下图所表示的形式:

Linux编程细节4-内核-进程管理

其中init_task是静态分配的init进程的进程描述符。进程的遍历可使用for_each_process,其定义如下(源码摘自2.6.34.14版本内核源码的include/linux/shced.h文件):

#define next_task(p) \
list_entry_rcu((p)->tasks.next, struct task_struct, tasks)

#define for_each_process(p) \
for (p = &init_task ; (p = next_task(p)) != &init_task ; )

5.5 parent成员

然后是parent成员,它是struct task_struct类型指针,它将进程组织成下图所表示的形式:

Linux编程细节4-内核-进程管理

通过parent指针任意一个进程可以一直向上追溯到它们共同的祖先init进程。

struct task_struct *task;

for(task = current; task != &init_task; task = task->parent)
;
/* task 现在指向 init */

5.6 children和sibling成员

最后是children和sibling成员,它们是struct list_head类型,它们将进程组织成下图所表示的形式:

Linux编程细节4-内核-进程管理

子进程的遍历可使用如下代码:

//遍历由链表头head指向的链表,并将每次结果返回给pos指向
#define list_for_each(pos, head) \
for (pos = (head)->next; prefetch(pos->next), pos != (head); \
pos = pos->next)

struct task_struct *task;
struct list_head *list;

//这里的list_for_each仅仅define了一个for而没有{},for后的{}现在接在后面
list_for_each(list, &current->children) {
//每次返回指向的list_head类型的指针list
//再用list_entry返回指向整个task_struct结构的指针task
task = list_entry(list, struct task_parent, sibling);
/* task 现在指向当前某个子进程 */
...
}

6 进程的创建

Linux中创建进程与其他系统有个主要区别,Linux中创建进程分2步:fork()和exec()。

6.1 fork函数

通过拷贝当前进程创建一个子进程

代码段:共享,新旧进程复用代码段
数据段:共享,但写时复制
堆栈段:共享,但写时复制

6.2 exec函数

读取可执行文件,将其载入到内存中运行

代码段:改写,新进程替换原来进程
数据段:改写,新开辟
堆栈段:改写,新开辟

6.3 创建的流程

1 调用dup_task_struct()为新进程分配内核栈,task_struct等,其中的内容与父进程相同。
2 check新进程(进程数目是否超出上限等)
3 清理新进程的信息(比如PID置0等),使之与父进程区别开。
4 新进程状态置为 TASK_UNINTERRUPTIBLE
5 更新task_struct的flags成员。
6 调用alloc_pid()为新进程分配一个有效的PID
7 根据clone()的参数标志,拷贝或共享相应的信息
8 做一些扫尾工作并返回新进程指针
9 创建进程的fork()函数实际上最终是调用clone()函数。

创建线程和进程的步骤一样,只是最终传给clone()函数的参数不同。

比如,通过一个普通的fork来创建进程,相当于:clone(SIGCHLD, 0)

6.4 init_task对象(静态构造进程链表头:init进程)

每个task_struct中都有一个tasks的域来链接到进程链表上去。而这个链表的头是init_task.它是0号进程(历史原因也叫做swapper进程)的PCB,0号进程永远不会被撤销,它被静态的分配到内核数据段上。也就是Init_task的PCB是由编译器预先分配的,在程序运行的过程中一直存在,直到程序结束。

构造init进程代码如下:

struct task_struct init_task = INIT_TASK(init_task);

#define INIT_TASK(tsk)\
{\
.state= 0,\
.stack= &init_thread_info,\
.usage= ATOMIC_INIT(2),\
.flags= PF_KTHREAD,\

...

.run_list = LIST_HEAD_INIT(tsk.run_list), \
.parent= &tsk,\
.real_parent= &tsk,\
.tasks= LIST_HEAD_INIT(tsk.tasks),\
.children= LIST_HEAD_INIT(tsk.children),\
.sibling= LIST_HEAD_INIT(tsk.sibling),\
...
}

6.5 copy_process函数(插入新的进程描述符task_struct)

新建一个进程(fork()->do_fork()函数),并且插入一个新的task_struct。

static struct task_struct *copy_process(...)
{
struct task_struct *p;

security_task_create(clone_flags);

p = dup_task_struct(current);
...
INIT_LIST_HEAD(&p->children);
INIT_LIST_HEAD(&p->sibling);

pid = alloc_pid(p->nsproxy->pid_ns);
p->pid = pid_nr(pid);

...
list_add_tail(&p->sibling, &p->real_parent->children);
list_add_tail_rcu(&p->tasks, &init_task.tasks);
...
return p;
}



7 进程的终止

和创建进程一样,终结一个进程同样有很多步骤:

子进程上的操作(do_exit):
1 设置task_struct中的标识成员设置为PF_EXITING
2 调用del_timer_sync()删除内核定时器, 确保没有定时器在排队和运行
3 调用exit_mm()释放进程占用的mm_struct
4 调用sem__exit(),使进程离开等待IPC信号的队列
5 调用exit_files()和exit_fs(),释放进程占用的文件描述符和文件系统资源
6 把task_struct的exit_code设置为进程的返回值
7 调用exit_notify()向父进程发送信号,并把自己的状态设为EXIT_ZOMBIE
8 切换到新进程继续执行
9 子进程进入EXIT_ZOMBIE之后,虽然永远不会被调度,关联的资源也释放掉了,但是它本身占用的内存(内核栈、thread_info结构、task_struct结构)还没有释放。这些由父进程来释放。

父进程上的操作(release_task):
父进程受到子进程发送的exit_notify()信号后,将该子进程的进程描述符和所有进程独享的资源全部删除。

孤儿进程:
如果子进程的父进程已经退出了,那么子进程在退出时,exit_notify()函数会先调用forget_original_parent(),然后再调用find_new_reaper()来寻找新的父进程
find_new_reaper()函数先在当前线程组中找一个线程作为父亲,如果找不到,就让init做父进程。(init进程是在linux启动时就一直存在的)


8 特殊进程概念

http://www.cnblogs.com/Anker/p/3271773.html

8.1 孤儿进程

孤儿进程,父进程先于子进程结束。一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。

Linux编程细节4-内核-进程管理

【危害】孤儿进程不会对系统造成危害。最经典的一个作用应该是避免产生僵尸进程,通过fork两次的方式,刻意杀掉其父进程,使之成为孤儿进程。

【构造孤儿进程】

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>

int main()
{
pid_t pid;
//创建一个进程
pid = fork();
//创建失败
if (pid < 0)
{
perror("fork error:");
exit(1);
}
//子进程
if (pid == 0)
{
printf("I am the child process.\n");
//输出进程ID和父进程ID
printf("pid: %d\tppid:%d\n",getpid(),getppid());
printf("I will sleep five seconds.\n");
//睡眠5s,保证父进程先退出
sleep(5);
printf("pid: %d\tppid:%d\n",getpid(),getppid());
printf("child process is exited.\n");
}
//父进程
else
{
printf("I am father process.\n");
printf("father process is exited.\n");
}
return 0;
}

运行结果:

Linux编程细节4-内核-进程管理


8.2 僵尸进程

僵尸进程,子进程先于父进程结束,而父进程未用wait或waitpid来监听子进程的状态,使子进程的资源无法释放。 

Linux编程细节4-内核-进程管理

8.2.1 原因

Linux的进程机制: 任何一个子进程(init除外)在进程退出(调用exit)之后,内核释放该进程所有的资源(打开的文件,占用的内存等)。但仍然为其保留一定的信息(内核栈、thread_info、进程描述符task_struct)。这时的状态即称为僵尸进程(Zombie),在进程列表(process table)中保留一个位置(slot),标记为defunct(PS中的Z)。这些信息(包含进程的退出状态信息)直到父进程通过wait / waitpid来读取时才释放。 因此僵尸状态是每个子进程必经的状态

8.2.2 危害

如果进程不调用wait / waitpid的话,那么保留的信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程. 此即为僵尸进程的危害,应当避免(所以应该用wait)。

8.2.3 构造僵尸进程

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>

int main()
{
pid_t pid;
//循环创建子进程
while(1)
{
pid = fork();
if (pid < 0)
{
perror("fork error:");
exit(1);
}
else if (pid == 0)
{
printf("I am a child process.\nI am exiting.\n");
//子进程退出,成为僵尸进程
exit(0);
}
else
{
//父进程休眠5s继续创建子进程
sleep(5);
continue;
}
}
return 0;
}

Linux编程细节4-内核-进程管理

8.2.4 避免方法

1,变为孤儿进程

在父进程结束之前,子进程可以一直保持僵尸状态,当父进程结束后,子进程成为孤儿进程,init进程就会负责回收僵尸子进程。(当然前提是父进程不要是服务器那种死循环进程)

2,两次fork法,获得子和孙,杀掉子

APUE P182个人认为避免僵尸进程这里需要两次fork的原因是这样的:如果fork一次,做成孤儿进程,当然可行【方法1】,但是里面要通过延时sleep函数来保证是孤儿进程而不是僵尸进程(通过sleep来保证父子死亡的顺序)。如果fork两次,那么里面完全不需要sleep函数了,因为父进程调用wait函数会阻塞到读到子进程退出状态为止。只要子进程退出了,那么孙进程立刻转为孤儿进程。不会出现僵尸进程状态。

Linux编程细节4-内核-进程管理

将孙进程弄成孤儿进程,由系统的init进程去领养这些孤儿进程,并将它们回收掉。init进程会自动wait其子进程(init进程被编写为无论何时只要有一个子进程终止,init就会调用一个wait函数获得其终止状态),因此被Init接管的所有进程都不会变成僵尸进程。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>

int main()
{
pid_t pid;
pid = fork();
if (pid < 0)
{
perror("fork error:");
exit(1);
}
//子进程
else if (pid == 0)
{
printf("I am the first child process.pid:%d\tppid:%d\n",getpid(),getppid());
pid = fork();
if (pid < 0)
{
perror("fork error:");
exit(1);
}
//子进程
else if (pid >0)
{
//子进程退出
printf("first process is exited.\n");
exit(0);
}
//孙进程
else
{
//睡眠3s保证子进程退出,这样孙进程被直接挂到init进程下
sleep(3);
printf("I am the second child process.pid: %d\tppid:%d\n",getpid(),getppid());
exit(0);
}
}
//父进程
else
{
//父进程处理子进程退出
if (waitpid(pid, NULL, 0) != pid)
{
perror("waitepid error:");
exit(1);
}
exit(0);
return 0;
}
}

Linux编程细节4-内核-进程管理

3,父进程wait/waitpid其子

进程一旦调用了wait,就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程, wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。

pid_t pid = fork();  //创建子进程
if(pid < 0)
{
perror("error:");
exit(1);
}
else if( pid == 0) //子进程代码段
{
......
exit(0);
}
else //父进程代码段
{
wait(NULL);
........
}

4,可以用signal函数为SIGCHLD安装handler

在一个进程终止或者停止时,会将SIGCHLD信号发送给其父进程。按系统默认将忽略此信号。如果父进程希望被告知其子系统的这种状态,则应捕捉此信号。信号的捕捉函数中通常调用wait函数以取得进程ID和其终止状态。这里可以用signal函数为SIGCHLD安装handler,在handler中调用wait回收。

#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>

static void sig_child(int signo);

int main()
{
pid_t pid;

//创建捕捉子进程退出信号
signal(SIGCHLD,sig_child);

pid = fork();
if (pid < 0)
{
perror("fork error:");
exit(1);
}
//子进程
else if (pid == 0)
{
printf("I am child process,pid id %d.I am exiting.\n",getpid());
exit(0);
}
//父进程
else
{
printf("I am father process.I will sleep two seconds\n");
//等待子进程先退出
sleep(2);
//输出进程信息
system("ps -o pid,ppid,state,tty,command");
printf("father process is exiting.\n");
return 0;
}
}

static void sig_child(int signo)
{
pid_t pid;
int stat;
//处理僵尸进程
while ((pid = waitpid(-1, &stat, WNOHANG)) >0)
printf("child %d terminated.\n", pid);
}

Linux编程细节4-内核-进程管理


5,父进程通知内核

如果父进程不关心子进程什么时候结束,那么可以用

signal(SIGCLD, SIG_IGN)

signal(SIGCHLD, SIG_IGN)

通知内核,自己对子进程的结束不感兴趣,,那么子进程结束后,内核会回收,并不再给父进程发送信号。

注意这里有这样几个区别:

APUE上SIGCLD语义写的有点不清楚,到底我们的系统是如何来处理SIGCLD信号呢?
1.SIG_DFL :默认的处理方式是不理会这个信号,但是也不会丢弃子进行状态,所以如果不用wait,waitpid对其子进行进行状态信息回收,会产生僵尸进程。
2.SIG_IGN :忽略的处理方式,在这种方式下,子进程状态信息会被丢弃,也就是自动回收了,所以不会产生僵尸进程,但是问题也就来了,wait,waitpid却无法捕捉到子进程状态信息了,如果你随后调用了wait,那么会阻塞到所有的子进程结束,并返回错误ECHILD,也就是没有子进程等待。
3.自定义处理方式:SIGCLD会立即检查是否有子进程准好被等待,这便是SIGCLD最大漏洞了,一旦在信号处理函数中加入了信号处理方式重建的步骤,那么每次设置SIGCLD处理方式时,都会去检查是否有信号到来,如果此时信号的确到来了,先是调用自定义信号处理函数,然后是调用信号处理方式重建函数,在重建配置的时候,会去检查信号是否到来,此时信号未被处理,会再次触发自定义信号处理函数,一直循环。所以在处理SIGCLD时,应该先wait处理掉了信号信息后,再进行信号处理方式重建。
   SIGCHLD在配置信号处理方式时,是不会立即检查是否有子进程准备好被等待,也不会在此时调用信号处理函数。

8.2.5  nop

xxx

8.3 nop

xxx

8.3,守护进程

8.3.1 守护进程概念

守护进程,又叫daemon进程,是Linux中的后台服务进程。他通常独立于控制终端并且周期性地执行某种任务或者等待处理某些发生的事件。
守护进程:生存期长,在系统引导装入时启动,在系统关闭时终止,没有控制终端(在top命令中tty显示为?),只在后台运行(注意不是&,而是完全与终端无关)。
守护进程脱离于终端是为了避免进程在执行过程中的信息在任何终端上显示并且进程也不会被任何终端所产生的终端信息所打断(比如Ctrl+D等信号)。守护进程程序的名称通常以字母“d”结尾(d表示daemon):例如,syslogd就是指管理系统日志的守护进程。所有守护进程的父进程都是init进程。

大多数守护进程都以超级用户(用户ID为0)特权运行。没有一个守护进程具有控制终端。内核守护进程以无控制终端方式启动。
用户层守护进程缺少控制终端可能是守护进程调用了setsid函数的结果。所有用户层守护进程都是进程组的组长进程以及会话的首进程,而且是这些进程组和会话中的唯一进程(APUE P342)

8.3.2 控制终端

一个会话期可以有一个单独的控制终端。这通常是我们在其上登录的终端设备(在终端登录情况)或伪终端设备(在网络登录情况)。建立与控制终端连接的对话期首进程,被称之为控制进程。如果一个会话期有一个控制终端,则它有一个前台进程组,其它进程组则为后台进程组。

通常,我们不必担心控制终端:当我们登录时,将自动为我们建立控制终端

SystemV系统:当会话组组长打开第一个尚未与一个对话期相关联的终端设备时,将此作为控制终端分配给此对话期。这假定对话期首进程在调用open时及有指定O-NOCTTY标志(3.3节)。
BSD系统:当对话期首进程以request参数为TIOCSCTTY调用ioctl时(第三个参数是空指针),BSD系统为会话期分配控制终端。为使此调用成功执行,此对话期不能已经有一个控制终端。(通常ioctl调用紧跟在setsid调用之后,setsid保证此进程是一个没有控制终端的对话期首进程。)


有时不管标准输入,标准输出是否重新定向,程序都要与控制终端交互作用。保证程序读写控制终端的方法是打开文件/dev/tty,在系统内核中,此特殊文件是控制终端的同义语。自然,如果程序没有控制终端,则打开此设备将失败。

控制终端可以接受一些特殊控制命令,比如Ctrl+C等信号。如果关闭对应的文件描述符,可以关闭控制终端,但是打开对应的文件描述符,只是纯文件的读写,不能恢复成“控制终端”了。

8.3.3 进程组vs会话组

一个或多个进程构成进程组,每个进程组有一个进程组组长一个或多个进程组构成会话组,一个会话组有一个会话首进程(建立与控制终端连接的该会话首进程也被称为控制进程)和一个前台进程组和任意多后台进程组
进程组:进程组由进程组ID来唯一标识。除了进程号PID之外,进程组ID也是一个进程的必备属性。每个进程组都有一个组长进程,其组长进程的进程号PID等于进程组ID,且该进程组ID不会因为组长进程的退出而受到影响。进程组的生命周期到组中最后一个进程终止, 或加入其他进程组为止 。进程组的作用:可以给一个进程组发送信号。(如果进程组组长被杀死,我的理解是只需要进程组ID,无需组长真的存在。对该进程组的存在没有任何影响。APUE P230)
会话组:通常,一个会话开始于用户登录,终止于用户退出,在此期间该用户运行的所有进程都属于这个会话期。会话组的生命周期到组中会话组组长终止为止(终端bash进程)

因此,若在bash中启动进程p,这时杀死bash,因为bash是p所在会话组的组长,因此杀死bash,p也被杀死。如果在bash中后台启动(&)进程p,这时杀死bash,同理p也被杀死
进程标识符(pid),进程组标识符(pgid),会话标识符(sid),前台进程组ID(tpgid),命令内容(comm)
ps -o pid,ppid,pgid,tpgid,sid,comm



Linux编程细节4-内核-进程管理
上图中,各个进程组如下所示:
  构造命令 进程组成员 前后台 进程组组长
会话领头进程   bash   bash
进程组1 proc5 | proc6 & proc5,proc6 后台进程组(可以多个) proc5
进程组1 proc7 | proc8 & proc7,proc8 后台进程组(可以多个) proc7
进程组2 proc4 | proc3  proc3,proc4 前台进程组(唯一一个) proc4

会话组信息如下:

  构造命令 会话组成员 会话组组长
会话组   会话领头进程,进程组1,进程组2,进程组3 bash

8.3.3 孤儿进程组

1 定义

孤儿进程组(orphan process group):该组中每个成员的父进程要么是该组中的一个成员,要么不是该组所属会话的成员。
对孤儿进程组的另一种描述:一个进程组不是孤儿进程组的条件是,该组中有一个进程,其父进程属于同一会话的另一个组中。  

2 说明

POSIX用一个session的概念来描述一次用户的登录以及该用户在此次登录后的操作,然后用作业的概念描述不同操作的内容,最后才用进程的概念描述不同操作中某一个具体的工作;其次,unix最初将所有的进程组织成了树的形式,这样就便于追踪每个进程也便于管理。因此所谓的孤儿进程组简单点说就是脱离了创造它的session控制的,离开其session眼线的进程组。unix中怎样控制进程,怎样证明是否在自己的眼线内,那就是树形结构了,只要处于以自己为根的子树的进程就是自己眼线内的进程,这个进程就是受到保护的,有权操作的,而在别的树枝上的进程原则上是触动不得的。

第一次登录shell构成一次会话,该次登录所启动的进程都挂在这个bash下,构成子树。当这次登录logout时,即会话bash终止,这时其未完成的各个进程组就构成了孤儿进程组,被init所接管。bash进程对其所管辖(session作用域)的所有进程组(孤儿进程组)先发SIGHUP再发SIGCOND。如果其中有进程处于SIGSTOP状态,接收到后默认处理为终止,但是完全可以忽略这个信号或者自己定义对该信号的反应,随后bash进程会发送的SIGCOND信号让这些孤儿进程组就继续安全的运行。再次以相同用户名登陆的话,由于之前用户的进程组都成了孤儿进程组,所以它再有恶意也不能控制它们了。

POSIX的规定是铁的纪律,铁的纪律要求作业控制要以session为基本,就是说不能操作别的session内的进程组,所以类似fg和bg等命令就不能操作孤儿进程,因此如果由于后台进程组由于读写终端被SIGSTOP信号停了,而后它又成了孤儿进程组的成员,那怎么办?别的session的作业控制命令又不能操作它,即使ps -xj找到了它然后手工发送了SIGCONT,那么它还是没法使用终端,这是POSIX的另一个纪律要求的,只有唯一和终端关联的session中的前台进程组的进程可以使用终端,因此只要有一个shell退出了,最好的办法就是将其session内的所有的进程都干掉,因此SIGHUP的原意就是如此,但是POSIX的基本限制就是session的ID是不能设置的,因为它是受保护的基本单位,但是进程组的ID是可以设置的,毕竟它只是区分了不能的作业,最后进程的PID也是不能设置的,因为它是进程的内秉属性,形成树形进程结构的关键属性。

3 示例代码

//《APUE》程序9-1:创建一个孤儿进程组
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <signal.h>
#include <stdlib.h>

//输出错误信息并退出
void error_quit(const char *str)
{
fprintf(stderr, "%s\n", str);
exit(1);
}

//处理SIGHUP信号
static void sig_hup(int signo)
{
printf("SIGHUP received, pid = %d\n", getpid());
}

static void pr_ids(char *name)
{
printf("%s: pid = %d, ppid = %d, pgrp = %d, tpgrp = %d, sid = %d\n",
name, getpid(), getppid(), getpgid(), tcgetpgrp(STDIN_FILENO), getsid(getpid()));
fflush(stdout);
}

int main()
{
char c;
pid_t pid;

pr_ids("parent");
pid = fork();
if( pid < 0 )
error_quit("fork error");
else if( pid > 0 )
{
sleep(5);
return 0;
}
else
{
pr_ids("child");
signal(SIGHUP, sig_hup);
kill(getpid(), SIGTSTP);
pr_ids("child");

//这里和书上说的不同,read函数能正常运行,不会发生错误(Ubuntu10.04)
int temp = read(STDIN_FILENO, &c, 1);
if( temp != 1 )
{
printf("read error from controlling TTY, "
"errno = %d\n", errno);
}
return 0;
}
}
结果说明:
xxx@xxx:~/xxx> ./xxx 
parent: pid = 17772, ppid = 7108, pgrp = 17772, tpgrp = 17772, sid= 7108
child: pid = 17773, ppid = 17772, pgrp = 17772, tpgrp = 17772, sid= 7108
SIGHUP received, pid = 17773
child: pid = 17773, ppid = 1, pgrp = 17772, tpgrp = 17772, sid= 7108
read error from controlling TTY, errno = 5
程序说明:
1:首先让父进程休眠5秒钟,让子进程在父进程终止之前运行。
2:子进程为SIGHUP信号建立信号处理程序,用于观察SIGHUP信号是否已经发送到子进程。
3:子进程用kill函数向自身发送SIGTSTP信号,模拟用终端停止一个前台作业。
4:父进程终止时,该子进程成为了一个孤儿进程,ppid=1。
5:现在,子进程成为一个孤儿进程组的成员。
6:父进程停止后,进程组成为了孤儿进程组,父进程会向新的孤儿进程组中处于停止状态的每个进程发送SIGHUP信号,接着又向其发送SIGCONT信号。
7:在处理了SIGHUP信号之后,子进程继续。对SIGHUP信号的默认动作是终止该进程,所以必须提供一个信号处理程序以捕捉该信号。

Linux编程细节4-内核-进程管理

8.3.4 守护进程编程规则

Linux编程细节4-内核-进程管理

1.第一次fork,创建子进程,父进程退出

功能:•所有工作在子进程中进行
•形式上脱离了控制终端
原因:第一次调用fork的目的是保证调用setsid的调用进程不是进程组长(setsid函数的特性,而setsid函数是实现与控制终端脱离的唯一方法);setsid函数使进程成为新会话的会话头和进程组长,并与控制终端断开连接;
fork一个子进程,杀死父进程。因为子进程继承了父进程了进程组ID,而其进程ID则是新分分配的,两者不可能相等,所以就保证了子进程不是一个进程组的组长。父进程退出后,使得子进程成为孤儿进程,孤儿进程运行一段时间后,由init进程统一接管。Linux编程细节4-内核-进程管理

2.子进程创建新会话

功能:•使用setsid()函数,创建新会话,并担任新会话组组长
•使子进程完全独立出来,脱离控制   
调用setsid()有以下3个作用:
        ●   让进程摆脱原会话的控制,成为新会话组组长
        ●   让进程摆脱原进程组的控制,成为新进程组组长
        ●   让进程摆脱原控制终端的控制
原因:注意调用此函数setsid的进程必须不是进程组的组长,否则会报错。使用 fork()创建的子进程全盘复制了父进程的会话期、进程组和控制终端等,虽然父进程退出了,但原先的会话期、进程组和控制终端等并没有改变,因此,还不是真正意义上的独立。而setsid()函数能够使进程完全独立出来,从而脱离所有其他进程的控制。这时新会话包含了唯一的一个新进程组,新进程组包含了唯一的该子进程。此时该子进程是新进程组组长也是新会话首进程。Linux编程细节4-内核-进程管理

3.第二次fork,创建孙进程,子进程退出(可选)

功能:•确保该守护进程不是会话首进程。
原因:第二次调用fork的目的是:确保该守护进程不是会话首进程。即使守护进程将来打开一个终端设备,也不会自动获得控制终端。这样防止该守护进程取得控制终端。因为控制终端与会话相关联。即使现在没有取得终端,只要是会话首进程,后期还是有可能取得的,这里直接杜绝掉,因为不是一个会话首进程,就绝对不会有机会分配到一个控制终端(这个不是终端这个“文件”,APUE P220最下面)。这里要注意忽略SIGHUP信号,使得孙进程成为孤儿进程组,被init收养。
struct sigaction    sa;
sa.sa_handler = SIG_IGN;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGHUP,&sa,NULL);
Linux编程细节4-内核-进程管理

4.改变当前目录为根目录

功能:
•chdir()函数
•防止占用可卸载的文件系统
•也可以换成其它路径
chdir("/");
原因:
使用fork()创建的子进程继承了父进程的当前工作目录。(当前工作目录是进程的资源之一,比如处理相对位置../)由于在进程运行过程中,当前目录所在的文件系统(如“/mnt/usb”等)是不能卸载的,这对以后的使用会造成诸多的麻烦(如系统由于某种原因需要进入单用户模式)。因此,通常的做法是让“/”作为守护进程的当前工作目录,这样就可以避免上述问题。当然,如有特殊需要,也可以把当前工作目录换成其他的路径,如/tmp。

5.重设文件权限掩码

原因:
•umask()函数
•防止继承的文件创建屏蔽字拒绝某些权限
•增加守护进程灵活性
umask(0);
原因:
使用fork()函数新建的子进程继承了父进程的文件权限掩码(文件模式创建屏蔽字),若不设置umask(0),则当守护进程要创建一个组可读、写的文件,而继承的文件模式创建屏蔽字可能屏蔽了这两种权限,于是所要求的组可读可写就无法实现(APUE P343)。

6.关闭不再需要的文件描述符

功能:
•继承的打开文件不会用到,浪费系统资源,无法卸载
•getdtablesize()
•返回所在进程的文件描述符表的项数,即该进程打开的文件数目
for(i=0;i<MAXFILE;i++)
{
 close(i);
}

原因:

使用fork()函数新建的子进程会从父进程那里继承一些已经打开的文件。这些被打开的文件可能永远不会被守护进程读或写,但它们一样消耗系统资源,而且可能导致所在的文件系统无法被卸载。守护进程已经与所属的控制终端失去了联系,因此,从终端输入的字符不可能达到守护进程,守护进程中用常规方法(如printf())输出的字符也不可能在终端上显示出来。所以文件描述符为0,1和2的3个文件(常说的输入/输出和报错这3个文件)已经失去了存在的价值,也应该被关闭。

某些守护进程打开/dev/null使其具有文件描述符0、1和2,这样,任何一个试图读写标准输入输出或标准出错的库例程都不会产生任何效果。

//因为所有文件描述符都先关闭了,所以默认从0打开 
fd0=open("/dev/null",O_RDWR);
fd1=dup(0);
fd2=dup(0);
Linux编程细节4-内核-进程管理
到第六步之前,所有的信息仍可以用printf打印出来,因为如上图所示,进程printf所涉及到的屏幕设备实际上在历次fork中都被继承下来了。只有到了这一部才被全部关闭。在此之后,printf完全失效。

8.3.5 守护进程代码示例

APUE P343
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
#include <signal.h>
#include <fcntl.h>
#include <syslog.h>
#include <sys/resource.h>

void daemonize(const char *cmd)
{
int i,fd0,fd1,fd2;
pid_t pid;
struct rlimit r1;
struct sigaction sa;

//1,设置文件创建掩码
umask(0);

//1,第一次fork
if((pid = fork()) < 0)
{
perror("fork() error");
exit(0);
}
else if(pid > 0)
{
//父进程退出
exit(0);
}
//2,创建新会话
setsid();
//3,第二次fork,创建孙进程避免获取终端
sa.sa_handler = SIG_IGN;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGHUP,&sa,NULL);
if((pid = fork()) < 0)
{
perror("fork() error");
exit(0);
}
else if(pid > 0)
{
//子进程退出
exit(0);
}
//4,修改当前工作目录
chdir("/");
//5,关闭不需要的文件描述符
getrlimit(RLIMIT_NOFILE,&r1);
if(r1.rlim_max == RLIM_INFINITY)
r1.rlim_max = 1024;
for(i=0;i<r1.rlim_max;++i)
close(i);
//6,重定向文件描述符012
fd0 = open("/dev/null",O_RDWR);
fd1 = dup(0);
fd2 = dup(0);
//日志操作
openlog(cmd,LOG_CONS,LOG_DAEMON);
if(fd0 != 0 || fd1 != 1 || fd2 != 2)
{
syslog(LOG_ERR,"unexpected file descriptors %d %d %d",fd0,fd1,fd2);
exit(1);
}
}

int main()
{
daemonize("ls");
sleep(30); //主进程休眠,以便查看守护进程状态
exit(0);
}
第一次调用fork的目的是保证调用setsid的调用进程不是进程组长。(而setsid函数是实现与控制终端脱离的唯一方法);setsid函数使进程成为新会话的会话头和进程组长,并与控制终端断开连接;
第二次调用fork的目的是:即使守护进程将来打开一个终端设备,也不会自动获得控制终端。(因为在SVR4中,当没有控制终端的会话头进程打开终端设备时,如果这个终端不是其他会话的控制终端,该终端将自动成为这个会话的控制终端),这样可以保证这次生成的进程不再是一个会话头。忽略SIGHUP信号的原因是,当第一次生成的子进程(会话头)终止时,该会话中的所有进程(第二次生成的子进程)都会收到该信号。

8.3.6 守护进程日志服务代码

日志服务函数
#include <syslog.h> 
void openlog(const char *ident, int option, int facility);
void syslog(int priority, const char *format, ...);
void closelog(void);
int setlogmask(int mask);

其中演示了如何使用syslog服务来写日志,因为守护进程的不与文件或终端关联,因此不能使之在标准错误上打印错误信息。
syslog函数: 
openlog ---打开连接
syslog ---写入消息
closelog---关闭连接

//Example: syslog_dameon.c
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<fcntl.h>
#include<sys/types.h>
#include<unistd.h>
#include<sys/wait.h>
#include<syslog.h>

#define MAXFILE 65535
int main()
{
pid_t pc;
int i,fd,len;
char *buf="This is a Dameon\n";
len =strlen(buf);
pc=fork();
if(pc<0)
{
printf("error fork\n");
exit(1);
}
//父进程
else if(pc>0)
{
exit(0); //1.创建子进程,父进程退出
}
//子进程
else
{
openlog("demo_update",LOG_PID, LOG_DAEMON);//启用日志服务

setsid(); //2.在子进程中创建新会话
chdir("/"); //3.改变当前目录为根目录
umask(0); //4.重设文件权限掩码
for(i=0; i<MAXFILE; i++) //5.关闭文件描述符
{
close(i);
}

while(1) //业务逻辑   
{
if((fd=open("/tmp/dameon.log",O_CREAT|O_WRONLY|O_APPEND,0600))<0) //open--以非缓存方式打开,
{
syslog(LOG_ERR, "open");//日志使用示例
exit(1);
}

write(fd, buf, len+1);
close(fd);
sleep(10);
}

closelog(); //关闭日志服务
exit(0);
}
}

8.3.7 单实例守护进程

为了正常运作,某些守护进程实现为单实例,即在任一时刻只运行该守护进程的一个副本。采用文件锁记录锁机制可以实现单实例守护进程,如果每一个守护进程创建一个文件,并且在整个文件上加上一把锁,那就只允许创建一把这样的写锁,之后试图再创建这样的一把写锁将会失败。这样就保证守护进程只有一个副本在运行。使用文件和记录锁保证只运行某守护进程的一个副本,守护进程的每个副本都试图创建一个文件,并将其进程ID写到该文件中。程序如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <signal.h>
#include <fcntl.h>
#include <syslog.h>
#include <sys/stat.h>

#define LOCKFILE "/var/run/daemon.pid"
#define LOCKMODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH )

int lockfile(int fd)
{
struct flock f1;
f1.l_type = F_WRLCK;
f1.l_start = 0;
f1.l_whence = SEEK_SET;
f1.l_len = 0;
return fcntl(fd,F_SETLK,&f1);
}

int already_running(void)
{
int fd;
char buf[16];
//打开文件,不存在则创建
fd = open(LOCKFILE,O-RDWR|O_CREAT,LOCKMODE);
if(fd < 0)
{
syslog(LOG_ERR,"can't open %s : %s",LOCKFILE,strerror(errno));
exit(1);
}
//对文件加锁
if(lockfile(fd)<0)
{
if(errno == EACCES | errno == EAGAIN)
{
close(fd);
return 1;
}
syslog(LOG_ERR,"can,t lock %s : %s",LOCKFILE,strerror(errno));
exit(1);
}
ftruncate(fd,0); //将文件长度截短为0
sprintf(buf,"%ld",(long)getpid());
write(fd,buf,strlen(buf)+1);
return 0;
}



8.3.8 守护进程重读配置文件

使用sigwait以及多线程 APUE P352
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <signal.h>
#include <fcntl.h>
#include <syslog.h>
#include <sys/stat.h>
#include <sys/resource.h>

#define LOCKFILE "/var/run/daemon.pid"
#define LOCKMODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH )

sigset_t mask;
int lockfile(int fd)
{
struct flock f1;
f1.l_type = F_WRLCK;
f1.l_start = 0;
f1.l_whence = SEEK_SET;
f1.l_len = 0;
return fcntl(fd,F_SETLK,&f1);
}
int already_running(void)
{
int fd;
char buf[16];

fd = open(LOCKFILE,O_RDWR|O_CREAT,LOCKMODE);
if(fd < 0)
{
syslog(LOG_ERR,"can't open %s : %s",LOCKFILE,strerror(errno));
exit(1);
}
if(lockfile(fd)<0)
{
if(errno == EACCES | errno == EAGAIN)
{
close(fd);
return 1;
}
syslog(LOG_ERR,"can,t lock %s : %s",LOCKFILE,strerror(errno));
exit(1);
}
ftruncate(fd,0);
sprintf(buf,"%ld",(long)getpid());
write(fd,buf,strlen(buf)+1);
return 0;
}

void daemonize(const char *cmd)
{
int i,fd0,fd1,fd2;
pid_t pid;
struct rlimit r1;
struct sigaction sa;
umask(0);
getrlimit(RLIMIT_NOFILE,&r1);
if((pid = fork()) < 0)
{
perror("fork() error");
exit(0);
}
else if(pid > 0)
exit(0);
setsid();
sa.sa_handler = SIG_IGN;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGHUP,&sa,NULL);
if((pid = fork()) < 0)
{
perror("fork() error");
exit(0);
}
else if(pid > 0)
exit(0);
chdir("/");
if(r1.rlim_max == RLIM_INFINITY)
r1.rlim_max = 1024;
for(i=0;i<r1.rlim_max;++i)
close(i);
fd0 = open("/dev/null",O_RDWR);
fd1 = dup(0);
fd2 = dup(0);
openlog(cmd,LOG_CONS,LOG_DAEMON);
if(fd0 != 0 || fd1 != 1 || fd2 != 2)
{
syslog(LOG_ERR,"unexpected file descriptors %d %d %d",fd0,fd1,fd2);
exit(1);
}
}

void reread()
{
printf("read daemon config file again.\n");
}
void * thread_func(void *arg)
{
int err,signo;
while(1)
{
sigwait(&mask,&signo);
switch(signo)
{
case SIGHUP:
syslog(LOG_INFO,"Re-reading configuration file.\n");
reread();
break;
case SIGTERM:
syslog(LOG_INFO,"got SIGTERM;exiting.\n");
exit(0);
default:
syslog(LOG_INFO,"unexpected signal %d.\n",signo);
}
}
return NULL;
}
int main(int argc,char *argv[])
{
pthread_t tid;
char *cmd;
struct sigaction sa;
if((cmd = strrchr(argv[0],'/')) == NULL)
cmd = argv[0];
else
cmd++;
daemonize(cmd);
if(already_running())
{
syslog(LOG_ERR,"daemon already running.\n");
exit(1);
}
sa.sa_handler =SIG_DFL;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGHUP,&sa,NULL);
sigfillset(&mask);
pthread_sigmask(SIG_BLOCK,&mask,NULL);
pthread_create(&tid,NULL,thread_func,0);
sleep(90);
exit(0);
}

8.3.9 守护进程的惯例

(1)若守护进程使用锁文件,那么该文件通常存放在/var/run目录中。如cron守护进程锁文件的名字是/var/run/crond.pid。
(2)若守护进程支持配置选项,那么配置文件通常存放在/etc中目录中。如syslogd守护进程的配置文件是/etc/syslog.conf。
(3)守护进程可以用命令行启动,通常是系统初始化脚本。
(4)若一守护进程有一配置文件,那么当该守护进程启动时,读取该文件,此后一把不会再查看它。

8.4 nop

xx

4 进程的堆栈空间

每一个进程都有自己的一个进程堆栈空间。在Linux界面执行一个执行码时,Shell进程会fork一个子进程,再调用exec系统调用在子进程中执行该执行码。
exec系统调用执行新程序时会把命令行参数和环境变量表传递给main函数,它们在整个进程堆栈空间中的位置如下图12-7所示。

Linux编程细节4-内核-进程管理

①    Text段(代码段,文本段):保存程序的执行码。在进程并发时,代码段是共享的且只读的,在存储器中只需有一个副本。
②    Data段(初始化数据段):它包含了程序中已初始化的全局变量、全局静态变量、局部静态变量。
例如,函数外定义的变量并赋值:int  count=30 ;此变量count存放在数据段中。
③    BSS段(未初始化数据段):它包含了程序中未初始化的全局变量、全局静态变量、局部静态变量,程序执行前操作系统将此段初始化为0。
例如,函数外定义的变量但没有赋值:long sum[1000] ;此变量存放在bss段中。
④    栈:程序执行前静态分配的内存空间,栈的大小可在编译时指定,Linux环境下默认为8M。栈段是存放程序执行时局部变量、函数调用信息、中断现场保留信息的空间。程序执行时,CPU堆栈段指针会在栈顶根据执行情况进行上下移动。
⑤    堆:程序执行时,按照程序需要动态分配的内存空间。malloc、calloc、realloc函数分配的空间都在堆上分配。


BSS段只保存没有值的变量,所以事实上它并不需要保存这些变量的映像。运行时所需要的BSS段大小记录在目标文件中,但BSS段并不占据目标文件的任何空间

Linux编程细节4-内核-进程管理


5 进程的pid

每个进程都会被分配一个唯一的数字编号,称为进程标识符或PID,它通常是一个范围从2到32768的正整数。当进程被启动时,系统将按顺序选择下一个未被使用的数字作为它的PID,当数字已经回绕一圈时,新的PID重新从2开始。数字1为特殊进程init保留,它负责管理其他的进程。所有其他的系统进程要么是由init进程启动,要么由被init进程启动的其他进程启动。