LinuxLinux进程控制及程序替换
Posted 阿润菜菜
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了LinuxLinux进程控制及程序替换相关的知识,希望对你有一定的参考价值。
进程控制及程序替换
进程创建
fork的用法
在linux中fork是一个很重要的函数,它可以已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
fork函数返回两个值,一个是子进程的进程号(pid),另一个是0。父进程可以通过pid来区分自己和子进程,子进程可以通过返回值0来判断自己是子进程 。一个进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。
下面是一个fork函数的用法示例:
#include <unistd.h>
#include <stdio.h>
int main()
int pid = fork(); // 产生两个进程,一个父进程,一个子进程
if (pid == -1) return -1;
if (pid) // 这里运行父进程代码
printf("I am father, my pid is %d\\n", getpid());
return 0;
else // 这里运行子进程代码
printf("I am child, my pid is %d\\n", getpid());
return 0;
fork的常见用法有那些?
- 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
- 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用execp()函数
进程调用fork,内核做了什么
在进程调用fork函数之后,当执行的程序代码转移到内核中的fork代码后,内核需要分配新的内存块和内核数据结构给子进程,内核数据结构包括PCB、mm_struct和页表,然后构建起映射关系,同时将父进程内核数据结构中的部分内容拷贝到子进程,并且内核还会将子进程添加到系统进程列表当中,最后内核空间中的fork代码执行完毕,操作系统中也就已经创建出来了子进程,最后返回用户空间,父子进程执行程序fork之后的剩余代码。
也就是说fork之前父进程独立执行,fork之后,父子进程两个执行流一起执行fork之后的剩余代码。fork之后,父子进程谁先执行,完全由调度器 决定。
参考
哈希表是一种数据结构,它可以快速地存储和查找数据。哈希表的原理是将数据的键值通过一个哈希函数转换为一个索引,然后将数据存储在对应索引的位置。当需要查找数据时,只需要再次计算键值的哈希值,就可以直接定位到数据的位置。
Linux系统中,每个Shell都有一个哈希表,用来记录执行过的命令及其路径。这样可以避免每次执行命令时都要在PATH路径下搜索命令的位置,提高了命令的执行效率。
当Linux系统创建一个子进程时,它会为子进程分配一个唯一的进程标识符(PID),并将子进程添加到系统进程列表中。系统进程列表实际上是一个哈希表,它以PID为键值,以指向进程控制块(PCB)的指针为数据。PCB是一个结构体,它包含了进程的各种信息和状态。通过这个哈希表,系统可以快速地根据PID找到对应的PCB,并进行相应的操作。
fork函数的返回值问题
fork函数被调用一次,但返回两次,分别在父进程和子进程中返回。这两个返回值的作用是让父进程和子进程能够区分自己的身份,并进行不同的操作。
fork函数的返回值有三种可能:
- 在子进程中返回0,表示成功创建了子进程。
- 在父进程中返回子进程的PID(进程标识符),表示成功创建了子进程,并告知父进程其ID。
- 如果出错,fork函数返回-1,表示创建子进程失败。
示例代码:
#include <stdio.h>
#include <assert.h>
#include <unistd.h>
int g_value = 100; //全局变量
int main()
// fork在返回的时候,父子都有了,return两次,id是不是pid_t类型定义的变量呢?返回的本质,就是写入!
// 谁先返回,谁就让OS发生写时拷贝
pid_t id = fork();
assert(id >= 0);
if(id == 0)
//child
while(1)
printf("我是子进程, 我的id是: %d, 我的父进程是: %d, g_value: %d, &g_value : %p\\n",\\
getpid(), getppid(), g_value, &g_value);
sleep(1);
g_value=200; // 只有子进程会进行修改
else
//father
while(1)
printf("我是父进程, 我的id是: %d, 我的父进程是: %d, g_value: %d, &g_value : %p\\n",\\
getpid(), getppid(), g_value, &g_value);
sleep(1);
问题1 :那fork返回之后,为什么给父进程返回子进程的pid,而给子进程返回0呢?
我们知道子进程只能有一个父进程。但是父进程可以有多个子进程,那么父进程找子进程是不具有唯一性的,所以就需要fork函数返回子进程的pid,通过子进程的pid来确定和找到具体的子进程。
问题2: 那么一个id变量,怎么能保存两个值,并且if和elseif语句同时执行呢?
在fork之后,父子进程谁先执行代码完全由调度器决定,所以父子进程谁先返回,那么就谁先对id变量进行赋值,后一个执行的进程又会对id变量进行写入,因为进程具有独立性,所以这个时候就会发生写时拷贝。
此时id变量打印出来的地址是相同的,但是内容就会不一样了,因为分别在父子进程中各有一个id变量的值了,他们的值是不同的。
参考虚拟地址空间知识
fork创建失败的原因
一般两个原因:
1、物理内存不够了用了;创建子进程也是需要消耗物理内存的!
2、系统中创建了太多的进程。父进程创建子进程的上限到了,OS为了限制用户父进程无限制的创建子进程,通常都会给父进程设置一个"进程上限";
最好最直接的解决办法:重启你的云服务器
进程等待
进程等待是什么?为什么要有?
首先,**OS为什么要有进程等待?**回答这个问题之前,我们先要搞清楚为什么要创造子进程,那当然是需要子进程帮助我们执行特定的任务,既然子进程帮助我们执行了任务,那么我们当然要关心一下子进程的执行结果,所以这个问题的回答就是:
- 避免内存泄漏(目前一定要做的)
- 获取子进程执行的结果(按需)
那么什么是进程等待呢?简单说就是:
进程等待(本质) — 通过系统调用,获取子进程退出码或者退出信号的方式(获取子进程退出码以此来知晓父进程交代给子进程的任务子进程完成的怎么样),并顺便释放内存(释放掉子进程的空间)
进程等待的方式
这里我们可以使用系统调用wait()/waitpid();
来实现!
wait(等待任意的子进程,只能是阻塞状态等待)
如果子进程退出,父进程不读取子进程退出的信息,那么子进程就会变为僵尸进程,从而导致内存泄露的问题。其实我们可以通过进程等待的方式来解决僵尸进程问题。通过wait函数回收僵尸进程剩余资源。
让父进程通过进程等待的方式,回收子进程剩余资源(PCB,内核栈等),获取子进程退出信息,父进程需要知道子进程的退出码和执行时间等信息,形象化的比喻就是父进程通过进程等待来给僵尸进程收尸。
pid_t wait(int*status);
Linux下包含头文件:sys/types.h和sys/wait.h
参数: 输出型参数用于获取进程退出码和信号(如果程序正常运行结束的话,信号是0);当然如果我们不关心进程的退出码和信号的话,我们可以将其设置为NULL;
返回值: 等待成功:返回等待进程的pid;等待失败(如父进程没有子进程),返回-1;
当父进程调用wait函数时,父进程也会自动的等待子进程运行完毕!相当于在子进程运行的这段期间,父进程相当于卡在了wait函数内部!这叫做阻塞等待!父进程会被OS放入一个等待队列中进行等待子进程的运行结束!
认识输出型参数status
输出型参数status
是一个用于接收函数返回值的变量,通常用于一些进程管理或系统调用的函数中,比如wait或waitpid。这些函数会在子进程结束时返回子进程的退出状态信息,而status参数就是用来存储这些信息的。status参数是一个指针类型,但不是要传递一个指针参数,而是一个由操作系统填充的输出型参数。status参数只使用了低16位,高两个字节没有用到,**其中的前7比特位代表子进程终止信号,后8比特位代表进程退出码。**如果传递NULL,表示不关心子进程的退出状态信息。
示例代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>
int main()
pid_t id = fork();
assert(id!=-1);
if(id==0)
// child process
int cnt=5;
while(cnt)
printf("child process running,pid:%d,ppid:%d,cnt:%d\\n",getpid(),getppid(),cnt--);
exit(10);
int status=0;
int ret=waitpid(id,&status,0);
if(ret>0)
// printf("wait success,exit code:%d,signal number:%d\\n",);
printf("status:%d\\n",status);
return 0;
结果:
这里status
的输出结果为2560,写成16比特位的形式就是0000 1010 0000 0000,上面说了前7比特位代表子进程终止信号,后8比特位代表进程退出码,所以1010实际上就是10,也就是僵尸进程的退出码,表示什么样的结果错误,这可以取决于我们自己,我们可以自己写个printf语句输出想输出的错误信息,然后可以看到终止信号是0 ,表示僵尸进程正常退出。
我们如何拿出到进程退出码和终止信号呢?
所以我们可以通过位运算获取退出码和退出信号!
也就是----我们可以利用(status指向的整形>>8)&0xFF的方式来拿出进程退出码!(status&0x7F)的方式来拿出终止信号;
waitpid(可以等待特定的子进程,可以非阻塞状态等待)
waitpid与wait功能相似,wait是处理最先处于僵尸状态的子进程,waitpid是处理指定pid的子进程
pid_t waitpid(pid_t pid,int*status,int options);
参数: pid:指定等待的子进程;pid=-1时则处理最先处于僵尸状态的子进程;
status:输出型参数;与wait的status功能一样;
option:决定父进程在等待的过程中是否可与去做其他事情—>0:不可以;WNOHANG:可以!
返回值:
option=0:等待成功,返回子进程pid;等待失败,返回-1;
option=WNOHANG:等待成功,返回子进程pid;子进程还没运行结束,返回0;等待失败(子进程不存在),返回-1,
上文了解,当子进程还没有死的时候,也就是没有退出的时候,父进程调用的wait或waitpit需要等待子进程退出,系统调用接口也不返回,这段时间父进程什么都没做,就一直等待子进程退出,这样的等待方式,称之为阻塞式等待。
那有没有非阻塞等待呢?
非阻塞式等待就是,不停的检测子进程状态,每一次检测之后,系统调用立即返回,在waitpid中的第三个参数设置为WNOHANG
,即为父进程非阻塞式等待。
如果等待的子进程状态没有发生变化,则waitpid会返回0值。多次非阻塞等待子进程,直到子进程退出,这样的等待方式又称之为轮询。如果等待的进程不是当前父进程的子进程,则waitpid会调用失败。
非阻塞等待有一个好处就是,不会像阻塞式等待一样,父进程什么都做不了,而是在轮询期间,父进程还可以做其他的事情
父进程如何获取子进程的退出信息的?
先来回答父进程在使用wait()/waitpid()的期间在做什么?
上面说了,父进程在wait的时候,如果子进程没有退出,父进程只能一直调用waitpid进行等待,这里的阻塞等待,不是在运行队列等待,而是在阻塞队列等待。
那么进程等待什么,我们说是等待子进程的退出码和退出信号,怎么获取呢?我们可以手动获取或者使用宏获取 ----> 宏WIFEXITED和WEXITSTATUS
宏WIFEXITED(status):若子进程是正常终止,则返回结果为真,用于查看进程是否正常退出。
WEXITSTATUS(status):若进程正常终止,也就是进程终止信号为0,这时候会返回子进程的退出码。
if(ret>0)
// 是否正常退出
if(WIFEXITED(status))
// 判断子进程退出码是什么
printf("child process exit normally,exit code:%d\\n",WEXITSTATUS(status));
else
printf("child process don't exit normally\\n");
// printf("wait success,exit code:%d,signal number:%d\\n",(status>>8)&0xFF,status & 0x7F);
测试结果:
那父进程是怎么实现的获取信息? ---- 读取子进程的内核数据结构
在子进程的pcb结构体中,有两个变量:
int exit_code;//用于记录当前进程的退出码;
int exit_signal//用于记录当前进程的终止信号;
当子进程进入僵尸状态,也就是子进程被终止掉了!OS会根据子进程终止时的退出码和终止信号来填充exit_code和exit_signal;当父进程使用wait()/waitpid()系统调用的时候,OS就会将子进程pcb里面的
exit_code和exit_signal存储于status指针指向的int空间中,而status所指向的空间是属于父进程的,父进程也就自然而然的拿到了子进程的退出码和终止信号!
进程终止
终止的三种方式
1、程序正常运行,最后结果正确;
2、程序正常运行,最后结果错误;
3、程序异常终止!(比如出现除以0、对空指针解引用、kill -9 命令杀掉进程等等情况)
程序崩溃(进程异常)终止的本质是 — 进程因为某些原因,导致进程收到了来着操作系统的信号(如kill -9)
那程序正常运行结束是会有退出码的?怎么查看呢
进程退出码
首先搞清楚进程退出码存在的前提就是程序正常运行完毕!在其他情况下,进程退出码没有意义!
什么是进程退出码呢?
我们知道一个程序是从main函数开始的,在写main函数的时候最会会写一个return 0;但有小伙伴会发现其实不写这个return 0 程序也能跑啊,那这个return 0是什么呢?为什么要写呢?
其实这个0就是进程退出码;这个值会返回给OS,来表明进程的的退出结果是什么样的,比如在系统内部0就是代表正常退出,非0就是代表错误退出,而非0有很多数字,具体的数字有标识为不同的错误信息。
return 0,表示该进程正常运行,且运行结果正确;
return 其他数据,表示该进程正常运行,但是运行结果错误,那么到底是为什么错误呢?作为父进程需要知道原因,我们此时return 的数字就表示这个错误原因,这个数字也是进程退出码!
而且系统内部不同的退出码都必须有相应的文字描述,来确定进程的具体退出原因,这个文字描述我们是可以自己定义的,当然也可以用系统默认的映射关系输出错误码的描述!
默认的C库错误信息表:
如何查看最近的退出码?
$? : 只会保留最近一次进程的退出码
我们使用 echo $?
即可查看
操作方式
上面提到了main函数的return,那其它函数return呢?这其实仅仅代表该函数内部返回–>进程执行,本质其实是main执行流执行!
如果我们在main函数以外的地方,遇到了不合理的地方想要提前终止进程(比如利用malloc开辟空间失败,我们就可以提前终止进程),我们就可以利用exit()/_exit()函数,来提前终止掉该进程!exit()/_exit()//的参数就相当于main函数的返回值 ----> main return XXX!0:表示正常运行结束,结果运行正确;其他数字表示正常运行结束,结果运行错误!
我们看到exit()/与_exit()看起来长得很像,那么他们有区别吗?
其实exit()是C语言库函数里面的函数接口,而_exit()不属于C语言,_exit()是系统调用;exit()内部也是通过调用_exit()系统调用来终止进程的,只不过exit()内部更加丰富,在结束进程之前,会做一些工作,比如:执行用户定义的清理函数、刷新缓冲区、关闭流等;但_exit()没有这么多前戏,就是纯粹的终止进程!它不会刷新缓存区,因此我们在调用_exit()结束进程的时候,没有刷新缓冲区,直接就结束掉进程了,如果我们要打印字符串就会一直停留在缓冲区,没有机会输出到屏幕上!-
也就是exit终止进程,会主动刷新缓冲区。_exit终止进程,不会刷新缓冲区。
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <stdlib.h>
4 int main()
5
6
7 printf("hello Linux\\n");
8
9 exit(111);
10
11 while(1) sleep(1);
12
结果:
如果缓冲区在操作系统里面,那么exit和_exit都会刷新缓冲区,因为这两个接口终止进程的工作最终都是要依靠操作系统来终止的,所以操作系统更加的底层,缓冲区如果在OS的话,这两个接口都应该刷新缓冲区,但是我们看到的现象并不是这样的,所以就说明缓冲区不在OS,他其实是用户级的缓冲区 --C库
所以如何理解进程退出?
其实就是OS内少了一个进程,OS要释放进程对应的内核数据结构+代码和数据(如果有独立的)
进程的程序替换
为什么要有程序替换?
我们先来回答父进程创建子进程的目的是什么?
创建子进程一般有两个目的:
1.让子进程执行父进程代码的一部分,也就是执行父进程对应的磁盘上的代码和数据的一部分。
2.让子进程加载磁盘上指定的程序到内存中,使其执行新的程序的代码和数据,这就是进程的程序替换
那么如何实现呢?
实现程序替换
Linux给我们提供了7个接口来帮住我们实现:
#include <unistd.h>
int execl(const char *path, const char *arg, …);
int execlp(const char *file, const char *arg, …);
int execle(const char *path, const char *arg, …, char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
int execve(const char *path, char *const argv[], char *const envp[]);
这些函数叫做exec函数组,我们通过调用这7个函数中的任意一个就可以完成程序替换。每个函数都有自己的特性:
单进程程序替换— 见见猪跑
int execl(const char* path,const char* arg,...); 一个程序替换函数
首先回答这个小知识— 参数列表里的(…) 是什么?
其实函数参数中的…是可变参数列表,可以给C语言函数传递不同个数的参数
替换函数怎么用呢?我们要执行一个程序,首先就是找到这个程序,然后在执行这个程序,执行程序的时候,也拥有不同的执行方式,通过执行选项的不同便可以使得程序以多种不同的方式执行(可变参数作用)
execl函数第一个参数path,表示我们要替换的程序在哪里,第二个参数及后面arg表示我们想用怎么样的方式运行我们的程序
注意:结尾必须以NULL结尾,表示参数输入完了
#include <stdio.h>
#include <unistd.h>
int main()
// .c --> .exe --> load into memory --> process --> running
printf("The process is running...\\n");
execl("/usr/bin/ls","ls","-a","-l","--color=auto",NULL);// 传参以NULL结尾,来表示传参结束
printf("The process finishes running...\\n");
return 0;
结果:
我们可以看到有一个现象就是第三句printf代码没有被执行,那它为什么没有被执行?
程序替换原理
首先根据上面我们写的代码,我们自己写的代码(比如:打印runing的语句)在运行起来的时候本身就已经是一个进程,那么这时候该进程就已经有了自己的内核数据结构了,比如:pcb、进程地址空间、页表等!我们知道我们这些进程的父进程都是bash,就是说一个进程是可以调用其他进程的。
现在当我们的进程运行到ececl语句的时候,会发生程序替换:一个进程去运行另一个新的程序
当我们自己写的程序运行到execl()语句时,就会根据ececl()的path(地址)参数,将ls命令从磁盘加载进物理内存!加载到物理内存的那个地方呢?加载到我们自己写的程序对应的物理内存上的位置!
也就是说用ls命令的数据和代码,替换原来老的程序的代码和数据,物理空间还是原来的物理空间,页表的映射关系也基本不变,如果ls命令数据和代码太多了,os会在页表增加一些映射关系!然后该进程开始重新运行一段新的程序!
所以将磁盘中指定程序的代码和数据直接覆盖掉物理内存中原来正在运行的进程的代码和数据,以达到程序替换的效果,这就是程序替换的本质。
在进程替换的时候是没有创建新进程的,而是在原有进程基础上,将指定程序的代码和数据覆盖到原来的代码和数据里。
当父进程派生的子进程发生程序替换时,防止父子进程原先共享的代码段和数据段被修改,操作系统会进行写时拷贝,将代码段和数据段重新复制一份给子进程,让子进程程序替换之后,不会影响父进程。这就是进程之间的独立性。
通过之前学习我们知道虚拟地址空间和页表可以保证进程之间的独立性,一旦有执行流要改变代码或数据,就会发生写时拷贝。
所以不是只有数据可能发生写入,代码也是有可能发生写入的,这两种情况都会发生写时拷贝。
进程替换的特点:
- 程序替换是整体替换,不是局部替换
- 替换进程只会影响调用进程,进程具有独立性(写时拷贝)
因为进程具有独立性,那么我们可不可以让子进程去做程序替换,来帮住我们去做特定的任务?
所以我们可以来看看多进程程序替换怎么实现的:
多进程程序替换
我们可以让子进程去执行一段与父进程完全不一样的代码:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/wait.h>
int main() //HUE
printf("The process is running...\\n");
pid_t id = fork();
assert(id!=-1);
if(id==0)
//child process
sleep(1);
execl("/usr/bin/ls","ls","-a","-l",NULL);
exit(1);// 如果调用失败,直接让子进程退出
int status = 0;
pid_t ret=waitpid(id,&status,0);
if(ret == id)
printf("wait success, exit code:%d , signal number:%d\\n",(status>>8)&0xFF,status&0x7F);
这就是最简单的多进程程序替换,通过上述那么我们可以解释一下shell的运行原理:首先shell从命令行接受到我们的命令后会创建一个子进程来执行我们的命令,然后在让该子进程调用execl函数来进程程序替换,替换掉子进程从Shell哪里继承下来的代码和数据!然后让子进程开始运行这段程序!
熟悉程序替换接口 — execl函数组
再次贴上来这几个接口: 我们来熟悉一下他们的具体用法
#include <unistd.h>
int execl(const char *path, const char *arg, …);
int execlp(const char *file, const char *arg, …);
int execle(const char *path, const char *arg, …, char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
int execve(const char *path, char *const argv[], char *const envp[]);
以execl为例:
int execl(const char *path, const char *arg, …);
各参数代表:
1. —你想执行谁?需要带路径— path
2. 加载执行它 — arg
3. 怎么执行— …
4. ----最后必须以NULL结束执行
int execlp(const char *file, const char *arg, …);
: 第一个参数不再是传递路径,而是只需要指定程序名即可,系统会自动在环境变量PATH中进行查找。
int execv(const char *path, char *const argv[]);
: 第二个参数改为以数组的方式传参—怎么执行
int execvp(const char *file, char *const argv[]);
:就是前面两个函数的集成,指定程序传参–执行谁?和vector数组传递怎么执行
int execve(const char *path, char *const argv[], char *const envp[]);
:这个函数比前面多了一个指针数组的参数,env指针数组就是自定义环境变量,也就意味着,程序替换的时候,不用系统环境变量,用自己定义的环境变量。
注意:这里的自定义环境变量是覆盖式传入的,也就是说当你传入自定义环境变量时,原来子进程从父进程继承的环境变量就会被覆盖,那这引出了一个问题:我们知道环境变量具有全局属性,可以被子进程继承下去,那这是为啥?其实int execve(const char *path, char *const argv[], char *const envp[]);
这个函数时真正的系统调用,我们知道main函数也是有参数的,它也需要被调用,它的参数其实就是来自于execle的。在bash创建子进程的时候就会调用将环境变量继承给子进程!而上面的六个函数都是对这个int execve()
系统调用的封装!
那怎么实现传入环境变量时不覆盖原来的环境变量呢?
我们可以利用putenv函数将指定的自定义环境变量导入到环境变量表里面,然后将environ作为参数传给替换程序,这样替换程序就可以获得自己定义的和系统默认的环境变量。
其他的两个execl函数依此类推就可以知晓其意义。
真正执行程序替换的其实只有execve这一个系统调用接口ÿ
LinuxLinux多线程
Linux多线程
线程
线程是进程的一个执行分支,是在进程内部(线程本质是在进程的地址空间内运行)运行的一个执行流。
Linux线程的原理:如果我们今天创建“进程”,不独立创建地址空间,用户级页表,甚至不进行IO将程序的数据和代码加载到内存,我们只创建task_struct,然后让新的PCB指向和老的PCB指向同样的mm_struct。然后,通过合理的资源分配(当前进程的资源),让每个task_struct都能使用进程的一部分资源。此时,我们的每个PCB被CPU调度的时候,执行的“粒度”是不是比原始进程执行的“粒度”要更小一些。(线程)
什么又是进程呢? 站在OS系统角度:承担分配系统资源的基本单位。 一个进程被创建好之后,后续可能内部存在多个执行流(线程)。
如何看待我们曾经一直在学、在用的进程呢? 本质是:承担系统资源的基本实体,不过内部只有一个执行流。
进程本质是承担分配系统资源的基本实体。
线程是OS调度的基本单位。
【总结】
- 在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”。
- 一切进程至少都有一个执行线程。
- 线程在进程内部运行,本质是在进程地址空间内运行。
- 在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化。
- 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流。
线程的优点
- 创建一个新线程的代价要比创建一个新进程小得多
- 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
- 线程占用的资源要比进程少很多
- 能充分利用多处理器的可并行数量
- 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
- 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
- I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
线程的缺点
性能损失
- 一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
健壮性降低
- 编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
缺乏访问控制
- 进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
编程难度提高
- 编写与调试一个多线程程序比单线程程序困难得多
线程异常
- 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
- 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出
线程用途
- 合理的使用多线程,能提高CPU密集型程序的执行效率
- 合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)
Linux进程VS线程
- 进程是资源分配的基本单位
- 线程是调度的基本单位
- 线程共享进程数据,但也拥有自己的一部分数据:
- 线程ID(LWP)
- 一组寄存器(上下文数据)
- 栈
- errno
- 信号屏蔽字
- 调度优先级
- 进程的多个线程共享 同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
- 文件描述符表
- 每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
- 当前工作目录
- 用户id和组id
Linux线程控制
POSIX线程库
- 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的
- 要使用这些函数库,要通过引入头文<pthread.h>
- 链接这些线程函数库时要使用编译器命令的“-lpthread”选项
创建线程
【错误检查】:
- 传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。
- pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)。而是将错误代码通 过返回值返回
- pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误, 建议通过返回值业判定,因为读取返回值要比读取线程内的errno变量的开销更小
代码示例:
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <pthread.h>
void *rout(void *arg)
int i;
for( ; ; )
printf("I'am thread 1\\n");
sleep(1);
int main( void )
pthread_t tid;
int ret;
if ( (ret=pthread_create(&tid, NULL, rout, NULL)) != 0 )
fprintf(stderr, "pthread_create : %s\\n", strerror(ret));
exit(EXIT_FAILURE);
int i;
for(; ; )
printf("I'am main thread\\n");
sleep(1);
线程终止
如果需要只终止某个线程而不终止整个进程,可以有三种方法:
- 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
- 线程可以调用pthread_ exit终止自己。
- 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程。
需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。
线程等待
- 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
- 创建新的线程不会复用刚才退出线程的地址空间。
调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:
- 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。
- 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数PTHREAD_ CANCELED。
- 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。
- 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。
分离线程
- 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
- 如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
分离的本质:是让主线程不用在join新线程,从而可以让新线程退出的时候,自动回收资源。
线程ID及进程地址空间布局
- pthread_ create函数会产生一个线程ID,存放在第一个参数指向的地址中。该线程ID和前面说的线程ID不是一回事。
- 前面讲的线程ID属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程。
- pthread_ create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。
- 线程库NPTL提供了pthread_ self函数,可以获得线程自身的ID:
pthread_t pthread_self(void);
pthread_t 到底是什么类型呢?取决于实现。对于Linux目前实现的NPTL实现而言,pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址。
主线程,不使用库中的栈结构,直接使用地址空间中的栈。
以上是关于LinuxLinux进程控制及程序替换的主要内容,如果未能解决你的问题,请参考以下文章