在第三代计算机的发展中由于出现了多道技术使得同一时刻能够在内存中同时运行多个程序,那么这样就产生了进程的概念.linux
在本篇博客中将介绍进程相关的理论知识,对操做系统进一步的认识.算法
进程( process),是指计算机中已运行的程序.进程曾经是分时系统的基本运行单位.在面向进程设计的系统(如早期的 UNIX,Linux2.4及更早的版本)中,进程是程序的基本执行实体,是操做系统的资源单位;在面向线程设计的系统(如当代多数操做系统, Linux2.6及更新的版本)中,进程自己不是基本运行单位,运行单位变为线程,进程是线程的容器.程序自己只是指令,数据及其组织形式的描述,进程才是(指令和数据)的真正运行实例.若干进程有可能与同一个程序相关系,且每一个进程皆能够同步(循序)或异步(平行)的方式独立运行.现代计算机系统由于空间共享(空间复用)可在同一段时间内以进程的形式将多个程序加载到存储器中,并借由时间共享(时间复用),以在一个处理器上表现出异步(平行)运行的感受.windows
用户下达运行程序的命令后,就会产生进程.同一程序可产生多个进程(一对多的关系),以容许同时又多位用户运行同一程序,却不会产生冲突.数组
开启进程须要一些必要的资源才能完成工做,如 CPU 使用时间,存储器,文件以及 I/O 设备,且为依序逐一进行,也就是每一个 CPU 内核任什么时候间内仅能运行一项进程.安全
一个计算机系统集成包括(或者说拥有)下列资源:异步
进程在运行中,状态会随时发生改变.所谓状态,就是指进程目前的动做:函数
各状态名称可能虽不一样的操做可以系统而不一样,对于单核系统( UP),任什么时候间可能有多个进程为等待,就绪,但一定仅有一个进程在运行.ui
对于一个进程来讲,操做系统为了可以在CPU离开后继续执行该进程,会把此时进程运行的全部状态保存下来,为此,操做系统和会维护一张表格,即进程表( process table),每一个进程占用一个进程表项(也称之为进程控制块).spa
对于上图中重要项的解释以下:操作系统
进程时暂时存储数据,以便稍后继续利用;其数量及类别因计算机体系结构有所差别;
在类 Unix 系统中可使用 ps 命令查询正在运行的进程,好比 ps -eo pid,comm,cmd,下图为执行结果:(-e 表示列出所有进程, -o pis,comm,cmd 表示咱们须要 PID,COMMAND,CMD 信息)
每一行表明一个进程.每一行分为三列.第一列为 PID(Process IDentity)是一个整数,每个进程都有一个惟一的 PID 来表示本身的身份,进程也能够根据 PID 来识别其余的进程.第二列 COMMAND 是该进程的简称.第三列 CMD 是进程所对应的程序以及运行时所带的参数.(第三列有一些由[]括起来的,它们是内核的一部分功能)
在第一行的 PID 为1,名字为 systemd(18.04,版本为16.04该名字为 init).这个进程是执行/sbin/init 这一文件(程序)产生的(不知道个人为何不是,查看了朋友的是/sbin/init😭).当 Linux 启动的时候, systemd 是系统建立的第一个进程,这一进程会一直存在,直到关闭计算机.
实际上,当计算机开机时,内核( kernel)只创建了一个systemd 进程. Linux 内核并不提供直接创建新进程的系统调用.剩下的全部进程都是 systemd 进程经过fork 机制创建的.新的进程要经过老的进程复制自身获得,这就是fork.fork 是一个系统调用.进程存活于内存中.每一个进程都在内存中分配有属于本身的一片空间(address space).当进程fork 的时候, Linux 在内存中开辟出一片新的内存空间给新的进程,并将老的进程空间中的内容复制到新的空间中,此后两个进程同时运行.
老进程成为新进程的父进程(parent process),而相应的,新进程就是老进程的子进程(child process).一个进程除了有一个 PID 以外,还会有一个 PPID(parent PID)来存储父进程的 PID. 若是咱们循着 PPID 不断向上追溯的话,总会发现其源头是 systemd 进程.因此说,全部的进程也构成一个以 systemd 为根的树状结构.
使用 pstree命令查看进程树:
能够看到 systemd 进程是整个进程树的根.
fork 一般做为一个函数调用,这个函数会有两次返回,将子进程的 PID 返回给父进程,0返回给子进程.实际上,子进程总能够查询本身的 PPID 来知道本身的父进程是谁,这样,一对父子进程就能够随时查询对方.在调用fork 函数后,程序会设计一个 if 选择结构.当 PID 等于0时,说明该进程为子进程,那么让它执行某些指令;而当 PID 为一个正整数时,说明为父进程,则执行另一些指令.由此,就能够在子进程创建以后,让它执行与父进程不一样的功能.
当子进程终结时,它会通知父进程,清空本身所占据的内存,并在内核里留下本身的退出信息( exit code, 若是顺利运行,返回0;若是有错误或异常情况,为>0的整数).在这个信息里,会解释该进程为何退出.父进程在得知子进程终结时,有责任对该子进程使用 wait 系统调用.这个 wait 函数能从内核中取出子进程的退出信息,清空该信息在内核中所占据的空间.可是,若是父进程早于子进程终结,子进程就会成为一个孤儿(orphand)进程.孤儿进程会过继给 systemd 进程, systemd 进程也就成了该进程的父进程. systemd 进程负责该子进程终结时调用 wait 函数,
一个糟糕的程序也彻底可能形成子进程的退出信息滞留在内核中的情况(父进程不对子进程调用 wait 函数),这样的状况下,子进程成为僵尸( zombie)进程.当大量僵尸进程积累时,内存空间会被挤占.
尽管在 UNIX 中,进程与线程是有联系但不一样的两个东西,但在 Linux 中,线程只是一种特殊的进程.多个线程之间能够共享内存空间和 IO 接口.因此,进程是 Linux 程序的惟一实现方式.
从系统调用fork 中返回时,两个进程除了返回值 PID 不一样外,具备彻底同样的用户级上下文.在子进程中, PID的值为0.在系统启动时有内核建立的进程1是惟一不经过系统调用fork 而建立的进程.也就是上图的 systemd进程.
内核为系统调用fork 完成下列操做:
下面是系统调用fork 的算法.内核首先确信有足够的资源来完成fork. 若是资源不知足要求,则系统调用fork 失败.若是资源知足要求,内核在进程表中找一个空项,并开始构造子进程的上下文.
输入:无 输出:对父进程是子进程的 PID, 对子进程是0 { 检查可用的内核资源 取一个空闲的进程表项和惟一的 PID 号 检查用户没有过多的运行进程 将子进程的状态设置为'建立'状态 将父进程的进程表中的数据拷贝到子进程表中 当前目录的索引节点和改变的根目录(若是能够)的引用数加1 文件表中的打开文件的引用数加1 在内存中作父进程上下文的拷贝 在子进程的系统级上下文中压入虚设系统级上下文层 /* 虚设上下文层中含有使子进程能 /* 识别本身的数据,使子进程被调度时 /* 从这里开始运行 if (正在执行的进程是父进程){ 将子进程的状态设置为'就绪'状态 return (子进程的 PID) //从系统到用户 } else { 初始化计时区 return 0; } }
来看下面的例子.该程序说明的是通过系统调用fork 以后,对文件的共享存取.用户调用改程序时应有两个参数,一个是已经有的文件名;另外一个是要建立的新文件名.该进程打开已有的文件,建立一个新文件,而后假定没有碰见错误,它调用fork 来建立一个子进程.子进程能够经过使用相同的文件描述符来继承的存取父进程的文件(即父进程已经打开和建立的文件).
固然,父进程和子进程要分别独立的调用rdwrt 函数并执行一个循环,即从源文件中读一个字节,而后写一个字节到目标文件中去.当系统调用 read 碰见文件尾时,函数rdwrt 当即返回.
#include <fcntl.h> int fdrd, fdwt; char c; main(int argc, char *argv[]) { if (argc != 3) { exit(1); } if ((fdrd = open(argv[1], O_RDONLY)) == -1) { exit(1); } if ((fdwt = creat(argv[2], 0666)) == -1) { exit(1); } fork(); // 两个进程执行一样的代码 rdwrt(); exit(0); } rdwrt() { for (;;) { if (read(fdrd, &c, 1) != 1) { return ; } write(fdwt, &c, 1); } }
在这个例子中,两个进程的文件描述符都指向相同的文件表项.这两个进程永远不会读或写到相同的文件偏移量,由于内核在每次 read 和 write 调用后,都要增长文件的偏移量.尽管两个进程彷佛是将源文件拷贝了两次,但由于它们分担了工做任务,所以,目标文件的内容依赖于内核调度两个进程的次序.若是内核这样调度两个进程:是它们交替的执行它们的系统调用,甚至使它们交替的执行每对 read 和 write 调用,则目标文件的内容和源文件的内容彻底一致.但考虑这样的状况:两个进程正要读源文件中的连续的字符'ab'.假定父进程读了字符'a',这时,内核在父进程write 以前,作了上下文切换来执行子进程。若是子进程 读到字符 "b",并在父进程被调度前,将它写到目标文件,那么目标文件将再也不含有 字符串 "ab",而是含有 "ba"了。内核并不保证进程执行的相对速率。
另外一个例子:
#include <string.h> char string[] = "Hello, world"; main() { int count, i; int to_par[2], to_chil[2]; // 到父、子进程的管道 char buf[256]; pipe(to_par); pipe(to_chil); if (fork() == 0) { // 子进程在此执行 close(0); // 关闭老的标准输入 dup(to_child[0]); // 将管道的读复制到标准输入 close(1); // 关闭老的标准输出 dup(to_par[1]); // 将管道的写复制到标准输出 close(to_par[1]); // 关闭没必要要的管道描述符 close(to_chil[0]); close(to_par[0]); close(to_chil[1]); for (;;) { if ((count = read(0, buf, sizeof(buf)) == 0) exit(); write(1, buf, count); } } // 父进程在此执行 close(1); // 从新设置标准输入、输出 dup(to_chil[1]); close(0); dup(to_par[0]); close(to_chil[1]); close(to_par[0]); close(to_chil[0]); close(to_par[1]); for (i = 0; i < 15; i++) { write(1, string, strlen(string)); read(0, buf, sizeof(buf)); } }
子进程从父进程继承了文件描述符0和1(标准输入和标准输出)。两次执行系统调用 pipe 分别在数组 to_par 和 to_chil 中分配了两个文件描述符。而后该进程 执行系统调用 fork,并复制进程上下文:象前一个例子同样,每一个进程存取 本身的私有数据。父进程关闭他的标准输出文件(文件描述符1),并复制(dup)从管道 线 to_chil 返回的写文件描述符。由于在父进程文件描述符表中的第一个空槽是刚刚 由关闭腾出来的,因此内核将管道线写文件描述符复制到了文件描述符表中的第一 项中,这样,标准输出文件描述符变成了管道线 to_chil 的写文件描述符。 父进程以相似的操做将标准输入文件描述符替换为管道线 to_par 的读文件 描述符。与此相似,子进程关闭他的标准输入文件(文件描述符0),而后复制 (dup) 管道 线 to_chil 的读文件描述符。因为文件描述符表的第一个空项是原先的标准 输入项,因此子进程的标准输入变成了管道线 to_chil 的读文件描述符。 子进程作一组相似的操做使他的标准输出变成管道线 to_par 的写文件描述 符。而后两个进程关闭从 pipe 返回的文件描述符。上述操做的结果是:当 父进程向标准输出写东西的时候,他其实是写向 to_chil--向子进程发送 数据,而子进程则从他的标准输入读管道线。当子进程向他的标准输出写的时候, 他其实是写入 to_par--向父进程发送数据,而父进程则从他的标准输入 接收来自管道线的数据。两个进程经过两条管道线交换消息。
不管两个进程执行的顺序如何,这个程序执行的结果是不变的。他们可能去执行睡眠 和唤醒来等待对方。父进程在15次循环后退出。而后子进程因管道线没有写进程而读 到“文件尾”标志,并退出。
windows 也是使用 PID 来惟一标识一个进程.
在一个进程内部,使用进程句柄来标识关注的每一个进程.使用 Windows API 从进程 PID 获取进程句柄:
OpenProcess(PROCESS_ALL_ACCESS, TRUE, procId); //或者PROCESS_QUERY_INFORMATION
使用 API 函数: GETModuleFileNameEx 或 GetProcessImageFileName 或QUeryFullProcessImageName 查询进程的 exe 文件名,使用 API 函数 GetCurrentProcess能够获取本进程的伪句柄(值为-1),只能用于本进程的 API 函数调用;不能被其余进程继承或复制.可用 API 函数 DuplicateHandle 得到进程的真句柄.
Windows 系统使用 CreateProcess 建立进程, WaitForSingleObject 可等待子进程的结束.例如:
#include <windows.h> #include <stdio.h> #include <tchar.h> void main() { STARTUPINFO si; PROCESS_INFORMATION pi; ZeroMemory(&si, sizeof(si)); si.cb = sizeof(si); ZeroMemory(&pi, sizeof(pi)); // Start the child process. if (!CreateProcess(NULL, // No module name (use command line) "demo.exe arg1", // Command line NULL, // Process handle not inheritable NULL, // Thread handle not inheritable FALSE, // Set handle inheritance to FALSE 0, // No creation flags NULL, // Use parent's environment block NULL, // Use parent's starting directory &si, // Pointer to STARTUPINFO structure &pi) // Pointer to PROCESS_INFORMATION structure,用于给出子进程主窗口的属性 ) { printf("CreateProcess failed (%d).\n", GetLastError()); return; } // Wait until child process exits. WaitForSingleObject(pi.hProcess, INFINITE); // Close process and thread handles. CloseHandle(pi.hProcess); CloseHandle(pi.hThread); }
建立的子进程能够继承父进程的:
子进程不能继承:
为继承句柄,父进程在建立(或者代开,复制)各类可继承对象句柄时,在 SECURITY_ATTRIBUTES 结构的 blnheritHandle 成员为 TRUE. 在 CreateProcess 的blnheritHandles 参数为 TRUE; 若是要继承标准输入,标准输出,标准错误的句柄, STARTUPINFO 结构的 dwFlags 成员包含 STARTF_USESTDHANDLES 标志位.
子进程终止时,全部打开的句柄被关闭,进程对象被处罚( signaled).进程的退出码( exit code)或者 ExitProcess,TerminateProcess 函数中指出,或者是 main,WinMain 函数返回值.若是进程因为一个致命异常(fatal exception)而终止,退出码是这个异常值,同时进程的全部执行中的线程的退出码也是这个异常值.
优雅的关闭其余进程的方法使用RegisterWindowMessage 登记私有消息,用 BroadcastSystemMessage 播放消息,收到消息的进程用ExitProcess关闭.
若是想要获取特定名字的进程的ID,须要枚举全部进程。传统办法是CreateToolhelp32Snapshot、Process32First、Process32Next函数;也可使用EnumProcesses、EnumProcessModules函数来获取全部的进程ID,一个进程的全部模块的句柄。示例以下:
PROCESSENTRY32 pe32; HANDLE hSnaphot; HANDLE hApp; DWORD dProcess; hSnaphot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); // 获取进程快照 Process32First(hSnaphot, &pe32); // 枚举第一个进程 do { if (lstrcmpi(pe32.szExeFile, _T("NotePad.exe")) == 0) { // 查找进程名称为 NotePad.exe dProcess = pe32.th32ProcessID; break; } } while (Process32Next(hSnaphot, &pe32)); // 不断循环直到枚举不到进程 hApp = OpenProcess(PROCESS_VM_OPERATION | SYNCHRONIZE, FALSE, dProcess); // 根据进程 ID 获取程序的句柄 if (!WaitForSingleObject(hApp, INFINITE)) // 等待进程关闭 AfxMessageBox(" 记事本已经关闭!"); // 另外一种方法 DWORD aProcId[1024], dwProcCnt, dwModCnt; HMODULE hMod[1000]; TCHAR szPath[MAX_PATH]; // 枚举出全部进程ID if (!EnumProcesses(aProcId, sizeof(aProcId), &dwProcCnt)) { //cout << "EnumProcesses error: " << GetLastError() << endl; return 0; } // 遍例全部进程 for (DWORD i = 0; i < dwProcCnt; ++i) { // 打开进程,若是没有权限打开则跳过 HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, aProcId[i]); if (NULL != hProc) { // 打开进程的第1个Module,并检查其名称是否与目标相符 if (EnumProcessModules(hProc, &hMod, 1000, &dwModCnt)) { GetModuleBaseName(hProc, hMod, szPath, MAX_PATH); if (0 == lstrcmpi(szPath, lpName)) { CloseHandle(hProc); return aProcId[i]; } } CloseHandle(hProc); } } return 0;