Unix系统 - 进程管理

写在前面:注意,本章除了讲解进程管理,还包含网络编程Socket API的知识。


进程和程序的概念
程序是硬盘的文件,是代码的编译链接产物。
运行起来的程序就是进程,进程是内存中运行的程序。
操作系统支持多进程的启动,每个进程内部支持多线程。进程之间是完全独立的。

一、进程

1.1基础知识

1.1.1进程ID

进程用 进程ID 做唯一的标识,叫PID,函数getpid()可以获取当前进程的pid。

1.1.2查看进程

查看进程的方式:
1、Windows用ctrl + alt + delete启动任务管理器;
2、Linux/Unix用ps命令查看进程。

ps 只能显示当前终端启动的进程。
ps -aux : Linux专用选项,Unix不直接支持(/usr/usb/ps可以用)。
ps -ef : Unix/Linux通用的选项

如果进程比较多,可以用管道实现分页,命令如下:
ps -aux | more
实现空格翻页、回车翻行、q退出。

ps命令可以查看进程的如下信息:
进程PID、进程的启动者(属主)、CPU和内存使用率、状态、
父进程的PID、启动的程序是哪个
其中,进程的状态主要状态包括:
S - 休眠状态,进程大多数处于休眠状态
s - 说明该进程有子进程(父进程)
R - 正在运行的进程
Z - 僵尸进程(已经结束但资源没有回收的进程)

1.1.2 父子进程概念

关于父进程和子进程
操作系统中的多进程是有启动的次序的,Unix系统先启动0进程,0进程再启动进程1和进程2(有些系统只启动进程1),然后0进程就休眠。进程1和进程2启动其他进程,其他进程再启动其他的进程,直到所有的进程都启动为止。
如果进程a启动了进程b,a叫b的父进程,b叫a的子进程。

1.1.3得到进程ID的函数

进程用PID表示,PID是一个非负的正数,PID可以延迟重用。因此PID在同一
时刻保证唯一。

几个常用的函数:
getpid() - 取当前进程的PID
getppid() - 取当前进程的父进程的PID
getupid() - 取当前用户的ID。

1.2 进程运行

创建子进程的方法:
方法一: fork() 创建子进程,通过复制父进程创建子进程。因此父进程对应相同的代码区。
方法二:vfork() + execl()创建子进程,父进程和子进程的代码区完全不同,父子进程执行
的是完全不同的代码。

1.2.1 方法一

1.2.1.1进程创建

fork()是一个非常复杂的简单函数:

pid_t fork();

返回子进程的PID或者0,失败返回-1.没有参数。

fork()是通过复制父进程的内存空间创建子进程,复制除了代码区之外的所有区域,
代码区父子进程共享(只读)。
fork()创建一个子进程,子进程从fork()当前位置开始执行,fork()之前的代码父进程
执行一次,fork()之后的代码父子进程分别执行一次(共2次)
fork()函数自身会返回两次,父进程返回子进程的PID,子进程会返回0.注意了是函数的返回。
fork()创建子进程时,会复制除了代码区之外的所有区域,包括缓冲区。
fork()创建子进程时,如果父进程有文件描述符,子进程会复制文件描述符,不复制
文件表(父子进程共用一个文件表)。

父子进程的关系:
fork()创建子进程后,父子进程同时运行,如果子进程先结束,子进程会给父进程发信号,父进程回收子进程的资源。
fork()创建子进程后,父子进程同时运行,如果父进程先结束,子进程会变成孤儿进程,认进程1(init进程)做新的父进程。init进程也叫孤儿院。
fork()创建子进程后,父子进程同时运行,如果子结束时父进程没有收到信号或没有及时处理,子进程将变成僵尸进程。
具体fork()原理参考下图:
在这里插入图片描述
例子一:
fork1.c

