第3章进程管理(三)

3.4 线程在Linux中的实现

线程机制是现代编程技术中常用的一种抽象概念。该机制提供在同一程序内共享内存地址空间运行的一组线程。这些线程还可以共享打开的文件和其它资源。线程机制支持并发程序设计技术,在多处理器系统上,线程也能保证真正的并行处理。

Linux实现线程的机制非常独特。从内核角度,并没有线程这个概念。Linux把所有的线程当做进程来实现。内核没有准备特别的调度算法或是定义数据结构来表征线程。相反,线程仅仅被视为一个与其他进程共享某些资源的进程。每个线程都拥有唯一属于自己的task_struct,在内核中,线程看起来就像是一个普通的进程(只是线程和其他一些进程共享某些资源,如地址空间)。

上述线程机制的实现与Microsoft Windos或是Sun Solaris等操作系统的实现差异非常大。这些系统都在内核中提供了专门支持线程的机制(这些系统把线程称作轻量级进程)。轻量级进程这种叫法本身概括了Linux在此处与其他系统的差异。在其他的系统中,相较于重量级的进程,线程被抽象成一种耗费较少的资源,运行迅速的执行单元。对于Linux来说,线程只是一种进程之间共享资源的手段。例如,有一个包含四个线程的进程,在提供专门线程支持的系统中,通常会有一个包含指向四个不同线程的指针的进程描述符。该描述符负责描述像地址空间、打开的文件这样的共享资源。线程本身再去描述它独占的资源。相反,Linux仅仅创建四个进程并分配四个普通的task_struct结构。建立这四个进程时指定他们共享某些资源。

1、创建线程

线程的创建,在调用clone()时需要传递一些参数标志来指明需要共享的资源。表3-1列举这些clone()用到的参数标志及其作用,这些是在<linux/sched.h>中定义的。

clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0);

备注:上述代码产生的结果,只是父子进程共享地址空间、文件系统资源、文件描述符和信号处理程序。新建的进程和它的父进程就是所谓的线程。

一个普通的fork()的实现是:

clone(SIGCHLD, 0);

而vfork()的实现是:

vfork(CLONE_VFORK | CLONE_VM | SIGCHLD, 0);

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

2、内核线程

内核线程需要在后台执行一些操作。这种任务可以通过内核线程完成-独立运行在内核空间的标准进程。内核线程和普通的进程的区别在于内核线程没有独立的地址空间。只在内核空间运行,从来不切换到用户空间去。内核进程,可以被调度,也可以被抢占。

Linux会把一些任务交给内核线程去做。在Linux系统上运行ps -ef命令,可以看到内核线程,这些线程在系统启动时由另外一些内核线程创建。内核线程也只能由其他内核线程创建。内核是通过从kthreadd内核线程中衍生出所有新的内核线程来自动处理这一点的。在linux/kthread.h中声明接口,从现有内核线程中创建一个新的内核线程的方法如下:

struct task_struct *kthread_create(int (*threadfn)(void *data),
                   void *data,  const char namefmt[], ...);

新的任务由kthread内核进程通过clone()系统调用而创建的。新的进程将运行threadfn函数,给其传递的参数为data,进程会被命名为namefmt。新创建的进程处于不可运行状态,如果不调用wake_up_process()唤醒它,不会主动运行。创建一个进程并让它运行起来,可通过调用kthread_run()来实现。

/**
 * kthread_run - create and wake a thread.
 * @threadfn: the function to run until signal_pending(current).
 * @data: data ptr for @threadfn.
 * @namefmt: printf-style name for the thread.
 *
 * Description: Convenient wrapper for kthread_create() followed by
 * wake_up_process().  Returns the kthread or ERR_PTR(-ENOMEM).
 */
#define kthread_run(threadfn, data, namefmt, ...)               \
({                                       \
    struct task_struct *__k                           \
        = kthread_create(threadfn, data, namefmt, ## __VA_ARGS__); \
    if (!IS_ERR(__k))                           \
        wake_up_process(__k);                       \
    __k;                                   \
})

内核线程启动后就一直运行直到调用do_exit()退出,或者内核的其他部分调用kthread_stop()退出,传递给kthread_stop()的参数为kthread_create()函数返回的struct task_struct结构体的地址:

int kthread_stop(struct task_struct *k);

3.5进程的终结

进程终归是要终结的。当一个进程终结时,内核必须释放进程所占有的资源。一般来说,进程的析构是自身引起的。它发生在进程调用exit()系统调用时,既可能显式地调用这个系统调用,也可能隐式地从某个程序的主函数返回。当进程接受到它既不能处理也不能忽略的信号或异常时,它还可能被动地终结。不管进程怎么终结,该任务大部分都要靠do_exit()(定义在kernel/exit.c)来完成,要做下面这些工作:

1)将task_struct中的标志成员设置为PF_EXITING。

