深入理解操作系统(21)第八章:异常控制流(2)系统调用和错误处理+进程控制(400个系统调用/errno/exit参数和返回值说明/fork/execve/waitpid/用户栈的组织结构)

本文详细介绍了操作系统中的关键概念,包括Linux的系统调用(如fork、exit、waitpid等)、错误处理机制(errno检查)、进程控制(PID获取、进程创建与终止)以及用户栈的组织结构。特别强调了fork函数的工作原理,如返回值、父子进程的异同,以及僵尸进程与waitpid的使用。此外,还探讨了execve函数在加载并运行程序中的作用。
摘要由CSDN通过智能技术生成

1. 系统调用和错误处理

1.1 目前,linux提供了大约400个系统调用

Unix系统提供了大量的系统调用,当应用程序想向内核请求服务时,比如读取一个文件,或者创建一个新的进程,都可以使用这些系统调用。
例如,linux提供了大约160个系统调用(现在已经400个了)。输入"man syscalls",你将得到完整的列表。

[root@localhost 10]# man syscalls
SYSCALLS(2)     Linux Programmer's Manual                                                

NAME
	syscalls - Linux system calls
……
	──────────────────────────────────────────────────────

	_llseek(2)                  1.2
	_newselect(2)               2.0
	_sysctl(2)                  2.0
	accept(2)                   2.0           See notes on socketcall(2)
	accept4(2)                  2.6.28
	access(2)                   1.0
	acct(2)                     1.0
	add_key(2)                  2.6.11
	adjtimex(2)                 1.0
	alarm(2)                    1.0
	alloc_hugepages(2)          2.5.36        Removed in 2.5.44
	bdflush(2)                  1.2           Deprecated (does nothing)
…… ……

1.2 包装函数+errno检查

标准C库提供了一组针对最常用系统调用的方便的包装(wrapper)函数。包装函数将参数打好包,通过适当的系统调用陷入内核,然后系统调用的
返回状态传递给调用程序。在我们下面章节的讨论中,我们把系统调用和它们相关的包装函数可互换地称为系统级函数
当linux系统级数遇到错误时,它们典型地会返回一并设置全局整数变量errno来表示什么出错了。
程序员应该总是检查这些错误,但是不幸的是,许多人都忽略了错误检查,因为它使代码变得臃肿,而且难以读懂。

例子:

#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>

int main ()
{
	//查看abc文件是否存在 成功0,失败-1
	if(0 != access("abc",F_OK)) 
	{
		printf("errno=%d\n",errno);
		printf("Error: %s\n", strerror(errno));
	}
	
	return(0);
}

结果:
errno=2
Error: No such file or directory

2. 进程控制

2.1 获取进程ID

每个进程都有一个惟一的正数(非零)进程ID(PID)。
getpid函数返回调用进程的PID
getppid函数返回它的父进程的PID〔也就是,创建调用进程的进程)。

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

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

//pid_t 其实就是 int

2.2 创建进程 fork

从程寻员的角度,我们可以认为进程总是处于下面三种状态之一:

1. 运行:进程要么在CPU上执行,要么在等待被执行且最终会被调度。
2. 暂停:进程的执行被挂起,且不会被调度。
	当收到SIGSTOP、SIGTSTP、SIDTTIN或者SIDTTOU信号时,进程就暂停,
	并且保持暂停直到它收到一个SIGCONT信号,在这个时刻,进程再次开始运行。
3. 终止:进程永远地停止了。进程会因为3种原因终止:
	收到一个信号,该信号的默认行为是终上进程
	从主程序返回
	调用exit函数

fork:

pid_t fork(void)

2.2.1 fork返回值

子进程返回0;
父进程返回子进程的pid;
错误返回-1

2.2.2 fork父子进程的异同

新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份拷贝,包括文本、数据和bss段、堆以及用户栈。子进程还获得与父进程任何打开文件述符相同的拷贝,这就意味着当父进程调用fork时,子进程可以读写父进程中打开的任何文件。

