多任务的概念人们已经非常熟悉了,它是指用户可以在同一时间内运行多个应用程序。Linux就是一种支持多任务的操作系统,它支持多进程、多线程等多任务处理和任务之间的多种通信机制。
Linux下多任务机制的介绍多任务处理是指用户在同一时间内运行多个应用程序,每个应用程序被称做一个任务。Linux就是一个支持多任务的操作系统,它比单任务系统的功能增强了许多。当多任务操作系统使用某种任务调度策略允许两个或更多进程并发共享一个处理器时,事实上处理器在某一时刻只会给一个任务提供服务。由于任务调度机制保证了不同的任务之间的切换速度十分迅速,因此给人多个任务同时运行的错觉。多任务系统中有3个功能单位:任务、进程和线程,下面分别进程介绍。
1、任务
任务是一个逻辑概念,指由一个软件完成的活动,或者是一系列共同达到某一个目的的操作。通常一个任务是一个程序的一次运行,一个任务包含一个或多个完成独立功能的子任务,这个独立的子任务就是进程或线程。例如,一个杀毒软件的一次运行是一个任务,目的是从各种病毒的侵害中保护计算机系统,这个任务包含多个独立功能的子任务(进程或线程),包含实时监控功能、定时查杀功能、防火墙功能及用户交互功能等。个人理解:就好比假设一个应用程序中由一个或多个可执行文件共同执行组成,那么此应用程序的一次执行就是一个任务,而这些可执行文件的执行就是一个进程的执行,而可执行文件是由一个线程或多个线程构成的,当只有一个线程构成了这个进程,则此时进程和线程就是一样的概念(可执行文件的一次运行)。
2、进程
进程的基本概念
进程是一个具有独立功能的程序在某个数据集上的一次动态执行过程,它是系统进行资源分配和调度的基本单位(个人理解:系统好比是一个大型的任务,由多个进程(可执行文件)构成,而资源分配和资源调度分别都是一个进程,所以进程是系统进行资源分配和调度的基本单位)。一次任务的运行可以并发激活多个进程,这些进程相互合作来完成该任务的一个最终的目标。
进程具有并发性、动态性、交互性、独立性和异步性等主要特性
- 并发性:指的是系统中多个进程可以同时并发执行,相互之间不受干扰。
- 动态性:指的是进程都有完整的生命周期,而且在进程的生命周期内,进程的状态是不断变化的。另外,进程具有动态的地址空间(包括代码、数据和进程控制块)。
- 交互性:指的是进程在执行过程中可能会与其他进程发生直接和间接的交互操作,如进程同步和进程互斥等,需要为此添加一定的进程处理机制。
- 独立性:指的是进程是一个相对完整的资源分配和调度的基本单位,各个进程的地址空间是相互独立的,只有采用某些特定的通信机制才能实现进程间的通信。
- 异步性:指的是每个进程都按照各自独立的、不可预知的速度向前执行。
- 交互式过程:这类进程进程与用户进程交互,因此要花很多时间等待用户的交互操作(键盘和鼠标操作等)。当接收到用户的交互操作后,这类进程应该很快被允许,而且相应时间的变化也应该很小,否则用户就会觉得系统反应迟钝或者不太稳定。典型的交互式进程有shell命令进程、文本编辑器和图形应用程序运行等。
- 批处理进程:这类进程不必与用户进行交互,因此进程在后台运行。因为这类进程通常不必很快地相应,因此往往受到调度器的“慢待”。典型的批处理进程有编译器的编译操作、数据库搜索引擎等。
- 实时进程:这类进程通常对调度响应时间有很高的要求,一般不会被低优先级的进程阻塞。它们不仅要求很短的响应时间,而且更重要的是响应时间的变化应该很小。典型的实时进程有视频和音频的应用程序、实时数据采集系统程序等。
Linux下的进程结构
- 运行状态(TASK_RUNNING):进程当前正在运行,或者正在运行队列中等待调度。
![Linux下C语言开发(多任务编程之任务、进程、线程)_学习](https://s2.51cto.com/images/blog/202107/29/3d6297f4a4658835f0898364bceed5e7.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=/resize,m_fixed,w_1184)
![Linux下C语言开发(多任务编程之任务、进程、线程)_学习_02](https://s2.51cto.com/images/blog/202107/29/bd9802d825b90bb9c4abcc9e03e2fbc8.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=/resize,m_fixed,w_1184)
![Linux下C语言开发(多任务编程之任务、进程、线程)_学习_03](https://s2.51cto.com/images/blog/202107/29/382c1c17601a43ef59a71236a3813d10.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=/resize,m_fixed,w_1184)
![Linux下C语言开发(多任务编程之任务、进程、线程)_学习_04](https://s2.51cto.com/images/blog/202107/29/f4ab2242812b1bb1dedd807f2dfdb3f5.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=/resize,m_fixed,w_1184)
- 可中断的阻塞状态(TASK_INTERRUPTIBLE):进程处于阻塞(睡眠)状态,正在等待某些事件发生或能够占用某些资源。处在这种状态下的进程可以被信号中断。接收到信号或被显式的唤醒呼叫(如调用wake_up系列宏:wake_up、wake_up_interruptible等)唤醒之后,进程转变为TASK_RUNNING状态。
- 不可中断的阻塞状态(TASK_UNINTERRUPTIBLE):此进程状态类似于可中断的阻塞状态(TASK_INTERRUPTILBE),只是它不会处理信号,把信号传递到这种状态下的进程不能改变它的状态。在一些特定的情况下(进程必须等待,直到某些不可被中断的事件发生),这种状态是很有用的。只有在它所等待的事件发生时,进程被显式的唤醒呼叫唤醒。
- 可终止的阻塞状态(TASK_KILLABLE):Linux内核2.6.25引入了一种新的进程状态,名为TASK_KILLABLE。该状态的运行机制类似于TASK_UNINTERRUPTILBE,只不过在该状态下的进程可以响应致命信号。它可以替代有效但可能无法终止的不可中断的阻塞状态,以及易于唤醒安全性欠佳的可中断的阻塞状态。
- 暂停状态(TASK_STOPPED):进程的执行被暂停,当进程收到SIGTOP、SIGTSTP、SIGTTIN、SIGTTOU等信号时,就会进入暂停状态。
- 跟踪状态(TASK_TRACED):进程的执行被调试器暂停。当一个进程被另一个进程监控是(如调试器使用ptrace()系统调用监控测试程序),任何信号都可以把这个进程置于跟踪状态。
- 僵尸状态(EXIT_ZOMBIE):进程运行结束,父进程尚未使用wait函数族(如使用waitpid()函数)等系统调用来“收尸”,即等待父进程销毁它。处于该状态下的进程“实体”已经放弃了几乎所有的内存空间,没有任何可执行代码,也不能调度,仅仅在进程列表保留一个位置,记载该进程的退出状态等信息供其他进程收集。
- 僵尸撤销状态(EXIT_DEAD):这是最终状态,父进程调用wait函数族“收尸”后,进程彻底有系统删除。
![Linux下C语言开发(多任务编程之任务、进程、线程)_学习_05](https://s2.51cto.com/images/blog/202107/29/b5ba6d68e6c3b5ec6e06b38639604d54.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=/resize,m_fixed,w_1184)
![Linux下C语言开发(多任务编程之任务、进程、线程)_学习_06](https://s2.51cto.com/images/blog/202107/29/975f0f7a6f5d0e07aea31beee57ef69a.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=/resize,m_fixed,w_1184)
![Linux下C语言开发(多任务编程之任务、进程、线程)_学习_07](https://s2.51cto.com/images/blog/202107/29/6700878017ebdb90f21b886a3a916d8c.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=/resize,m_fixed,w_1184)
![Linux下C语言开发(多任务编程之任务、进程、线程)_学习_08](https://s2.51cto.com/images/blog/202107/29/c7e800c924620a61c40f56f5b1553941.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=/resize,m_fixed,w_1184)
![Linux下C语言开发(多任务编程之任务、进程、线程)_学习_09](https://s2.51cto.com/images/blog/202107/29/cd961a412c1d1e00f62d0eb487834976.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=/resize,m_fixed,w_1184)
进程的创建、执行和终止
进程的内存结构
![Linux下C语言开发(多任务编程之任务、进程、线程)_学习_10](https://s2.51cto.com/images/blog/202107/29/aae37e0417480698d7973877c40d4d02.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=/resize,m_fixed,w_1184)
![Linux下C语言开发(多任务编程之任务、进程、线程)_学习_11](https://s2.51cto.com/images/blog/202107/29/2fac88367124b786901cc206733b0f2e.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=/resize,m_fixed,w_1184)
- 只读段:包含程序代码(.init和.exit)和只读数据(.rodata)
- 数据段:存放的是全局变量和静态变量。其中可读可写数据段(.data)存放已经初始化的全局变量和静态变量,BSS数据段(.bss)存放未初始化的全局变量和静态变量
- 堆:由系统自动分配释放,存放函数的参数值、局部变量的值、返回地址等
- 堆栈:存放动态分配的数据,一般由程序员动态分配和释放。若程序员不释放,程序结束时可能由操作系统回收。
- 共享库的内存映射区域:这是Linux动态连接器和其他共享库代码的映射区域。
![Linux下C语言开发(多任务编程之任务、进程、线程)_学习_12](https://s2.51cto.com/images/blog/202107/29/55ed3e519a5ec39ca145f86983f1c2a7.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=/resize,m_fixed,w_1184)
![Linux下C语言开发(多任务编程之任务、进程、线程)_学习_13](https://s2.51cto.com/images/blog/202107/29/442ce21f7e7d711109593607058c8011.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=/resize,m_fixed,w_1184)
![Linux下C语言开发(多任务编程之任务、进程、线程)_学习_14](https://s2.51cto.com/images/blog/202107/29/f1824cadf0a1dad67a1a664950b64e5d.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=/resize,m_fixed,w_1184)
![Linux下C语言开发(多任务编程之任务、进程、线程)_学习_15](https://s2.51cto.com/images/blog/202107/29/d9ff8dff88089c3aa2ecd4ad5338aba8.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=/resize,m_fixed,w_1184)
3、线程
![Linux下C语言开发(多任务编程之任务、进程、线程)_学习_16](https://s2.51cto.com/images/blog/202107/29/5d54717d7670dacca7e20f1cad76f7ed.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=/resize,m_fixed,w_1184)
2.1Linux多任务编程——进程
2.1.1— fork
pitd_t fork(void);
创建一个新的子进程,其父进程为调用 fork() 函数的进程;
返回值:成功:子进程返回 0,父进程返回 子进程 PID;失败 返回 -1;
- 1>新创建的子进程PID,与父进程PID不同;
- 2>子进程 从 fork() 返回值开始运行,返回值为 0;父进程 fork() 返回新创建的子线程 PID
fork函数入门知识
一个进程,包括代码、数据和分配给进程的资源。fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程,也就是两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同,两个进程也可以做不同的事。
一个进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值不同。相当于克隆了一个自己。
我们来看一个例子:
1. /*
2. * fork_test.c
3. * version 1
4. * Created on: 2010-5-29
5. * Author: wangth
6. */
7. #include <unistd.h>
8. #include <stdio.h>
9. int main ()
10. {
11. pid_t fpid; //fpid表示fork函数返回的值
12. int count=0;
13. fpid=fork();
14. if (fpid < 0)
15. printf("error in fork!");
16. else if (fpid == 0) {
17. printf("i am the child process, my process id is %d/n",getpid());
18. printf("我是爹的儿子/n");//对某些人来说中文看着更直白。
19. count++;
20. }
21. else {
22. printf("i am the parent process, my process id is %d/n",getpid());
23. printf("我是孩子他爹/n");
24. count++;
25. }
26. printf("统计结果是: %d/n",count);
27. return 0;
}
运行结果:
运行结果是:
i am the child process, my process id is 5574
我是爹的儿子
统计结果是: 1
i am the parent process, my process id is 5573
我是孩子他爹
统计结果是: 1
在语句fpid=fork()之前,只有一个进程在执行这段代码,但在这条语句之后,就变成两个进程在执行了,这两个进程的几乎完全相同,将要执行的下一条语句都是if(fpid<0)……
为什么两个进程的fpid不同呢,这与fork函数的特性有关。fork调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值:
1)在父进程中,fork返回新创建子进程的进程ID;
2)在子进程中,fork返回0;
3)如果出现错误,fork返回一个负值;
在fork函数执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID。我们可以通过fork返回的值来判断当前进程是子进程还是父进程。
引用一位网友的话来解释fpid的值为什么在父子进程中不同。“其实就相当于链表,进程形成了链表,父进程的fpid(p 意味point)指向子进程的进程id, 因为子进程没有子进程,所以其fpid为0.
fork出错可能有两种原因:
- 1)当前的进程数已经达到了系统规定的上限,这时errno的值被设置为EAGAIN。
- 2)系统内存不足,这时errno的值被设置为ENOMEM。
创建新进程成功后,系统中出现两个基本完全相同的进程,这两个进程执行没有固定的先后顺序,哪个进程先执行要看系统的进程调度策略。
每个进程都有一个独特(互不相同)的进程标识符(process ID),可以通过getpid()函数获得,还有一个记录父进程pid的变量,可以通过getppid()函数获得变量的值。
fork执行完毕后,出现两个进程.
有人说两个进程的内容完全一样啊,怎么打印的结果不一样啊,那是因为判断条件的原因,上面列举的只是进程的代码和指令,还有变量啊。
执行完fork后,进程1的变量为count=0,fpid!=0(父进程)。进程2的变量为count=0,fpid=0(子进程),这两个进程的变量都是独立的,存在不同的地址中,不是共用的,这点要注意。可以说,我们就是通过fpid来识别和操作父子进程的。
还有人可能疑惑为什么不是从#include处开始复制代码的,这是因为fork是把进程当前的情况拷贝一份,执行fork时,进程已经执行完了int count=0;fork只拷贝下一个要执行的代码到新的进程。
fork进阶知识
先看一份代码
1. /*
2. * fork_test.c
3. * version 2
4. * Created on: 2010-5-29
5. * Author: wangth
6. */
7. #include <unistd.h>
8. #include <stdio.h>
9. int main(void)
10. {
11. int i=0;
12. printf("i son/pa ppid pid fpid/n");
13. //ppid指当前进程的父进程pid
14. //pid指当前进程的pid,
15. //fpid指fork返回给当前进程的值
16. for(i=0;i<2;i++){
17. pid_t fpid=fork();
18. if(fpid==0)
19. printf("%d child %4d %4d %4d/n",i,getppid(),getpid(),fpid);
20. else
21. printf("%d parent %4d %4d %4d/n",i,getppid(),getpid(),fpid);
22. }
23. return 0;
}
2.1.2— exec函数族
exec函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的内容,换句话说,就是在调用进程内部执行一个可执行文件。这里的可执行文件既可以是二进制文件,也可以是任何Linux下可执行的脚本文件。
与一般情况不同,exec函数族的函数执行成功后不会返回,因为调用进程的实体,包括代码段,数据段和堆栈等都已经被新的内容取代,只留下进程ID等一些表面上的信息仍保持原样,颇有些神似"三十六计"中的"金蝉脱壳"。看上去还是旧的躯壳,却已经注入了新的灵魂。只有调用失败了,它们才会返回一个-1,从原程序的调用点接着往下执行
可以使用 fork 创建一个子进程,在子进程中进行 exec 函数的调用 去完成某些比较危险的操作。使用时一定要注意这一点。
exec家族一共有六个函数,分别是:
(1)int execl(const char *path, const char *arg, …);
(2)int execle(const char *path, const char *arg, … , char * const envp[]);
(3)int execv(const char *path, char *const argv[]);
(4)int execve(const char *filename, char *const argv[], char *const envp[]);
(5)int execvp(const char *file, char * const argv[]);
(6)int execlp(const char *file, const char *arg, …);
其中只有execve是真正意义上的系统调用,其它都是在此基础上经过包装的库函数。