进程的关键概念
进程: 程序的一个执行实例(正在执行的程序)
程序是静态的概念,例:将源文件通过编译链接后在当前路径下 生成的可执行文件 pro
gcc myprocess.c –o pro
当程序运行起来了,系统中就多了一个进程,担当分配系统资源(CPU时间,内存)的实体
(当我们运行程序的时候,相关文件就会从磁盘加载到内存,操作系统通过先描述,再组织的方法对文件进行管理,从而只让想要执行的程序加载到内存,一个加载到内存的程序称为进程)
#可以使用top指令再Linux中查看进程情况
top
#每个进程都有一个唯一非负整数的PID,通过ps打印全部进程并提炼init进程
ps -aux | grep init
( 前台进程: 进程状态后跟’+’ ; 后台进程: 进程状态后不跟’+’)
进程控制块
PCB(process control block) 进程控制块作用:当有很多执行各种业务的程序加载到内存中,操作系统对这些进程要如何进行管理呢? 即利用PCB来完成 先描述的思想。
(进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合,称之为PCB,Linux操作系统下的PCB: task_struct)
进程控制块对进程进行管理概括:
磁盘中的可执行程序在将要运行时,所有的进程中的数据(并不是程序本身)加载到内存中,此时操作系统会建立起一个PCB来保存每一个程序的信息,PCB就会对每一个进程都建立起相应的结构体(即进程控制块)将对应的进程的属性、代码等匹配的传到这个结构体中,(即:先描述)此时,操作系统就会将每一个进程控制块都连接起来,形成链表结构,并返回头结点。这样便通过数据结构的形式将进程的信息组织起来。
数据:与内存直接相互联系,一般内存可以分为代码区、常量区、静态区(全局区)、堆、栈几个部分,每个部分存放不同类型的数据;当有程序执行需要开辟内存时,就从里面抽取出一块区域出来然后分成栈堆这些部分,不用了就清空。
(进程等价于操作系统内核对于进程的相关数据结构与此进程的代码和数据)
栈区(stack):由编译器自动分配与释放,存放为运行时函数分配的局部变量、函数参数、返回数据、返回地址等。其操作类似于数据结构中的栈。
堆区(heap):一般由程序员自动分配,如果程序员没有释放,程序结束时可能有OS回收。其分配类似于链表。
全局区(静态区static):存放全局变量、静态数据、常量。程序结束后由系统释放。在 C 语言中,全局变量又分为初始化的(data)和未初始化的(bss),未被初始化的对象存储区可以通过 void* 来访问和操纵,程序结束后由系统自行释放,,在 C++ 里面没有这个区分了,他们共同占用同一块内存区。
常量区(文字常量区):存放常量字符串,程序结束后有系统释放。
代码区:存放函数体(类成员函数和全局区)的二进制代码。
(开发经验:对PCB的管理就是对数据结构中链表的管理,即链表的增删查改)
(进程管理使用PCB,主要为了层次化设计开发:将操作系统内核层开发和应用层开发分隔开,操作系统需要对进程进行管理, 管理的方法就是: 先描述再组织, 为了更好描述进程所以需要PCB,而应用层则仅仅需要相应的函数接口即可相互通信,比如获取pid号的函数 getpid())
进程的相关性质
并行性:多个进程在多个CPU下分别同时运行
并发性:多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以运行(如:时间片轮转(一般为同一优先级进程)或抢占式调度(一般为不同优先级进程))
独立性:多进程运行,需要独享各种资源,多进程运行期间互不干扰
竞争性:系统进程数目众多,但是CPU资源少,甚至最极端只有1个,因此进程之间是存在竞争属性的。为了高效完成任务,更加合理竞争相关系统资源,则由进程优先级决定
进程的相关操作
进程创建
fork函数
fork() 作用:创建一个进程
/*
fork函数调用成功,返回两次
返回值为0, 代表当前进程是子进程
返回值非负数, 代表当前进程为父进程
若调用失败,返回-1
*/
pid_t fork(void);
在开发中,fork()创建一个子进程的目的一般目的有如下
vfork函数
/*
与fork函数的区别
关键区别一:
vfork 直接使用父进程存储空间,不拷贝。
关键区别二:
vfork保证子进程先运行,当子进程调用exit退出后,父进程才执行。
*/
#include <unistd.h>
pid_t vfork(void);
(vfork 创建的子进程应该使用 _exit 函数来终止自己,而不应该调用 exit 函数。这样可以避免对父进程地址空间中的数据和资源产生不可预测的影响,确保子进程的终止操作不会影响到父进程。)
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#define EXE "test"
int main(void)
{
int pid = 0;
char* argv[3] = {EXE, "world", NULL};
printf("begin\n");
printf("now pid : %d\n", getpid());
if((pid = vfork()) != 0)
{
//父进程
}
else
{
//子进程
execve(EXE, argv, argv);
_exit(0);
}
printf("end\n");
return 0;
}
进程退出
正常退出 {
1. Main函数调用return 0
2.进程调用exit(),标准c库函数
3.进程调用_exit()或者 _Exit(), 属于系统调用
4.进程最后一个线程返回
5.最后一个线程调用pthread_exit
6.通过指令结束进程
kill -9 PID killall Process_Name
}
异常退出{
1.调用abort
2.当进程收到某些信号时,如ctrl+C
3.最后一个线程对取消(cancellation)请求做出响应
}
(getppid() 获取父进程的pid)
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
int x=10;
pid_t ret= fork();
if(ret == 0)
{
//子进程
while(1)
{
printf("我是子进程,我的pid是: %d,我的父进程是: ppid: %d, %d, %p\n", getpid(),
getppid(), x, &x);
sleep(1);,
}
}
else if(ret>0)
{
//父进程
while(1)
{
printf("我是父进程,我的pid是: %d,我的父进程是: ppid: %d, %d, %p\n", getpid(),
getppid(), x, &x);
x=321;
sleep(2);
}
}
else {
}
return 0;
}
/*
使用同一块空间的数据(空间地址相同), 在父进程修改数据后不会对子进程数据产生影响(采用写时拷贝,开辟各自数据空间),
并且if和else if同时成立(fork之后, 执行流会变成两个,谁先运行由调度器决定,fork之后的代码共享,通常通过if 和 else if 来进行执行流)
*/
进程状态
概念:反映进程执行过程的变化,状态随着进程的执行和外界条件的变化而转换
(一般进程状态分为四个基本状态,即运行态,就绪态,阻塞态,挂起态。或者精简为三个状态,即运行态,就绪态(挂起态), 阻塞态)
如图为实时操作系统的状态图(四状态图):
运行态
对于运行状态并不是指, 进程在CPU上进行运行, 而是代表进程在CPU的运行队列中排队(等待被执行的状态)
就绪态(挂起态)
挂起进程在操作系统中可以定义为暂时被淘汰出内存的进程,机器的资源是有限的,在资源不足的情况下,操作系统对在内存中的程序进行合理的安排,其中有的进程被暂时调离出内存,当条件允许的时候,会被操作系统再次调回内存,重新进入等待被执行的状态。即就绪态,系统在超过一定的时间没有任何动作。(因为计算资源而暂时被CPU调度出内存)
阻塞态
正在进行的进程由于发生某事件而暂时无法继续执行时,便放弃处理而处于暂停状态,即进程的执行受到阻塞,我们把这种暂停状态叫阻塞(进程阻塞),通常这种处于阻塞状态的进程也排成一个队列(阻塞队列)。有的系统则根据阻塞原因的不同而处于阻塞状态进程排成多个队列。(阻塞就是不被调度)
(相当于当你去办理业务,需要填表时你还未填表,这时候工作人员为了不让你占用过多时间,就会让你拿表离开,等你填完表后再来,让其他人先办理业务,为的就是不耽误你后面人的时间让他们继续办理业务)
Linux内核(kernel源代码分析)
/*
* The task state array is a strange "bitmap" of
* reasons to sleep. Thus "running" is zero, and
* you can test for combinations of others with
* simple bit tests.
*/
static const char * const task_state_array[] = {
"R (running)", /* 0 运行状态 */
"S (sleeping)", /* 1 睡眠状态 */
"D (disk sleep)", /* 2 深度睡眠状态 */
"T (stopped)", /* 4 停止状态 */
"t (tracing stop)",/* 8 停止追踪*/
"X (dead)", /* 16 死亡状态*/
"Z (zombie)", /* 32 僵尸状态*/
};
R运行状态(running)
表明进程要么是在运行中要么在运行队列里。 task_struct (Linux下的PCB)
S睡眠状态(sleeping)
表明进程在等待事件完成(睡眠有时也叫 可中断睡眠(interruptible sleep))
当使用C函数通过printf访问的是外设,由于外设处理很慢,因此进程等待显示器就绪需要花费很长时间,于是就会把访问进程的PCB转移到外设的PCB上排队,即对应了普遍操作系统层面上的阻塞状态。
D磁盘休眠状态(Disk sleep)
表明在这个状态的进程通常会等待IO的结束,(也叫不可中断睡眠(uninterruptible sleep))
当进程向磁盘写入数据时, 进程就会变成阻塞状态, 当内存资源紧张,磁盘存储过满, 此时如果没有D状态, CPU就会在内存资源紧张的前提下, 杀掉这个进程,可是这时磁盘的数据并未写完,此进程已经被杀掉了, 无法写入数据了, 于是就有了D状态的概念, 防止这个进程被杀死。(D状态下的进程是无法被操作系统杀死,只能断电处理!)
(磁盘休眠一般只会在高IO的情况发生下,当内存资源紧张,且如果操作系统中存在多个D状态的进程,那么表明该操作系统即将崩溃了。)
T停止状态(stopped)
表明进程暂时停止,T状态也是阻塞状态中的一种,因为其代码不被CPU执行
可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT信号让进程继续运行。
kill -19 PID # 该进程由S+状态变成T状态(停止状态)
kill -18 PID # 该进程由T状态变成S状态,且由前台进程变成后台进程
X死亡状态(dead)
该状态只是一个返回状态,无法在任务列表里看到这个状态。
t追踪暂停状态(tracing stop)
该状态是一种特殊的停止状态,即 进程在调试时就处于追踪暂停状态 (gdb调试)
Z僵尸状态(zombie)
进程被创建出来是为了完成任务的,而完成的结果也是需要被关注的,即进程完成的结果会被其父进程或者操作系统接收,因此在进程退出的时候,不能释放该进程对应的资源,而是保存一段时间,让父进程或者操作系统来进行读取。因此在这个进程退出后到被回收(OS、父进程)前的状态就是僵尸状态。(在实际开发中:需要等待子进程的退出,即一般父进程等待子进程退出并收集子进程的退出状态(一般使用wait() 或 waitpid() 函数))
#include <sys/types.h>
#include <sys/wait.h>
/*
wait使调用者阻塞,waitpid有一个选项,可以使调用者不阻塞
status参数:是一个整型数指针
非空:
子进程退出状态放在它所指向的地址中。
空:
不关心退出状态
*/
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);
危害:
对于僵尸状态的进程,事实上不是数据存在在内存,而是其对应的PCB在内存中占用空间,因此如果这种进程过多导致PCB占用内存过大,并且父进程和操作系统没有及时回收这种资源,那么就极易发生内存泄漏。(因此,除了malloc或者new,系统层面上也是有可能发生内存泄漏。)
孤儿进程
父进程如果不等待子进程退出,在子进程之前就结束了自己的"生命周期",此时子进程就叫孤儿进程(父进程先退出,子进程不被领养,子进程后续再退出,无人回收)
(Linux避免系统存在过多孤儿进程,1号init进程收留孤儿进程,变成孤儿进程的父进程,之后再由init进程回收)
#include<stdio.h>
#include<unistd.h>
int main()
{
pid_t id= fork();
if(id==0)
{
//child
while(1)
{
printf("我是子进程: pid: %d, ppid: %d\n", getpid(),getppid());
sleep(1);
}
}
else
{
//parent
int cnt = 11;
while(1)
{
printf("我是父进程: pid: %d, ppid: %d\n", getpid(),getppid());
sleep(1);
if(cnt--<=0)
break;
}
}
}
(没有看到父进程的僵尸状态,因为父进程被系统回收)
进程操作的拓展
exec族函数
作用:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
//函数原型:int execl(const char *path, const char *arg, ...);
/*
exec配合fork使用
实现功能: 当父进程检测到输入为1的时,创建子进程把配置文件的字段值修改掉。
*/
int main()
{
pid_t pid;
int data = 11;
while(1){
printf("please input a data\n");
scanf("%d",&data);
if(data == 1){
pid = fork();
if(pid > 0){
wait(NULL);
}
if(pid == 0){
execl("./changData","changData","config.txt",NULL);
}
}
else{
printf("wait ,do nothing\n");
}
}
return 0;
}
system函数
函数作用:执行 dos(windows系统) 或 shell(Linux/Unix系统) 命令,参数字符串command为命令名。(在windows系统下参数字符串不区分大小写)
在windows系统中,system函数直接在控制台调用一个command命令。在Linux/Unix系统中,system函数会调用fork函数产生子进程,由子进程来执行command命令,命令执行完后随即返回原调用的进程。
/*
使用system语句获取system()函数的参数及相应实现功能
*/
system(“HELP”);
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
//int system(const char *command);
/*
system()函数的返回值如下:
成功,则返回进程的状态值;
当sh不能执行时,返回127;
失败返回-1;
*/
int main(void)
{
char ret[1024] = {0};
system("ps");
printf("ret=%s\n",ret);
return 0;
}
popen函数
函数作用:popen()会调用fork()产生子进程,然后从子进程中调用/bin/sh -c 来执行参数command 的指令,并获取输出结果!!!!!!!!!
参数type :可使用 "r"代表读取,"w"代表写入。依照此type 值,popen()会建立管道(无名管道)连到子进程的标准输出设备或标准输入设备,然后返回一个文件指针。随后进程便可利用此文件指针来读取子进程的输出设备或是写入到子进程的标准输入设备中;(所有使用文件指针(FILE*)操作的函数也都可以使用,除了fclose()以外)。
返回值:若成功则返回文件指针, 否则返回NULL, 错误原因存于errno 中(如调用 fork() 或 pipe() 失败,或者不能分配内存)。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
//size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream)
/*
比system在应用中的好处:
可以获取运行的输出结果
FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);
*/
int main(void)
{
char ret[1024] = {0};
FILE *fp;
fp = popen("ps","r");
int nread = fread(ret,1,1024,fp);
printf("read ret %d byte, ret=%s\n",nread,ret);
pclose(fp);
return 0;
}
此进程必须由 pclose() 函数关闭,而不是 fclose() 函数!!!!
pclose() 函数关闭标准 I/O 流,等待命令执行结束,然后返回 shell 的终止状态。如果 shell 不能被
执行,则 pclose() 返回的终止状态与 shell 已执行exit命令一样。
Ending撒花!!!!!!!