Linux系统编程——进程

一、进程概念

  1. 基础

    程序:死的。只占用磁盘空间。 --剧本
    进程:活的。运行起来的程序。占用内存,cpu等系统资源。 --戏

  2. 并发
    并发的出现基于CPU的发展。然后有了多道程序设计(多进程并发执行)。

  3. CPU执行过程
    多少位系统实际指的就是寄存器的大小,32位就是寄存器4bytes,64位就是寄存器8bytes.
    Linux系统编程——进程

  4. 虚拟内存和物理内存映射

3-4G空间是内核区域,无论多少个进程都对应物理内存的一块区域。
而user区域是普通用户区域,每个进程都对应相应的物理内存区域。

MMU功能:
	1.映射虚拟内存和物理内存
		#当程序执行时,都有一个虚拟内存,其中的变量信息相应的对应物理内存。通过MMU来映射两者间的关系。
	2.修改内存访问级别
		#当映射0-3G区间的数据时,MMU设置其为3级,当0-3G的数据进到3-4G空间时,MMU将相应的内存访问级别由3级切换到0级。

Linux系统编程——进程
Linux系统编程——进程

二、PCB进程控制块

	每个进程在内核中都有一个进程控制块(PCB)来维护进程线相关的信息。
	Linux内核的进程控制块是task_struct结构体。
  1. task_struct结构体

查看结构体定义(好几百行)语句: grep -r 'struct task_struct {' /usr/src/

task_struct结构体常用成员简介:
	1.进程id.
		系统中每个进程有唯一的id,C语言中用pid_t类型表示,就是一个非负整数。
	2.进程状态
		就绪、运行、挂起、停止等。
	3.进程切换时需要保存和恢复的一些CPU寄存器。
	4.描述虚拟地址空间的信息(MMU的映射关系存在这个结构体里)
	5.描述控制终端的信息
	6.当前工作目录(current working directory)
	7.umask掩码
	8.文件描述符表,包含很多指向file结构体的指针。
	9.和信号相关的信息。
	10.用户id和组id
	11.会话和进程组
	12.进程可以使用的资源上限(resource limit)。

Linux系统编程——进程

  1. 环境变量
    每个进程也存有环境变量,存放位置在PCB块user区域的最上面部分。
    Linux系统编程——进程

三、进程控制

  1. fork
    Linux系统编程——进程
man 2 fork

1.NAME
       fork - create a child process

2.SYNOPSIS
       #include <sys/types.h>
       #include <unistd.h>

       pid_t fork(void);
3.RETURN VALUE
       On success, the PID of the child process is returned in the parent, and 0 is returned in the child.  On failure, -1 is returned in the parent, no
       child process is created, and errno is set appropriately.
4.demo
5.父子谁先执行取决于内核调度算法。
  1. getpid 和 getppid
man 2 getpid

1.NAME
       getpid, getppid - get process identification

2.SYNOPSIS
       #include <sys/types.h>
       #include <unistd.h>

       pid_t getpid(void);
       pid_t getppid(void);

3.DESCRIPTION
       getpid() returns the process ID (PID) of the calling process.  (This is often used by routines that generate unique temporary filenames.)

       getppid() returns the process ID of the parent of the calling process.  
  1. 循环fork Demo
利用fork返回值特性。
demo:
  1. getuid
1.NAME
       getuid, geteuid - get user identity

2.SYNOPSIS
       #include <unistd.h>
       #include <sys/types.h>

       uid_t getuid(void);
       uid_t geteuid(void);

3.DESCRIPTION
       getuid() returns the real user ID of the calling process.
  1. getgid
1.NAME
       getgid, getegid - get group identity

2.SYNOPSIS
       #include <unistd.h>
       #include <sys/types.h>

       gid_t getgid(void);
       gid_t getegid(void);

3.DESCRIPTION
       getgid() returns the real group ID of the calling process.

       getegid() returns the effective group ID of the calling process.

4.ERRORS
       These functions are always successful.

四、进程共享

父子进程不同:
	进程id、返回值、各自的父进程、进程创建时间、闹钟、未决信号集。
	
父子进程共享:
	全局变量:读时共享,写时复制原则。
	1.文件描述符
	2.mmap映射区。

Linux系统编程——进程

五、GDB调试

Linux系统编程-GDB Command

