Linux 练习五(进程操作函数)


使用环境:Ubuntu18.04
使用工具:VMWare workstations ,xshell

  作者在学习Linux的过程中对常用的命令进行记录,通过思维导图的方式梳理知识点,并且通过xshell连接vmware中ubuntu虚拟机进行操作,并将练习的截图注解,每句话对应相应的命令,读者可以无障碍跟练。第五次练习的重点在于Linux的进程管理进行操作。

1 Linux进程

  • 进程是一个程序以此执行的过程,程序是静态的,进程是动态的,进程包含创建、调度、休眠、消亡等过程。进程是资源分配的基本单位,线程是调度的基本单位。我们主要通过进程控制块PCB来查看进程的描述信息、控制信息等。
  • 进程的调度主要由CPU来完成,由CPU分配时间片,每个进程轮流使用CPU,当进程的时间片用完后就会进行进程调度。进程调度算法有很多,例如先进先出算法FIFO、最近最久未使用算法LRU等。

1.1 进程标识

  • 操作系统会为每一个进程分配唯一一个整型标识号pid。进程除了自身的pid意外,还有父进程的ppid,所有进程的祖先进程都是同一个进程,叫做init进程,ID为1,init进程是内核自举后的一个启动进程。init进程负责引导系统、启动守护进程并且运行必要的程序。
  • 进程的pid和ppid可以分别通过函数getpid()和getppid()获得。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
	printf("pid:%d ppid:%d\n",getpid(),getppid());
	return 0;
}

在这里插入图片描述

1.2 进程的用户ID和组ID

  • 进程在运行过程中,也有不同的身份,便于进程的权限控制。例如,假设当前登录用户为user,他运行了one程序,则lone在运行过程中就具有user的身份,该ls进程的用户ID和组ID分别为user和user所属的组。这类型的ID叫做进程的真实用户ID和真实组ID。真实用户ID和真实组ID可以通过函数getuid()和getgid()获得。
  • 与真实ID对应,进程还具有有效用户ID和有效组ID的属性,内核对进程的访问权限检查时,它检查的是
    进程的有效用户ID和有效组ID,而不是真实用户ID和真实组ID。缺省情况下,用户的(有效用户ID和有
    效组ID)与(真实用户ID和真实组ID)是相同的。有效用户id和有效组id通过函数geteuid()和getegid()获
    得。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
	printf("uid:%d gid:%d euid:%d egid:%d\n",getuid(),getgid(),geteuid(),getegid());
	return 0;
}

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

1.3 进程的状态

  • 进程的状态模型图有7种状态图、5种状态图和3种状态图,本文主要讲解三种主要状态即就绪态运行态阻塞态
    在这里插入图片描述
  • 运行态:该进程正在运行,占用CPU中,如果时间片用完进程未结束转为就绪态,如果资源被剥夺,转为阻塞态。
  • 就绪态:该进程已经具备运行的条件,等待分配CPU的处理时间片,分配资源后可以转为运行态。
  • 阻塞台:又叫等待态,如果等待的资源分配了,可以将其唤醒转为就绪态

1.4 Linux下的进程结构

  • Linux系统是一个多进程的系统,它的进程之间具有并行性、互不干扰等特点。也就是说,进程之间是分
    离的任务,拥有各自的权利和责任。其中,每个进程都运行在各自独立的虚拟地址空间,因此,即使一
    个进程发生了异常,它也不会影响到系统的其他进程。
  • Linux中的进程包含了3个段,分别是数据段代码段堆栈段
  1. “数据段”放全局变量、常数以及动态数据分配的数据空间。数据段分成普通数据段(包括可读可写/只读数据段,存放静态初始化的全局变量或常量)、BSS数据段(存放未初始化的全局变量)以及堆(存放动态分配的数据)。
  2. “代码段”存放的是程序代码的数据。
  3. “堆栈段”存放的是子程序的返回地址、子程序的参数以及程序的局部变量等。
  • 关于栈区和堆区好多人分不清,给出一点笔者自己的理解,堆是向上”生长“的,因为动态分配内存是从低地址往高地址分配的。栈是用来保存函数调用状态的,所以里面存储的都是子程序相关的内容,而且由于栈的特性,栈经常用来保存状态,比如计算机的进程中断,就是用栈来保存现场(进程的信息)的。
    在这里插入图片描述