#include <stdio.h>
#include <unistd.h>
int main() {
   
	printf("begin\n");
	pid_t pid = fork();
	printf("end%d\n",pid);
}
//执行结果:
begin
end11710
end0
如果begin没有加\n
#include <stdio.h>
#include <unistd.h>
int main() {
   
	printf("begin");//begin在输出缓冲区,子进程复制
	pid_t pid = fork();//缓冲区,而不是执行第5行
	printf("end%d\n",pid);
}
//执行结果:
beginend11710
beginend0

fork2.c

#include <stdio.h>
#include <unistd.h>
int main() {
   //父子进程使用不同的分支
	pid_t pid = fork();
	if(!pid){
   //父子进程都有,但子进程符合条件
		printf("我是子进程\n");
	}
	else{
   //父子进程都有,父进程执行,子进程不进行
		printf("我是父进程\n");
	}
}

例子二:要求在父子进程中,分别打印出父子进程的PID
//格式:我是子进程1234,我是父进程1233

#include <stdio.h>
#include <unistd.h>
int main() {
   
	pid_t pid = fork();
	if(!pid){
   //父子进程都有,但子进程符合条件
		printf("我是子进程%d,父进程是%d\n",getpid(),getppid());
		//getpid()获得当前pid,getppid()获得当前的父进程pid
	}
	else{
   //父子进程都有,父进程执行,子进程不进行
		printf("我是父进程%d,子进程是%d\n",getpid(),pid);
	}
}

fork3.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int i1 = 10;
int main() {
   
	int i2 = 10;
	int* pi = malloc(4);
	*pi = 10;
	pid_t pid = fork();
	if(!pid) {
   //子进程执行的分支
		i1 = 20; i2 = 20; *pi = 20;
		printf("child:i1=%d,i2=%d,*pi=%d\n",i1,i2,*pi);
		//打印结果是i1=20 i2=20 *pi=20
		printf("child:i1=%p,i2=%p,*pi=%p\n",&i1,&i2,pi);
		//fork()创建的子进程会复制父进程的虚拟内存地址,但映射到
		//不同的物理内存上,同时把原来的值拷贝过来。复制完成后父进
		//程的内存就独立了。
		exit(0);//结束子进程
	}
	sleep(1);
	//父进程 
	printf("father:i1=%d,i2=%d,*pi=%d\n",i1,i2,*pi);
	//打印结果是i1=10 i2=10 *pi=10
	//为什么是10,可以参考上面的图,改变子进程的栈区堆区全局
	//区,父进程的是不变的
	printf("father:i1=%p,i2=%p,*pi=%p\n",&i1,&i2,pi);
	//打印的虚拟内存地址和子进程一样
}

练习三:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
int main() {
   
	int fd = open("a.txt",O_RDWR|O_CREAT|O_TRUNC,0666);
	if(fd == -1)perror("open"),exit(-1);
	pid_t pid = fork();//有两个fd,子进程将复制fd
	if(pid == 0){
   //子进程执行的分支
		write(fd,"hello",5);//只复制描述符,不复制文件表
		close(fd);//关闭子进程的fd
		exit(0);
	}
	write(fd,"12345",5);
	close(fd);//关闭父进程的fd
	return 0;
}
//运行结果是12345hello(也就是说父和子进程没有相互覆盖,
因为不复制文件表,共用一个文件表,只有一个文件偏移量)

如果代码改成下面的,先fork(),在open"a.txt",则会出现hello和12345的覆盖

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
int main() {
   
	pid_t pid = fork();//先fork()的话,没有复制,而是创建
	int fd = open("a.txt",O_RDWR|O_CREAT|O_TRUNC,0666);
	if(fd == -1)perror("open"),exit(-1);
	if(pid == 0){
   //子进程执行的分支
		write(fd,"hello",5);//只复制描述符,不复制文件表
		close(fd);//关闭子进程的fd
		exit(0);
	}
	write(fd,"12345",5);
	close(fd);//关闭父进程的fd
	return 0;
}