六、exec函数族

正常:fork创建子进程后执行的是和父进程相同的程序。
利用exec:当子进程调用exec函数族中的一种函数时,该子进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。
		  调用exec并不创建新进程,所以调用exec前后该进程的id从未改变。
  1. exec函数族介绍
man 3 exec

1.NAME
       execl, execlp, execle, execv, execvp, execvpe - execute a file
	l list //list 命令行参数列表
	p path //搜索file时使用系统path变量,一般指/bin下的可执行程序
	v vector //使用命令行参数数组
	e environment //使用环境变量数组,不使用进程原有的环境变量,设置新加载程序运行的环境变量

注:
	只有execve是真正的系统调用。其他几个都调用execv来实现其功能。
	
2.SYNOPSIS
       #include <unistd.h>

       extern char **environ;

       int execl(const char *pathname, const char *arg, .../* (char  *) NULL */);
       int execlp(const char *file, const char *arg, ... /* (char  *) NULL */);
       int execle(const char *pathname, const char *arg, .../*, (char *) NULL, char *const envp[] */);
       int execv(const char *pathname, char *const argv[]);
       int execvp(const char *file, char *const argv[]);
       int execvpe(const char *file, char *const argv[],char *const envp[]);

3.RETURN VALUE
       The exec() functions return only if an error has occurred.  The return value is -1, and errno is set to indicate the error.

Linux系统编程——进程

  1. execl 详解
man execl

l list //list 命令行参数列表

执行指定的应用程序。

int execl(const char *pathname, const char *arg, .../* (char  *) NULL */);
//pathname一般用绝对路径,避免出错
//arg之后为参数列表。arg为参数列表的第一个,一般为pathname所指向的程序的程序名
//无论pathname所指向的程序是否有参数,都要以 NULL 参数结尾。
//如果pathname所指向的程序有参数,arg后面应设置相应的参数。

例:
	 execl("/home/pl/Desktop/LinuxCode/process/fork","haha",NULL);//demo1
	 execlp("/bin/ls","ls","-l","-h",NULL); //demo2

DEMO:
	https://github.com/Panor520/LinuxCode/tree/master/process/execl.c
  1. execlp
效果同 execvp,只是换了传参方式。

man execlp

l list //list 命令行参数列表
p path //搜索file时使用path变量,一般指/bin下的可执行程序

借助PATH环境变量,执行PATH环境变量中存在的程序

int execlp(const char *file, const char *arg, ... /* (char  *) NULL */);

	参数file:要加载的程序名字,当PATH中没有指定的程序名时会出错。
	//arg之后为参数列表。arg为参数列表的第一个,一般为pathname所指向的程序的程序名
	//无论pathname所指向的程序是否有参数,都要以 NULL 参数结尾。
	//如果pathname所指向的程序有参数,arg后面应设置相应的参数。
例:
	execlp("ls","ls","-l","-h",NULL); //demo1
    execlp("date","date",NULL); //demo2

DEMO:
	https://github.com/Panor520/LinuxCode/tree/master/process/execlp.c
  1. execvp
效果同 execlp,只是换了传参方式。

man execvp

p path //搜索file时使用系统path变量,一般指/bin下的可执行程序
v vector //使用命令行参数数组

int execvp(const char *file, char *const argv[]);

参数file:要加载的程序名字,当系统PATH中没有指定的程序名时会出错。
	//arg之后为参数列表。argv为参数指针数组,同man函数的char * argv[]

例:
	 char *args[] = {"ls","-l","-h",NULL};
 	 execvp("ls",args);
DEMO:
	https://github.com/Panor520/LinuxCode/tree/master/process/execvp.c //效果同execlp 

七、孤儿进程和僵尸进程

  1. 孤儿进程
孤儿进程:
	父进程先于子进程结束,则子进程称为孤儿进程,子进程的父进程称为init进程(也称为被init进程收养)。

注意:
	可以用 kill 命令终止该子进程。
  1. 僵尸进程
僵尸进程:
	进程终止,父进程尚未回收;子进程残留资源(PCB)存放在内核中,变成僵尸(zombie)进程。
	实际就是指子进程运行结束,而父进程尚未回收(指的是运行未结束,),子进程的状态就是[zombie]。
	如果父进程执行结束回收完毕(此时满足孤儿进程特性)那该子进程就变为孤儿进程。
	
	任何一个子进程(init除外)在exit()之后,并非马上就消失掉,而是留下一个称为僵尸进程(Zombie)的数据结构,等待父进程处理。
	
