资源分配和调度的基本单位
程序执行的基本单位
用户态的轻量级线程,线程内部调度的基本单位
进程切换时,操作系统会保存当前进程的CPU状态(如寄存器、页表等),并加载新进程的保存状态到CPU
保存和设置程序计数器、少量寄存器和栈的内容
先将寄存器上下文和栈保存,等切换回来的时候再进行恢复
CPU资源、内存资源、文件资源和句柄等
程序计数器、寄存器、栈和状态字
拥有自己的寄存器上下文和栈
不同进程之间切换实现并发,各自占有CPU实现并行
一个进程内部的多个线程并法执行
同一时间只能执行一个协程,而其他协程处于休眠状态,适合对任务进行分时处理
切换虚拟地址空间,切换内核栈和硬件上下文,CPU高速缓存失效、页表切换,开销很大
切换时只需保存和设置少量寄存器内容,因此开销很小
切直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快
进程间通信需要借助操作系统
线程间可以直接读写进程数据段(如全局变量)来进行通信
共享内存、消息队列
1、进程是资源分配的基本单位,运行一个可执行程序会创建一个或多个进程,进程就是运行起来的可执行程序
2、线程是资源调度的基本单位,也是程序执行的基本单位,是轻量级的进程。每个进程中都有唯一的主线程,且只能有一个,主线程和进程是相互依存的关系,主线程结束进程也会结束。多提一句:协程是用户态的轻量级线程,线程内部调度的基本单位
线程是调度的基本单位(PC,状态码,通用寄存器,线程栈及栈指针);进程是拥有资源的基本单位(打开文件,堆,静态区,代码段等)。
一个进程内多个线程可以并发(最好和CPU核数相等);多个进程可以并发。
线程不拥有系统资源,但一个进程的多个线程可以共享隶属进程的资源;进程是拥有资源的独立单位。
线程创建销毁只需要处理PC值,状态码,通用寄存器值,线程栈及栈栈指针即可;进程创建和销毁需要重新分配及销毁task_struct结构。
这个要分不同系统去看:
如果是32位系统,用户态的虚拟空间只有3G,如果创建线程时分配的栈空间是10M,那么一个进程最多只能创建300个左右的线程。
如果是64位系统,用户态的虚拟空间大到有128T,理论上不会受虚拟内存大小的限制,而会受系统的参数或性能限制。
顺便多说一句,过多的线程将会导致大量的时间浪费在线程切换上,给程序运行效率带来负面影响,无用线程要及时销毁。
在用户态多线程模型中,同一个进程内的多个线程共享以下资源:
1、进程的内存空间(代码段、数据段、堆、全局变量等)
2、文件描述符等进程资源
每个线程独有的部分包括:
1、线程ID(tid)
2、独立的栈空间
3、寄存器状态(包括程序计数器PC)
关键特性:
1、对共享资源(如全局变量int i)的访问需要同步机制保证线程安全
2、线程调度顺序不可预知
3、线程切换只需保存栈、寄存器和PC,比进程切换开销小很多
注意:线程之间无法直接访问彼此的栈空间,这是保证线程独立性的关键设计。
进程是资源分配的基本单位,其结构主要包括:
1、代码段(只读,可被多个进程共享)
2、数据段(全局变量、静态变量等)
3、堆栈段(堆用于动态内存分配,栈用于函数调用)
父子进程关系:
1、fork() 后,子进程复制父进程的代码、数据和堆栈(但并非直接共享,而是读数据时复制,即修改时才真正拷贝)。
2、execv() 可让子进程加载新代码段,与父进程完全分离(如 3、shell 执行程序:fork() + execv())。
关键点:
1、父子进程初始共享数据,但写时复制保证独立性。
2、execv() 替换代码段后,子进程成为全新进程。
非抢占式的调度算法,按照请求的顺序进行调度。有利于长时间,但不利于短作业,因为短作业必须一直等待前面的长作业执行完毕才能执行,而长作业又需要执行很久时间,造成了短作业等待时间过长。
非抢占式的调度算法,按估计运行时间最短的顺序进行调度。长时间有可能会饿死,处于一直等待短作业执行完毕的状态。因为如果一直有短作业到来,那么长时间永远得不到调度。
最短作业优先的抢占式版本,按剩余运行时间的顺序进行调度。当一个新的作业到达时,其整个运行时间与当前进程的剩余时间做比较。如果新的进程需要的时间更少,则挂起当前进程,运行新的进程。否则新的进程等待。
将所有就绪进程按先来先服务的原则排成一个队列,每次调度时,把CPU时间分配给队首进程,该进程可以执行一个时间片。当时间片用完时,由计时器发出时钟中断,调度程序便停止该进程的执行,并将它送往就绪队列的末尾,同时继续把CPU时间分配给队首的进程。
时间片轮转算法的效率和时间片大小有关系,如果时间片较小。则会导致切换过于频繁,在切换上就会花过多时间。反之时间片过长,则会让实时性降低。
为每个进程分配一个优先级,按优先级进行调度。为了防止低优先级的进程等不到调度,可以随着时间的推移增加等待进程的优先级
一个进程需要执行100个时间片,如果采用时间片轮转调度算法,那么需要交换100次。多级队列是为了这种需要连续执行多个时间片的进程考虑,它设置了多个队列,每个队列时间片大小都不同,例如1,2,4,8…。进程在第一个队列没执行完,就会被转移下一个队列。这种方式下,之前的进程只需要交换7次。每个队列优先权也不同,最上面的优先权最高。因此只有上一个队列没有进程在排队,才能调度当前队列上的进程。可以看作是时间轮转调度算法和优先级调度算法结合。
无名管道(内存文件):管道是一种半双工的通信方式,数据是能单向流动,而且只能在具有亲缘关系的进程之间使用(如fork创建的子进程)。进程的亲缘关系通常是指父子进程关系。
有名管道:也是半双工的通信方式,但是运行在没有亲缘关系的进程之间使用,管道是先进先出的通信方式。
共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的IPC方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与信号量,配合使用来实现进程间的同步和通信。
//写入数据端
int main() {
// 1. 生成 key
key_t key = ftok("shmfile", 65);//shmfile是文件路径,65是自己定义的0-255
// 2. 创建共享内存(大小 1024 字节)
int shmid = shmget(key, 1024, 0666 | IPC_CREAT);
// 3. 映射共享内存
char *data = (char *) shmat(shmid, NULL, 0);
// 4. 写入数据
strcpy(data, "你好,这是进程A写入的共享内存数据!");
printf("进程A已写入数据: %s\n", data);
// 5. 分离共享内存
shmdt(data);
return 0;
}
//读取数据端
int main() {
// 1. 生成 key(必须和写入端一样)
key_t key = ftok("shmfile", 65);
// 2. 获取共享内存
int shmid = shmget(key, 1024, 0666);
// 3. 映射共享内存
char *data = (char *) shmat(shmid, NULL, 0);
// 4. 读取数据
printf("进程B读取到数据: %s\n", data);
// 5. 分离 & 删除共享内存(只需要做一次)
shmdt(data);
shmctl(shmid, IPC_RMID, NULL); // 删除共享内存段
return 0;
}
消息队列是有消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。链表格式如下:
msg_pool[] —> [msg0][msg1][msg2]…[msgN]
↑ ↑
read_index write_index
⽤于通知接收进程某个事件已经发⽣,⽐如按下ctrl + C就是信号。
信号量是⼀个计数器,可以⽤来控制多个进程对共享资源的访问。它常作为⼀种锁机制,实现进程、线程的对临界区的同步及互斥访问。
信号量是一个整数 + 两个基本操作:
1、P 操作(Proberen,尝试,常记为 wait()、down() 或 sem_wait())
→ 如果信号量值 > 0,执行并将值减 1;
→ 如果信号量值 = 0,阻塞等待。
2、V 操作(Verhogen,增加,常记为 signal()、up() 或 sem_post())
→ 将信号量值加 1;
→ 如果有进程/线程等待这个信号量,则唤醒一个。
与其他通信机制不同的是,它可以用于不同机器的进程通信。
1、POSIX信号量:可⽤于进程同步,也可⽤于线程同步。
2、POSIX互斥锁 + 条件变量:只能⽤于线程同步。
int pthread_join(pthread_t tid, void** retval);
主线程调⽤,等待⼦线程退出并回收其资源,类似于进程中wait/waitpid回收僵⼫进程,调⽤pthread_join的线程会被阻塞。
tid:创建线程时通过指针得到tid值。
retval:指向返回值的指针。
#include
#include
#include
void* thread_func(void* arg) {
int *ret = malloc(sizeof(int));
*ret = 42; // 假设线程要返回 42
return ret;
}
int main() {
pthread_t tid;
void *retval;
pthread_create(&tid, NULL, thread_func, NULL);
pthread_join(tid, &retval);
printf("Thread returned: %d\n", *(int*)retval);//也就是thread_func函数的返回值ret
free(retval); // 别忘了释放线程返回的内存
return 0;
}
pthread_exit(void *retval);
⼦线程执⾏,⽤来结束当前线程并通过retval传递返回值,该返回值可通过pthread_join获得。
retval:同上。
主线程、⼦线程均可调⽤。主线程中pthread_detach(tid),⼦线程中pthread_detach(pthread_self()),调⽤后和主线程分离,⼦线程结束时⾃⼰⽴即回收资源。
守护进程是在后台运行、无控制终端的进程,通常用于周期性任务或服务,如 服务器(httpd)等。
创建守护进程要点:
1、后台运行:方法是调用fork() 创建子进程,父进程退出。
2、脱离控制终端:子进程调用 setsid(),创建新会话,脱离原有终端、会话、进程组。
3、防止重新打开终端:再次 fork(),父进程退出,子进程不再是会话组长。
4、关闭所有文件描述符:关闭从父进程继承的 0 ~ maxfd 所有文件描述符。
5、切换工作目录:chdir(“/”),避免阻止挂载文件系统卸载。
6、重置文件权限掩码:umask(0),防止继承的掩码影响新建文件权限。
7、处理 SIGCHLD 信号:设置 signal(SIGCHLD,SIG_IGN),防止产生僵尸进程。
如果⽗进程先退出,⼦进程还没退出,那么⼦进程的⽗进程将变为init进程。(注:任何⼀个进程都必须有⽗进
程)。
⼀个⽗进程退出,⽽它的⼀个或多个⼦进程还在运⾏,那么那些⼦进程将成为孤⼉进程。孤⼉进程将被init进程(进
程号为1)所收养,并由init进程对它们完成状态收集⼯作。
如果⼦进程先退出,⽗进程还没退出,那么⼦进程必须等到⽗进程捕获到了⼦进程的退出状态才真正结束,否则这
个时候⼦进程就成为僵⼫进程。
1、通过signal(SIGCHLD, SIG_IGN)通知内核对⼦进程的结束不关⼼,由内核回收。这样一来,内核会自动帮你回收子进程资源,你啥都不用管,子进程死了也不会变成僵尸。
2、⽗进程调⽤wait/waitpid等函数等待⼦进程结束,如果尚⽆⼦进程退出wait会导致⽗进程阻塞。waitpid可以
通过传递WNOHANG使⽗进程不阻塞⽴即返回。
3、注册一个 SIGCHLD 的信号处理函数,一旦子进程结束,系统自动发 SIGCHLD,你在这个函数里调用 wait() 来收回。
4、通过两次调⽤fork。⽗进程⾸先调⽤fork创建⼀个⼦进程然后waitpid等待⼦进程退出,⼦进程再fork⼀个孙
进程后退出。这样⼦进程退出后会被⽗进程等待回收,⽽对于孙⼦进程其⽗进程已经退出所以孙进程成为⼀个孤⼉进程,孤⼉进程由init进程接管,孙进程结束后,init会等待回收。
会话(Session)
└── 进程组(Process Group)
├── 父进程(Parent Process)
│ └── 子进程(Child Process)
└── 作业(Job)
1、父进程:通过 fork() 创建子进程的进程。
2、子进程:fork() 产生的新进程。
3、fork() 返回值:父进程得到子进程PID,子进程返回0。
4、两个进程代码一样,但数据空间独立,像“孪生兄弟”。
1、会继承:用户身份、环境变量、内存内容、堆栈、进程组等。
2、不会继承:进程号不同,资源使用为0。
1、是一组进程的集合,有一个“组长进程”(其PID = PGID)。
2、用于统一管理多个进程(如统一接收信号)。
1、shell 控制的“任务单位”,可以包含一个或多个进程组。
2、前台作业:你能直接交互的任务(shell被挂起)。
3、后台作业:在后台执行,shell可继续接受命令。
会话(Session)是⼀个或多个进程组的集合。⼀个会话可以有⼀个控制终端。在xshell或者WinSCP中打开⼀个窗
⼝就是新建⼀个会话。
1、main函数的⾃然返回,return
2、调⽤exit 函数,属于c的函数库
3、调⽤_exit 函数,属于系统调⽤
4、调⽤abort 函数,异常程序终⽌,同时发送SIGABRT信号给调⽤进程。
5、接受能导致进程终⽌的信号:ctrl+c (^C)、SIGINT(SIGINT中断进程)
1、频繁修改:需要频繁创建和销毁的优先使⽤多线程
2、计算量:需要⼤量计算的优先使⽤多线程 因为需要消耗⼤量CPU资源且切换频繁,所以多线程好⼀点
3、相关性:任务间相关性⽐较强的⽤多线程,相关性⽐较弱的⽤多进程。因为线程之间的数据共享和同步⽐较简
单。
4、多分布:可能要扩展到多机分布的⽤多进程,多核分布的⽤多线程。