2)调用del_timer_sync()删除任一内核定时器。根据返回的结果,它确保没有定时器在排队,也没有定时器处理程序在运行。

3)如果BSD的进程记账功能是开启的,do_exit()调用acct_update_integrals()来输出记账信息。

4)然后调用exit_mm()函数释放进程占用的mm_struct,如果没有别的进程使用它们,就彻底释放它们。

5)接下来调用exit_sem()函数。如果进程排队等候IPC信号,它则离开队列。

6)调用__exit_files()和__exit_fs(),以分别递减文件描述符、文件系统数据的引用计数。如果某个引用计数的数值降为0,那么代表没有进程在使用相应的资源,此时可以释放。

7)接着把存放在task_struct的exit_code成员中的任务退出代码置为由exit()提供的退出代码,或者去完成任何其他由内核机制规定的退出动作。退出代码放在这里供父进程随时检索。

8)调用exit_notify()向父进程发送信号,给子进程重新找养父,养父为线程组中的其他线程或init进程,并把进程状态设成为EXIT_ZOMBIE。

9)do_exit()调用schedule()切换到新的进程。因为处于EXIT_ZOMBIE状态的进程不会再被调度,这是进程所执行的最后一段代码。do_exit()永不返回。

至此,与进程相关联的所有资源都被释放掉了。进程不可运行并处于EXIT_ZOMBIE退出状态。它占用的所有内存就是内核栈、thread_info结构和task_struct结构。此时进程存在的唯一目的就是向它的父进程提供信息。父进程检索到信息后,或者通知内核那是无关的信息后,由进程所持有的剩余内存被释放,归还给系统使用。

1、删除进程描述符

在调用do_exit()之后,线程已经僵死不能再运行,但是系统保留了其进程描述符。这样做可以让系统有办法在子进程终结后仍能获得信息。因此,进程终结时所需的清理工作和进程描述符的删除被分开执行。在父进程获得已终结的子进程的信息后,或者通知内核它并不关注那些信息后,子进程的task_struct结构才被释放。

wait()这一族函数都是通过唯一的一个系统调用wait4()来实现的。它的标准动作是挂起调用它的进程,直到其中的一个子进程退出,此时函数会返回该子进程的PID。此外,调用该函数时提供的指针会包含子函数退出时的退出代码。

当最终需要释放进程描述符时,release_task()会被调用,完成以下工作:

1)它调用__exit_signal(),该函数调用__unhash_process(),__unhash_process又调用detach_pid()从pidhash上删除该进程,同时也要从任务列表中删除该进程。

2)__exit_signal()释放目前僵死进程所使用的所有剩余资源,并进行最终统计和记录。

3)如果这个进程是线程组的最后一个进程,并且领头进程已经死掉,那么release_task()就要通知僵死的领头进程的父进程。

4)release_task()调用put_task_struct()释放进程内核栈和thread_info结构所占的页,并释放task_struct所占的slab告诉缓存。

至此,进程描述符和所有进程独享的资源就全部被释放掉了。

2、孤儿进程造成的进退维谷

如果父进程在子进程之前退出,必须有机制保证子进程能找到一个新的父亲,否则这些孤儿进程就会在退出时永远处于僵死状态,白白地耗费内存。对于这个问题,解决方法是给子进程在当前线程组内找一个线程作为父亲,如果不行,就让init进程做为它们的父亲。在do_exit()中会调用exit_notify(),该函数会调用forget_original_parent(),而forget_original_parent()会调用find_new_reaper()来寻找父过程(kernel/exit.c):

static struct task_struct *find_new_reaper(struct task_struct *father)
{
        struct pid_namespace *pid_ns = task_active_pid_ns(father);
        struct task_struct *thread;

        thread = father;
        while_each_thread(father, thread) {
                if (thread->flags & PF_EXITING)
                        continue;
                if (unlikely(pid_ns->child_reaper == father))
                        pid_ns->child_reaper = thread;
                return thread;
        }