1.5 Linux下的进程管理

  • process进程: 操作系统会为每一个进程分配大小为4G的虚拟内存空间,其中1G给内核空间 3G给用户空间{代码区 数据区 堆栈区}
  • ps命令可以查看活动进程 ps -aux命令查看所有进程 **ps -aux| grep ‘aa’**查找指定(aa)进程 ps –ef可以显示父子进程关系
  • ps -aux看%cpu(cpu使用量) %mem(内存使用量) stat状态{S睡眠 T暂停 R运行 Z僵尸}
  • vi a.c &(&表示后台运行),一个死循环,按ctrl+z可以把进程暂停,再执行[bg作业ID]可以将该进程带入后台。
  • 利用jobs可以查看后台任务,fg 1把后台任务带到前台,这里的1表示作业ID
  • kill -9 进程号:表示向某个进程发送9号信号,从而杀掉某个进程 利用pkill a可以杀死进程名为a的进程
  • free命令用来查看物理内存
  • fdisk –l查看磁盘及磁盘分区情况
    在这里插入图片描述
    在这里插入图片描述

2 进程的创建

  • Linux下有四类创建子进程的函数:system() , fork() , exec*() , popen()

2.1 system函数

  • system函数通过调用shell程序/bin/sh -c 来执行string所指定的命令,该函数内部就是通过调execve(“/bin/sh”,…)函数来实现的。通过system创建子进程后,原进程和子进程格子运行,相互间关联较少。system调用成功,返回0。
  • system函数后面的参数还可以是一个可执行程序,例如:system(“/home/wangxiao/1”); 如果想要执行system后面进程的时候,不至于对当前进程进行阻塞,可以利用&将/home/wangxiao/1调到后台运行。
#include <stdio.h>
#include <stdlib.h>
int main()
{
	system("ls -l"); 
	system("clear");
	return 0;
}

2.2 fock函数

#include <unistd.h>
pid_t fork(void);
  • fork函数从已存在的进程中创建一个新的进程。新进程为子进程,原进程为父进程。它和其他函数的区别在于:它执行一次返回两个值。其中父进程的返回值是子进程的进程好,子进程返回0,如果出错则返回-1。可以通过返回值来判断是父进程还是子进程。
  • fork函数创建子进程的过程为:使用fork函数得到的子进程是父进程的一个复制品,它从父进程继承了进程的地址空间,包括进程上下文、进程堆栈、内存信息、打开的文件描述符、信号控制设定、进程优先级、进程组号、当前工作目录、根目录、资源限制、控制终端,而子进程所独有的只有它的进程号、资源使用和计时器等。通过这种复制方式创建出子进程后,原有进程和子进程都从函数fork返回,各自继续往下运行,但是原进程的fork返回值与子进程的fork返回值不同,在原进程中,fork返回子进程的pid,而在子进程中,fork返回0,如果fork返回负值,表示创建子进程失败。
#include <stdio.h>                                                                 
#include <stdlib.h>
#include <unistd.h>

int main()
{
	printf("父进程的进程id为:%d\n",getpid());
	pid_t iRet = fork();
	
	if(iRet < 0){   //创建出错
	    printf("子进程创建失败!\n");
	}
	else if(iRet == 0){ //代表子进程
	    printf("子进程id:%d,父进程id:%d!\n",getpid(),getppid());
	}
	else{   //代表父进程
	    printf("父进程成功,子进程id:%d\n",iRet);
	}
	return 0;
}

