做者:世至其美
博客地址:hqber.com
转载须注明以上信息, 更多文章,请访问我的博客:hqber.comlinux
进程是处于运行状态的程序和相关资源的总称,是资源分配的最小单位。缓存
线程是进程的内部的一个执行序列,是CPU调度的最小单位。多线程
Linux系统对于线程实现很是特殊,他并不区分线程和进程,线程只是一种特殊的进程罢了。从上面四点要素来看,拥有前三点而缺第四点要素的就是线程,若是彻底没有第四点的用户空间,那就是系统线程,若是是共享用户空间,那就是用户线程。并发
进程做为分配资源的基本单位,而把线程做为独立运行和独立调度的基本单位,因为线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提升系统多个程序间并发执行的程度。编辑器
进程和线程的主要差异在于它们是不一样的操做系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不一样执行路径。线程有本身的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,因此多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行而且又要共享某些变量的并发操做,只能用线程,不能用进程。ide
总结:linux中,进程和线程惟一区别是有没有独立的地址空间。函数
32位机器上,大约有1.7KB,进程描述符完整描述一个正在执行的进程的全部信息。spa
任务队列(双向循环链表)操作系统
进程描述符struct task_struct(源代码 | linnux/sched.h | v5.4).net
struct task_struct { volatile long state; // -1为不可运行, 0为可运行, >0为已中断 int lock_depth; // 锁的深度 unsigned int policy; // 调度策略:通常有FIFO,RR,CFS pid_t pid; // 进程标识符,用来表明一个进程 struct task_struct *parent; // 父进程 struct list_head children; // 子进程 struct list_head sibling; // 兄弟进程 }
linux采用slab分配器分配task_struct结构
目的:对象复用和缓存着色。
slab分配器动态生成task_struct,只需在栈底(相对于向下增加的栈)或栈顶(相对于向上增加的栈)建立一个新结构struct thread_info。
PID最大值默认为32768(short int 短整形的最大值<linux/threads.h>)可经过修改/proc/sys/kernel/pid_max提升上限。
current宏查找当前正在运行进程的进程描述符。
x86系统中,current把栈指针后13个有效位屏蔽掉,用来计算出thread_info的偏移。
current_thread_info函数
movl $-8192,%eax andl %esp,%eax
<img src="https://i.loli.net/2021/01/26/xnVIHZK9qjo8TXd.png" alt="1571556447032" style="zoom: 67%;" />
陷入内核执行
init进程
init进程目的:读取系统的初始化脚本,并执行其余的相关程序,最终完成系统启动的整个过程。
task_struct中记录父子进程
其余操做系统提供产生(spawn)进程机制,首先在新地址空间里建立进程,读入可执行文件,最后开始执行。
UNIX将上述机制流程分红两步fork()和exec()
使地址空间上的页的拷贝推迟到实际发生写入的时候才进行。
原理:若是有进程试图修改一个页,就会产生一个缺页中断。内核处理缺页中断的方式就是对该页进行一次透明复制。这时会清除页面的COW属性,表示着它再也不被共享。
fork()的实际开销就是复制父进程的页表以及给子进程建立惟一的进程描述符。
在如今linux内核中,fork()其实是由clone()系统调用实现的
<img src="https://i.loli.net/2021/01/26/m5xWw9acZnbro4H.png" alt="1571051424824" style="zoom: 80%;" />
注:内核有意让子进程先执行,并不是总能如此,由于通常子进程都会立刻调用exec()函数,这样能够避免写时拷贝的额外开销。由于父进程先执行,可能往地址空间写入。
vfork()和fork()区别:vfork()不拷贝父进程的页表项。
vfork():子进程做为父进程的一个单独线程在它的地址空间里运行,父进程被阻塞,直到子进程退出或执行exec(),子进程不能向地址空间写入。
线程建立和进程建立基本一致,经过调用clone()函数传递的参数标志,指明须要共享的资源。
建立线程
clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0); // CLONE_VM : 地址空间 // CLONE_FS : 文件系统 // CLONE_FILES : 文件描述符 // CLONE_SIGHAND : 信号处理程序及被阻断的信号
建立进程(等同fork()函数)
clone(SIGCHLD,0);
建立进程(等同vfork()函数)
clone(CLONE_VFORK | CLONE_VM | SIGCHLD, 0)
内核线程只在内核空间执行,从不切换到用户空间。
内核线程和普通进程的区别:内核线程没有独立的地址空间。(task_struct的mm指针被设置为NULL)
内核线程只能由其余内核线程建立,经过kthreadd内核线程衍生出全部新的内核线程。(kthreadd是全部内核线程的祖宗)
kthreadd内核线程是在内核初始化时被建立,循环执行kthreadd函数,它的做用是管理调度其它的内核线程。
kthreadd函数的做用是运行kthread_create_list全局链表中维护的kthread。能够调用kthread_create函数建立一个kthread,它会被加入到kthread_create_list链表中,同时kthread_create函数会唤醒kthreadd_task。kthreadd在执行kthread会调用老的接口,kthreadd内核线程在运行kthread时,会调用老接口kernel_thread,它会运行一个名为“kthread”的内核线程,去运行建立kthread,被执行的kthread会从kthread_create_list链表中删除,而且kthreadd会不断地调用scheduler让出CPU,这个线程不能关闭。
建立内核线程,不运行
kthread_create函数(源代码 | linux/kthread.h | v5.4)是经过clone()系统调用,建立一个内核线程,但新建立的线程处于不可运行状态。
kthread_create(threadfn, data, namefmt, arg...)
建立内核线程,并运行
kthread_run函数(源代码 | linux/kthread.h | v5.4),经过调用kthread_create函数建立内核线程,而后调用wake_up_process()进行唤醒。
#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; \ })
内核线程中止
int kthread_stop(struct task_struct *k);
释放所占用的资源,并告知父进程。
通常来讲,进程的析构是自身引发的,它发生在进程调用exit()系统调用的时候。
既能够显式地调用exit()这个系统调用,也能够隐性地从某个程序的主函数返回。(C语言编辑器会在main()函数的返回点后面放置调用exit代码)
终结的任务大部分都靠do_exit()(<kernel/exit.c>)
wait族函数都是经过惟一但很复杂的一个系统调用wait4()来实现的,挂起调用它的进程,直到其中的一个子进程退出,此时函数会返回子进程的PID。此外,调用此函数时提供的指针会包含子函数的退出代码。
做者:世至其美
博客地址:hqber.com
转载须注明以上信息, 更多文章,请访问我的博客:hqber.com