linux系统编程之多任务编程

目录

一、进程的概述

1、程序和进程的区别:

2、单道和多道程序设计:

3、并行和并发的区别

4、进程控制块PCB

5、进程的状态

6、进程号

1、pid ppid

2、 pgid:进程组号

3、获取进程号的函数getpid

4、获取父进程号的函数getppid

5、获取进程的pgid的函数getpgid

7、创建子进程

1、pid_t fork(void);

fork出来的子进程的父进程之间的关系

2、vfork创建子进程:

8、特殊的进程

1)孤儿进程(无危害):

2)僵尸进程

3)守护进程

9、父进程回收子进程资源

1、wait函数(带阻塞)

10、创建多进程

1、知识点引入 :创建两个子进程

 问题引入:

解决问题:防止子进程创建孙进程

问题引入:怎么区分几个进程?

 解决问题:根据 i 的不同,区分,父进程中 i 等于子进程数

 案例:创建多线程完成提前以及任务数

11、进程的补充

1)终端:

步骤:

12、进程组

13、会话

创建会话的步骤

函数原型:pid_t setsid(void)

14、exec函数族

案例1:在代码中使用execl执行ls命令

案例2:在代码中使用execlp执行ls命令

案例三:在代码中使用execp执行ls命令

案例四:vfork和exec配合使用


一、进程的概述

1、程序和进程的区别:

程序是静态的,存储在磁盘空间

进程是动态的,可以调度、执行、消亡。在内存空间。

2、单道和多道程序设计:

1、单道(被淘汰):所有进程只能一个一个排队执行,后面的必须排队

2、多道:内存中存放多道独立运行的程序,在管理程序的控制下,能够穿插运行。

时钟中断作为理论基础,并发时,任何进程都不愿意放弃cpu,所以需要一种强制让进程让出cpu资源的手段。

3、并行和并发的区别

都是指多个任务同时执行

并行(多核):在微观上,同一时刻,有多条指令在多个处理器上同时执行。

并发(单核):同一时刻只能有一条指令执行,但是因为快速轮转,在宏观上具有多个进程同时执行的效果。

4、进程控制块PCB

1、程序运行的时候,内核会为每进程分配一个PCB,维护进程相关的信息。linux内核的进程控制块是task_struct结构体。在系统内核空间里面。

2、进程的状态有就绪、运行、挂起、停止等状态。进程是分配资源的基本单位。

3、当进程切换时,需要保存和恢复一些cpu寄存器、描述虚拟地址空间的信息、描述控制终端的信息、当前工作目录、umask掩码、文件描述符表、包括很多指向file的指针、和信号相关的信息,包括组id、用户id,会话和进程组,进程可以使用的资源上限。

 

5、进程的状态

就绪态、执行态、等待态。

1)就绪态:执行条件全部满足,等待cpu的调度。

2)执行态:正在被cpu调度执行。

3)等待态:条件不满足,等待条件满足。

 

6、进程号

1、pid ppid

2、 pgid:进程组号

进程组是一个或者多个进程的集合,它们之间相互关联,进程组可以收到同一个终端的各种信号,关联的进程有一个进程组号。没有加入进程组的也有个pgid

3、获取进程号的函数getpid

头文件:

        #include <sys/types.h>

        #include <unistd.h>

功能:获取本进程的pid

原型:pid_t getpid(void);

返回值:本进程号

4、获取父进程号的函数getppid

头文件

        #include <sys/types.h>

        #include <unistd.h>

功能:获取父进程的pid

原型:pid_t getppid(void);

返回值:父进程号

5、获取进程的pgid的函数getpgid

头文件

         #include <sys/types.h>

         #include <unistd.h>

功能:获取进程的pgid

原型:pid_t getpgid(pid);

返回值:进程的进程组id,如果pid参数为0的时候,返回的就是当前进程进程组号。否则就是指定进程的进程组号。组长pid与组的pgid相同

7、创建子进程

1、pid_t fork(void);

调用就产生进程

头文件

        #include <sys/types.h>

        #include <unistd.h>

