一、进程
进程:处于执行期的程序。(资源分配的最小对象)
线程:进程中活动的对象。(CUP调度的最小单位)
Linux下,通常调用fork产生一个新的进程,fork实际调用clone()系统调用实现的,最终通过exit()或退出main函数结束。父进程,调用wait()获取已退出子进程的状态。
二、进程描述符及任务结构
Linux内核把进程存放在任务队列(task_struct)类型的双向循环链表中,task_struct类型的成员包括:打开的文件、进程地址空间、挂起的信号等等。
三、进程状态
- TASK_RUNNING(运行):处于执行期,或在执行队列中等待
- TASK_INTERRUPTIBLE(可中断):阻塞,正在等待某一条件的到达
- TASK_UNINTERRUPTIBLE(不可中断):除接受信号也不会唤醒外基本与上述状态相同,例如accept()系统调用
- __TASK_TRACED(被其他进程跟踪)
- __TASK_STOPED(停止):停止运行,没有运行也不能运行
四、进程家族树
Linux下所有进程都是PID为1的(initd)进程的子进程,RHEL7.0中为systemd进程
每个进程的task_struct结构体中都有指向父进程的指针(parent),以及指向子进程链表的指针(children)。并且可通过宏 next_task(task) 和 prev_task(task) 可实现,获取前一个和后一个进程。
五、进程创建
fork创建一个子进程,通过exec读入可执行文件,最后开始运行。知道调用exit()系统调用,获退出main函数。
传统的fork()系统调用直接把所有的资源复制给新进程。这种实现过于简单并且效率低,有可能拷贝的数据不用于共享,前功尽弃。Linux下的fork()系统调用实现了写时复制,顾名思义写时复制是一种推迟甚至免除拷贝数据的机制。调用完fork()之后子进程共享父进程数据,当需要写入时,才复制一份给子进程。如果调用完fork()后,立马调用exec(),则将免除拷贝数据。
vfork创建子进程后,父进程阻塞,等待子进程退出或者调用exec后,才开始运行。而fork后父进程与子进程执行先后不确定。vfork()系统调用一般用exec()装入其他程序进行运行。
fork的具体实现:
- 调用dup_task_struct()创建内核栈,thread_info、task_struct
- 检查当前进程是否达到最大子进程数
- 分开子进程,并初始化子进程
- 子进程设置为TASK_UNINTERRUPTIBLE状态
- 调用copy_flags()以更新task_struct中flags成员,是否为超级用户
- 调用alloc_pid()分配一个合法的pid
- 根据标志,copy_process()拷贝父进程打开的文件、信息、函数、空间等等
- 完成工作,返回子进程指针
六、线程
线程仅仅被视为一个与其他进程共享某些资源的进程,内核中,它就是普通的进程
线程的创建于普通的进程类似,只不过在调用clone()系统调用时,通过传递的参数表明需要共享的资源:
clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND,0)
fork的实现 clone(SIGHAND,0)
vfork的实现 clone(CLONE_VFORK | CLONE_VM | SIGHAND ,0)
内核线程:
没有独立的地址空间,只在内核空间运行。可通过ps -ef查看内核线程
内核线程通过kthread_create()创建,如果不用wake_up_process()唤醒它,它不会主动运行,可调用thread_run()达到目的。
内核线程终结1、调用do_exit()系统调用;2、调用kthread_stop()系统调用
七、进程终结
进程终结,内核必须释放它的资源,并告知其父进程
如果父进程在子进程退出前终结,子进程将成为孤儿进程,寄存在1号进程低下,所谓的守护进程的实现就是,父进程调用fork成功运行子进程后,自己退出。
终结方式:
- 调用exit()
- 退出main函数(C语言main函数返回后会隐式调用exit())
- 不能处理也不能忽略的信号或异常
终结所做的工作:
- 将task_struct结构中标志为PF_EXITING
- 调用del_timer_sync()系统调用删除内核定时器(确保没有在可执行队列也没有运行)
- BSD通过exit()调用acct_update_integrals()来输出记账
- 调用exit_mm()来释放空间
- 调用sem_exit()离开IPC队列
- 调用exit_files()和exit_fs()递减文件描述符和文件系统的引用计数,如果递减后为0,则释放
- 完成退出动作
- 调用exit_notify()找父进程,并发信号,并将自己状态改为EXIT_ZOMBIE(僵尸进程)
- do_exit()调用schedule()切换到新进程运行,do_exit()永不返回
- 完成终结,(此时还占内核栈、thread_info、task_struct结构)供父进程查看状态
删除进程描述符:
调用do_exit()后,进程将不在运行,但系统保留进程描述符。父进程可通过调用wait()来获取子进程状态。(wait()通过调用唯一的wait4()实现)。(调用wait()的进程会挂起,直到子进程退出)
完成的具体工作:
- 调用_exit_signal()删除任务列表中该任务
- 释放剩余资源
- 如果此进程为进程组中最后一个进程,并且领头进程已经僵死,则调用release_task()通知僵死进程的父进程
- release_task()调用put_task_struct()释放进程内核栈、和thread_info所占的页、以及task_struct所占用的slab高速缓存
至此,进程描述符和全部资源将释放。