在这里插入图片描述

  • 这个地方作者练习的时候也很疑惑,问什么ifelse明明是单分支选择,为什么if和else中的printf都打印了。如果各位读者还记得程序的堆栈区,fork会返回两次正是因为子进程复制了父进程的堆栈,使得两个进程都停留在fork函数中等待返回,两次返回时不同的,一次在父进程中返回,一次在子进程中返回。父进程和子进程就好像链表一样,进程形成了链表,父进程的fork函数返回的值指向子进程的进程id, 因为子进程没有子进程,所以其fork函数返回的值为0。
    在这里插入图片描述

2.3 exec函数族

  • exec函数族的工作过程与fork完全不同, fork是在复制一份原进程,而exec函数是用exec的第一个参数指定的程序覆盖现有进程空间(也就是说执行exec族函数之后,它后面的所有代码不再执行)。
  • int execl(const char *path, const char *arg, ...) path是包括执行文件名的全路径名 arg是可执行文件的命令行参数,多个用,分割注意最后一个参数必须为NULL。
//main.c
#include <stdio.h>
#include <string.h>
int main()
{
	execl(./a.out” ,”a.out”  , NULL);//执行a.out文件
	printf("hello");//不会执行
	return 0 ;
}
  • 当进程通过exec类系统调用开始某个程序的执行时,内核分配给进程的虚拟地址空间由以下内存区域组成:
    1、程序的可执行代码
    2、程序的初始化数据
    3、程序的未初始化数据
    4、初始化程序栈(即用户态栈)
    5、所需共享库的可执行代码和数据
    6、堆(由程序动态请求的内存)

2.4 popen函数

  • popen函数类似于system函数,与system的不同之处在于它使用管道工作。原型为:
#include <stdio.h>
FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);

command为可执行文件的全路径和执行参数
type可选参数为”r”或”w”,如果为”w”,则popen返回的文件流做为新进程的标准输入流,即stdin,如果为”r”,则popen返回的文件流做为新进程的标准输出流。
pclose等待新进程的结束,不是杀掉进程。

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int main()
{
	FILE *read_fp;
	char buffer[BUFSIZ + 1];//BUFSIZ是宏定义8192
	int chars_read;//读取文件字符数量
	memset(buffer, '\0', sizeof(buffer));//给缓冲区buf置空
	read_fp = popen("ps -ax", "r");//使用读的方式读取ps -ax命令,并且给到read_fp指针
	if (read_fp != NULL) { //循环读取打印
		chars_read = fread(buffer, sizeof(char), BUFSIZ, read_fp);//将读取的内容放入缓冲区buf
		while (chars_read > 0) { //如果读取的字符数量不为0
			buffer[chars_read - 1] = '\0'; //将缓冲区的最后一个字符改为\0,方便打印输出
			printf("Reading:-\n %s\n", buffer);
			chars_read = fread(buffer, sizeof(char), BUFSIZ, read_fp);
		}
		pclose(read_fp);
		exit(EXIT_SUCCESS);
	}
	exit(EXIT_FAILURE);
}
  • 内核暂停一个进程执行时,就会把几个相关处理器寄存器的内容保存在进程描述符中,这些寄存器包括:
    1、 程序计数器(PC)和栈指针(SP)寄存器
    2、 通用寄存器
    3、 浮点寄存器
    4、 包含CPU状态信息的处理器控制寄存器(处理器状态字)
    5、 用来跟踪进程对RAM访问的内存管理寄存器
  • 内核决定恢复执行一个进程时,它用进程描述符中合适的字段来装载CPU寄存器。

3 进程控制与终止

3.1 进程的控制

  • ★★★孤儿进程:如果父进程先于子进程退出,则子进程就成为孤儿进程,此时该进程将自动被PID为1的进程(即init)接管。孤儿进程退出后,他的清理工作由祖先进程init自动处理。但是在init处理子进程之前,他将一直消耗系统资源,类似于内存泄漏。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
	pid_t pid = fork();
	
	if(pid == 0){ //判断是否为子进程,0是子进程,非0是父进程
	   while(1);
	}
	else{	//父进程则退出,让子进程变成孤儿进程
	   exit(10);                                                           
	}
	return 0;
}