注意:
	僵尸进程不能使用kill命令清除掉。	
		因为kill命令只是用来终止进程的,而僵尸进程已经终止。

父进程调用 wait 或 waitpid 解决僵尸进程问题。
  • wait
man (2) wait

有多个子进程被创建时,也只能回收一个子进程,会回收最先执行完的子进程。

父进程调用wait函数功能:
	1.阻塞等待子进程退出
	2.回收子进程残留资源
	3.获取子进程结束状态(退出原因)

1.NAME
       wait, waitpid, waitid - wait for process to change state

2.SYNOPSIS
       #include <sys/types.h>
       #include <sys/wait.h>

       pid_t wait(int *wstatus);
       
3.参数
	     wstatus为值结果参数。正常返回子进程退出状态的值,通过宏函数获取相应状态。详见 man wait
	     
	     宏函数简析:
	    		1)WIFEXITED(wstatus)为非0 -> 进程正常结束
	    			WEXITSTATUS(wstatus)如上宏为真 -> 获取进程退出状态(exit的参数)
	    			
	    		2)WIFSIGNALED(wstatus)为非0 -> 进程异常终止
	    			WTERMSIG(wstatus)如上宏为真 -> 获取使进程终止信号的编号
	    			WCOREDUMP(wstatus)如WIFSIGNALED(wstatus)为真 并且发生coredump错误 -> 返回true.
	    			
	    		3)WIFSTOPPED(wstatus)为非0 -> 进程处于暂停状态
	    			WSTOPSIG(wstatus)如上宏为真 -> 获取使进程暂停信号的编号
	    			WIFCONTINUED(wstatus)为真 -> 进程暂停后已经继续运行

4.RETURN VALUE
       wait(): on success, returns the process ID of the terminated child; on error, -1 is returned.


5.DEMO:
	https://github.com/Panor520/LinuxCode/tree/master/process/zombie_wait.c
	https://github.com/Panor520/LinuxCode/tree/master/process/wait_while
  • waitpid
man (2) waitpid

注:
	waitpid为wait的升级版,拥有wait的功能且可指定pid以及option。
	一次wait或waitpid调用只能清理一个子进程,清理多个子进程应使用循环。

1.

		#include <sys/types.h>
        #include <sys/wait.h>\
        
		pid_t waitpid(pid_t pid, int *wstatus, int options);

2.参数:
	1)pid :指定回收的子进程pid
		-1:回收任一子进程,相当于wait。
		0 : 回收和当前调用waitpid一个组的所有子进程。
			默认父进程fork的子进程和父进程都是同一组的。
		<-1: 回收指定进程组内的任意子进程。
				负号代表进程组。
					例:
						进程1000 fork出1001、1002、1003、1004,此时进程1000、1001、1002、1003、1004同属进程组1000.
						然后1003独立出来,并fork出1005、1006,此时1003、1005、1006同属进程组1003,而1000、1001、1002、1004还同属1000进程组。
						若回收进程组1000,则参数为-1000;如果回收进程组1003,则参数为-1003.
						
	2)status:(传出)回收进程的状态
		   不指定 填 NULL
	3)options:WNOHANG (指定回收方式为,非阻塞)
			不指定填 0.
			若设置WNOHANG(非阻塞),不管是否指定的进程结束,都会直接返回0.
3.返回值:
	>0:表成功回收的子进程pid.
	0:函数调用时,参3 options 指定了WNOHANG ,并且没有子进程结束。
	-1:error. errno
	
5.DEMO:
	https://github.com/Panor520/LinuxCode/tree/master/process/zombie_waitpid.c
	https://github.com/Panor520/LinuxCode/tree/master/process/waitpid_while.c
  • wait和waitpid总结
waitpid为wait的升级版,拥有wait的功能且可指定pid以及option。
一次wait或waitpid调用只能清理一个子进程,清理多个子进程应使用循环。
	
进程回收推荐使用 waitpid 。

回收多个子进程推荐使用如下写法(不要把wait写到1的地方):
while(1){
	if(ret == -1)
		break;
}

八、进程间通信(IPC)