fork()与open()关系如下图:
在这里插入图片描述
例子四:接下来这个程序验证父和子进程是否同时进行:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
   
	pid_t pid = fork();
	if(!pid) {
   //子进程的分支
		printf("子进程是%d,父进程是%d\n",getpid(),getppid());
		//getpid()获得当前pid
		sleep(2);//休眠2秒
		printf("子进程是%d,父进程是%d\n",getpid(),getppid());
		exit(0);
	}
	sleep(1);//导致父进程先结束
	printf("pid = %d\n",getpid());//父进程pid
	return 0;
}
执行结果:
lioker:Desktop$ gcc 1.c 
lioker:Desktop$ ./a.out 
子进程是12760,父进程是12759
pid = 12759
lioker:Desktop$ 子进程是12760,父进程是1862
//因为父进程先结束,所以子进程重新认定一个父进程
1.2.1.2 进程终止

接下来讲进程的退出
正常退出:
1在main()中执行return语句
2执行exit()函数
3执行_Exit()或_exit()函数
4最后一个线程退出
5主线程退出
非正常退出:
1被信号打断导致退出
2最后一个线程被取消

今天研究exit()、_exit()和_Exit()都是用来退出进程的,区别在于:
_exit和_Exit()是一样的,区别在于头文件不同(uc/标c)
exit()和_Exit()区别主要在于退出的方式不同:
_Exit()是立即退出,exit()不是立即退出,还可以调用一些其他函数后再退出。
可以使用atexit()函数注册一些函数,这个函数在exit()之前会被自动调用,return也会调用。
补充一下函数指针:

void fa(void);//函数声明
void (*fa)(void);//函数指针(fa是函数名)

以下为exit()、atexit()函数程序代码:

#include <stdio.h>
#include <stdlib.h>
void fa(void){
   
	printf("fa called\n");
}
int main() {
   
	atexit(fa);//注册退出前的函数fa,现在不调用
	printf("begin\n");
	exit(0);
	printf("end\n");
}//运行结果为begin -> fa call -> end

改为下面的:

#include <stdio.h>
#include <stdlib.h>
void fa(void){
   
	printf("fa called\n");
}
int main() {
   
	atexit(fa);//注册退出前的函数fa,现在不调用
	printf("begin\n");
	_exit(0);
	printf("end\n");
}//运行结果为begin

再改为下面的:

#include <stdio.h>
#include <stdlib.h>
void fa(void){
   
	printf("fa called\n");
}
int main() {
   
	atexit(fa);//注册退出前的函数fa,现在不调用
	printf("begin\n");
	printf("end\n");
}//运行结果为begin -> end -> fa call。这是因为系统自动在
//主函数最后加了return 0;

wait()和waitpid()
wait和waitpid()可以让父进程等待子进程的结束,并取得子进程的退出状态和退出码(return后面的值或exit()括号中的值)
wait()和waitpid()的区别在于wait()很固定,而waitpid()更灵活。
wait()是等待任意一个子进程结束后返回,而waitpid()可以选择等待的子进程,也可以不等待。
wait()等待的子进程包括僵尸子进程,因此wait()也叫殓尸工。

pid_t wait(int* status)

参数是一个传出参数(指针),用来带出结束子进程的退出码和退出状态;
关于返回,有结束子进程就返回他的PID,没有就等待,父进程自己阻塞,如果出了错就返回-1。
宏函数WIFEXITED(status)可以判断是否正常退出,
WEXITSTATUS(status)可以取到退出码。
代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
   
	pid_t pid = fork();
	if(!pid) {
   //子进程的分支
		sleep(2);
		printf("子进程%d即将结束\n",getpid());
		exit(100);//进程正常退出,并且完成了功能
		//exit(-1),负数,表示进程正常退出,但没有完成任务
	}
	int status;
	pid_t wpid = wait(&status);//如果子进程不结束,父进程则阻塞等待子进程结束
	printf("等待到了%d的退出\n",wpid);
	if(WIFEXITED(status)){
   //说明正常退出了
		printf("正常退出,退出码:%d\n",WEXITSTATUS(status));
	}
}
//运行结果:
lioker:Desktop$ gcc 1.c 
lioker:Desktop$ ./a.out 
子进程13436即将结束
等待到了13436的退出
正常退出,退出码:100