        if (unlikely(pid_ns->child_reaper == father)) {
                write_unlock_irq(&tasklist_lock);
                if (unlikely(pid_ns == &init_pid_ns))
                        panic("Attempted to kill init!");

                zap_pid_ns_processes(pid_ns);
                write_lock_irq(&tasklist_lock);
                /*
                 * We can not clear ->child_reaper or leave it alone.
                 * There may by stealth EXIT_DEAD tasks on ->children,
                 * forget_original_parent() must move them somewhere.
                 */
                pid_ns->child_reaper = init_pid_ns.child_reaper;
        }

        return pid_ns->child_reaper;
}

分析:

这段代码视图找到进程所在的线程组内的其他进程。如果线程组内没有其他的进程,它就找到并返回的是init进程。现在,给子进程找到合适的养父进程了,只需要遍历所有子进程并为它们设置新的父进程。

reaper = find_new_reaper(father);

            list_for_each_entry_safe(p, n, &father->children, sibling) {
                struct task_struct *t = p;
                do {
                        t->real_parent = reaper;
                        if (t->parent == father) {
                                BUG_ON(task_ptrace(t));
                                t->parent = t->real_parent;
                        }
                        if (t->pdeath_signal)
                                group_send_sig_info(t->pdeath_signal,
                                                    SEND_SIG_NOINFO, t);
                } while_each_thread(p, t);
                reparent_leader(father, p, &dead_children);
        }

然后调用ptrace_exit_finish()同样进行新的寻父过程,不过这次是给ptraced的子进程寻找父亲。

kernel/ptrace.c

/*
 * Detach all tasks we were using ptrace on.
 */
void exit_ptrace(struct task_struct *tracer)
{
        struct task_struct *p, *n;
        LIST_HEAD(ptrace_dead);

        write_lock_irq(&tasklist_lock);
        list_for_each_entry_safe(p, n, &tracer->ptraced, ptrace_entry) {
                if (__ptrace_detach(tracer, p))
                        list_add(&p->ptrace_entry, &ptrace_dead);
        }
        write_unlock_irq(&tasklist_lock);

        BUG_ON(!list_empty(&tracer->ptraced));

        list_for_each_entry_safe(p, n, &ptrace_dead, ptrace_entry) {
                list_del_init(&p->ptrace_entry);
                release_task(p);
        }
}

分析:

这段代码遍历两个链表:子进程链表和ptrace子进程链表,给每个子进程设置新的父进程,是2.6内核的一个新特性。当一个进程被跟踪时,它的临时父亲设定为调试进程。此时,如果它的父进程退出了,系统会为它和它的所有兄弟重新找一个父进程。在之前的内核中,这就需要遍历系统所有的进程来找这些子进程。现在的解决办法是在一个单独的被ptrace跟踪的子进程链表中搜索相关的兄弟进程-用两个相对较小的链表减轻了遍历带来的消耗。

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
现代操作系统第四版是一本经典的操作系统教材,第主要讲解了进程的概念、进程控制块、进程状态以及进程调度等内容。以下是第的主要内容概述: 1.进程的概念:进程是程序在执行过程中分配和管理资源的基本单位,每个进程都有自己的地址空间、数据栈、指令计数器、寄存器和文件描述符等。 2.进程控制块:进程控制块是操作系统内核中用于管理进程的数据结构,包含了进程的状态、进程ID、优先级、程序计数器、寄存器、内存分配情况、打开文件列表等信息。 3.进程状态:进程状态包括运行态、就绪态、阻塞态和创建态等,进程在不同状态之间转换,操作系统根据进程状态来进行进程调度。 4.进程调度:进程调度是操作系统内核中的一个重要模块,负责决定哪个进程可以获得CPU的使用权,进程调度算法包括先来先服务、短作业优先、时间片轮转等。 5.进程同步:进程同步是指多个进程之间的协作,包括互斥、信号量、管程等机制,用于保证多个进程之间的正确性和一致性。 6.进程通信:进程通信是指多个进程之间的信息交换,包括共享内存、消息队列、管道等机制,用于实现进程之间的数据传输和共享。 以下是现代操作系统第四版第的相关代码示例: ```c #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> int main() { pid_t pid; int status; pid = fork(); if (pid < 0) { printf("Fork error\n"); exit(1); } else if (pid == 0) { printf("Child process\n"); exit(0); } else { printf("Parent process\n"); wait(&status); printf("Child exit status: %d\n", WEXITSTATUS(status)); } return 0; } ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值