1.进程和线程1.1定义
进程是处于运行状态的程序和相关资源的合称,是资源分配的最小单位。
线程是进程的内部的一个执行序列,是CPU调度的最小单位。
Linux系统对于线程实现十分特殊,他并不分辨线程和进程,线程只是一种特殊的进程罢了。从前面四点要素来看,拥有前三点而缺第四点要素的就是线程,假如完全没有第四点的用户空间,那就是系统线程,倘若是共享用户空间,那就是用户线程。
1.2主要区别
进程作为分配资源的基本单位linux 用户分配空间,而把线程作为独立运行和独立调度的基本单位,因为线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开支都会小得多,能更高效的增强系统多个程序间并发执行的程度。
进程和线程的主要差异在于它们是不同的操作系统资源管理方法。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程形成影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程挂掉就等于整个进程死掉,所以多进程的程序要比多线程的程序强壮,但在进程切换时,花费资源较大,效率要差一些。但对于一些要求同时进行而且又要共享个别变量的并发操作,只能用线程,不能用进程。
总结:linux中,进程和线程惟一区别是有没有独立的地址空间。
2.进程描述符及任务结构
32位机器上,大概有1.7KB,进程描述符完整描述一个正在执行的进程的所有信息。
任务队列(单向循环数组)
进程描述符structtask_struct(源代码|linnux/sched.h|v5.4)
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; // 兄弟进程
}
2.1分配进程描述符2.1.1slab分配器
linux采用slab分配器分配task_struct结构
目的:对象复用和缓存着色。
slab分配器动态生成task_struct,只需在栈底(相对于向上下降的栈)或栈顶(相对于向下下降的栈)创建一个新结构structthread_info。
2.1.2进程描述符储存
PID最大值默认为32768(shortint短整形的最大值)可通过更改/proc/sys/kernel/pid_max提升上限。
current宏查找当前正在运行进程的进程描述符。
x86系统中,current把栈表针后13个有效位屏蔽掉,拿来估算出thread_info的偏斜。
current_thread_info函数
movl $-8192,%eax
andl %esp,%eax
2.1.3进程状态
深陷内核执行
系统调用异常处理程序2.1.4进程家族树
init进程
init进程目的:读取系统的初始化脚本,并执行其他的相关程序,最终完成系统启动的整个过程。
task_struct中记录兄妹进程
parent表针(指向父进程)children子进程数组3.进程创建
其他操作系统提供形成(spawn)进程机制,首先在新地址空间里创建进程,读入可执行文件,最后开始执行。
UNIX将上述机制流程分成两步fork()和exec()
3.1写时拷贝(copy-on-write)
使地址空间上的页的拷贝延后到实际发生写入的时侯才进行。
原理:假如有进程企图更改一个页,还会形成一个缺页中断。内核处理缺页中断的方法就是对该页进行一次透明复制。这时会消除页面的COW属性,表示着它不再被共享。
3.2fork()函数
fork()的实际开支就是复制父进程的页表以及给子进程创建惟一的进程描述符。
在现今linux内核中,fork()实际上是由clone()系统调用实现的
3.2.1copy_process()函数dup_task_struct()为新进程创建一个内核栈,thread_info结构和task_struct与当前进程相同。母子进程描述符是完全相同的。(分配空间)检测并确保新创建这个进程后,当前用户所拥有的进程数量没有超出给它分配的资源的限制。(检测边界)子进程与父进程区别开。进程描述符的许多成员都要被清0或设初始值,这些不是承继来的进程描述符的成员,主要是统计信息。task_struct中的大多数数据都仍然未被更改。(子进程初始化)子进程的状态被设置为TASK_UNINTERRUPTIBLE(不可中断,阻塞状态),以保证它不会投入运行。(设置子进程状态)copy_process()调用copy_flags()以更新task_struct的flags成员。(设置标志位)
调用alloc_pid()为新进程分配一个有效的PID。(为子进程分配pid)按照传递给clone()的参数,copy_process()拷贝或共享打开的文件、文件系统信息、信号处理函数、进程地址空间和命名空间等。通常情况下,这种资源会被给定的进程的所有线程共享;否则,这种资源对每位进程是不同的linux 用户分配空间,因而被拷贝到这儿。(将资源参数标志形参给结构体)copy_process()做收尾工作并返回一个指向子进程的表针,再回到do_fork()函数,假若copy_process()函数成功返回,新创建的子进程被唤起并让其投入运行。(返回子进程表针,并唤起子进程执行)
注:内核有意让子进程先执行,并非总能这么,由于通常子进程就会马上调用exec()函数,这样可以防止写时拷贝的额外开支。由于父进程先执行,可能往地址空间写入。
3.3vfork函数
vfork()和fork()区别:vfork()不拷贝父进程的页表项。
vfork():子进程作为父进程的一个单独线程在它的地址空间里运行,父进程被阻塞,直至子进程退出或执行exec(),子进程不能向地址空间写入。
C/C++Linux服务器开发/后台构架师【零声教育】-学习视频教程-腾讯课堂
【文章福利】:小编整理了一些个人认为比较好的学习书籍、视频资料共享在群文件上面,有须要的可以自行添加哦!正在跳转(832218493须要自取)
4.线程创建
线程创建和进程创建基本一致,通过调用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)
4.1内核线程
内核线程只在内核空间执行,从不切换到用户空间。
内核线程和普通进程的区别:内核线程没有独立的地址空间。(task_struct的mm表针被设置为NULL)
内核线程只能由其他内核线程创建,通过kthreadd内核线程衍生出所有新的内核线程。(kthreadd是所有内核线程的祖宗)
4.1.1kthreadd内核线程
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函数创建内核线程linux中文乱码,之后调用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);
5.进程终结
释放所占用的资源,并告知父进程。
通常来说,进程的析构是自身造成的,它发生在进程调用exit()系统调用的时侯。
既可以显式地调用exit()这个系统调用,也可以隐性地从某个程序的主函数返回。(C语言编辑器会在main()函数的返回点旁边放置调用exit代码)
终结的任务大部份都靠do_exit()()
5.1do_exit()函数将task_struct中标志成员设置成PF_EXITING调用del_timer_sync()删掉任一内核定时器。确保没有定时器在排队,也没有定时器处理程序在运行。假如BSD的记帐功能是开启的,do_exit()调用acct_update_integrals()来输出记帐信息。调用exit_mm()函数释放进程占用的mm_structlinux怎么读,假如没有别的进程同时使用它们(也就是说,这个地址空间没有被共享),就彻底释放它们。调用sem__exit()函数,假若进程排队等待IPC讯号,它则离开队列。调用exit_files()和exit_fs()分别递减文件描述符,文件系统数据引用计数,假如其中某个引用计数的数值降为零,那就不用代表没有进程在使用相应的资源,此时可以释放。把储存在task_struct的exit_code()成员中的任务退出代码置为由exit()提供的退出代码,或则去完成任何其他有内核机制规定的退出动作。退出代码储存在这儿供父进程随时检索。调用exit_notify向父进程发送讯号,给子进程重新找父亲(其他线程或init进程),并将储存在task_struct结构中的exit_state设置为EXIT_ZOMBIE。do_exit调用schedule()切换到新的进程,由于处于EXIT_ZOMBIE状态的进程不会被调度,所以这是进程所执行的最后一段代码,do_exit()永不返回。5.2wait族函数
wait族函数都是通过惟一但很复杂的一个系统调用wait4()来实现的,挂起调用它的进程,直至其中的一个子进程退出,此时函数会返回子进程的PID。据悉,调用此函数时提供的表针会包含子函数的退出代码。