父进程和新创建的子进程之间最大的区别在于它们有不同的PID。

fork函数是有趣的是它只被调用一次,却会返回两次:一次是在调用进程(父进程)中,一次是在新创建的子进程中。
在父进程中,fork返回子进程的PID。
在子进程中,fork返回零。

2.2.3 例子1:简单fork例子说明:

#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>

int main ()
{
		pid_t pid;
		int x = 1;
		pid = fork();
		if(0 == pid)
		{
				printf("child=%d\n",++x);
				exit(0);
		}

		printf("parent=%d\n",--x);
		exit(0);
}

结果:
[root@localhost 8]# ./a.out 
parent=0
[root@localhost 8]# child=2

[root@localhost 8]#

2.2.4 fork 详细说明

这个简单的例子有一些微妙的方面:

1. 调用一次,返回两次
	fork函数被父进程调用一次,但是却返回两次:一次是返回到父进程,一次是返回到新创建的子进程。
	对于只创建一个子进程的程序来说,这还是相当简单的。
	但是含有多个fork实例的程序可能就会令人迷惑,需要仔细地推敲了。
	
2. 并发执行
	父进程和子进程是并发运行的独立进程。
	内核能够以任意方式交替执行它们逻辑控制流中的指令。
	当我们在系统上运行这个程序时,父进程先完成它的printf语句,然后是子进程。
	然而,在另一个系统上可能正好相反。
	一般而言,作为程序员,我们无法对不同进程中的指令交执行做任何假设。
	
3. 相同的但是独立的地址空间
	如果我们能够在fork函数在父进程和子进程中返回后立即终止这两个进程,我们会看到每个进程的地址空间都是相同的。
	每个进程有相同的用户栈、相同的本地变量值、相同的堆、相同的全局变量值,以及相同的代码。
	因此,在我们的示例程序中,当fork函数返回时,本地变量x在父进程和子进程中都为1。
	然而,因为父进程和子进程是独立的进程,它们每个都有自己的私有地址空间。
	后面,父进程和子进程对x所做的任何改变都是独立的,不会反映在另一个进程的存储器中。
	这就是为什么当父进程和子进程调用它们各自的printf函数时,它们中的变量x会有不同的值。

4. 共享文件
	当我们运行示例程序时,我们注意到父进程和子进程都把它们的输出显示在屏幕上。
	原因是子迅程继承了父进程所有的打开文件。当父进程调用fork时,stdout文件是被打开的,并指向屏幕。
	了进程继承了这个文件,因此它的输出也是指向屏暮的。

关于fork有个cow
参考:Linux:COW 写时拷贝技术
https://blog.csdn.net/lqy971966/article/details/118784913

2.2.5 例子2:多个fork说明

图8.14
在这里插入图片描述

2.3 终止进程 exit

从程寻员的角度,我们可以认为进程总是处于下面三种状态之一:

1. 运行:进程要么在CPU上执行,要么在等待被执行且最终会被调度。
2. 暂停:进程的执行被挂起,且不会被调度。
	当收到SIGSTOP、SIGTSTP、SIDTTIN或者SIDTTOU信号时,进程就暂停,
	并且保持暂停直到它收到一个SIGCONT信号,在这个时刻,进程再次开始运行。
3. 终止:进程永远地停止了。进程会因为3种原因终止:
	收到一个信号,该信号的默认行为是终上进程
	从主程序返回
	调用exit函数

2.3.1 exit函数

void exit(int status);

1.功能:

关闭所有文件,终止正在执行的进程。	

2.参数:

exit以status退出状态来终止进程。
exit() 里面的参数,是传递给其父进程的

3.使用:

exit(0)表示正常退出

2.3.2 exit参数说明!!!

每个运行着的程序都是进程,而进程就会有父进程,父进程通常是直接启动你的进程,父进程死亡的进程会被 init 收养,其父进程变为 init,而 init 的父进程是进程 0,进程 0 则是系统启动时启动的第一个进程。

