上次介绍了环境变量:Linux:进程概念(四.main函数的参数、环境变量及其相关操作)
牵扯到内存,肯定有事这张图啦。这次我们写段代码验证一下
#include <stdio.h>
#include <unistd.h>
int a;
int init_a = 0;
int main()
{
printf("code:%p\n", main);//代码
printf("uninit data:%p\n", &a);//未初始化变量
printf("init data:%p\n", &init_a);//初始化变量
char* arr = (char*)malloc(10);
printf("heap:%p\n", arr);//堆
printf("stack:%p\n", &arr);//栈
return 0;
}
- 字符串常量通常存储在高地址,其地址是固定的,不可更改。
- 字符串的下标从0到n一直在增加,即字符串中的每个字符在内存中是连续存储的。
- `main` 函数地址通常是程序中最底部的地址,因为它是程序的入口点。
- 初始化数据(如全局变量、静态变量)存储在比 `main` 函数地址高的位置,因为它们在程序启动时需要被初始化。
- 未初始化数据(如全局未初始化变量、静态变量)存储在比初始化数据更高的位置,因为它们在程序启动时不需要被初始化。
- 堆区是用于动态内存分配的区域,在堆区中存储动态分配的内存。
- 堆区是向上增长的,即分配的内存地址逐渐增加,地址比未初始化数据高。
- 栈区用于存储函数的参数值、局部变量和函数调用返回地址等信息。
- 栈区是向下增长的,即栈顶地址逐渐减小,整体比堆区的地址要高。
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if (id == 0)
{
int cnt = 0;
while (1)
{
printf("child, pid: %d, ppid: %d, g_val: %d, &g_val: %p\n", getpid(), getppid(), g_val, &g_val);
sleep(1);
cnt++;
if (cnt == 3)
{
g_val = 200;
printf("child change g_val: 100->200\n");
}
}
}
else
{
while (1)
{
printf("father, pid: %d, ppid: %d, g_val: %d, &g_val: %p\n", getpid(), getppid(), g_val, &g_val);
sleep(1);
}
}
return 0;
}
我们都知道父子进程共享代码段,那么一开始二者访问的**
g_vla
**值和地址均相同 后面子进程中改变**g_val
**的值后,二者值不同,但是地址还是一样
进程地址空间是操作系统中一个重要的概念,它描述了一个进程在运行时所能访问的虚拟地址范围。每个进程都有自己独立的地址空间,使得多个进程可以同时运行而互相不干扰
地址空间是指一个进程可以使用的内存范围,通常由连续的地址组成。对于32位系统,进程地址空间通常是从0到4GB,这个范围内包含了代码段、数据段、堆、栈等部分,用于存放程序的指令、数据以及动态分配的内存(就是我们上面那个图) 每个进程都有自己独立的地址空间,使得进程间可以互相隔离,不会相互干扰。在这个地址空间内,操作系统会进行地址映射,将进程的虚拟地址映射到物理内存上,以实现对内存的访问和管理。 当一个进程被创建时,操作系统会为该进程分配一块内存空间,用来存放进程的地址空间。这个地址空间是虚拟的,因为它并不直接对应物理内存中的连续空间,而是通过页表和页表项来映射到物理内存中的不同位置。
页表(Page Table)是操作系统中用于管理虚拟内存的重要数据结构之一。它将进程的虚拟地址映射到物理内存中的实际地址,实现了虚拟内存的地址转换和管理
代码和数据实际上是存储在物理内存中的,而进程空间(或称为虚拟地址空间)里存储的是代码和数据的虚拟地址。这些虚拟地址通过页表等机制映射到物理内存中的实际地址。
每个进程都有自己的虚拟地址空间,这个空间是逻辑上连续的,但并不一定在物理内存中连续。操作系统负责维护页表,将虚拟地址转换为物理地址,从而实现进程对内存的访问。
操作系统肯定也要对进程地址空间进行管理,那就说明也需要:先描述再组织 进程地址空间是数据结构,具体到进程中就是特定数据结构的对象。需要注意的是:这个结构体里不保存代码和数据
地址空间和页表的结合是操作系统中实现虚拟内存管理的关键机制,它们的存在有助于解耦进程管理和内存管理,并提供了保护内存安全的重要手段。
我们之前已经讲了在代码里可以使用fork()函数来。创建子进程规则是:子进程与父进程共享代码,写时拷贝 进程调用fork,当控制转移到内核中的fork代码后,内核做:
当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以开始它们自己的旅程
通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本
Linux系统中,当使用fork()
系统调用创建子进程时,子进程会继承父进程的地址空间的一个副本。但实际上,在fork()
之后到子进程开始写数据之前,父进程和子进程所共享的是同一个物理内存页面。只有当其中一个进程尝试修改写入时,操作系统才会进行页面复制,确保每个进程都有自己的数据副本,从而避免了不必要的内存复制开销
页表除了有一系列对应的虚拟地址和物理地址以外,还有一列代表权限(针对物理地址的内容的权限)
具体来说,权限字段通常包含以下几种权限:
除了读和写权限外,页表的权限字段还可能包含其他类型的权限,例如执行权限(x),它决定了进程是否可以在该页面上执行代码。在某些系统中,还可能存在特殊的权限字段,如用于控制页面共享、缓存策略等的字段。
所以上面写时拷贝的过程里:可以看到在修改内容之前,数据段里的权限也都是只读,这不对吧?(因为,全局变量我们是可以修改的啊)这是在创建子进程后,数据段的页表映射权限由rw权限变为r 为什么要改啊:改后,如果我们尝试写入,会发生错误,这时操作系统就会来完成写入拷贝,又发现你是数据段的本该可以写入,就又把需要写入的进程对应的页表映射由r权限改为rw了
main
函数的返回值通常被称为进程退出码或返回状态码。在C、C++等编程语言中,main
函数是程序的入口点,当程序执行完毕后,main
函数会返回一个整数值给操作系统,这个整数值就是进程退出码。操作系统会根据这个退出码来判断程序是正常结束还是出现了某种错误。 我们自己写main
函数时,总是写一个return 0
Linux系统中,你可以使用**
echo $?
**命令来查看上一个执行的命令或进程的退出码
但是光看一个数字,我们怎么能知道错误的原因呢? 这就需要把错误码转换为错误描述 错误码就是函数的
strerror()
函数是一个C库函数,用于将错误代码转换为对应的错误信息字符串。它接受一个整数参数errno
,返回一个指向错误信息字符串的指针。strerror函数的在头文件string.h
中,
errno
是一个全局变量,用于在C语言中表示发生错误时的错误码。当函数或系统调用发生错误时,errno
会被设置为相应的错误码,以便程序可以根据错误码进行适当的错误处理。error是最近一次函数进行调用的返回值
char *strerror(int errnum);
其中,errnum
参数是一个整数,代表特定的错误码。strerror函数会根据错误码在系统的错误码表中查找对应的错误信息,并将其作为字符串返回。
进程出现异常说明进程收到了异常信号,每种信号都有自己的编号(宏编号),而不同的信号编号能表明异常的原因
kill -l
命令在 Unix 和 Linux 系统中用于列出所有可用的信号。执行这个命令将显示系统支持的所有信号的列表以及它们的编号。这对于了解不同信号的含义和用途非常有用,特别是在处理进程和进程间通信时。
下面是一个 kill -l
命令的典型输出示例:
$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL
5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE
9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2
13) SIGPIPE 14) SIGALRM 15) SIGTERM 16) SIGSTKFLT
17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU
25) SIGXFSZ 26) SIGVTALRM 27) SIGPROF 28) SIGWINCH
29) SIGIO 30) SIGPWR 31) SIGSYS 34) SIGRTMIN
35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3 ...
...
63) SIGRTMAX-1 64) SIGRTMAX
一些常见的信号及其用途包括:
SIGTERM
:请求进程终止。进程可以捕获这个信号并清理资源后正常退出。SIGINT
:通常由用户按下 Ctrl+C 产生,用于中断前台进程。SIGKILL
:强制终止进程,不能被进程捕获或忽略。SIGHUP
:当控制终端(controlling terminal)被关闭时发送给进程,常用于让进程重新读取配置文件。正常的从main()
函数返回
调用exit()
函数
#include <unistd.h>
void exit(int status);
参数**status
**定义了进程的终止状态,也就是程序的退出码用于表示程序的执行状态,并帮助调用程序理解程序结束的原因
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
void Print()
{
printf("Now it's calling the Print function\n");
exit(10);
}
int main()
{
while(1)
{
printf("I'm a process:%d\n",getpid());
sleep(3);
Print();
}
return 0;
}
调用_exit()
函数
与exit()函数的不同:
使用ctrl + c,能使异常信号终止
Linux系统中,任何进程最终执行完毕后都会返回一个状态码,这个状态码通常被称为“退出码”或“返回码”(exit code)。这个退出码是一个整数,用于表示进程执行的结果或状态。根据惯例,退出码0通常表示成功,而非零值表示出现了某种错误。
Linux的上下文中,我们通常讨论的是“信号”(signal),这些信号用于在进程之间传递信息或通知进程发生了某种事件(如中断、终止等)
当进程创建和进程终止时,操作系统会执行一系列的操作来确保系统的稳定性和资源管理的有效性。
kill -9
这样的强制终止命令也无法直接“杀死”它。因为僵尸进程本身已经终止,只是其退出状态还没有被父进程读取wait()
或waitpid()
系统调用(进行进程等待)。这些调用会阻塞父进程,直到有子进程退出,并返回已退出子进程的PID和退出状态wait
方法在Linux 编程中是一个重要的系统调用,它主要用于监视先前启动的进程,并根据被等待的进程的运行结果返回相应的 Exit 状态。在父进程中,wait
方法常被用来回收子进程的资源并获取子进程的退出信息,从而避免产生僵尸进程。
wait
函数允许父进程等待其子进程结束,并可以获取子进程的退出状态。在C语言中的用法和参数:
函数原型
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
参数status
:这是一个指向整数的指针,用于存储子进程的退出状态。如果父进程不关心子进程的退出状态,可以将这个参数设为 NULL
。
返回值
-1
,并设置全局变量 errno
以指示错误原因。#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();//创建子进程
if (id == 0)
{
//这里面是子进程
int count = 5;
while (count--)
{
printf("child is running. pid:%d , ppid:%d\n", getpid(), getppid());
sleep(1);
//这里循环5秒
}
printf("子进程将退出,马上就变成僵尸进程\n");
exit(0);//子进程退出了
}
//这里是父进程
printf("父进程休眠\n");
sleep(10);
printf("父进程开始回收了\n");
pid_t rid = wait(NULL);//让父进程进程阻塞等待
if (rid > 0)
{
printf("wait successfully, rid:%d\n", rid);
}
printf("父进程回收了\n");
sleep(5);
return 0;
}
代码一共15秒
waitpid
是 Unix 和 Linux 系统编程中用于等待子进程结束并获取其状态的系统调用。它的原型如下:
pid_t waitpid(pid_t pid, int *status, int options);
返回值
options
参数中设置了 WNOHANG
,并且没有已退出的子进程可收集,则 waitpid
返回0。参数
- Pid=-1,等待任一个子进程。与wait等效。
- Pid>0.等待其进程ID与pid相等的子进程
NULL
。- `WIFEXITED(status)`:宏函数,如果子进程正常退出,返回非零值;否则返回0。
- `WEXITSTATUS(status)`:宏函数,如果 `WIFEXITED(status)` 为真,则返回子进程的退出码。(后面就能理解这两个用处)options:这是一个位掩码,用于修改
- `WNOHANG`:如果指定了此选项,`waitpid` 将不会阻塞,而是立即返回(父进程不会等待子进程了)。如果指定的子进程没有结束,则 `waitpid` 返回0;如果子进程已结束,则返回子进程的ID。
- 传递 `0` 作为 `options` 参数时,你实际上是在告诉 `waitpid`使用最传统的阻塞方式等待子进程终止,并且只关心那些已经终止的子进程
如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。 如果不存在该子进程,则立即出错返回。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
// child
int cnt = 5;
while(cnt)
{
printf("Child is running, pid: %d, ppid: %d\n", getpid(), getppid());
sleep(1);
cnt--;
}
exit(1);
}
int status = 0;
pid_t rid = waitpid(id, &status, 0); // 阻塞等待
if(rid > 0)
{
printf("wait successfully, rid: %d, status: %d\n", rid, status);
}
return 0;
}
那我们怎么直接获得退出码和信号编号呢?
我们能自己针对status进行位运算
使用上面的WIFEXITED(status)、WEXITSTATUS(status)
int main()
{
pid_t id = fork();
if (id == 0)
{
int cnt = 5;
while (cnt)
{
printf("Child is running, pid: %d, ppid: %d\n", getpid(), getppid());
sleep(1);
cnt--;
}
exit(1);
}
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if (rid > 0)
{
if (WIFEXITED(status))
{
printf("wait successfully, rid: %d, status: %d\n", rid, status);
printf("wait successfully, rid: %d, status: %d, exit code: %d\n",
rid, status,WEXITSTATUS(status));
}
else
{
printf("wait wrongly\n");
}
}
return 0;
}
阻塞等待(wait()与waitpid( , , 0)):
非阻塞等待:
int main()
{
pid_t id = fork();
if (id == 0)
{
int cnt = 5;
while (cnt)
{
printf("Child is running, pid: %d, ppid: %d\n", getpid(), getppid());
sleep(1);
cnt--;
}
exit(1);
}
int status = 0;
pid_t rid = waitpid(id, &status, WNOHANG);
while (1)
{
if (rid > 0)
{
//子进程结束了
printf("wait successfully, rid: %d, status: %d, exit code: %d\n",
rid, status, WEXITSTATUS(status));
break;
}
else if (rid = 0)
{
//子进程还没结束
//这里能写希望在子进程没结束期间希望父进程干什么
}
else
{
//到这里就说明出错了
perror("waitpid");
break;
}
}
return 0;
}
今天的内容也是不少了,累死了。感谢大家支持!!!