在 Linux 系统中, 进程和线程⼏乎没有区别。
进程
⽤ C 语⾔写⼀个 hello 程序, 编译后得到⼀个可执⾏⽂件, 在命令⾏运⾏就可以打印出⼀句 hello world, 然后程序退出。
在操作系统层⾯, 就是新建了⼀个进程, 这个进程将我们编译出来的可执⾏⽂件读⼊内存空间, 然后执⾏, 最后退出。
你编译好的那个可执⾏程序只是⼀个⽂件, 不是进程, 可执行文件必须要载入内存, 包装成⼀个进程才能真正跑起来。 进程是要依靠操作系统创建的,每个进程都有它的固有属性, ⽐如进程号(PID) 、 进程状态、 打开的⽂件等等, 进程创建好之后, 读⼊你的程序, 你的程序才被系统执⾏。
操作系统是如何创建进程的呢? 对于操作系统, 进程就是⼀个数据结构, 直接来看 Linux 的源码:
struct task_struct {
// 进程状态
long state;
// 【mm 指向的是进程的虚拟内存, 也就是载⼊资源和可执⾏⽂件的地⽅】
struct mm_struct *mm;
// 进程号
pid_t pid;
// 指向⽗进程的指针
struct task_struct __rcu *parent;
// ⼦进程列表
struct list_head children;
// 存放⽂件系统信息的指针
struct fs_struct *fs;
// 【files指向⼀个数组, 这个数组⾥装着所有该进程打开的⽂件的指针】
struct files_struct *files;
};
task_struct 就是 Linux 内核对于⼀个进程的描述, 也可以称为「进程描述符」。
files是⼀个⽂件指针数组。 ⼀般来说, ⼀个进程会从 files[0] 读取输⼊, 将输出写⼊ files[1] , 将错误信息写⼊ files[2] 。举个例⼦, C 语⾔的 printf 函数是向命令⾏打印字符, 但是从进程的⾓度来看, 就是向 files[1] 写⼊数据; 同理, scanf 函数就是进程试图从 files[0] 这个⽂件中读取数据。
每个进程被创建时, files 的前三位被填⼊默认值, 分别指向标准输⼊流、 标准输出流、 标准错误流。 我们常说的「⽂件描述符」 就是指这个⽂件指针数组的索引, 所以程序的⽂件描述符默认情况下 0 是输⼊, 1 是输出,2 是错误。对于⼀般的计算机, 输⼊流是键盘, 输出流是显⽰器, 错误流也是显⽰器,所以现在这个进程和内核连了三根线。 因为硬件都是由内核管理的, 进程需要通过「系统调⽤」 让内核进程访问硬件资源。如果写的程序需要其他资源, ⽐如打开⼀个⽂件进⾏读写, 进⾏系统调⽤, 让内核把⽂件打开, 这个⽂件就会被放到 files 的第 4个位置。同理,输入重定向和输出重定向就是分别把1和2位置指向改成磁盘文件。
管道符也类似, 把⼀个进程的输出流和另⼀个进程的输⼊流接起⼀条「管道」 , 数据就在其中传递。
进程v.s.线程
系统调⽤ fork() 可以新建⼀个⼦进程, 函数 pthread() 可以新建⼀个线程。 但⽆论线程还是进程, 都是⽤ task_struct 结构表⽰的, 唯⼀的区别就是共享的数据区域不同。
线程看起来跟进程没有区别, 只是线程的某些数据区域和其⽗进程是共享的, ⽽⼦进程是拷⻉副本,⽽不是共享。 就⽐如说, mm 结构和 files 结构在线程中都是共享的。所以说, 多线程程序要利⽤锁机制, 避免多个线程同时往同⼀区域写⼊数据, 否则可能造成数据错乱。现实中数据共享的并发更普遍!
只有 Linux 系统将线程看做共享数据的进程, 不对其做特殊看待, 其他的很多操作系统是对线程和进程区别对待的, 线程有其特有的数据结构,
扫码关注公众号:瑞行AI,欢迎交流AI算法、数据分析等技术,提供技术方案咨询和就业指导服务!