编译后运行,输入ps -ef 可以看到如下:
在这里插入图片描述

  • ★★★僵尸进程:如果子进程先退出,系统不会自动清理掉子进程的环境,必须由父进程调用wait或者waitpid函数来完成清理工作,如果父进程不做清理工作,则已经退出的子进程成为僵尸进程。在系统中如果存在的僵尸进程过多,就会影响系统的性能,必须对僵尸进程进行处理。
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);
  • wait和waitpid都将暂停父进程,等待一个已经退出的子进程,并进行清理工作;wait函数随机地等待一个已经退出的子进程,并返回该子进程的pid;waitpid等待指定pid的子进程;如果为-1表示等待所有子进程。
  • status参数是传出参数,存放子进程的退出状态;通常用下面的两个宏来获取状态信息:
    WIFEXITED(status) 如果子进程正常结束,它就取一个非0值。传入整型值,非地址
    WEXITSTATUS(status) 如果WIFEXITED非零,它返回子进程的退出码
  • options用于改变waitpid的行为,其中最常用的是WNOHANG,它表示无论子进程是否退出都将立即返回,不会将调用者的执行挂起。
  • 写一个僵尸进程,并且用ps -aux快速查看发现僵尸进程
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
	pid_t pid = fork();
	if( pid == 0 ){ //pid为0是子进程,就正常退出
		printf("我是子进程\n");
	}
	else{	//父进程休眠
		printf("我是父进程");
		while(1);//让父进程一直执行
		//sleep(20);//让父进程休眠
	}
	return 0;
}

在这里插入图片描述

  • 使用wait避免僵尸进程
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
	pid_t pid = fork();
	if( pid == 0 ){
		exit(10);
	}
	else{
		int cid = wait(NULL); //NULL表示等待所有进程,只想把僵尸进程消灭掉
		printf("正在等待的子进程id为%d\n",cid);
		sleep(10); //通常要将sleep放在wait的后面,要不然也会出现僵尸进程
	}
}
  • 使用waitpid避免僵尸进程
  #include <stdio.h>
  #include <stdlib.h>
  #include <unistd.h>
  #include <sys/types.h>
  #include <sys/wait.h>
  #include <signal.h>
  
  void SignChildPsExit(int iSignNo)
  {
      int iExitCode;
      pid_t pid = waitpid(-1,NULL,0);//表示等待任何进程,并阻塞
      //如果写成waitpid(-1,NULL,WNOHANG);表示不等待任何进程,不阻塞
      printf("序号是%d  子进程号是%d\n",iSignNo,pid);
      if(WIFEXITED(iExitCode)){   //判断子进程是否正常退出
          printf("子进程的退出码为:%d\n",WIFEXITED(iExitCode));         
      }
      sleep(10);
  }
  
  int main()
  {
      signal(SIGCHLD, SignChildPsExit);
      //SIGCHLD是子进程退出时给父进程发的信号,用函数捕捉他
      printf("父进程id为%d\n",getpid());
      pid_t iRet = fork();
      if(iRet == 0)
          exit(3);
  }

3.2 进程的终止

进程的终止有五种方式:

  1. main函数的自然返回,进程的自杀
  2. 调用exit函数
  3. 调用_exit函数
  4. 调用abort函数
  5. 接收到能导致进程终止的信号ctrl+c SIGINT ctrl+\ SIGQUIT