功能

从一个已经创建的进程中创建一个新进程,新进程称为子进程,原进程称为父进程。优先执行父进程

返回值

成功:子进程中返回0,父进程中返回子进程ID。

失败返回-1:

1、进程数已经打到系统上限。

2、系统内存不足。

fork出来的子进程的父进程之间的关系

1)子进程是父进程的复制品,继承了整个进程的地址空间。

2)地址空间:包括地址的上下文、进程堆栈、打开的文件描述符、信号控制设定、进程优先级、进程组号。

3)子进程所独有的只有自己的进程号、计时器等。因此、使用fork的代价是很大的。

4)子进程是从父进程的fork之后运行。

案例:使用fork创建子进程

#include<stdio.h>
#include<unistd.h>
#include <sys/types.h>
int main(int argc, char const *argv[])
{
	pid_t pid = fork();
	if( pid < 0)
	{
		perror("fork");
		return -1;
	}
	else if(pid==0)
	{
		printf("子进程:pid=%d 它的父进程ppid= %d",getpid(),getppid());
	}
	else if(pid>0)
	{
		printf("父进程:pid=%d\n",getpid());
	}
	getchar();
	return 0;
}

运行结果:

使用ps -ef查看

 

2、vfork创建子进程:

原型:pit_t vfork(void)

功能:创建一个子进程,使用vfork创建的子进程会优先执行子进程

返回值:创建成功子进程中返回0,父进程中返回子进程号id,失败返回-1.

案例:使用vfork创建子进程

#include<stdio.h>
#include<unistd.h>
#include <sys/types.h>
int main(int argc, char const *argv[])
{
	pid_t pid = vfork();
	if( pid < 0)
	{
		perror("fork");
		return -1;
	}
	else if(pid==0)
	{
		for(int i =5;i>0;i--)
		{
			printf("子进程%d i=%d\n",getpid(),i);
			sleep(1);
		}
          _exit(-1);
	}
		
	else if(pid>0)
	{
		for(int i =5;i>0;i--)
		{
			printf("父进程%d i=%d\n",getpid(),i);
			sleep(1);
		}
	}
	return 0;
}

运行结果

 分析:子进程是先运行,只有当子进程结束的时候,在运行父进程,因为子进程和父进程使用同一块空间。

验证分析

int main(int argc, char const *argv[])
{
	int num=10;
	pid_t pid = vfork();
	if( pid < 0)
	{
		perror("vfork");
		return -1;
	}
	else if(pid==0)
	{

			num =1000;
			printf("子进程%d num=%d\n",getpid(),num);
			_exit(1);
	}
		
	else if(pid>0)
	{		
			printf("父进程%d num=%d\n",getpid(),num);		
		
	}
	return 0;
}

 

8、特殊的进程

孤儿进程、僵尸进程、守护进程。

1)孤儿进程(无危害):

父进程先结束、子进程还在运行,这个子进程就是孤儿进程,会被1号进程接管,由一号进程负责给子进程回收资源。

案例

#include<stdio.h>
#include<unistd.h>
#include <sys/types.h>
int main(int argc, char const *argv[])
{
	pid_t pid = fork();
	if( pid < 0)
	{
		perror("fork");
		return -1;
	}
	
	else if(pid==0)
	{
		
		while(1)
		{
		printf("子进程:pid=%d 它的父进程ppid= %d\n",getpid(),getppid());
		sleep(1);
		}
	}

	else if(pid>0)
	{
		printf("父进程将在三秒后结束\n");
		sleep(3);	
	}
	return 0;
}

运行结果:

 分析:当父进程结束,但是子进程还在运行,子进程会被1号进程收养,同时脱离终端显示,此时只有使用kill杀死。

2)僵尸进程

1、子进程结束,父进程没有回收资源(PCB),子进程就是僵尸进程。

案例:

