进程相关知识
来源:互联网 发布:淘宝网时装 编辑:程序博客网 时间:2024/05/21 10:15
进程
(关于每CPU变量的详细解释可以参照《Linux设备驱动第三版》8.4节)
每个线程代表一个进程的执行流。
这里有个例子:就是象棋。其中一个线程是用来对棋盘进行控制,另一个线程是用来判断棋的策略的。然而,
如果这个过程仅仅只是一个进程,那么第一个线程在等待一个用户动作的时候并不能简单的对
分时系统调用进行判断。 这样的话,第二个线程将会被阻塞,而我们需要的是第一个线程应该不能被
阻塞。
我们需要的是轻量级进程对多线程进行支持。
我们现在所说的同步是针对用户而言的,并不是针对计算机体系而言。因为在单CPU上,是不可能实
现真正意义上 的同步的。一个实现多线程应用的简单方法就是要使一个轻量级进程关联于每个线程。
这样的话,线程能够通过 简单的分享同一内存地址获得同一组应用数据结构。
进程描述符(Process Descriptor)
进程状态(Process State)
set_task_state()和函数set_current_state()的解析
识别进程
进程描述符操作
alloc_thread_info()
free_thread_info()
进程之间的关系
如何组织进程
等待队列的操作
进程描述符(Process Descriptor)
一个进程描述符除了包括进程的一大堆属性外,还包括几个指向其他数据结构的指针。我们通常使用
task_struct
来表示一个进程描述符类型,当然,内核还使用类型为task_t来表示与task_struct相同类型的
结构。
进程状态(Process State)
首先要声明的一点就是:进程状态的每个值之间都是互斥的;
下面是对进程状态的列举:
TASK_RUNNING
TASK_INTERRUPTIBLE
发生硬件中断、释放一个进程等待所需要的资源、传递信号都是唤醒处于等待这一专题状态进程的条件。
TASK_UNINTERRUPTIBLE
处于这个状态的进程尽管收到一个信号,但是它的状态是不会改变的。这个进程状态标志很少使用。但是,
这里有个实例,需要使用这个状态标志。当一个进程打开一个设备文件并且这个对应的设备驱动开始为一个对应
的硬件设备进行探测时。这个设备驱动直到探测完毕才能被中断或者是硬件设备在一个不可预测的状态被遗弃了。
这个时候,很明显的,设备驱动能被中断了,因为相应的资源对象已经能使自己再进一
步的进行工作。
TASK_STOPPED
进程执行已经停止;这个进程在接受到SIGSTOP/SIGTSTP/SIGTTIN/SIGTTOU信号以后才会进入到这种状态。
TASK_TRACED
这个状态值主要是用在,当进程被一个调试程序所停止。当一个进程被另外的一个监视时,每个信号将这个
进程设置为此状态。
EXIT_ZOMBIE
僵死状态,当父进程没有使用wait4()等其他相关的函数时,由于子进程的资源没有被释放,父进程可能需要使用,
这个时候,进程就会处于这样的状态。
EXIT_DEAD
这个状态和EXIT_ZOMBIE相类似,只是,处于这个状态的进程,是因为其父进程使用了wait()这样的一个函数来释放资源。
下面是对一个进程的状态赋值的表示方法:p->state=TASK_RUNNING;其中p表示选定的进程。当然,可以使用set_task_state和set_current_state宏:
这两个函数设置一个指定的进程或者是当前执行进程的状态。而且,这些宏保证这些任务操作在运行
的过程中的 是不会和系统的其他指令产生混淆的。这样的话就保证了执行程序的一定安全性。
下面是对函数set_task_state()和函数set_current_state()的解析。
函数Set_task_state()
#define __set_task_state(tsk, state_value) /
do { (tsk)->state = (state_value); } while (0)
//这个函数有别于set_taskl_state(tsk,state_value),因为前者
没有使用mb()这样的一个函数,而仅仅是设置了state这个变量值,对于保护内存事件发生的次序根本就没有执行。
所以,后者更加具有安全性。
#define set_task_state(tsk, state_value) /
set_mb((tsk)->state, (state_value))
这里要深入的解释函数set_mb((tsk)->state,(state_value)):
#define set_mb(var, value) do { var = value; mb(); }
while (0)
#define mb() __asm__ __volatile__ ("" :::
"memory")//这个函数所实现的功能就是barrior(),
功能是PC采用内存一致性模型,使用mb强加的严格的CPU内存事件次序,保证程序的执行看上去就象是遵循顺序
一致性(SC)模型,当然,即使对于UP,由于内存和设备见仍然有一致性问题,这些MB也是必须的。
Set_current_state()函数:
下面是对set_current_state()函数的一个简要的解析:
/*
* set_current_state() includes a barrier so that the
write of current->state
* is correctly serialised wrt the caller's subsequent
test of whether to
* actually sleep:
*
* set_current_state(TASK_UNINTERRUPTIBLE);
* if (do_i_need_to_sleep())
* schedule();
*
* If the caller does not need such serialisation then
use __set_current_state()
*/
#define __set_current_state(state_value) /
do { current->state = (state_value); } while
(0)//这个函数的两者类似于上面的函数
#define set_current_state(state_value)
set_mb(current->state, (state_value))
//这个函数的功能和set_task_state()这一函数的功能是基本上一致的,只是两者所作用的对象的不同而已:
此函数对应的是对所选定的进程进行设置,而后者是对当前进程进行设置。/
识别进程
内核为了识别进程,给每个进程分配了一个PID,这个值能够使内核更加容易地去操作进程。
当然PID有最大值和最小值。但是,超级用户可以更改这个值。内核通过位图pidmap_array bitmap来探
测PID的使 用与否。Linux使用线程组,由于线程组中的线程有一个共同的PID,所以,只要内核给线
程组发送一个信号,整 个线程组就共同使用一个PID来进行操作。
整个线程组的PID为线程组的第一个线程的PID。那也是第一个轻量级进程的PID。这个PID保存在进程描述符的tgid域中。
进程描述符操作
每个进程描述符别存放在动态的存储单元中,而不是存放在静态的存储单元中。这样
内核使用alloc_thread_info()和free_thread_info()宏分配和释放存储thread_info()结构和内核的内存区。
下面来分析alloc_thread_info()函数和free_thread_info()函数
1、alloc_thread_info()
#define alloc_thread_info(tsk) ((struct thread_info *)
__get_free_pages(GFP_KERNEL,1))
# define __get_free_pages(x,y) ((unsigned
long)mmap(NULL, PAGE_SIZE << (y), PROT_READ|PROT_WRITE,
MAP_PRIVATE|MAP_ANONYMOUS, 0, 0))
上面的函数mmap()是用来将一个文件或者其它对象映射到内存。文件被映射到多个页上,如果文件的
大小不是所有 页的大小之和,最后一个页不被使用的空间将会清0。Munmap函数执行相反的操作,删
除特定地址区域的对象映射。
下面是对这个函数mmap的解释:
用法:
#include
void *mmap(void *start, size_t length, int prot, int
flags, int fd, off_t offset);
int munmap(void *start, size_t length);
参数:
start:映射区的开始地址。
length:映射区的长度。
prot:期望的内存保护标志,不能与文件的打开模式冲突。是以下的某个值,可以通过or运算合理地组合在一起
PROT_EXEC //页内容可以被执行
PROT_READ //页内容可以被读取
PROT_WRITE //页可以被写入
PROT_NONE //页不可访问
flags:指定映射对象的类型,映射选项和映射页是否可以共享。它的值可以是一个或者多个以下位的组合体
MAP_FIXED //使用指定的映射起始地址,如果由start和len参数指定的内存区重叠于现存的映射空间,
重叠部分将 会被丢弃。如果指定的起始地址不可用,操作将会失败。并且起始地址必须落在页的边界
上。
MAP_SHARED //与其它所有映射这个对象的进程共享映射空间。对共享区的写入,相当于输出到文件。
直到msync() 或者munmap()被调用,文件实际上不会被更新。
MAP_PRIVATE //建立一个写入时拷贝的私有映射。内存区域的写入不会影响到原文件。这个标志和以
上标志是互斥的,只能使用其中一个。
MAP_DENYWRITE //这个标志被忽略。
MAP_EXECUTABLE //同上
MAP_NORESERVE //不要为这个映射保留交换空间。当交换空间被保留,对映射区修改的可能会得到
保证。当交换 空间不被保留,同时内存不足,对映射区的修改会引起段违例信号。
MAP_LOCKED //锁定映射区的页面,从而防止页面被交换出内存。
MAP_GROWSDOWN //用于堆栈,告诉内核VM系统,映射区可以向下扩展。
MAP_ANONYMOUS //匿名映射,映射区不与任何文件关联。
MAP_ANON //MAP_ANONYMOUS的别称,不再被使用。
MAP_FILE //兼容标志,被忽略。
MAP_32BIT
//将映射区放在进程地址空间的低2GB,MAP_FIXED指定时会被忽略。当前这个标志只在x86-64平台上得到支持。
MAP_POPULATE //为文件映射通过预读的方式准备好页表。随后对映射区的访问不会被页违例阻塞。
MAP_NONBLOCK //仅和MAP_POPULATE一起使用时才有意义。不执行预读,只为已存在于内存中的
页面建立页表入口。
fd:有效的文件描述词。如果MAP_ANONYMOUS被设定,为了兼容问题,其值应为-1。
offset:被映射对象内容的起点。
返回说明:
成功执行时,mmap()返回被映射区的指针,munmap()返回0。失败时,mmap()返回MAP_FAILED[其值
为(void *)-1],munmap返回-1。
errno被设为以下的某个值
EACCES:访问出错
EAGAIN:文件已被锁定,或者太多的内存已被锁定
EBADF:fd不是有效的文件描述词
EINVAL:一个或者多个参数无效
ENFILE:已达到系统对打开文件的限制
ENODEV:指定文件所在的文件系统不支持内存映射
ENOMEM:内存不足,或者进程已超出最大内存映射数量
EPERM:权能不足,操作不允许
ETXTBSY:已写的方式打开文件,同时指定MAP_DENYWRITE标志
SIGSEGV:试着向只读区写入
SIGBUS:试着访问不属于进程的内存区
2、free_thread_info()
#define free_thread_info(ti) free_pages((unsigned long)
(ti), 1)
# define free_pages(x,y) munmap((void *)(x),
(y)*PAGE_SIZE)
函数munmap()在上面已经解释了。
标志当前进程
从效率的观点看,刚才所讲的thread_info结构与内核态堆栈之间的紧密结合提供了主要的好处是:内
核很容易 从esp寄存器的值获得当前在CPU上正在运行进程的thread_info结构的地址。
双向链表
下面我们将会继续阐述内核跟踪系统中各种进程的细节,首先来了解双向链表的特殊数据结构的作用。
(有关 更加详细的解释请到IBM中国网站上去获取更加详细的有关Linux内核对双链表的使用)
对每个链表,必须实现一组原子操作:初始化链表,插入和删除一个元素,扫描链表等等。当然,这
样会很浪费资源, 因为要反复的进行没有多少用处的工作。
因此,Linux内核使用了list_head数据结构,字段next和prev分别表示通用双向链表向前向后的指针元
素。 新的链表是由LIST_HEAD(list_name)宏创建的。下面是该宏的具体定义和解释:
下面是对函数系列list_add(struct list_head * new,struct list_head
*head)的解释
#ifndef CONFIG_DEBUG_LIST
static inline void list_add(struct list_head *new,
struct list_head *head)
{
__list_add(new, head, head->next); //在head的后面插入结点
}
#else
extern void list_add(struct list_head *new, struct
list_head *head);
#endif
static inline void list_add_tail(struct list_head *new,
struct list_head *head)
{
__list_add(new, head->prev, head);
//在head的前面插入结点,这是一个很有意思的操作,只要你的心思没有完全花费在这个链表操作上面,你会被这个东西弄糊涂的哦!!
}
可以发现,上面的两个函数实现的作用一样的,都是在执行函数__list_add(struct list_head
*new,struct
list_head *head);,只是两者的作用结果突同而已。
下面就是对该函数进行解析:
#ifndef CONFIG_DEBUG_LIST
static inline void __list_add(struct list_head *new,
struct list_head *prev, struct list_head *next)
{
next->prev = new;
new->next = next;
new->prev = prev;
prev->next = new;
}//在考虑上面函数实现的过程中,使用作图或者是使用空间想象的办法来理解,其结果是在prev和
next之间插入new。
#else
extern void __list_add(struct list_head *new, struct
list_head *prev, struct list_head *next);
#endif
当然,Linux还支持另外的一种双向链表:即散列表;它与list_head有着明显的区别,因为它不是循环
链表。 对于散列表而言重要的是空间而不是在固定的时间内找到表中的最后一个元素。要注意的是散
列表不是循环列表。
进程链表
进程列表是一个双向链表,它将所有进程描述符链接起来。进程链表的头是init_task描述符,它是所谓
的0进程
或者swapper进程的进程描述符.init_task的tasks.prev字段指向链表中最后插入的进程描述符的
tasks字段。
SET_LINKS和REMOVE_LINKS宏分别用于从进程链表中插入和删除一个进程描述符,这些宏考虑了父子进程之间的关系。
下面分析这两个宏。
SET_LINKS:
REMOVE_LINKS:
还有一个宏:for_each_process.这个宏的功能是扫描整个进程链表,其定义中使用的
函数list_entry()是为了获得相应的结点值,这个函数定义如下:
/**
* list_entry - get the struct for this entry
* @ptr: the &struct list_head pointer.
* @type: the type of the struct this is embedded in.
* @member: the name of the list_struct within the
struct.
*/
#define list_entry(ptr, type, member) /
container_of(ptr, type, member)
//可以很容易地发现,list_entry函数就是container_of(pointer,type,member)
这里要详细地解析container_of(pointer,type,member)这个函数:
/* Copied here from <linux/kernel.h> - we're userspace.
*/
#define container_of(ptr, type, member) ({const typeof(
((type *)0)->member ) *__mptr = (ptr); (type *)( (char
*)__mptr - offsetof(type,member) );})
TASK_RUNNING状态的进程链表
提高调度程序运行速度的诀窍是建立多个可运行进程链表,每种进程优先权对应一个不同的链表。每
个task_struct描述符
包含一个list_head类型的字段run_list.如果进程的优先权等于k,run_list字段把该进
程链入优先权 K的可运行链表中。
进程之间的关系
Pidhash表以及链表
在几种情况下,内核必须能从进程的PID导出相应的进程描述符指针。例如:为killl()系统调用提供服
务时就会 发生这样的情况:当进程P1希望向另一个进程P2发送一个信号时,P1调用kill()系统调用,
其参数为P2的PID, 内核从这个PID导出其对应的进程描述符,然后从P2的进程描述符中取出记录挂
起信号的数据结构指针。(意思是说,每一个进程描述符中总是存在一个能够接收信号并且识别信号
的结构体)
顺序扫描进程链表并检查进程描述符的PID字段是可行但是相当低效的。为了加速查找,引入了4个
散列表。 需要4个散列表是因为进程描述符包含了表示不同类型PID的字段,而且每种类型PID 需要
它自己的散列表。 当我们知道了PID之后,接下来我们要在PID散列表中查找到相应的表项,那么就
需要使用表索引的转换来完成。
内核初始化期间动态地为4个散列表分配空间,并把它们的地址存入pid_hash数组。一个散列表的长度
依赖于可以使用的RAM的容量。
下面是pid_hash数组的定义:static struct hlist_head *pid_hash;
使用pid_hashfn宏转化为表索引,pid_hashfn宏定义为:
#define pid_hashfn(nr) hash_long((unsigned long)nr,
pidhash_shift)
下面是函数hash_long的源代码:
static inline unsigned long hash_long(unsigned long val,
unsigned int bits)
{
unsigned long hash = val;
#if BITS_PER_LONG == 64
/* Sigh, gcc can't optimise this alone like it does for
32 bits. */
unsigned long n = hash;
n <<= 18;
hash -= n;
n <<= 33;
hash -= n;
n <<= 3;
hash += n;
n <<= 3;
hash -= n;
n <<= 4;
hash += n;
n <<= 2;
hash += n;
#else
/* On some cpus multiply is faster, on others gcc will
do shifts */
hash *= GOLDEN_RATIO_PRIME;
#endif
/* High bits are more random, so use them. */
return hash >> (BITS_PER_LONG - bits);
}
上面的结构基本上等价于:
Unsigned long hash_long(unsigned long val,unsigned int
bits)
{
Unsigned long hash=val*0x9e370001UL;
Return hash>>(32-bits)
}
static int
pidhash_shift;//变量pidhash_shift是用来存放表索引的长度(以位为单位的长度)
当然,散列函数并不总是能确保PID与表的索引一一对应。两个不同的PID散列到相同的表索引称为冲突。
struct pid
{
atomic_t count;
/* Try to keep pid_chain in the same cacheline as nr for
find_pid */
int nr;
struct hlist_node pid_chain;
/* lists of tasks that use this pid */
struct hlist_head tasks[PIDTYPE_MAX];
struct rcu_head rcu;
};
接下来是对《深入理解LINUX内核》99页中的图进行分析(详细内容请参照此书):
这个图象给出了PIDTYPE_TGID类型散列表的例子。Pid_hash数组的第二个元素存放散列表的地址,也就是用
hlist_head结构的数组表示链表的头。在散列表第71项为起点形成的链表中,有两个PID号246和4351的进程描述符。
PID的值存放在pid结构的nr字段中,而pid结构在进程描述符中。这里有三个相互连接的NR=4351的是一个线程组。
我们来考虑线程组4351的PID链表:散列表中的进程描述符的pid_list字段中存放链表的头,同时每个PID链表中指向前
一个元素和后一个元素的指针也存放在每个链表元素的pid_list字段中。
下面是处理PID散列表的函数和宏:
Do_each_task_pid(nr,type,task);
#define do_each_pid_task(pid, type, task) /
do { /
struct hlist_node *pos___; /
if (pid != NULL) /
hlist_for_each_entry_rcu((task), pos___, /
&pid->tasks[type], pids[type].node) }
也许你不了解函数hlist_for_each_entry_rcu((task),pos_,&pid->tasks[type],pids[type].node),不过没关系啦!下面就为你介绍这个函数:
/**
* hlist_for_each_entry_rcu - iterate over rcu list of
given type
* @tpos: the type * to use as a loop cursor.
* @pos: the &struct hlist_node to use as a loop cursor.
* @head: the head for your list.
* @member: the name of the hlist_node within the struct.
*
* This list-traversal primitive may safely run
concurrently with
* the _rcu list-mutation primitives such as
hlist_add_head_rcu()
* as long as the traversal is guarded by
rcu_read_lock().
*/
#define hlist_for_each_entry_rcu(tpos, pos, head,
member) /
for (pos = (head)->first; /
rcu_dereference(pos) && ({ prefetch(pos->next); 1;}) &&
/
({ tpos = hlist_entry(pos, typeof(*tpos), member);
1;});pos = pos->next)
如何组织进程
对于不同的状态,需要不同的处理要求,Linux选择了下列方式之一:
没有为处于TASK_STOPPED/EXIT_ZOMBIE或者EXIT_DEAD状态的进程建立专门的链表。
没有为处
于、状态的的进程建立专门的链表。
等待队列
(希望等待特定事件的进程把自己放进合适的等待队列,并放弃控制权)
等待队列由双向链表实现,其元素包括指向进程描述符的指针。每个等待队列都有一个等待队列头,
即类型wait_queue_head_t的数据结构:
struct __wait_queue_head{
Spinlock_t lock;
Struct list_head task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;
因为等待队列是由中断处理程序和主要内核函数修改的,因此必须对其双向链表进行保护免对其进行同时访问。
所以,这里面就定义了锁来进行保护操作。其中task_list字段是等待进程链表的头。
等待队列链表中的元素类型为wait_queue_t:
struct __wait_queue {
unsigned int flags;
#define WQ_FLAG_EXCLUSIVE 0x01
void *private; //指向等待队列所对应的进程值,即私有的进程数据。
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 sync, void *key);
有两种睡眠过程:互斥进程(等待队列元素的flags字段为1)由内核有选择地唤醒,而非互斥进程
(flags为0)总是由内核在事件发生时唤醒。等待相关事件的过程就是非互斥的。
等待队列的操作
可以调用DECLARE_WAIT_QUEUE_HEAD(name)宏定义一个新的等待队列的头,他静态地声明一个叫
做name的等待队列的头
变量并对该变量的lock和task_list字段进行初始化。函数init_waitqueue_head()
可以用来初始化动态分配的等待队列的头变量。
下面一一介绍这些宏或者是函数:
1、DECLARE_WAIT_QUEUE_HEAD(name)
#define DECLARE_WAIT_QUEUE_HEAD(name) /
wait_queue_head_t name =
__WAIT_QUEUE_HEAD_INITIALIZER(name)
#define __WAIT_QUEUE_HEAD_INITIALIZER(name) { /
.lock = __SPIN_LOCK_UNLOCKED(name.lock), /
.task_list = { &(name).task_list, &(name).task_list } }
2、Init_waitqueue_head();
void init_waitqueue_head(wait_queue_head_t *q)
{
spin_lock_init(&q->lock);
INIT_LIST_HEAD(&q->task_list);
}
下面是对spin_lock_init()函数中的__spin_lock_init()函数的定义:
void __spin_lock_init(spinlock_t *lock, const char
*name, struct lock_class_key *key)
{
#ifdef CONFIG_DEBUG_LOCK_ALLOC
/*
* Make sure we are not reinitializing a held lock:
*/
debug_check_no_locks_freed((void *)lock, sizeof(*lock));
lockdep_init_map(&lock->dep_map, name, key, 0);
#endif
lock->raw_lock =
(raw_spinlock_t)__RAW_SPIN_LOCK_UNLOCKED;
lock->magic = SPINLOCK_MAGIC;
lock->owner = SPINLOCK_OWNER_INIT;
lock->owner_cpu = -1;
}///////////////////////
1、函数init_waitqueue_entry(q,p)如下所示初始化wait_queue_t结构的变量q:
static inline void init_waitqueue_entry(wait_queue_t *q,
struct task_struct *p)
{
q->flags = 0; //声明等待队列q为非互斥进程;
q->private = p; //将进程p赋给q->private成员;
q->func = default_wake_function;
//使用default_wake_function来唤醒非互斥进程p;
}
下面是函数default_wake_function的定义:
int default_wake_function(wait_queue_t *curr, unsigned
mode, int sync, void *key)
{
return try_to_wake_up(curr->private, mode, sync);
}
由此可以看出,default_wake_function(wait_queue_t *curr,unsigned
mode,int sync,void *key)函数为函数try_to_wake_up()的简单封装;
下面就对try_to_wake_up()函数进行讲解:
try_to_wake_up()函数通过把进程状态设置为TASK_RUNNIING,并把该进程插入本地CPU的运行队列
来唤醒睡眠 或者停止的进程。这个函数的参数有:
1、被唤醒进程的描述符指针(p);
2、可以被唤醒的进程状态掩码(state);
3、一个标志(sync),用来禁止被唤醒的进程抢占本地CPU上正在运行的进程。
这个函数执行以下的操作:
1、
调用函数task_rq_lock(p,&flags)禁用本地中断,并获得最后执行进程的CPU所拥有的运行队列
的rq锁。
2、 检查进程的状态p->state是否属于被当作参数传递给函数的状态掩码state,如果不是,就跳到第
9步终止函数。
3、 如果p->array字段不等于NULL,那么进程已经属于某个运行队列,因此跳到第8步;
4、 在多处理器系统中,该函数检查要被唤醒的进程是否应该从最近运行的CPU的运行队列迁移
到另外一个CPU 的运行队列。
5、
如果进程处于TASK_UNINTERRUPTIBLE状态,函数递减目标运行队列的nr_uninterruptible字
段,并把进程描述符的p->activated字段设置为-1。
6、 调用activate_task()函数,它依次执行下面的子步骤:
A、 调用sched_clock()获取以纳秒为单位的当前时间数。
B、 调用recalc_task_prio(),把进程描述符的指针和上一步计算出的时间截传递给它。
C、 设置p->activated字段的值
D、 使用在第6a步中计算的时间截设置p->timestamp字段
E、 把进程描述符插入活动进程集合;
7、 如果目标CPU不是本地CPU,或者没有设置sync标志,就检查可运行的新进程的动态优先级
是否比rq运行队列中当前进程的动态优先级高;
8、 把进程p->state字段设置为TASK_RUNNING状态;
9、 调用task_rq_unlock()来打开rq运行队列的锁并打开本地中断。
10、返回1(如果成功唤醒进程)或者0(如果进程没有被唤醒)。
- 进程相关知识
- 守护进程相关知识
- 进程的相关知识
- 进程相关知识
- 进程的相关知识
- 进程相关知识
- 进程的相关知识
- 进程、线程相关知识汇总
- 【Linux】进程相关知识总结
- 进程的相关知识总结
- 进程控制的相关知识
- Linux进程快照相关知识
- 多进程的相关知识
- 操作系统—进程相关知识
- linux之进程相关知识
- 进程相关知识的整理
- python多进程相关知识
- Linux进程阻塞的相关知识
- 主动维护
- 成功开发iPhone软件的10个步骤
- 关于遇到的问题的小结
- 教你一天不困的25种方法!!
- 优化程序统计信息
- 进程相关知识
- ARM芯片选型简易指南
- 你的成功在于每天养成的习惯
- 管理自动工作量资料档案库(AWR)
- HTTP Live Streaming (HLS) 视频直播技术
- 2011年3月6日,17:25:07
- 『已解决』expected expression before ‘struct’
- Adobe将支持HTTP流媒体直播 预示着ipad将可以用flash吗?
- 漫谈互联网产品商业需求文档(BRD)的设计