Linux7:多进程初步:进程描述,创建进程,特殊的进程,进程资源清理,进程退出,exec函数族以及system函数

Linux7:多进程初步:进程相关概念,创建进程,特殊的进程,进程资源清理,进程退出,exec函数族以及system函数

1.进程描述:

进程初探:

程序
可执行的二进制代码文件
进程
程序被加载到内存中运行
系统中基本的执行单元
联系
具有一定独立功能的程序的一次运行活动,操作系统动态执行的单元,包含程序从调度到消亡的整个过程是动态的过程
运行着的程序都是一个进程
系统本身也运行着许多管理系统资源和用户访问的程序
一个程序可以被加载多次成为不同的进程

查看进程:

windows操作系统下:
任务管理器:
在这里插入图片描述

Linux 操作系统下:
shell命令:

ps命令:

ps -ef
  UID      PID        PPID             C       STIME       TTY           TIME             CMD
用户ID    进程ID号    父进程ID      CPU占用率    开始时间    启动的终端   占用CPU总时间      启动命令


 ps -aux//查看进程的详细信息
  stat
   S:睡眠
   R:运行
    执行或即将运行状态
   D:不可中断的睡眠(等待)
    通常等待输入或输出的完成
   T:停止
    通常是被shell或调试器控制停止
   N:低优先级任务
    nice值被修改
   Z
    僵尸进程
   X
    死进程
   s:进程是会话期首进程
   +:后台进程
    和用户交互的任务
   l:进程是多线程的
   <:高优先级任务

在这里插入图片描述
在这里插入图片描述
进程号:

进程的标识号(pid) 无符号整形 唯一
内核限制进程号小于等于32767,达到时重置进程号计数器
计数器重置从300开始,1-300被系统进程和守护进程占用
32位开发平台最大是32767,64位达2的22次方
/proc/sys/kernel/pid_max

进程状态:

执行态:该进程正在运行,即进程正在占用 CPU, 任何时候都只有一个进程。
就绪态:进程已经具备执行的一切条件,正在等待分配 CPU 的处理时间片。
等待态:进程正在等待某些事件,当前不能分配时间片, 进程不能使用 CPU,若等待事件发生(等待的资源分配到)则可将其唤醒,变成就绪态

相关概念:

父进程
创建/启动一个进程的进程称之为该进程的父进程
子进程
相对于改程序的父进程,该进程为子进程
父子进程是相对的概念

内存布局:

每一个进程有独立的空间
文本段
包含进程运行的程序机器语言指令
只读性
防止进程通过错误指针修改自身的指令
数据段
初始化数据段
包含显示初始化的全局变量和静态变量
程序加载到内存的时候读取这部分变量
未初始化数据段(BSS段)
未进行初始化的全局变量和静态变量
程序启动之前,系统将本段所有的内存初始化为0

动态开辟的内存

动态增长和收缩的段,由栈帧组成,存放局部变量、函数参数值等

如图:
在这里插入图片描述

 size命令 
  //查看进程所占资源

任务调度:

按一定的算法,从一组待运行的进程中选出一个进程占用CPU

进程特点:

动态性
并发性
独立性
异步性

杀死进程:

ps  //查看进程
kill -9 进程ID   //杀死进程

2.进程的使用:

子进程:

用户进程创建子进程,子进程存在于系统,独立于父进程
可被系统调度,可被分配系统资源

查看进程号:

getpid:
功能
获得进程id
函数原型
pid _t getpid(void)
所属头文件
#include <sys/types.h>
#include <unistd.h>
参数

返回值
调用该函数的进程id

getppid:
功能
获得父进程id
函数原型
pid_t getppid(void)
所属头文件
#include <sys/types.h>
#include <unistd.h>
参数

返回值
调用该函数的进程的父进程id

创建进程:

fork函数:
功能:
创建新进程
原型
pid_t fork(void)
所属头文件
<unistd.h>
参数

返回值
在父进程中返回子进程的PID,在子进程中返回0,失败返回-1
特点
fork成功后,会创建一个子进程,子进程会复制父进程资源父子进程同时从fork函数以下开始并行运行。互不干扰。拥有独立的数据段、堆栈,但无法确定父子进程的运行顺序

代码演示demo2.c:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
    
    
pid_t pid;
pid=getpid();
fork();//创建一个进程,两个进程在运行,返回两个pid
if(pid==getpid())
{
    
    
 printf("fujincheng\n");
}else
{
    
    
 printf("zijincheng\n");
}
printf("my pid :%d\n当前pid:%d\n",pid,getpid());
return 0;
}