#include<stdio.h>
#include<unistd.h>
#include <sys/types.h>
int main(int argc, char const *argv[])
{
	pid_t pid = fork();
	if( pid < 0)
	{
		perror("fork");
		return -1;
	}
	
	else if(pid==0)
	{
		
		
		printf("pid=%d ,ppid=%d",getpid(),getppid());
		return -1;
		
	}
	else if(pid>0)
	{
		
			printf("父进程");
			while(1);		
	}
	return 0;
}

运行结果

 

 分析:当子运行完毕,但是父进程还在被while(1)阻塞,没有回收子进程资源,所以子进程成为僵尸进程     defunct:僵尸进程

3)守护进程

是脱离终端的孤儿进程 ,在后台运行,为特殊服务存在,一般用于服务器。

创建守护进程

1)创建子进程,父进程必须退出,所有工作在子进程中进行形式上脱离了终端。

2)在子进程中创建新会话(必须) setsid()函数使子进程完全独立出来,脱离控制

3)改变当前目录为根目录(不是必须) chdir()函数防止占用可卸载的文件系统也可以换成其它路径

4)重设文件权限掩码(不是必须)umask()函数防止继承的文件创建屏蔽字拒绝某些权限增加守护进程灵活性

5)关闭文件描述符(不是必须)继承的打开文件不会用到,浪费系统资源,无法卸载6)开始执行守护进程核心工作(必须)守护进程退出处理程序模型

案例

#include<stdio.h>
#include<unistd.h>
#include <sys/types.h>
#include<sys/wait.h>
#include <sys/stat.h>
int main(int argc, char const *argv[])
{
	pid_t pid = fork();
	//父进程退出
	if(pid>0)
	{
		_exit(-1);
	}
	setsid();  //创建新会话
	chdir("/");//改变当前目录为根目录
	umask(0002);//修改文件权限掩码
	//关闭文件描述符
	close(0);
	close(2);
	close(1);

	while(1)
	{	//创建守护任务
		;
	}
	return 0;
}

 

9、父进程回收子进程资源

父进程可以调用wait或waitpid得到它的退出状态同时彻底清楚掉这样的进程。

每个进程结束的时候,内核释放该进程的所有资源、包括打开的文件等、,但是仍然保留了一定的信息,这些信息主要是指PCB进程控制块

父进程可以是使用wait和waitpid得到它的退出状态已经清除掉这个进程。

wait、waitpid基本都是在父进程中调用

1、wait函数(带阻塞)

头文件

        #include <sys/types.h>

        #include <sys/wait.h>

原型: pid_t wait(int *wstatus)

参数:进程退出时候的状态。

功能:等待任意一个子进程结束、如果任意一个子进程结束了,此函数就会回收该进程的资源。

返回值:成功返回结束子进程的pid,失败返回-1,如果没有子进程或者子进程已经结束,那么会立即返回

案例

#include<stdio.h>
#include<unistd.h>
#include <sys/types.h>
#include<sys/wait.h>
int main(int argc, char const *argv[])
{
	pid_t pid = fork();
	if( pid < 0)
	{
		perror("fork");
		_exit(-1);
	}
	
	else if(pid==0)
	{
		for(int i =0;i<5;i++)
		{

		printf("子进程:%d还有%ds退出\n",getpid(),i);
		sleep(1);
		}
		printf("子进程:%d退出了\n",getpid());
		_exit(-1);
	}
	else if(pid>0)
	{			
             printf("父进程:%d 准备回收子进程\n",getpid());	
             wait(NULL);
             printf("父进程:%d等到了子进程结束",getpid());
						
	}
	return 0;
}

运行结果

 分析:当父进程运行到wait时,会阻塞父进程,去等待子进程结束后,回收资源后再继续执行下面代码。

补充:如果关心状态值

else if(pid>0)
	{			
                    int status;
    		printf("父进程:%d 准备回收子进程\n",getpid());	
    		pid_t pid  = wait(&status);
    		//如果是正常退出
    		if(WIFEXITED(status))
            	{
            	//退出的状态
            	printf("子进程退出的状态值:%d\n",WEXITSTATUS(status));
            	}
    		printf("父进程:%d等到了子进程:%d结束\n",getpid(),pid); 
			
	}