exit() 里面的参数,是传递给其父进程的。
对父进程来说,你的进程仿佛是一个函数,而函数可以有返回值。

2.3.3 为什么要使用 exit() 函数?

是历史原因,虽然现在大多数平台下,直接在 main() 函数里面 return 可以退出程序。但是在某些平台下,在 main() 函数里面 return 会导致程序永远不退出(因为代码已经执行完毕,程序却还没有收到要退出的指令)。

换句话说,为了兼容性考虑,在特定的平台下,程序最后一行必须使用 exit() 才能正常退出,
这是 exit() 存在的重要价值。

2.3.4 exit和return的区别:

按照ANSI C,在最初调用的main()中使用return和exit()的效果相同。

但要注意这里所说的是“最初调用”。

区别1:如果main()在一个递归程序中,exit()仍然会终止程序;
	   但return将控制权移交给递归的前一级,直到最初的那一级,此时return才会终止程序。

区别2:return和exit()的另一个区别在于,即使在除main()之外的函数中调用exit(),它也将终止程序。

2.5 回收子进程(僵尸进程+waitpid)

2.5.1 僵尸进程

当一个进程由于某种原因终上时,内核并不是立即把它从系统中清除。取而代之的是,进程被保持在一种终止状态中,直到被它的父进程回收(reaped)。当父送程回收己终止的子进程时,内核将子进程的退出状态传递给父进程,然后抛弃己终止的进程,从此时开始该进程就不存在了。

一个终止了但还未被回收的进程称为僵死进程(zombie)

为什么已终止的子进被称为僵死进程?
在民间传说中,僵尸是活着的尸体,一种半生半死的实体。僵死进程已经终止了,而内核仍保留着它的某些状态(内核栈,进程号,hread_info结构和task_struct结构等)直到父进程回收它为止,从这个意义上说它们是类似的。

如果父进程没有回收它的僵死子程就终止了,那么内核就会安排init进程来回收它们。
init进程的PID为1,并且是在系统初始化时由内核创建的。长时间运行的程序,比如shell或者服务器,总是应该回收它们的僵死子进程。即使僵死子进程没有运行,它们仍然消耗系统的存储器资。

僵尸进程 & 孤儿进程
参考:
https://blog.csdn.net/lqy971966/article/details/119116896

2.5.1 waitpid

一个进程可以通过调用waitpid函数来等待它的子进程终止或者暂停。

pid_t waitpid(pid_t pid,int *status,int options) 

说明:

1. 从本质上讲,系统调用waitpid和wait的作用是完全相同的。
	wait就是经过包装的waitpid
2. 但waitpid多出了两个可由用户控制的参数pid和options,从而为我们编程提供了另一种更灵活的方式。

参数:

1. pid
	pid>0时,只等待进程ID等于pid的子进程,其他不管
	pid=-1时,等待任何一个子进程退出,没有任何限制
	pid=0时,等待同一个进程组中的任何子进程
	pid<-1时,等待一个指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值
2. status 一般置0
	参数status用来保存被收集进程退出时的一些状态,它是一个指向int类型的指针。
	但如果我们对这个子进程是如何死掉的毫不在意,只想把这个僵尸进程消灭掉,
	(事实上绝大多数情况下,我们都会这样想),我们就可以设定这个参数为NULL
3. options: options提供了一些额外的选项来控制waitpid
	目前在Linux中只支持 WNOHANG 和 WUNTRACED 两个选项

   可以通过用常量WNOHANG和WUNTRACED的不同组合来设置options,修改默认行为:
	1. WNOHANG :如果没有等待集合中的任何子进程终止,那么就立即返回(返回值为0)0
	2. WUNTRACED :挂起调用进程的执行,直到等待集合中的一个进程变成终止的或者被暂停。
			返回的PID为导致返回的终止或暂停了进程的PID。
	3. WNOHANG | WUNTRACED:立即返回,如果没有等待集合中的任何子进程停止或终上,
			那么返回值为0,或者返回值等于那个被停止或者终止子进程的PID