Linux环境下进程地址空间相互独立,要交换数据必须通过内核,
进程间通讯IPC, InterProcess Communication:
		在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从
	内核缓冲区把数据读走,内核提供的这种机制叫做IPC。
	
	进程间通讯方式有:
		1)管道。   (使用最简单)  (有血缘关系)
		2)信号。	(开销最小)
		3)共享映射区。	(无血缘关系)
		4)本地套接字。	(最稳定)

Linux系统编程——进程

  1. 管道
一、第一种管道pipe

	1.实现原理:
		内核借助环形队列机制,使用内核缓冲区实现。
	2.特质:
		1.伪文件。
		2.管道中的数据只能读取一次。
		3.数据在管道中,只能单向流动。
	3.局限性:
		1.自己写,不能自己读。
		2.数据不可以反复读。
		3.半双工通信。
		4.共同祖先进程间可用。
	
	4.函数实现
		man pipe
		
		1)NAME
		       pipe, pipe2 - create pipe
		
		2)SYNOPSIS
		       #include <unistd.h>
		       int pipe(int pipefd[2]);
		
		3)PARAMETER
			pipefd[0] :读端
			pipefd[1] :写段
		
		4)RETURN VALUE
			成功 0
			失败 -1 errno
		
		5)管道读写行为
			读:
				1.管道中有数据:
						(1)read返回实际的字节数。
				2.管道中无数据:
						(1)管道写端被全部关闭,read返回0(类似读到文件末尾)
						(2)写端没有被全部关闭,read阻塞等待(不久的将来可能有数据到达,此时让出cpu)
			写:
				1.管道读端全部被关闭:
						(1)进程异常终止(也可使用捕捉SIGPIPE信号,使进程不终止)
				2.管道读端没有全部关闭
						(1)管道已满,write阻塞 //这种情况很少见,管道中的数据写在内存中,若管道临时开辟空间满了,内核会自动扩容
						(2)管道未满,write将数据写入,并返回实际写入字节数。
			允许一个pipe有多个写端,一个读端。
		
		6)DEMO:
			https://github.com/Panor520/LinuxCode/tree/master/process/pipe.c
		
		7)查看管道缓冲区大小
				(1)命令
						ulimit -a 
							pipe size
				(2)函数fpathconf
					man fpathconf 
					
					long fpathconf(int fd, int name);
					fd:传pipefd两个之一即可
					name :_PC_PIPE_BUF
					成功:返回管道大小
					失败:-1 errno	
		8)管道优劣
			优:
				简单,相比信号,套接字实现进程间通讯,简单很多。
			劣:
				只能用于父子、兄弟进程(有共同祖先)间通讯。此问题由fifo有名管道解决。

二、第二种管道FIFO
	FIFO常被称为命名管道,以区分管道(pipe),无共同祖先的进程间也可通讯。
	FIFO是Linux基础文件的一种,该种文件在磁盘上没有数据块,仅仅用来标识内核中一条通道,各进程可以打开这个文件进程read/write,实际是在读写内核通道,以此实现进程间通讯。
	
	1.创建方式//会生成相应的管道文件,可通过 ls查看
		1)命令: mkfifo 管道名 
		2)库函数:
			(1)NAME
     		 	 mkfifo, mkfifoat - make a FIFO special file (a named pipe)

			(2)SYNOPSIS
			       #include <sys/types.h>
		  	  	   #include <sys/stat.h>
		
		    	   int mkfifo(const char *pathname, mode_t mode);
		    (3)parameter
		    		pathname 要创建的管道名
		    		mode	要创建的管道文件的权限 //例0644	   
				//一旦使用mkfifo创建一个FIFO,就可以使用open打开它,常见的文件I/O函数都可用于fifo。
				//如:close、read、write等
			
	2.fifo读写行为	
		可以一个读端多个写端,也可以一个写端多个读端。
	
	3.代码实现注意事项
		必须先开启读端进程,再开启写端才会有用数据读出,否则等于管道没通,会始终处于阻塞状态。
	
	4.Demo
		读端:https://github.com/Panor520/LinuxCode/tree/master/process/fifo_w.c
		写端:https://github.com/Panor520/LinuxCode/tree/master/process/fifo_r.c
  1. 文件实现通讯

    这个就是利用普通文件实现信息传递,实现思想就是正常的写读文件,过时内容,不做赘述。
    
  2. 共享映射区(mmap)
    Linux系统编程——进程