会返回退出的状态值_exit里的状态值。

10、创建多进程

1、知识点引入 :创建两个子进程

 问题引入:

使用循环创建子进程,为什么创造了三个子进程? 进程数,2的N次方,子进程数2的N次方-1,孙也算子进程。

 

解决问题:防止子进程创建孙进程

for(int i = 0;i<2;i++)
	{
		pid_t PID  =  fork();
		if(PID == 0)
		{
			break;
		}
	}

问题引入:怎么区分几个进程?

 解决问题:根据 i 的不同,区分,父进程中 i 等于子进程数

#include<stdio.h>
#include<unistd.h>
#include <sys/types.h>
#include<sys/wait.h>
#define N 2
int main(int argc, char const *argv[])
{
	int i = 0;
	for( i;i<N;i++)
	{
		pid_t PID  =  fork();
		if(PID == 0)
		{
			break;
		}
	}
	if(i==0)
	{
		printf("这是子进程1:pid=%d,ppid=%d\n",getpid(),getppid());

	}
	else if(i==1)
	{
		printf("这是子进程2:pid=%d,ppid=%d\n",getpid(),getppid());


	}
	else if(i==N)
	{
		printf("这是父进程3:pid=%d,ppid=%d\n",getpid(),getppid());

	}	
	getchar();
	return 0;
}

运行结果

 案例:创建多线程完成提前以及任务数

#include<stdio.h>
#include<unistd.h>
#include <sys/types.h>
#include<sys/wait.h>
#define N 2
int main(int argc, char const *argv[])
{
	int i = 0;
	for( i;i<N;i++)
	{
		pid_t PID  =  fork();
		if(PID<0)
		{
			perror("fork");
			return -1;
		}
		if(PID == 0)
		{
			break;
		}
		//子进程分别完成任务
	}
	if(i==0)
	{
		printf("这是子进程1:pid=%d,ppid=%d\n",getpid(),getppid());	
		int j=5;
		for(j;j>0;j--)
		{
			printf("子进程1的生命周期还有:%ds\n",j);
			sleep(1);
		}		
		return 1;

	}
	else if(i==1)
	{
		printf("这是子进程2:pid=%d,ppid=%d\n",getpid(),getppid());
		int j=3;
		for(j;j>0;j--)
		{
			printf("子进程2的生命周期还有:%ds\n",j);
			sleep(1);
		}		
		return 1;
	}
	else if(i==N) //回收所有子进程资源
	{
		printf("这是父进程3:pid=%d\n",getpid());
		printf("父进程准备开始回收子进程\n");
		while(1)
		{
			pid_t PID1 = waitpid(-1,NULL,WNOHANG);
			if(PID1>0)
			{
			printf("父进程回收了子进程%d\n",PID1);
			}
			//还有子进程在运行,再回去扫描
			else if(PID1==0)
			{
				continue;
			}
			else if (PID1==-1)
			{
				printf("所有子进程都回收\n");
				break;
			}
		}
	}	
	return 0;
}

运行结果

 同时也印证了OS的多道程序设计

缺点:要在一开始就确定任务个数,不能在中途增加任务。

11、进程的补充

1)终端:

用户在登陆终端的时候,会分配一个shell进程。这个终端成为shell进程的控制终端,控制终端是保存在PCB中的信息。fork会复制PCB中的信息,因此由shell进程启动的其他进程的控制终端也是这个终端。

 

步骤:

1、bash进程的PCB里面保存了控制终端权限,当你在当前bash进程中创建了一个进程a.out后,子进程a.out会fork bash进程

bash暂时失去对终端的控制权限,而a.out 的PCB是复制的bash的PCB,暂时获得了对终端的控制。

2、当a.out 又fork了 一个子进程后,他们同时获得对终端的控制权限(输入输出)。

3、当a.out和a.out的子进程都结束后,a.out、a.out的子进程失去了对终端的控制权限,但是a.out的子进程遗留了一部分输出权限。

12、进程组