waitpid()有更多的选择

pid_t waitpid(pid_t pid,int *status,int option);

参数ststus和wait()一样,pid可以等待哪些或哪个子进程,option可以设定是否等待。
pid的值可能是:
== -1 等待任意子进程,与wait()等效; >0 等待指定子进程(指定pid);
==0 等待本进程组的任一子进程; <-1 等待进程组ID等于pid绝对值的任一子进程
注:后两个有时候用不到,了解即可
option的值:0 阻塞,父进程等待;WNOHANG 不阻塞,直接返回0
关于返回值:有子进程结束时返回子进程的pid,出错返回-1
如果为阻塞方式,没有子进程结束则继续等待;
如果是WNOHANG,若子进程不立即可用,则不阻塞,返回0。
代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
   
	pid_t pid = fork();
	if(pid == -1)perror("fork"),exit(-1);
	if(!pid) {
   //子进程的分支
		sleep(1);
		printf("子进程%d即将结束\n",getpid());
		exit(100);
	}
	pid_t pie2 = fork(); 
	if(!pid2){
   
		sleep(3);
		printf("子进程%d即将结束\n",getpid());
		exit(200);
	}
	int status;
	pid_t wpid = waitpid(pid2,&status,0);//如果waitpid()里的pid2改成-1即
	                                     //waitpid(-1,&status,0)则pid和pid2
										 //谁先结束选谁
	if(WIFEXITED(status)){
   
		printf("等到了%d子进程,退出码:%d\n",wpid,WEXITSTATUS(status));
	}  
}

1.2.2 方法二

接下来讲vfork() + execl()方式创建子进程:

vfork()和fork()在语法上没有区别,唯一的区别在于vfork()不复制父进程的任何资源,而是直接占用父进程的资源运行代码,父进程处于阻塞状态,直到子进程结束或者调用了exec系列函数(比如:execl())。
vfork()和execl()的合作方式:
vfork()可以创建新的进程,但没有代码和数据,execl()创建不了进程,但可以为进程提供代码和数据。
和fork()不同,vfork()创建子进程后确保子进程先运行,父进程到子进程调用到了execl()之后才能运行。
注:vfork()如果占用的是父进程的资源,必须用exit()显式退出。

代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(){
   
	pid_t pid = vfork();
	if(!pid){
   
		printf("子进程%d开始运行\n",getpid());
		sleep(3);
		printf("子进程%d结束\n",getpid());
		exit(0);//vfork()占用父进程资源,必须用exit()退出
	}
	printf("父进程结束\n");
	return 0;
} 

execl()是exec系统函数中的第一个,功能是启动一个全新的进程,替换当前的进程。启动的这个进程会全面覆盖旧进程,但不会新建进程。(会替换各种区域,但pid不变)

execl("程序的路径","执行的命令","参数",NULL);

只有第一个参数是必须正确的,第二个参数必须存在但可以不正确,第三个和第四个参数可以没有,NULL代表参数结束了。
比如运行我们的程序:

execl("./b.out","b.out",NULL);

代码如下:

#include <stdio.h>
#include <unistd.h>
int main() {
   
	printf("begin\n");
	execl("./bin/ls","ls","-l",NULL);
	printf("end\n");
}
//运行结果:
begin
total 16
-rw------- 1 lioker lioker  131 421 17:45 1.c
-rwxrwxr-x 1 lioker lioker 8648 421 17:45 a.out
这是因为execl()覆盖掉了旧进程,所以printf("end\n");未执行