前3种方式为正常的终止,后2种为非正常终止。但是无论哪种方式,进程终止时都将执行相同的关闭打开的文件,释放占用的内存等资源。只是后两种终止会导致程序有些代码不会正常的执行比如对象的析构、atexit函数的执行等。

  • exit和_exit函数都是用来终止进程的。当程序执行到exit和_exit时,进程会无条件的停止剩下的所有操作,清除包括PCB在内的各种数据结构,并终止本程序的运行。但是它们是有区别的,exit和_exit的区别如图所示:
    在这里插入图片描述
  • 两者的最大区别在exit在退出之前会检查文件的打开情况,将文件缓冲区中的内容写回文件,就是清理I/O缓冲操作。比如有一些数据,认为已经写入文件,实际上因为没有满足特定的条件,它们还只是保存在缓冲区内,这时用_exit函数直接将进程关闭,缓冲区中的数据就会丢失。因此,如想保证数据的完整性,建议使用exit函数。
#include <stdlib.h> //exit的头文件
#include <unistd.h> //_exit的头文件
void exit(int status);
void _exit(int status);
  • status是一个整型的参数,可以利用这个参数传递进程结束时的状态。一般来说,0表示正常结束;其他的数值表示出现了错误,进程非正常结束。
//exit的举例,调用exit后,缓冲区的记录可以正常输出
#include <stdio.h>
#include <stdlib.h>
int main()
{
	printf("Using exit...\n");
	printf("This is the content in buffer");
	exit(0);
}

_exit函数会直接使进程停止运行,清除其使用的内存空间,并销毁其在内核中的各种数据结构;

//_exit的调用,会发现最后的This is the content in buffer没有打印输出,说明该函数无法输出缓冲区中的记录
#include <stdio.h>
#include <unistd.h>
int main()
{
	printf("Using exit...\n");
	printf("This is the content in buffer");
	_exit(0);
}

在这里插入图片描述
这里可以看到确实没有打印最后一行缓冲区的内容,调查到的资料显示,_exit为了让程序尽快退出。

4 进程间打开文件的继承

4.1 用fork继承打开的文件

  • fork打开的子进程会自动继承父进程打开的文件,进程以后,父进程关闭打开的文件也不会对子进程造成影响。实际上,前面我们了解到,子进程会拷贝父进程的大部分内容,其中子进程也会拷贝父进程的PCB,文件描述符和文件指针都存放在父进程的PCB中,子进程会将其拷贝。所以等同于两个进程打开了同一文件,所以父进程关闭文件时,不会对子进程PCB中的文件指针和文件描述符造成任何影响。
  • 使用文件指针的父进程和子进程读写的例子:
#include<stdio.h>
#include<unistd.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<stdlib.h>

int main()
{
	FILE* fp = fopen("./file1","r+");
	char buf[32] = {'\0'};
	fread(buf,sizeof(char),6,fp); //从文件中读取6个字符到buf中
	printf("父进程读取文件内容%s\n",buf);
	
	//创建子进程,处理父进程	
	if(fork() == 0){
		fclose(fp);//父进程中直接将文件关闭,清空文件指针
		fp = NULL;//但是子进程在创建fock的时候就已经拷贝了父进程的PCB中fp的内容
	} 	
	
	sleep(3);//给父进程关闭文件的时间
	//子进程尝试使用fp指针读取接下来的文件内容
	//实际上就连父进程中fp的偏移也被子进程拷贝了
	if(fread(buf,sizeof(char),5,fp)==0){
		printf("子进程继续读取失败\n");
		exit(0);
    }
    else{
        printf("子进程继续读取内容为%s\n",buf);
        exit(0);    
    }

	return 0;
}

在这里插入图片描述

  • 父进程通过文件描述符关闭文件读取,子进程再通过文件描述符读取一样可以读到文件的内容。文件描述符可以通过fileno函数获取,这里就不演示了,读者自行尝试。

