)
多任务编程也叫进程编程进程就是程序的一次执行过程或者说是正在运行中的程序。我们在用电脑时可以一边打游戏一边听歌还可以回消息等等这些看起来是同时在运行的但其实是每个程序只跑一小会儿就被踢下去换下一个。因为切换太快毫秒级人眼 / 人感官根本察觉不到就以为它们在同时跑。这叫做宏观上并行微观上串行。那么是怎么做到让每个程序都跑一小会呢这里就要提到调度算法了比如时间片轮转操作系统给每个进程分配一个时间片time slice通常是 几毫秒到几十毫秒比如 Linux 默认约 100ms 量级实际更复杂然后每个程序依次运行然后循环往复A-B-C-D-A-B.....。一、基础知识1.程序与进程线程概念本质通俗解释程序存放在磁盘上的可执行文件静态菜谱放在书架上不会自己动进程程序的一次执行过程动态有独立的地址空间按照菜谱做菜的一个厨房有自己的锅碗瓢盆内存线程进程内部的一条执行流共享进程的地址空间同一个厨房里的多个厨师共用锅碗瓢盆但各做各的菜程序数据代码data已初始化的数据bss未初始化的数据text正文段也就是代码区进程PCB[栈|堆|data|bss|text]PCB块是用来记录和保存程序运行过程中的动态信息方便操作系统调度。线程下一篇再讲2.进程的一生1进程的通用模型所有OS的进程都符合这个模型新建状态表示进程正在被创建还没有准备好就绪状态代表进程一切已经就绪等待CPU调度运行运行状态是进程正在CPU上执行阻塞状态指进程在等待IO、资源等某个事件当等待的事件发生后进程会回到就绪状态终止状态则是进程执行完毕正在被撤销。2linux特有状态含义就绪Linux不区分就绪和运行都叫 RUNNING。区别只在于 是否正占有 CPU。图里分成了 就绪 和 占有 CPU 执行 两个圈但内核里是同一个状态。浅度睡眠等资源可以被信号唤醒比如sleep、等键盘输入。你CtrlC能打断它就是因为这个。深度睡眠等硬件 IO比如读磁盘不能被信号打断只能等资源到位wake_up()唤醒。这就是为什么有时候进程卡死kill -9都杀不掉 —— 它在这个状态。暂停收到SIGSTOP/SIGTSTP信号被挂起或者调试时ptrace暂停。收到SIGCONT恢复。僵死进程已经执行完do_exit()但父进程还没wait()回收它PCB 还留着。3.状态编码R RUNNING就绪和正在运行S INTERRUPTIBLE浅睡最常见D UNINTERRUPTIBLE深睡杀不死的那种T STOPPED被任务控制信号暂停t:debug暂停Z ZOMBIE僵尸态被接管Xdead资源被回收了永远看不到4.常用命令下面将展示在Vmware中常用的一些命令以及他的运行效果1ps aux | grep a.out列含义USER进程所属用户这里都是linux普通用户PID进程 ID 号每个进程唯一编号kill 杀进程就用这个数字%CPUCPU 占用百分比你7256达到 89.3%CPU 疯狂跑死循环%MEM占物理内存百分比VSZ虚拟内存大小单位 KBRSS实际占用物理内存单位 KBTTY终端。pts/0你运行程序的终端窗口pts/1是你敲 ps 命令的另一个终端STAT进程状态最重要一列START进程几点启动19:32TIME进程累计占用 CPU 时间COMMAND启动这个进程的命令2ps -eLf | grep a.out字段含义UID运行进程的用户 ID这里都是linuxPID进程 ID 号PPID父进程 PID重点看谁 fork 出来的LWP轻量级线程 ID单进程单线程时 LWPPIDCCPU 占用调度计数NLWP该进程一共有多少个线程数STIME进程启动时间TTY属于哪个终端pts/0终端 0TIME累计占用 CPU 时间CMD执行的命令3看进程树pstree可以看到整个进程树。pstree -sp pid号可以看到从1到当前进程的4kill发一个信号给进程用法kill -信息编号 进程pid号信息编号有9SIGKILL让进程结束杀死进程19暂停进程18恢复进程killall -9 a.out 可以杀死所有进程。二、代码实现1.创建一个子进程形式pid_t fork(void)头文件#includesys/types.h #includeunistd.h功能复制调用fork的进程来创建一个新进程返回值成功时父进程返回子进程的pid号子进程返回0失败时返回-1,errno注意在创建完进程后父进程和子进程谁先运是不确定的和进程的调度算法优先级等因素有关也不一定是一个进程执行一次运行到哪取决于操作系统怎么调度。getpid能返回当前进程的pid号。要注意在代码中什么位置的的代码会被什么执行。例题1连续fork两次第一次父进程7910创建了一个子进程7911第二次fork时父进程又创建7912因为子进程会复制父进程所以子进程也会执行一次fork创建7913。2fork()fork()||fork()这里面涉及逻辑运算的短路特性以及前面提到的子进程的返回值为0父进程的返回值是子进程的pid号一定0例如在1生成的子进程7944中fork()的返回值为0那么fork()fork()就直接不计算了直接计算3以及fork()fork()为1时整体一定为真3也不会继续进行。注意fork之后字进程继承了什么继承了没继承代码段、数据段、堆、栈写时复制PID文件描述符打开的文件父进程的 PID环境变量、当前工作目录未处理的信号用户 ID、组 ID锁有些锁不继承要注意父子进程拥有独立的4G内存空间各自拥有独立的数据段数据之间不会相互影响无法进行数据传递。但是这种又好又坏好处是如果子进程崩了其他进程不会受到影响只是交换数据不太方便。3文件复制一个文件父进程复制前半部分子进程复制后半部分。fork之后父子进程文件共享:子进程会继承父进程中已经打开的文件描述符 父子进程使用的是同一个文件表相互之间的操作会受到影响如果不想相互间受到影响可以fork之后再打开文件父子进程各自独立操作 。因为我们不能确定是先运行父进程还是先运行子进程所以在父进程复制前半部分的时候一定要将读写指针移动到开头。2.进程执行exec 系统调用能够加载磁盘上其他独立的可执行程序使进程可以运行不属于当前程序的代码普通函数调用只能执行本程序内部已经编译好的代码。exec 实现程序之间的解耦。就像先租一间新房子fork 得到新 PID然后把新房子里面全部东西清空替换进程镜像叫外面完全另一队工人外部可执行程序搬进来干活。1int execl(const char *path, const char *arg, ...);int execv(const char *path, char *const argv[]);参数path ---表示要执行的文件的 路径 --- 要求路径名中 要包含可执行文件名arg ---表示的是可执行文件的名字... ---可变参数 注意: 最终以NULL作为结尾这块往往给的是可执行文件需要用到的一些参数区别list --- 逐个罗列从第二个参数开始逐个罗列vector --- 表示 从第二个参数开始 组织成 数组的形式which 文件名可以查看可执行文件的文件路径。用法他们可以达到相同的效果区别就是execv需要把参数写在一个数组里面。2int execlp ( const char *file, const char *arg, ...);int execvp ( const char *file, char *const argv[]);带PPATH表示寻找可执行文件到系统的环境变量PATH中寻找此时不需要指定路径file参数 --只需要指定可执行文件的名字即可其余参数与上一个相同这里就不举例了。3int execle(const char *path, const char *arg,..., char * const envp[]);int execvpe(const char *file, char *const argv[], char *const envp[]);如果要执行的文件需要使用到环境变量则可以通过带e的这两个函数传递环境变量个可执行文件。主播不太懂这个环境变量有什么用啊先记住这个函数吧如果后面学明白了会在之后的博客仔细讲的。上面这六个函数经常和下面这个函数结合使用char *strtok(char *str, const char *delim);功能:提取字符串参数:str为要解析字符串给成NULL 表示接着原先的字符串继续拆分delim 为分隔标记遇到这些符号就会自动将前面的作为一个字符串。返回值:成功 返回子字符串的首地址失败 返回NULL字符串解析完也返回NULL例子当遇到 \n时拆分第二句为连续拆分的使用方法。3.进程退出1进程结束:A.正常结束main函数中 returnexit() 库函数_exit,_Exit系统调用主线程退出主线程调用pthread_exitB.异常结束往往是因为信号导致的结束abort() 发送一个SIGABRTsignal 发信号结束了进程 kill pid -9 最后一个线程被pthread_cancle2函数exit 和_exit退出状态终止的进程会通知父进程自己使如何终止的。如果是正常结束终止则由exit传入的参数带回。如果是异常终止则有内核通知异常终止原因的状态。任何情况下父进程都能使用waitwaitpid获得这个状态以及资源的回收。a.void exit(int status)功能:让进程退出,并刷新缓存区参数status:进程退出的状态exit(int status)参数是完整 int32 位Linux 内核只保存 status 的低 8 位给父进程有效范围0~255所以需要将后8位提取出来所以status0377可以得到真正有效的退出码。EXIT_SUCCESS 0EXIT_FAILURE 1我们可以使用这两个宏定义让代码更直观0代表正常终止1代表失败。b._exitvoid _exit(int status);功能:让进程退出,不刷新缓存区参数:status:进程退出状态exit和_exit的区别exit - 刷新缓存区 - atexit注册的退出函数 - _exitc.int atexit(void (*function)(void));功能:注册进程退出前执行的函数参数:function:函数指针指向void返回值void参数的函数指针返回值:成功返回0失败返回非0作用关闭你自己打开的文件释放堆上 malloc 的内存写日志记录程序正常退出清理临时文件。当程序调用exit或者由main函数执行return时,所有用atexit注册的退出函数,将会由注册时顺序倒序被调用可以多次注册。4.资源回收a.pid_t wait(int *wstatus);头文件#include sys/types.h #include sys/wait.h功能该函数可以阻塞等待任意子进程退出并回收该进程的资源和获取退出状态值。一般用于父进程回收子进程状态。参数status为进程退出时候的状态如果不关心其退出状态一般用NULL表示如果要回收进程退出状态则用WEXITSTATUS回收。返回值成功 回收的子进程pid失败 -1WIFEXITED(wstatus) //判断是否是正常结束WEXITSTATUS(wstatus) //获取正常结束时的退出状态值WIFSIGNALED(wstatus) //判断是否时信号结束的WTERMSIG(wstatus) //是哪个信号b.pid_t waitpid(pid_t pid, int *wstatus, int options);功能回收子进程状态参数pid为子进程的pid号wstatus有以下四种选择-1 回收指定进程组内的任意子进程-1 回收任意子进程组内外0 回收和当前调用waitpid一个组的所有子进程组内0 回收指定ID的子进程options为0时是阻塞等待为WNOHANG表示非阻塞等待。阻塞等待是父进程会停留在这一句等待子进程退出。返回值成功 返回接收资源的子进程pid非阻塞是返回值为 0 表示没有任何子进程状态改变失败 -1 。