1.基础介绍
		(如上图)
		存储映射I/(Memory-mapped I/O)使一个磁盘文件与存储空间中的一个缓冲区相映射。
		故读缓冲区中数据,就相当于读文件中的相应字节。类似的,将数据存入缓存区,则相应的字节就自动写入文件。
		此过程不涉及read或write函数,仅使用地址(指针)完成I/O操作。
		使用这种方法,首先应通知内核,将一个指定文件映射到存储区域中。这个工作通过mmap函数实现。
		
2.mmap
	man mmap

	1)NAME
    		mmap, munmap - map or unmap files or devices into memory

	2)SYNOPSIS
       		#include <sys/mman.h>

      	 	void *mmap(void *addr, size_t length, int prot, int flags,
                  int fd, off_t offset);
			int munmap(void *addr, size_t length);//释放创建的映射区

	3)parameter
			addr:	指定映射区的地址。通常传NULL,表示让系统自动分配
			length:共享内存映射区的大小。(<=文件的实际大小)
			prot:  共享内存映射区的读写属性。
						PROT_READ、PROT_WRITE、PROT_READ|PROT_WRITE
			flags: 标注共享内存的共享属性。
						MAP_SHARED(内存,磁盘都会被写入)、MAP_PRIVATE(仅写入内存,不写入磁盘)
			fd:	用于创建共享内存映射区的那个文件的 文件描述符。
			offset:默认0,表示映射文件全部。偏移位置。
						必须是 4K 的整数倍。
						
	4)RETURN VALUE
			mmap()
				成功:	映射区的首地址
				失败:	MAP_FAILED, errno
			munmap()
				On success, munmap() returns 0.  On failure, it returns -1, and errno is set
	5)使用注意事项
			1.用于创建文件映射区的文件大小为0,实际指定非0大小创建映射区,出“总线错误”。
			2.用于创建文件映射区的文件大小为0,实际指定0大小创建映射区,出“无效参数”错误。
			3.用于创建文件映射区的文件读写属性为 只读,映射区属性为 读、写。出“无效参数”错误。
			4.创建映射区,需要read权限。mmap的读写权限应<=文件open的权限。 
			5.文件描述符fd,在mmap创建映射区完成即可。后续访问文件,用 地址访问。
			6.offset 必须是 4k(4096)的整数倍。(原因:存储映射由MMU进行,而MMU映射的最小单位为4k)
			7.对申请的映射区内存,不能越界访问。
			8.munmap用于释放的地址,必须是mmap申请的返回的地址。
			9.映射区访问权限为 “私有” MAP_PRIVATE,只需要open文件时,有读权限,用于创建映射区即可。
	6)总结
			1.创建映射区的过程中,隐含着一次对映射文件的读操作(也就是fd参数必须在open时被赋予O_RDONLY权限)。
			2.当MAP_SHARED时,要求:映射区的权限应<=文件打开的权限(出于对映射区的保护)。
			3.映射区的释放和文件关闭无关,只要映射建立成功,文件可以立即关闭。
			4.mmap 创建映射区出错概率非常高,一定要检查返回值,确保创建成功再进行后续。
	
	7)DEMO:
			1.简单的mmap用法:
					https://github.com/Panor520/LinuxCode/tree/master/process/fifo_w.c
			2.父子进程间通讯:
					https://github.com/Panor520/LinuxCode/tree/master/process/fifo_w.c
			3.无血缘关系用法(必须掌握):
					https://github.com/Panor520/LinuxCode/tree/master/process/fifo_w.c
					多个写端,一个读端注意事项:
							mmap数据可以重复读取(缓冲区,读完数据还存在),而fifo只能读取一次(fifo是管道,读一次该数据就没了)。
	
	8)匿名映射(知道即可)
			只能用于血缘关系进程间通讯
			mmap(void *addr, size_t length, int prot, int flags,
                  int fd, off_t offset);		
            fd 指定为-1.
            flags 加上 MAP_ANONYMOUS
            
	9)/dev/zero文件用来创建映射区		
上一篇:Aliyun ECS实例安装docker(内核版本:Linux 5.10.23-5.al8.x86_64 x86_64)


下一篇:多线程