下面把execl()与vfork()函数结合使用

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(){
   
	pid_t pid = vfork();
	if(!pid){
   
		execl("/bin/ls","ls","-l",NULL);
		printf("child\n");//打印不出来,不执行
		exit(0);//有意义,针对execl()出错时,退出子进程
	}
	printf("父进程开始运行\n");
	sleep(1);
}
//运行结果:
父进程开始运行
total 16
-rw------- 1 lioker lioker  212 421 18:04 1.c
-rwxrwxr-x 1 lioker lioker 8800 421 18:05 a.out

代码:两个.c文件
proc.c

#include <stdio.h>
int main(){
   
	printf("pid=%d\n",getpid());
}

vfork3.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(){
   
	pid_t pid = vfork();
	if(!pid){
   
		printf("pid=%d\n",getpid);
		execl("./proc","proc",NULL);
		exit(0);//有意义,针对execl()出错时,退出子进程
	}
	printf("父进程开始运行\n");
	sleep(1);
}
//执行结果:
pid=10583
父进程开始运行
pid=10583
也就是说,两个pid一样。这是因为execl()的pid不会改变

1.2进程间通信(IPC)

IPC - 进程间通信(InterProcess Communication, IPC)

Unix系统早期都是多进程解决问题的,因此多个进程之间需要交互数据,而进程间不能直接交互数据,IPC就是解决这个问题的。(后来就又出现了多线程技术)。

IPC主要包括:
1、文件
2、信号
3、管道
4、共享内存
5、消息队列
6、信号量集(与信号没有任何的关系)
7、网络socket

其中,共享内存、消息队列和信号量集都是XSI IPC,遵循相同的规范。因此编程有很多共性的地方。
而管道是古老的IPC,目前很少使用。
下面是IPC原理图:
在这里插入图片描述

1.2.1信号

1.2.1.1基础知识

信号是Unix/Linux系统下最常见的一种软件中断方式

中断就是让程序停止当前正在运行的代码,转而执行其他代码的过程。

中断分为软件中断和硬件中断,软件中断就是用软件的方式中断代码。

信号导致代码中断的案例很多,比如:
ctrl C ->信号2
kill -9 -> 信号9
段错误 -> 信号11
总线错误 -> 信号7(不确定,不同系统不一定)
Unix系统信号从1到48,Linux系统从1-64,但不确定连续,而且规范中没有规定信号的数量。

信号都有一个宏名称,以SIG开头,比如:信号2叫SIGINT,宏名称的本质就是一个非负整数,查看信号的命令:
kill -l
注:编程时,信号使用宏名称,因为有些系统信号数字不同,但宏名称是一样的。

常见的一些宏名称:
SIGINT - 信号2 ctrl+C
SIGQUIT - 信号3 ctrl+
SIGKILL - 信号9
… …

在Linux系统中,1-31是不可靠信号,是早期的信号,不支持排队,所以有可能丢失;34-64是可靠信号,支持排队,不可能丢失。信号分为可靠信号和不可靠信号。(可以比喻成,饭店生意太火,如果有地方支持排队吃饭,就是可靠。饭店太小不支持排队,顾客就走了丢失了)。
当信号是不可靠信号时进程只处理一个,当信号是可靠信号时,进程则依据排队一个一个处理。

信号的处理方式:信号只是一个整数,实现中断的功能依靠信号处理。信号的处理方式有三种:
1默认处理,是系统提供的,多半是退出进程。
2忽略信号,信号来了不做额外处理,直接忽略。
3自定义处理函数,信号按程序员的代码处理。
注:有些信号是不能被自定义和忽略了,比如信号9直接将进程强制退出;
进程可以给其他进程发信号,但只能给本用户的进程发信号,root可以给所有的用户进程发信号。

1.2.1.2 signal()、kill()、alarm()、sleep()

提前说明:kill()函数和kill命令的作用是向进程发送信号;而signal()函数的作用是接收信号并把信号传递给特定的函数中,就是说得有外界发送一个信号之后(比如按键盘ctrl+C)signal()才能收到信号然后起作用。