多个进程的集合

1)当父进程创建子进程的时候,默认父进程和子进程是同一进程组。

2)进程组组长ID和进程组id相同。

3)可以通过kill -SIGKILL -进程组号(是负的)来将整个进程组进程全部杀死。

4)一个进程组只要有一个进程组在,就存在,与组长进程是否存在无关。

一个进程可以为自己和子进程设置进程组。

头文件

        #include <sys/types.h>

        #include <unistd.h>

函数原型:pid_t setpgid(pid_t pid ,pid_t pgid)

功能:改变进程默认所属的进程组。通常可用加入一个现有的进程组或者创建一个新的进程组。

返回值:成功返回0,失败返回-1

13、会话

1)会话是一个或者多个进程组的集合。一个会话可以有一个控制终端,建立与控制终端连接的会话首进程为称为控制进程。

2)一个会话中的几个进程组可以被分为,前台进程组合后台进程组。如果一个会话有一个控制终端,则它有一个前台进程组,其他其他进程为后台进程组。

3)如果终端接口检测到断开连接,则将挂断信号发送到控制进程(会话首进程)。

4)如果进程ID==进程组ID==会话ID ,那么这个进程为会话首进程。

创建会话的步骤

1)调用进程不能是进程组组长,该进程变成会话首进程。

2) 该调用进程是组长进程,则返回出错。

3)该进程成为一个新进程组的组长进程

4)需要有root权限,Ubuntu不需要。

5)新会话丢弃原有的控制终端,该会话没控制终端。

6)建立新会话时,先调用fork,父进程终止、子进程调用setsid

函数原型:pid_t setsid(void)

头文件

#include <sys/types.h>

#include <unistd.h>

功能:创建一个会话,以自己的ID设置进程组ID,同时也是会话ID。

返回值

成功:返回调用进程的会话ID

失败:返回-1

案例:

int main(int argc, char const *argv[])
{
	pid_t pid = fork();
	if(pid>0)
	{
		return -1;
	}
	 else if(pid == 0)
	{
		int i =setsid();
		if(i == -1)
		{
			perror("setsid");
			return -1;
		}
	} 
	while(1);
	return 0;
}

 

14、exec函数族

 

1)这几个里面只有execve里面才是真正意义上的系统调用

2)函数中有 l (list)的就是表明使用列表方式传参,有v(vector) 就是使用指针数组传参

有p表明到系统环境中找可执行文件。

案例1:在代码中使用execl执行ls命令

execl(可执行文件位置,可执行文件名,可执行文件的选项,以NULL结尾)

int main(int argc, char const *argv[])
{
	printf("执行前:\n");
	execl("/bin/ls","ls","-a","-l","-h",NULL);
	printf("执行后:\n");
	return 0;
}

分析:没有打印执行后这句话,这就是execl的特点不会返回除非启动失败。调用的进程会直接使用当前进程的进程号、父进程号、控制终端、根目录、未处理信号等。

 

案例2:在代码中使用execlp执行ls命令

#include<stdio.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
	printf("执行前:\n");
	execlp("ls","ls","-a","-l","-h",NULL);
	printf("执行后:\n");
	return 0;
}

案例三:在代码中使用execp执行ls命令

#include<stdio.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
	printf("执行前:\n");
	char *avg[]={"ls","-a","-l","-h",NULL};
	execvp("ls",avg);
	printf("执行后:\n");
	return 0;
}

案例四:vfork和exec配合使用

#include<stdio.h>
#include<unistd.h>
#include <sys/types.h>
#include<sys/wait.h>
#include <sys/stat.h>
int main(int argc, char const *argv[])
{
	pid_t pid = vfork();
	if(pid ==0)
	{
		execlp("ls","ls","-a","-l","-h",NULL);
	}
	if(pid>0)
	{
		int i= 0;
		for(i;i<4;i++)
		{
			printf("i=%d\n",i);
			sleep(1);
		}
	}
	return 0;
}

运行

分析:会先运行子进程,子进程中又使用excelp调用其他程序。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值