进程是正在运行的程序,包括下列部分的抽象:
这些东西都存在于进程控制块(PCB)中。在linux中,是struct task_struct
。
我们查看/proc/
目录,就能看到进程号为
的进程的相关信息。
一个进程如果想查看自己的相关信息,可以访问/proc/self
目录。
+-------------------------------------------------------------------+
| dr-x------ 2 tavi tavi 0 2021 03 14 12:34 . |
| dr-xr-xr-x 6 tavi tavi 0 2021 03 14 12:34 .. |
| lrwx------ 1 tavi tavi 64 2021 03 14 12:34 0 -> /dev/pts/4 |
+--->| lrwx------ 1 tavi tavi 64 2021 03 14 12:34 1 -> /dev/pts/4 |
| | lrwx------ 1 tavi tavi 64 2021 03 14 12:34 2 -> /dev/pts/4 |
| | lr-x------ 1 tavi tavi 64 2021 03 14 12:34 3 -> /proc/18312/fd |
| +-------------------------------------------------------------------+
| +----------------------------------------------------------------+
| | 08048000-0804c000 r-xp 00000000 08:02 16875609 /bin/cat |
$ ls -1 /proc/self/ | 0804c000-0804d000 rw-p 00003000 08:02 16875609 /bin/cat |
cmdline | | 0804d000-0806e000 rw-p 0804d000 00:00 0 [heap] |
cwd | | ... |
environ | +----------->| b7f46000-b7f49000 rw-p b7f46000 00:00 0 |
exe | | | b7f59000-b7f5b000 rw-p b7f59000 00:00 0 |
fd --------+ | | b7f5b000-b7f77000 r-xp 00000000 08:02 11601524 /lib/ld-2.7.so |
fdinfo | | b7f77000-b7f79000 rw-p 0001b000 08:02 11601524 /lib/ld-2.7.so |
maps -----------+ | bfa05000-bfa1a000 rw-p bffeb000 00:00 0 [stack] |
mem | ffffe000-fffff000 r-xp 00000000 00:00 0 [vdso] |
root +----------------------------------------------------------------+
stat +----------------------------+
statm | Name: cat |
status ------+ | State: R (running) |
task | | Tgid: 18205 |
wchan +------>| Pid: 18205 |
| PPid: 18133 |
| Uid: 1000 1000 1000 1000 |
| Gid: 1000 1000 1000 1000 |
+----------------------------+
线程是内核调度任务到CPU上运行的基本单位。一个线程有下列性质:
Linux对线程的实现有所不同。(线程和进程的)基本单元称为task,于是进程和线程对应的数据结构就是struct task_struct
,这个结构用于描述进程和线程。在struct task_struct
中,并不会记录资源,而是用指针指向对应的资源。
如下图,假如有两个线程在一个进程里(有相同的线程组ID,即PID),它们就会指向同一个描述资源的数据结构(比如打开的文件、地址空间、namespace)。如果两个线程不属于一个进程,那么它们指向的描述资源的数据结构必然不同。
一般来说,PID和TGID是相同的。但理论上操作系统内核可以为一个进程内的线程分配不同的TGID,只是在实际的Linux实现中通常没有这种情况。
clone()
在Linux中,开新线程pthread_create()
或者新进程fork()
的时候,都使用了clone()
系统调用:
int clone(int (*fn)(void *_Nullable), void *stack, int flags,
void *_Nullable arg, ... /* pid_t *_Nullable parent_tid,
void *_Nullable tls,
pid_t *_Nullable child_tid */ );
它允许调用者自己决定哪些资源可以被共享,主要通过flags组成的二进制掩码来向clone()
函数传达下列信息:
比如,用了这三个flag:CLONE_FILES | CLONE_VM | CLONE_FS就意味着开了一个线程。如果没用它们,就意味着开了个进程。
容器技术中主要使用cgroup和namespace实现资源的隔离。比如说,如果没有容器技术,那么所有的进程都可以在/proc
目录下看到。在容器中运行的进程就不对别的容器可见(或者可杀)了。
/*
* A structure to contain pointers to all per-process
* namespaces - fs (mount), uts, network, sysvipc, etc.
*
* The pid namespace is an exception -- it's accessed using
* task_active_pid_ns. The pid namespace here is the
* namespace that children will use.
*
* 'count' is the number of tasks holding a reference.
* The count for each namespace, then, will be the number
* of nsproxies pointing to it, not the number of tasks.
*
* The nsproxy is shared by tasks which share all namespaces.
* As soon as a single namespace is cloned or unshared, the
* nsproxy is copied.
*/
struct nsproxy {
atomic_t count;
struct uts_namespace *uts_ns;
struct ipc_namespace *ipc_ns;
struct mnt_namespace *mnt_ns;
struct pid_namespace *pid_ns_for_children;
struct net *net_ns;
struct time_namespace *time_ns;
struct time_namespace *time_ns_for_children;
struct cgroup_namespace *cgroup_ns;
};
在进程控制块中,
struct task_struct {
... ...
struct fs_struct *fs;
struct files_struct *files;
struct nsproxy *nsproxy; // 名称空间指针
... ...
};
上面是struct nsproxy
的结构,可以用于对不同类型的资源进行分隔(基于名称空间实现)。
目前,它支持IPC、网络(网络协议栈隔离,参考docker网络)、cg(计算资源使用隔离,比如CPU占比和mem上界)、mount(访问文件隔离)、PID(允许不同的Namespace下的进程可以有同一个PID)、时间的名称空间。
访问当前进程是一个频繁的操作,比如:
struct task_struct
结构体,即表示当前进程。 例如 current ->pid
就能得到当前进程的pid, current->comm
就能得到当前进程的名称。如下图,为了在多核环境下支持进程控制块的快速访问,每个CPU核都有一个变量,用来存储当前运行进程的控制块的指针:
另一种访问struct task_struct
结构体的方法是使用current宏。如下代码展示了current宏被用来进行进程控制块访问的细节。
/* how to get the current stack pointer from C */
register unsigned long current_stack_pointer asm("esp") __attribute_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));
}
#define current current_thread_info()->task
下图展示了linux内核做进程上下文切换的过程:
这里的T0指线程0,T1指线程1。
在上述流程中,比如用户线程调用了系统调用,首先进入了内核态,把用户态的CPU上下文写到了线程自己的内核栈上,然后调用了schedule()
方法,主动放弃CPU,进行上下文切换,切换到另一个线程,继续运行。
下图展示了task状态的变换逻辑。
TASK_INTERRUPTIBLE 和 TASK_UNINTERRUPTIBLE 有以下区别:
在 TASK_INTERRUPTIBLE 状态下,线程正在等待某个条件的满足,但它可以被一个信号中断(interrupt)而唤醒。
当线程进入 TASK_INTERRUPTIBLE 状态时,它会进入可中断的等待队列,等待条件的满足。
如果线程收到了一个信号,如 Ctrl+C 发送的 SIGINT,它会从睡眠状态中被唤醒,然后可以选择如何处理这个信号(比如直接结束进程)。
在 TASK_UNINTERRUPTIBLE 状态下,线程也在等待条件的满足,但它无法被信号中断。
当线程进入 TASK_UNINTERRUPTIBLE 状态时,它会进入不可中断的等待队列。
这种状态通常用于一些关键性的操作,例如文件系统的写操作。在这种情况下,即使线程收到了信号,也不能被中断,以确保关键操作的完整性。
阻塞当前线程是高性能的重要操作——在当前线程等待IO操作的时候,去运行别的线程。
为了完成阻塞步骤,需要:
我们可以调用wake_up
函数唤醒线程,它主要做:
#define wake_up(x) __wake_up(x, TASK_NORMAL, 1, NULL)
/**
* __wake_up - wake up threads blocked on a waitqueue.
* @wq_head: the waitqueue
* @mode: which threads
* @nr_exclusive: how many wake-one or wake-many threads to wake up
* @key: is directly passed to the wakeup function
*
* If this function wakes up a task, it executes a full memory barrier before
* accessing the task state.
*/
void __wake_up(struct wait_queue_head *wq_head, unsigned int mode,
int nr_exclusive, void *key)
{
__wake_up_common_lock(wq_head, mode, nr_exclusive, 0, key);
}
static void __wake_up_common_lock(struct wait_queue_head *wq_head, unsigned int mode,
int nr_exclusive, int wake_flags, void *key)
{
unsigned long flags;
wait_queue_entry_t bookmark;
bookmark.flags = 0;
bookmark.private = NULL;
bookmark.func = NULL;
INIT_LIST_HEAD(&bookmark.entry);
do {
spin_lock_irqsave(&wq_head->lock, flags);
nr_exclusive = __wake_up_common(wq_head, mode, nr_exclusive,
wake_flags, key, &bookmark);
spin_unlock_irqrestore(&wq_head->lock, flags);
} while (bookmark.flags & WQ_FLAG_BOOKMARK);
}
/*
* The core wakeup function. Non-exclusive wakeups (nr_exclusive == 0) just
* wake everything up. If it's an exclusive wakeup (nr_exclusive == small +ve
* number) then we wake all the non-exclusive tasks and one exclusive task.
*
* There are circumstances in which we can try to wake a task which has already
* started to run but is not in state TASK_RUNNING. try_to_wake_up() returns
* zero in this (rare) case, and we handle it by continuing to scan the queue.
*/
static int __wake_up_common(struct wait_queue_head *wq_head, unsigned int mode,
int nr_exclusive, int wake_flags, void *key,
wait_queue_entry_t *bookmark)
{
wait_queue_entry_t *curr, *next;
int cnt = 0;
lockdep_assert_held(&wq_head->lock);
if (bookmark && (bookmark->flags & WQ_FLAG_BOOKMARK)) {
curr = list_next_entry(bookmark, entry);
list_del(&bookmark->entry);
bookmark->flags = 0;
} else
curr = list_first_entry(&wq_head->head, wait_queue_entry_t, entry);
if (&curr->entry == &wq_head->head)
return nr_exclusive;
list_for_each_entry_safe_from(curr, next, &wq_head->head, entry) {
unsigned flags = curr->flags;
int ret;
if (flags & WQ_FLAG_BOOKMARK)
continue;
ret = curr->func(curr, mode, wake_flags, key);
if (ret < 0)
break;
if (ret && (flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
break;
if (bookmark && (++cnt > WAITQUEUE_WALK_BREAK_CNT) &&
(&next->entry != &wq_head->head)) {
bookmark->flags = WQ_FLAG_BOOKMARK;
list_add_tail(&bookmark->entry, &next->entry);
break;
}
}
return nr_exclusive;
}
int autoremove_wake_function(struct wait_queue_entry *wq_entry, unsigned mode, int sync, void *key)
{
int ret = default_wake_function(wq_entry, mode, sync, key);
if (ret)
list_del_init_careful(&wq_entry->entry);
return ret;
}
int default_wake_function(wait_queue_entry_t *curr, unsigned mode, int wake_flags,
void *key)
{
WARN_ON_ONCE(IS_ENABLED(CONFIG_SCHED_DEBUG) && wake_flags & ~WF_SYNC);
return try_to_wake_up(curr->private, mode, wake_flags);
}
/**
* try_to_wake_up - wake up a thread
* @p: the thread to be awakened
* @state: the mask of task states that can be woken
* @wake_flags: wake modifier flags (WF_*)
*
* Conceptually does:
*
* If (@state & @p->state) @p->state = TASK_RUNNING.
*
* If the task was not queued/runnable, also place it back on a runqueue.
*
* This function is atomic against schedule() which would dequeue the task.
*
* It issues a full memory barrier before accessing @p->state, see the comment
* with set_current_state().
*
* Uses p->pi_lock to serialize against concurrent wake-ups.
*
* Relies on p->pi_lock stabilizing:
* - p->sched_class
* - p->cpus_ptr
* - p->sched_task_group
* in order to do migration, see its use of select_task_rq()/set_task_cpu().
*
* Tries really hard to only take one task_rq(p)->lock for performance.
* Takes rq->lock in:
* - ttwu_runnable() -- old rq, unavoidable, see comment there;
* - ttwu_queue() -- new rq, for enqueue of the task;
* - psi_ttwu_dequeue() -- much sadness :-( accounting will kill us.
*
* As a consequence we race really badly with just about everything. See the
* many memory barriers and their comments for details.
*
* Return: %true if @p->state changes (an actual wakeup was done),
* %false otherwise.
*/
static int
try_to_wake_up(struct task_struct *p, unsigned int state, int wake_flags)
{
...
这种情况下,即使我们在跑系统调用,也可能被其他线程抢占。抢占的过程需要特殊的同步原语:preempt_disable和preempt_enable。
禁用抢占和自旋锁:为了简化在可抢占内核中的处理,并且考虑到在多处理器(SMP)情况下仍然需要同步机制,当使用自旋锁时,内核会自动禁用抢占。自旋锁是一种锁定机制,它会在某个线程或进程试图获取锁时一直自旋等待,而不是放弃CPU执行权。因此,为了避免多线程竞争条件,内核会禁用抢占,确保在持有自旋锁期间,当前执行的任务不会被抢占。
设置标志和重新启用抢占:如果在执行期间出现需要抢占当前任务的条件,例如当前任务的时间片已经用完,那么会设置一个标志(flag)。当抢占被重新启用时,例如通过执行自旋锁的解锁操作(spin_unlock()),内核会检查这个标志。如果需要抢占,那么调度器会被调用来选择一个新的任务来执行。这意味着内核会在自旋锁解锁的时候检查是否需要切换到其他任务,以确保任务的公平执行和时间片的分配。
我们说内核运行在进程上下文,如果内核在运行系统调用时。
在进程上下文中,我们可以用current宏访问当前进程的信息。
在进程上下文中我们可以sleep()(等待一个特定的条件)
在进程上下文中我们可以访问用户空间(除非我们在内核线程上下文中运行,此时都不涉及用户空间一说)
内核核心(kernel core)或设备驱动程序有时需要执行需要阻塞(即等待某些条件满足)的操作。这可能涉及等待硬件设备的数据就绪、等待其他内核线程的完成等。由于这些操作可能导致线程阻塞,因此内核需要一种机制来管理这些线程,以便它们能够以阻塞方式运行。
内核线程是一种特殊类别的任务,它们不会使用用户空间的资源。这意味着它们没有分配给它们的用户地址空间,不会打开用户空间的文件,也不会执行与用户空间相关的系统调用。内核线程的工作完全在内核空间进行,它们主要用于内核内部的任务。