返回值:

1. 成功返回子进程pid
2. 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0
3. 错误,返回-1

wait 和 waitpid 详解及代码示例
参考:https://blog.csdn.net/lqy971966/article/details/110818165

2.6 让进程休眠(sleep pause)

unsigned int sleep(unsigned int secs)
参数:秒
返回:还要休眠的秒数
sleep就是阻塞式等待 线程中使用就卡死了

sleep返到0(如果请求的时间量己经到了),或者返回剩下的要休眠的秒数。
后一种情况是可能的,例如当sleep函数被一个信号中断过早返回时。

我们发現很有用的另一个函数是pause函数,该数让调用函数休眠,直到该进程收到一个信号为止。

int pause(void)

2.6.1 sleep单位, linux 秒,windows 毫秒

Windows:
Sleep(1); //停留1毫秒

Linux:
sleep(1); //秒

2.7 加载并运行程序

2.7.1 execve,参数列表和环境变量列表

execve函数在当前进程的上下文中加载并允许一个新程序。

#include<unistd.h>
int execve(char *filename, char *argv[], char *envp);

execve函数加载并运行可执行目标文件filename,且带参数列表argv和环境变量列表envp。
只有当出现错误时,例如不能发现filename,execve才会返回到调用程序。
所以,不像fork会一次调用返回两次,execve调用一次并从不返回。
如图8.17所示,参数列表是用数据结构表示的。
argv变量指向一个以null结尾的指针数组,其中每个指针都指向一个参数串。按照习俗,argv[0]是可执行目标文件的名字。
图8.17
在这里插入图片描述

环境变量的列表是由一个类似的数据结构表示的,如图8.18所示。
envp变量指向一个以null结尾的指针数组,其中每个指针指向一个环境变量串,其中每个串都是形如"NAME=VALUE"的名字一值对。

int main(int argc, char *argv[], char *envp[]);

图8.18
在这里插入图片描述

2.7.2 用户栈的组织结构

当main开始在一个Linux系统上执行时,用户栈有如图8.19所示的组织。
让我们从栈底(高地址)往栈顶(低地址),依次看一看。

1. 首先是参数是环境字符串,它们都是连续地存放在栈中的,一个接一个,没有分隔。
2. 紧随其后,在栈的更上层里,是以null结尾的指针数组,其中每个指针都指向栈中的一个环境变量串。
   全局变量environ指向这些指针中的第一个envp[0]。
3. 紧随环境变量数组其后的是以null结尾的argv[]数组,其中每个元素都指向栈中一个参数串。
4. 在栈的顶部是main函数的3个参数:
	envp它指向envp[]数组;
	argv它指向argv[]数组,
	argc,它给出argv[]中非空指针的数量。

图8.19
在这里插入图片描述

2.7.3 程序与进程

这是一个适当的地方,停下来,确认一下你是否理解了程序和进程之间的区别。

1. 程序是代码和数据的集合;
   程序可以作为目标模块存在于磁盘上,或者作为段存在于地址空间中。

2. 进程是执行中程序的一个特殊实例
   程序总是运行在某个进程的上下文中。

如果你想要理解fork和execve函数,理解这个差异是很重要的。
fork函数在新的子进程中运行相同的程序,新的子进程是父进程的一个复制品。
execve函数在当前进裎的上下文中加载并运行一个新的程序,它会覆益当前迸程的地址空间,但并没有创建一个新进租。新的程序仍然有相同的PID,并且继承了调用execve函数时打开的所有文件描迷符。

2.7.4 execve 例子

参考:
Linux exec 系列函数:execl execv等
https://blog.csdn.net/lqy971966/article/details/110532621

linux system 和 execl 函数对比
https://blog.csdn.net/lqy971966/article/details/110532718

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值