DAY1 线程和进程
概念与区别
在Windows中,线程和进程是两种不同的内核对象,概念区分明确,但常常协同工作,因此在实际讨论中有时不做严格区分。
在Linux中,线程和进程在内核实现上是相似的概念,都被抽象为统一的“任务”(Task)。这种设计使得使用线程时需要特别注意资源共享和同步的问题。
进程是程序的一次动态执行实例,是操作系统进行资源分配的基本单位。每个进程都有独立的虚拟地址空间和系统资源。
线程是进程内的一个执行流,是CPU调度的基本单位。在Linux中,线程本质上是一种共享地址空间的轻量级进程,多个线程共同组成一个进程,它们共享进程的资源(如内存空间、文件描述符等)。
函数式API
与Windows的面向对象API(如CreateProcess、CreateThread)不同,Linux提供了基于函数的系统api来操作进程和线程。你可以通过man 章节号 函数名在命令行中查看相关手册。其中第2章是系统调用,第3章是库函数。
1. 进程信息获取函数
在Linux中,以下几个是基础的进程信息获取函数:
#include <unistd.h> // Unix标准头文件,包含POSIX操作系统API
pid_t getpid(void); // 返回当前进程的进程ID(PID)
pid_t getppid(void); // 返回当前进程的父进程ID(PPID)
其中 unistd.h 是Unix标准头文件,名称源于“Unix Standard”。它定义了POSIX操作系统API,包含大量系统调用和常量的声明。pid_t 是进程ID的类型定义,通常定义为int或int32_t,用于存储进程ID。
我们来编写一个简单的测试案例:
#include <unistd.h>
#include <iostream>
using namespace std;
int main(){
cout << getpid() << endl;
pid_t parent;
parent = getppid();
cout << "parent = " << parent << endl;
getchar();
}
返回结果: 10044 parent = 9323
每次运行程序时,返回的PID通常都不同,因为进程ID是由操作系统动态分配的。暂时不要关闭这个程序,我们尝试在进程树中找到它。在命令行中输入:
pstree -p
如果找不到a.out进程,可能是因为getchar()在某些环境下不阻塞导致程序已退出。你可以让程序进入一个死循环来确保它持续运行。
我们可以看到所有进程都是从systemd(1)(或较老系统中的init(1))派生出的树状结构。其中一个分支是bash(9323),而我们的程序a.out(10044)是它的子进程。这里的bash是运行a.out的终端shell,也就是a.out的父进程。systemd(或init)是Linux系统的第一个进程(PID为1),它是所有用户进程的最终祖先。
2. 创建进程函数
手册中的函数定义如下:
#include <unistd.h>
pid_t fork(void);
返回值说明:
- 成功时:会返回2次,一次在父进程中返回子进程的PID,一次在子进程中返回0,在宏观上是同步并行执行的
- 失败时:在父进程中返回-1,不会创建子进程,同时设置errno来指示具体错误
让我们再来编写一个简单的测试案例:
#include <unistd.h>
#include <iostream>
using namespace std;
int main(){
cout << "child = " << fork()<< endl;
cout << "parent = " << getppid()<< endl;
cout << "self = " << getpid()<< endl;
getchar();
}
返回结果: child = 11375 parent = 9323 self = 11374 child = 0 parent = 11374 self = 11375
fork函数的作用是复制当前进程及其资源,创建出一个子进程,之后父子进程并发执行。虽然输出显示父进程先执行、子进程后执行,但这只是cout调用的时序问题,在宏观上是同步并行的。
如果你循环调用cout的话,就可以看到父子的输出的先后顺序会不时地改变。你可以自己编译并执行一下下面这个测试案例:
#include <unistd.h>
#include <iostream>
using namespace std;
int main(){
pid_t flag;
flag = fork();
while(1){
if(0 == flag){
cout << "I am child" << endl;
sleep(1); //暂停1s
}
else{
cout << "I am parent" << endl;
sleep(1);
}
}
}
返回结果(截取): I am parent I am child I am parent I am child I am child I am parent I am parent I am child
这也可以进一步说明fork()创建的子进程与父进程是并发的。在linux服务器的许多高并发场景中,会频繁地需要创建进程。
我们再来看一个测试案例:
#include <unistd.h>
#include <iostream>
using namespace std;
int main(){
pid_t p1;
pid_t p2;
p1 = fork();
p2 = fork();
cout << "========" << endl;
cout << p1 << endl;
cout << p2 << endl;
getchar();
}
返回结果: ======== 11888 11889 ======== 11888 0 ======== 0 11890 ======== 0 0
我们先假设有一个父进程a,首先它创建了一个子进程b(11888);然后a再次创建子进程c(11889),b创建了d(11890)。在创建子进程时会复制父进程的资源,所以你会看到子进程d能够拿到b的pid值11888,但是它自己是0。
顺便了解一下,Linux系统会对进程创建过程进行优化,采用写时复制(Copy-On-Write, COW)技术:子进程创建时不立即复制父进程的资源,而是共享同一份资源;只有当某个进程试图修改共享资源时,系统才会为该资源创建独立的副本。