守护进程

  • Daemon运行在后台也被称为后台服务进程”。 它是没有控制终端与之相连的进程。它独立与控制终端、会话周期的执行某种任务。守护进程脱离终端是为了避免进程在执行过程中的信息在任何终端上显示并且进程也不会被任何终端所产生的任何终端信息所打断。由于在linux中,每一个系统与用户进行交流的界面称为终端,每一个从此终端开始运行的进程都会依赖这个终端,这个终端就称为这些进程的控制终端。当控制终端被关闭时,相应的进程都会自动关闭。但是守护进程却能突破这种限制,它被执行开始运转,直到整个系统关闭时才退出。
  • 简单来说守护进程Daemon就是后台运行的进程,几乎所有的服务器程序如Apache和wu-FTP,都用daemon进程的形式实现。很多Linux下常见的命令如inetd和ftpd,末尾的字母d通常就是指daemon。
  • 守护进程的特性:
  1. 最重要的特性就是后台运行
  2. 守护进程必须与运行前的环境隔离开,通过从父进程继承环境的方式,与之隔离。

daemon守护进程的编程规则:

  • 首先,用fork创建一个子进程,同时让父进程退出。后序的工作在子进程中完成。这样做可以交出控制台的控制权,并准备将自己进程作为进程组长。前面讲过,该子进程或变成孤儿进程,由1号进程收养。
int pid = fork();
if(pid>0)
	exit(0);
  • 在子进程中创建会话,使用系统函数setsid()。子进程全盘拷贝了父进程的会话期、进程组、控制终端等,虽然父进程退出了,但会话期、进程组、控制终端并没有改变,因此,还不是真正意义上的独立开来。而调用setsid函数会创建一个新的会话并自任该会话的组长,调用setsid函数有下面3个作用:
  1. 让进程脱离原会话的控制
  2. 让进程脱离原进程组的控制
  3. 让进程脱离原控制终端的控制。
  • 进程组:是一个或多个进程的集合。进程组有唯一的组号GID。组内每个进程都有一个组长进程,其组长进程的ID号就是组ID号。且组ID号不会因为组长进程退出受到影响。

  • 会话周期:会话期是一个或多个进程组的集合。通常,一个会话开始于用户登录,终止于用户退出,在此期间该用户运行的所有进程都属于这个会话期。

  • 控制终端:由于在linux中,每一个系统与用户进行交流的界面称为终端,每一个从此终端开始运行的进程都会依赖这个控制终端。

  • 创建守护进程的准备

  1. 改变当前目录为根目录
    使用fork函数创建的子进程继承了父进程的当前工作目录。由于在进程运行中,当前目录所在的文件是不能卸载的,这对以后的使用会造成很多的不便。利用chdir(“/”);把当前工作目录切换到根目录。
  2. 重设文件权限掩码
    umask(0);将文件权限掩码设为0,Deamon创建文件不会有太大麻烦;
  3. 关闭所有不需要的文件描述符
    新进程会从父进程那里继承一些已经打开了的文件。这些被打开的文件可能永远不会被守护进程读写,而它们一直消耗系统资源。另外守护进程已经与所属的终端失去联系,那么从终端输入的字符不可能到达守护进程,守护进程中常规方法(如printf)输出的字符也不可能在终端上显示。所以通常关闭从0到MAXFILE的所有文件描述符。
for(i=0;i<MAXFILE;i++)
	close(i);

(注:有时还要处理SIGCHLD信号signal(SIGCHLD, SIG_IGN);防止僵尸进程(zombie))

  • Daemon守护进程实例:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
void Daemon()
{
	const int MAXFD=64;
	int i=0;
	if(fork()!=0) //父进程退出
	exit(0);
	setsid(); //成为新进程组组长和新会话领导,脱离控制终端
	chdir("/"); //设置工作目录为根目录
	umask(0); //重设文件访问权限掩码
	for(;i<MAXFD;i++) //尽可能关闭所有从父进程继承来的文件
	close(i);
}

int main()
{
	Daemon(); //成为守护进程
	while(1){
		sleep(1);
	}
	return 0;
}

各位读者好,创作不易,如果这篇文章看到了最后,请点个赞鼓励一下作者。Linux系列将继续进行。

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值