运行结果:
在这里插入图片描述

vfork函数:
功能
创建子进程,并且阻塞父进程
原型
pid_t vfork(void)
所属头文件
<unistd.h> <sys/type.h>
参数

返回值
在父进程中返回子进程的PID,在子进程中返回0,失败返回-1
特点
vfork成功后,会创建一个子进程,子进程共用(独占)父进程资源,子进程退出父进程才会得到执行。分享父进程的数据段、堆,一定是子进程先运行
注意
使用vfork的时候,为了保证子进程比父进程先运行,只有子进程运
行exec或者exit函数之后,才会去运行父进程
,否则会发生段错误

代码演示:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
    
    
  int cnt=0;
  pid_t pid;
  pid=vfork();//创建子进程
  if(pid>0)
  {
    
    
   while(1){
    
    
   printf("fujincheng\n");
   sleep(1);
   }
  }else if(pid==0)
  {
    
    
   while(1){
    
    
   printf("ZIjincheng\n");
   sleep(1);
   cnt++;
   if(cnt==3)
   {
    
    
     exit(0);
   }
   }
  }
 return 0;
}

代码实现:当子进程中cnt==3时,退出,再去执行父进程

运行结果:
在这里插入图片描述

特殊进程:

0号进程:
操作系统的引导程序
祖先进程:
操作系统启动的第一个程序,1号进程
孤儿进程:
父进程先退出,子进程被init接管,子进程退出后init会回收其占用的相关资源
在Ubuntu的图形界面下,孤儿进程会被upstart收养而不是init
upstart是Ubuntu使用的用来代替init的,可更快的启动系统,以及在硬件热拔插的时候启动或者停止相关服务
ps -x |grep +僵尸进程的进程号
缺点
子进程的相关资源无法清理回收
僵尸进程:
子进程退出,父进程没有做清理工作
一种非常特殊的进程,它几乎已经放弃了所有内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置,记载该进程的退出状态等信息供其他进程收集,除此之外,僵尸进程不再占有任何内存空间
父进程退出会清理子进程

进程等待:

wait以及waitpid会等待指定的子进程退出然后做清理工作
wait函数
功能
等待调用它的进程,直到子进程结束
函数原型
pid_t wait(int *status);
所属头文件
<sys/types.h><sys/wait.h>
参数
status 若为空,则代表任意状态结束的子进程
status 若不为空,则代表指定状态结束的子进程
一般默认为NULL
返回值
成功返回终止的那个子进程的id,失败返回-1

wait代码演示:

#include <sys/types.h>
#include <sys/wait.h> 
#include <unistd.h> 
#include <stdlib.h>
 #include <stdlib.h> 
#include <stdio.h> 
int main() 
{
    
     
pid_t pid,pc; 
int status; 
printf("wait实例:\n"); 
pid=fork(); 
if(pid<0)//创建出错, 
printf("error ocurred!\n"); 
else if(pid == 0) //如果是子进程 
{
    
     
printf("我是子进程的ID=%d\n",getpid());
 sleep(10); //睡眠10秒 
exit(7); 
} 
else //父进程 
{
    
     
pc=wait(&status); //等待子进程结束; 得到子进程的ID
if(WIFEXITED(status)) //子程序正常结束返回非0值,异常返回0
{
    
     
printf("我是父进程,我等待的子进程的id号=%d\n",pc); 
printf("退出码是%d\n",WEXITSTATUS(status)); 
} 
else 
{
    
     
printf("子进程退出异常!\n"); 
} 
} 
exit(0);
}

代码实现:父进程等待子进程退出。
在第3行结果打印出来前有10 秒钟的等待时间,这就是我们设定的让子进程睡眠的时间,只有子进程从睡眠中苏醒过来,它才能正常退出,也就才能被父进程捕捉到
运行结果:
在这里插入图片描述

waitpid函数
功能
暂时停止目前进程的执行,直到有信号来到或子进程结束
原型
pid_t waitpid(pid_t pid,int * status,int options);
所属头文件
#include<sys/types.h>
#include<sys/wait.h>
参数
status:保存进程退出时状态
pid
0:等待指定的pid
-1:等待任意的PID
options
0:同 wait,阻塞父进程,等待子进程退出–一般为0
返回值
成功返回子进程识别码(PID) ,如果有错误发生则返回返回值-1
waitpid(pid,NULL,0);//指定的进程退出
waitpid(-1,NULL,0)//同wait(NULL);

waitpid代码演示:

#include <sys/types.h>
#include <sys/wait.h> 
#include <unistd.h> 
#include <stdlib.h>
 #include <stdlib.h> 
#include <stdio.h> 
int main() 
{
    
     
pid_t pid,pc;
 pid=fork(); 
int status; 
if(pid<0) 
{
    
     
printf("创建进程失败!\n"); 
} 
else if(pid==0) 
{
    
     
printf("我是子进程,我的ID=%d\n",getpid());
 sleep(10); 
exit(0); 
} 
else 
{
    
     
do
{
    
     
pc=waitpid(pid,&status,WNOHANG);//使用了WNOHANG参数,waitpid就不会等待,直接返回0. 
// pc=waitpid(pid,&status,0); 
if(pc==0) 
{
    
     
printf("没有收集到子进程!\n"); 
sleep(1); 
} 
}while(pc==0);//等不到,继续等, 
if(pid==pc) 
printf("等到了子进程\n");
 else printf("出错了\n");
 printf("我是父进程,我要等的进程id是%d\n",pc); 
} 
exit(0); 
}

运行结果:

在这里插入图片描述
从结果看出:

先执行父进程,输出"没有收集到子进程",然后sleep(1),此时执行了子进程,输出"我是…31842",sleep(10);然后执行父进程,因为父进程不会等待子进程,所以不断的收集子进程,直到子进程sleep(10),结束后,才会收集到.

进程退出:

方法:
exit();//正常结束一个进程会清理缓冲区
0 表示正常结束;其他值表示错误,进程非正常结束
函数的参数可在shell中查看
_exit();直接进入内核释放用户进程的地址空间
exit 和_exit 函数都是用来终止进程的。当程序执行到 exit 或_exit 时,进程会无条件地停止剩下的所有操作,清除包数据结构,并终止本进程的运行。
return //自然返回也会结束进程
return语句会被编译器翻译为调用exit
信号
ctrl + c
ctrl +
kill -9 进程号
//杀死指定进程

退出清理函数:

函数原型
int atexit(void (*function)(void));
函数功能
注册退出清理函数,在任意进程结束的时候,自动执
行退出清理函数
函数参数
void (*function)(void)
函数指针,指向一个无参数,无返回值的函数
函数返回值
成功返回0,失败返回非0值
注意:该函数不需要被调用,只要在函数
开始之前进行注册,在进程结束的时候就会自己执行

参考代码:

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
int i=0;
void clean(void)
{
    
    
i++;
printf("i=%d\n",i);
}
int main()
{
    
    
atexit(clean);
fork();
fork();
}

运行结果:
在这里插入图片描述
atexit:
是在进程结束的时候自动执行清理函数。

3.执行程序:

常用的函数族:

exec 函数族就提供了一个在进程中启动另一个程序执行

#include <unistd.h>
extern char **environ;

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[]);

使用原因:
当进程认为自己不能再为系统和用户做出任何贡献时,就可以调用任何 exec 函数族让自己重生
如果一个进程想执行另一个程序,那么它就可以调用 fork 函数新建一个进程,然后调用任何一个 exec,这样看起来就好像通过执行应用程序而产生了一个新进程。

exec函数族:
int execl(const char *path, const char *arg, …);逐个列举的方式
int execlp(const char *file, const char *arg, …);从PATH 环境变量中查找文件并执行
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[]);
l->list
命令参数以列表的方式提供,NULL结尾
v->vector
命令参数以二维数组形式提供,数组的每一行为一个命令行参数
e->environment
传递给程序的环境列表

execl
功能
使用完整的文件目录来查找对应的可执行文件。注意目录必须以“/”开头,否则将其视为文件名
原型
int execl(const char *path, const char *arg, …);
所属头文件
#include <unistd.h>
参数
path:执行文件的路径
arg:可执行文件所需要的参数
参数要以NULL 结尾 (char *)0
返回值

特点
当调用execl函数的时候,程序的代码段发生变化,变为execl要执行的功能的代码段,必须以 NULL 表示结束,如果使用逐个列举方式,那么要把它强制转化成一个字符指针
system:
运行shell指令系统函数正常函数调用

参考代码:execl和system函数的比较:

演示代码:

/*******************************
*文件名称: stack.c
*创建时间:20.10.10
*修改时间:20.10.10
*文件功能:顺序栈
*文件版本:v1.0
********************************/
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#define MAX 7
enum result
{
    
    
 ERR=-1,OK
};
struct stack
{
    
    
 int data[MAX];
 int top;
};
int push(struct stack *p,int num);
int pop(struct stack *p,int *d);
int main()
{
    
    
 struct stack s;
 s.top = -1;
 int i = 0,num;
 for(i=0;i<9;i++)
 {
    
    
  if(push(&s,i)==-1)
   printf("%d\t栈满,无法继续添加数据\n",i);
 } 
 printf("~~~~~~~~~~~~~\n");
 //execl("/bin/ls","ls","-l",NULL);//覆盖原程序,进行执行
 system("ls -l");//执行终端指令  ---执行完,继续原程序
 for(i=0;i<9;i++)
 {
    
    
  if(pop(&s,&num) ==0)
   printf("%d 出栈\n",num);
  else
   printf("栈空\n");
 }
 return 0;
}

execl和fork函数的搭配使用:

演示代码:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
    
    
 int num = 10;
 pid_t pid = fork();
 if(pid < 0)
 {
    
    
  perror("fork");
  return -1;
 }
 else if(pid == 0)
 {
    
    
  printf("child process\n");
  execl("/bin/ls","ls","-l",NULL);
 }
 else
 {
    
    
  wait(NULL);
  printf("parent process\n");  
 }
 return 0;
}

运行结果:
在这里插入图片描述
在这里插入图片描述

execl和vfork函数的搭配使用:

vfork父子进程资源共享:在调用exit,exec函数族之前和父进程共享资源。
补充:子进程结束必须要exit或者函数族,如果不使用,运行时会出现段错误。

演示代码:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
    
    
 int num = 10;
 pid_t pid = vfork();
 if(pid < 0)
 {
    
    
  perror("vfork");
  return -1;
 }
 else if(pid == 0)
 {
    
    
  num++;
  printf("child process\n");
  execl("./w","./w","/home/li/text","./89",NULL);
  exit(0);
 }
 else
 {
    
    
  printf("parent: num=%d\n",num);
  printf("parent process\n");
 }
 return 0;
}

运行结果:和fork一样
在这里插入图片描述
在这里插入图片描述

execlp:
execlp()会从PATH 环境变量所指的目录中查找符合参数file的文件名,找到后便执行该文件,然后将第二个以后的参数当做该文件的argv[0]、argv[1]……,最后一个参数必须用空指针(NULL)作结束。如果用常数0来表示一个空指针,则必须将它强制转换为一个字符指针,否则将它解释为整形参数
原型
int execlp(const char * file,const char * arg,…,(char *)0);

演示代码:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
    
     
  //execl("/bin/ls","ls","-l",NULL);
  execlp("ls","ls","-l",NULL);
 return 0;
}

运行结果:

在这里插入图片描述

execle:
可以传递一个指向环境字符串指针数组的指针。
例如:
char *env_init[] = {“AA=aa”,”BB=bb”,NULL};

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
    
     
  char *envp[]={
    
    "AA=HELLO","BB=123",NULL};//sizeof(envp)=12;shuzuzhizhen(4)
  //execl("/bin/ls","ls","-l",NULL);
  //execlP("ls","ls","-l",NULL);
  execle("/bin/ls","ls","-l",NULL,envp);  
 return 0;
}

运行结果:
在这里插入图片描述

execv:
应先构造一个指向各参数的指针数组,然后将该数组的地址作为这些函数的参数。
例如:
如char *arg[]这种形式,且arg最后一个元素必须是NULL
char *arg[] = {“ls”,”-l”,NULL};

演示代码:
execv.c

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
extern char **environ;
int main()
{
    
     
 char *arg[]={
    
    "./hello",NULL}; 
 execv("./hello",arg); 
 return 0;
}

hello.c

#include <stdio.h>
#include <unistd.h>
extern char **environ;
int main()//循环打印环境变量
{
    
    
 printf("%s is called %d\n",__FILE__,__LINE__);
 
 while(*environ)
 {
    
    
  printf("%s\n",*environ++);
 } 
  return 0;
}

运行结果:
在这里插入图片描述

5.shell命令调用:

system
函数原型
#include <stdlib.h>
int system(const char *command);
函数功能
执行可执行程序
函数参数
command–要执行的可执行程序
函数返回值
参考man帮助

system函数通过调用shell程序/bin/sh–c来执行string所指定的命令,该函数在内部是通过调用fork、
execve(“/bin/sh”,…)、waitpid函数来实现的。通过system创建子进程后,原进程和子进程各自运行,相互间
关联较少。如果system调用成功,将返回0。

参考文章:
多进程初步
参考结构流程图:
多进程初步
注:需要使用xmind软件进行查看

猜你喜欢

转载自blog.csdn.net/weixin_40734514/article/details/109025613
今日推荐