1.2.1.2.1 signal()

signal()/sigaction()可以设置信号的处理方式
signal()使用了一个函数指针,原型:
void (*f)(int)
函数指针 signal(int 信号值,函数指针)

其中,第二个参数函数指针可以是以下三个值:
SIG_IGN - 忽略该信号
SIG_DFL - 恢复默认处理
传入一个程序员自定义函数名 - 自定义处理函数

返回,成功返回之前的处理方式,失败返回SIG_ERR。
注:signal()函数只是设定了信号处理方式,自身并没有发信号,因此信号处理函数在信号到来时才执行。

kill命令用于发信号,格式:
kill -信号 进程pid
信号0没有实际的意义,用于测试是否有发信号的权限。
代码:

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
int main(){
   
	printf("pid=%d\n",getpid());
	while(1);
}
//在一个终端运行:
lioker:Desktop$ ./a.out 
pid=15428
再打开一个终端:kill -0 154281无反应
kill -11 15428则段错误Segmentation fault (core dumped)
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
void fa(int signo){
   //系统会自动把信号的值传递给参数
	printf("捕获了信号%d\n",signo);
}
int main(){
   
	signal(SIGINT,fa);//信号2交给fa
	if(signal(SIGQUIT,SIG_IGN) == SIG_ERR)//达到把信号3忽略的目的
		perror("signal 3"),exit(-1);
	signal(9,fa);//这里信号9调signal()没作用	
	printf("pid=%d\n",getpid());
	while(1);
}
//执行结果:
pid=15547
^C捕获了信号2
^C捕获了信号2 
^\^\^\^\^\^\
Killed
就是说,在另一个终端执行kill -9 15547,这个进程才被杀死。

注意了,关于常用的信号的用法:
1、头文件<signal.h>
2、fa这个函数
3、signal(SIGINT,fa);signal(SIGQUIT,SIG_IGN) ;
改成下面的:

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
void fa(int signo){
   //系统会自动把信号的值传递给参数
	printf("捕获了信号%d\n",signo);
	signal(signo,SIG_DFL);//第一次给fa,第二次恢复默认
}
int main(){
   
	signal(SIGINT,fa);//信号2交给fa
	if(signal(SIGQUIT,SIG_IGN) == SIG_ERR)//达到把信号3忽略的目的
		perror("signal 3"),exit(-1);
	signal(9,fa);//这里信号9调signal()没作用	
	printf("pid=%d\n",getpid());
	while(1);
}
//执行结果:
pid=15547
^C捕获了信号2
在输入一次^C就终止了,因为恢复默认了

父子进程之间的信号处理:
如果父进程用fork()创建的子进程,子进程的与父进程的信号处理方式完全一致(照抄)。如果父进程用vfork()+execl()方式创建的子进程,父进程自定义处理函数的子进程改为默认(因为子进程已经没有了处理函数),其他照抄。
代码验证一下:
fork.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
void fa(int signo){
   
	printf("捕获了信号%d\n",signo);
}
int main(){
   
	signal(SIGINT,fa);//信号2交给了fa
	signal(SIGQUIT,SIG_IGN);//忽略信号3
	pid_t pid = fork();
	if(!pid){
   
		printf("child:pid=%d\n",getpid());
		while(1);
	}
	printf("父进程%d结束\n",getpid());
}
lioker:Desktop$ gcc 1.c 
lioker:Desktop$ ./a.out 
父进程16192结束
child:pid=16193
lioker:Desktop$ kill -2 16193
捕获了信号2
lioker:Desktop$ kill -3 16193//信号3被忽略
lioker:Desktop$ kill -9 16193//被杀死

练习:验证一下vfork()+execl()的情况
proc.c

proc.c
#include <stdio.h>
int main(){
   
	while(1);
}

vfork.c
#include <stdio.h>
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值