LInux --- 进程的创建与终止

前言:
在上一篇的文章当中我们了解到进程地址空间,既然我们的理论基础有了,那我们今天来进一步了解一下进程的控制

进程的控制是

进程控制在本文中主要分为四部分
{
  1.   进程的创建
  2.   进程的等待
  3.   进程的替换
  4.   进程的终止
}


一、进程创建

    1.fork函数


        a.fork()`函数用于从一个已存在的进程中创建一个新进程(子进程),子进程的代码与数据拷贝父进程,但各自拥有独立的执行流。
        b.写时拷贝:默认情况下,父子进程的代码和数据是共享的,只有在写入时才会进行写时拷贝,创建各自的副本。

​
​
写时拷贝

写时拷贝是一种资源分配的策略。

当一个进程创建子进程时,在很多情况下,并不会立即为子进程复制一份父进程的资源(如内存空间、文件等),而是让父子进程先共享同一份资源。
只有当其中一个进程尝试对共享资源进行写操作时,操作系统才会为执行写操作的进程复制一份该资源的副本,这样做的目的是避免不必要的资源复制,从而提高效率。

在进程创建中的应用及原理
创建进程时:以fork()函数创建子进程为例,调用fork()后,子进程和父进程会共享物理内存页,它们的页表项指向相同的物理内存地址。也就是说,在创建的瞬间,父子进程看起来好像拥有各自独立的内存空间,但实际上它们在物理层面是共享同一块内存的。

写入操作:
当父进程或子进程尝试修改某个内存页的数据时,操作系统会检测到这个写操作
(检测机制大概是,操作系统会在“fork”子进程的时候,将父进程对自己文件数据的权限改为只读,而子进程本身对该文件就是只读,一旦有进程要修改就发生写时拷贝,将父子进程的权限修改回来)
然后会为执行写操作的进程创建一个该内存页的副本,将原来共享的内存页中的数据复制到新的副本中,接着让执行写操作的进程的页表项指向这个新副本,而另一个进程仍然使用原来的内存页。这样,两个进程就拥有了各自独立的数据副本,之后的写操作就可以独立进行,不会相互影响。

优点
减少内存使用:避免了在创建子进程时立即复制大量数据,减少了系统的内存开销,尤其是在创建大量子进程或者复制大内存块时,这种优化效果更加明显。
提高性能:减少了不必要的数据复制操作,加快了进程创建的速度,降低了进程创建的消耗。

​

​

2.fork常规用法


        a.多进程执行:父进程可以创建多个子进程来并行执行不同的任务(在同一段代码区)。
        b.程序替换:子进程可以通过`exec`系列函数替换当前执行的程序(这一点在后面,会有更详细的解释)。

3. 返回值:(作为进行进程区分的依据)


         a.父进程返回子进程的PID。
         b.子进程返回0。
         c.失败返回-1(原因:系统进程数超限、内存不足)。
         d.执行顺序:由调度器决定,父子进程可能交替运行。(主要取决于调度器的算法)
 

代码示例

  2 #include <stdio.h>
  3 #include <unistd.h>
  4 #include <sys/types.h>
  5 
  6 int main() {
  7     int num = 10;
  8     pid_t pid = fork();
  9     if (pid < 0) {
 10         perror("fork failed");
 11         return 1;
 12     } else if (pid == 0) {
 13         // 子进程
 14         //在这一段内,子进程可以完成不同的任务
 15         printf("Child process: num = %d, address = %p\n", num, &num);
 16         printf("Child process: num = %d, address = %p\n", num, &num);
 17         printf("Child process: num = %d, address = %p\n", num, &num);
 18         printf("Child process: num = %d, address = %p\n", num, &num);
 19         printf("Child process: num = %d, address = %p\n", num, &num);
 20     }
 21      else {
 22         // 父进程
 23         sleep(1); // 确保子进程先执行写操作
 24         printf("Parent process: num = %d, address = %p\n", num, &num);
 25         printf("Parent process: num = %d, address = %p\n", num, &num);
 26         printf("Parent process: num = %d, address = %p\n", num, &num);                                                                                                                                       
 27         printf("Parent process: num = %d, address = %p\n", num, &num);
 28     }
 29     return 0;
 30 }

结果

代码解析:这就是一个最简单的利用fork函数,创建子进程完成某一个任务的代码,当然也有同学可能对一个变量“id”却有两个值感到困惑,关于这一点我的上一篇文章对此有所解释。

直达链接:进程地址空间

   4.fork调用失败的原因


         a.系统限制:系统中进程数过多或实际用户进程数超过限制会导致`fork`失败。
           或者操作系统的内存不足也有可能导致。

关于操作系统的最大进程数,在不同的系统下是有所不同,这里简单介绍一下Linux的查看方法

# ulimit -a | grep processes

在服务器上,运行这行命令即可,这是本人测试的结果,对此有兴趣的可以查阅相关资料。 

max user processes              (-u) 6943

  5.注意事项:

         a.需根据”fork“函数返回值区分父子进程逻辑

         b.避免频繁fork导致资源耗尽。


二.进程的终止

2.1 概念 

1. 进程终止的概念
       进程终止是指操作系统停止一个正在运行的进程,并释放该进程所占用的系统资源(如内存、文件描述符等)的过程。
  衍生概念:
         a.进程返回值:`main`函数的返回值可以被父进程获取,用于判断子进程的执行情况。
           
   如何查看退出码:通过`echo $?`可以查看上一个进程的退出码,用于判断子进程的退出状态。



注:退出码与返回值是一对本质很相近的概念,只不过返回值是从语言的角度衡量程序的结果,而退出码是从操作系统的角度衡量进程的退出结果。 

2. 退出结果

 当然,在这一过程的涉及到的退出码与返回值是可以自己设置的,不过一般情况下,不建议个人单独设置,使用系统或者库自带的就可以了。

现在我带大家,一起查看一下Linux系统自带的返回值

本次我们要运用到“strerror”接口

这是man手册上的官方文档

 代码样例

结果

可以看到Linux定义了多达133种退出码,0为进程运行结果成功!

0:Success
1:Operation not permitted
2:No such file or directory
3:No such process
4:Interrupted system call
5:Input/output error
6:No such device or address
7:Argument list too long
8:Exec format error
9:Bad file descriptor
10:No child processes
11:Resource temporarily unavailable
12:Cannot allocate memory
13:Permission denied
14:Bad address
15:Block device required
16:Device or resource busy
17:File exists
18:Invalid cross-device link
19:No such device
20:Not a directory
21:Is a directory
22:Invalid argument
23:Too many open files in system
24:Too many open files
25:Inappropriate ioctl for device
26:Text file busy
27:File too large
28:No space left on device
29:Illegal seek
30:Read-only file system
31:Too many links
32:Broken pipe
33:Numerical argument out of domain
34:Numerical result out of range
35:Resource deadlock avoided
36:File name too long
37:No locks available
38:Function not implemented
39:Directory not empty
40:Too many levels of symbolic links
41:Unknown error 41
42:No message of desired type
43:Identifier removed
44:Channel number out of range
45:Level 2 not synchronized
46:Level 3 halted
47:Level 3 reset
48:Link number out of range
49:Protocol driver not attached
50:No CSI structure available
51:Level 2 halted
52:Invalid exchange
53:Invalid request descriptor
54:Exchange full
55:No anode
56:Invalid request code
57:Invalid slot
58:Unknown error 58
59:Bad font file format
60:Device not a stream
61:No data available
62:Timer expired
63:Out of streams resources
64:Machine is not on the network
65:Package not installed
66:Object is remote
67:Link has been severed
68:Advertise error
69:Srmount error
70:Communication error on send
71:Protocol error
72:Multihop attempted
73:RFS specific error
74:Bad message
75:Value too large for defined data type
76:Name not unique on network
77:File descriptor in bad state
78:Remote address changed
79:Can not access a needed shared library
80:Accessing a corrupted shared library
81:.lib section in a.out corrupted
82:Attempting to link in too many shared libraries
83:Cannot exec a shared library directly
84:Invalid or incomplete multibyte or wide character
85:Interrupted system call should be restarted
86:Streams pipe error
87:Too many users
88:Socket operation on non-socket
89:Destination address required
90:Message too long
91:Protocol wrong type for socket
92:Protocol not available
93:Protocol not supported
94:Socket type not supported
95:Operation not supported
96:Protocol family not supported
97:Address family not supported by protocol
98:Address already in use
99:Cannot assign requested address
100:Network is down
101:Network is unreachable
102:Network dropped connection on reset
103:Software caused connection abort
104:Connection reset by peer
105:No buffer space available
106:Transport endpoint is already connected
107:Transport endpoint is not connected
108:Cannot send after transport endpoint shutdown
109:Too many references: cannot splice
110:Connection timed out
111:Connection refused
112:Host is down
113:No route to host
114:Operation already in progress
115:Operation now in progress
116:Stale file handle
117:Structure needs cleaning
118:Not a XENIX named type file
119:No XENIX semaphores available
120:Is a named type file
121:Remote I/O error
122:Disk quota exceeded
123:No medium found
124:Wrong medium type
125:Operation canceled
126:Required key not available
127:Key has expired
128:Key has been revoked
129:Key was rejected by service
130:Owner died
131:State not recoverable
132:Operation not possible due to RF-kill
133:Memory page has hardware error
134:Unknown error 134
135:Unknown error 135
136:Unknown error 136
137:Unknown error 137
138:Unknown error 138
139:Unknown error 139
140:Unknown error 140
141:Unknown error 141
142:Unknown error 142
143:Unknown error 143
144:Unknown error 144
145:Unknown error 145
146:Unknown error 146
147:Unknown error 147
148:Unknown error 148
149:Unknown error 149
提出一个问题:一个函数返回值是否可以不为0

所以,我们就可以得出结论,main函数的返回值在大部分的情况下等于它运行后进程的退出码,反映了它自身的退出状态! 所以,main函数的返回值我们不能乱写,以后我们写大项目的时候,我们就可以从进程的退出码中推断错误!

方便简单的查看退出码的方式为:利用一个指令"echo $?"

解释一下,第一个$?返回的是上面一个函数的返回值,同时也揭露了返回值不可以任意设。

{

        补充:返回值出现255的情况,可能是以下几种情况        

  • 进程异常终止:在一些系统或程序约定中,较大的非零值往往表示异常。255 超出了常见的简单错误代码范围,可能意味着进程遇到严重问题,如内存耗尽、非法指令等,导致异常结束。
  • 父进程接收限制:父进程获取子进程返回值时,有的系统仅取低 8 位,即返回值范围实际是 0 - 255。255 是这个范围内的最大值,代表一种特殊的异常或未定义状态。
  • 自定义错误标识:开发者也可能将 255 定义为特定的错误或终止标识,用于在程序中表示某种严重或自定义的异常情况。

在我们的代码中应该是第二种情况。

}

而第二个$?则是返回上一个$?的返回值.

3. 常见终止进程的方法


   a.正常终止:main函数返回、调用`exit`函数、调用`_exit`函数(值得注意的是return只代表函数结束,但exit系列函数代表进程结束,大家注意区分)。
   b.异常退出:通过输入`ctrl + c`或信号终止(Kill系列)。

对于crtl+c方式,最常见的情况就是在命令行当中,对当前进程进行终止。

代码示例
  1 #include<stdio.h>
  2 #include<unistd.h>
  3 
  4 int main()
  5 {
  6   while(1)
  7   {
  8     printf("这是一段错误代码,做演示\n");                                                                                                                                                                    
  9   }                                                                                                                                      
 10                                                                                                                                          
 11   return 0;                                                                                                                              
 12 } 

结果

当然这仅是一段示例代码,在命令行当中遇到某些进程无法正常退出,都可以使用该方法强杀进程。
 

异常退出:

异常退出的方法,最常见的是操作系统发出信号强制杀死了进程,关于信号我会在以后的博客当中,有专门的篇幅去介绍

在这里仅是介绍一下,kill系列的命令与信号

在Linux系统中,kill命令用于向运行中的进程发送信号,默认发送的信号是终止信号,会请求进程退出。当然kill(杀)可能会引起误解,实际上发送的信号可能与杀死进程无关。(这主要看的是kill命令后的参数)

常见的kill 命令

1  kill PID 
2  kill -9 PID
//前者为请求目标进程退出,后者为强制杀死目标进程。

注:如果有对PID不了解的同学,可以查询相关资料,后面我也会专门出一篇博客,介绍进程的几个基本属性。

在这里,我们只需明白操作系统是通过每个进程特定的PID查找对应的进程的即可(类似于每个人的身份证号,用于查找进程)

当然我们在Linux的命令行下,也可以通过

1 kill -l

找出kill系列的信号

结果 

 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
38) SIGRTMIN+4	39) SIGRTMIN+5	40) SIGRTMIN+6	41) SIGRTMIN+7	42) SIGRTMIN+8
43) SIGRTMIN+9	44) SIGRTMIN+10	45) SIGRTMIN+11	46) SIGRTMIN+12	47) SIGRTMIN+13
48) SIGRTMIN+14	49) SIGRTMIN+15	50) SIGRTMAX-14	51) SIGRTMAX-13	52) SIGRTMAX-12
53) SIGRTMAX-11	54) SIGRTMAX-10	55) SIGRTMAX-9	56) SIGRTMAX-8	57) SIGRTMAX-7
58) SIGRTMAX-6	59) SIGRTMAX-5	60) SIGRTMAX-4	61) SIGRTMAX-3	62) SIGRTMAX-2
63) SIGRTMAX-1	64) SIGRTMAX	

 其他的退出方法还有我们熟知的return,与我们下面重点介绍的exit系列接口 


我们先观察一副图,或许会对后面的问题有所帮助。

4. exit函数和_exit接口(_Exit接口)


    a._exit`函数:系统调用接口,终止进程时不刷新缓冲区。
    b. exit`函数:库函数,终止进程时刷新缓冲区,并执行清理工作。(exit就是C语言在库的方面封装了_exit接口)

同样关于它们的信息,可以直接在man函数手册上查看

exit函数

 

_exit接口(_Exit接口是与_exit等价的,在这里只介绍_exit)

​ 

1 通过文档我们可以发现,这两个接口没有返回值
2 但是有一个奇怪的参数,这个参数有啥用呢??

3 很简单,在我们前面的讨论中,发现我们对一个函数的返回值是有天然的要求的,
4 如果进程异常退出了,返回值就变得没意义了,
5 但我们仍需知道操作系统为啥要杀死我们的进程,
6 而这个问题的答案就隐藏在这个参数当中,
7 简单来说:这是一个状态码,用于反映进程的退出状态
(父进程可以得到它,关于如何得到在进程等待部分我们会讲解 )。

`_exit`是系统调用,`exit`是库函数,`exit`在调用`_exit`前会执行一些清理操作。

在这里的会执行一些清理操作又是什么意思呢???同样我们通过代码去解释它。

代码

  1 #include<stdio.h>
    2 #include<unistd.h>
    3 
    4 void test1()
    5 {
    6 
    7   char* str="这是一段0.6用来验证exit与_exit不同的代码\n ";
    8   int cnt=5;
    9   while(cnt--)
   10   {
   11     printf("%d:%s",cnt,str);
   12   }
   13   //exit(0);
          _exit(0);
   14 }                                                                                                                                                                                                          
   15 
   16 int main()
   17 {
   18   //hello 0.6
   19   test1();
   20   return 0;                                                                                              
   21 } 

结果一:

exit的结果

_exit的结果

看起来,两者并没有啥不同,但是我们修改一下代码呢?

新代码

    1 #include<stdio.h>
    2 #include<unistd.h>
    3 
    4 void test1()
    5 {
    6   //这是新代码,唯一与旧代码不同的地方,str删除了一个“\n” ,
        //导致不会数据会先刷新
    7   char* str="这是一段0.6用来验证exit与_exit不同的代码";
    8   int cnt=5;
    9   while(cnt--)
   10   {   
   11     printf("%d:%s",cnt,str);
   12   }
   13   _exit(0);
   14 }                                                                                                                                                                                                          
   15 
   16 int main()
   17 {
   18   //hello 0.6
   19   test1();
   20   return 0;                                                                                              
   21 } 

结果二

_exit的结果

exit的结果

结果很明显揭露了,我们在使用_exit接口时,进程是直接终止的,它并没有将数据从缓冲区当中刷新出来。这就是它们最大的不同。

解释:

        还记得我们在导语看到的那张图吗,现在我们对_exit是一个大箭头指向内核的画法,有所猜测了吧。是的,_exit是操作系统为我们提供的一个纯粹的退出接口,而我们常用的exit()接口,则是C语言的库中为我们封装的函数接口,它的底层正是_exit。

          同时我们借助这个接口,也对缓冲区的分布有所猜测,显然缓冲区不在内核级的内存当中,为啥?因为操作系统不会做效率低下,浪费空间的事情

如果缓冲区在内核级内存当中,进程写入了数据但是我们的操作系统却没有利用到它,这不是一个大的浪费吗?当然关于缓冲区的具体分析还是要等到基础IO部分再进行详细的探讨。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值