• 【linux】进程控制——1


    目录

    fork()

    1、fork创建子进程,操作系统都做了什么?

    2、细节

    3、fork常规用法

    4、fork失败的原因

    5、进程创建

    6、进程终止

    7、进程终止常见的方式

    8、main函数返回值的意义?

    9、用代码如何退出一个进程?

    10、僵尸进程的回收机制

    11、进程等待的必要性

    12、父进程通过wait/waitpid可以拿到子进程的退出结果,为什么要用wait/waitpid?直接使用全局变量不行吗?

    13、既然进程是独立的,进程退出码,不也是子进程的数据吗?父进程凭什么能够拿到?

    总结


    fork()

    1、fork创建子进程,操作系统都做了什么?

    我们知道进程 = 内核数据结构 + 进程代码和数据

    操作系统会分配新的内存块和内核数据结构给子进程,将父进程部分数据结构内容拷贝至子进程,添加子进程到系统进程列表中,fork返回,开始调度器调度(这时fork并没有执行完)

    2、细节

    创建子进程,给子进程分配对应的内核结构必须是子进程私有的,子进程也要有代码和数据,可是一般而言,子进程没有加载的过程,也就是说子进程没有自己的代码和数据,子进程只能使用父进程的代码和数据。

    对于父子进程的代码:都是不可写的,只能读取,所以可以父子共享

    数据:不需要将不会被访问的或者只会读取的数据拷贝一份

    拷贝:将来的父进程或者是子进程写入的数据

    一般而言即便是操作系统也无法提前知道哪些空间被写入,即使提前拷贝了,并不会立马使用,所以就会有写时拷贝技术,将父子进程分离,这是高效使用内存的表现。

    创建子进程就相当于浅拷贝,一份父进程给子进程页表指向相同的物理地址,当修改变量时使用深拷贝。fork之后父子进程代码共享是所有的代码都共享。


    我们的代码汇编之后,会有很多行代码,而且每行代码加载到内存之后,都有对应的地址。

    因为进程随时有可能被中断(可能并没有执行完),下次回来,还必须从之前的位置继续运行,就要求CPU必须随时记录程序执行的位置,所以CPU内有对应的寄存器数据用来记录当前进程的执行位置,EIP有的教程又会叫他PC,它是程序计数器,用来记录当前正在执行代码的下一行代码的地址。


    CPU实际上特别的傻,它只会做三件事,取指令,分析指令,执行指令

    当创建子进程时父进程的上下文数据也会给子进程,虽然父子进程各自调度,各自会修改EIP,但是已经不重要了,因为子进程已经认为自己的EIP起始值就是fork之后的代码。


    写时拷贝技术使父子进程彻底分离,保证了进程独立性

    写时拷贝的数据在父子进程中就不再是只读的了,两者相互分离互不干扰。

    3、fork常规用法

    1、父进程复制自己,使父子进程执行不同的代码段(子承父业)

    2、一个进程要执行不同的程序(父子进行不同的事情)

    4、fork失败的原因

    1、系统中有太多的进程

    2、用户的进程超过了限制(一个普通的用户创建的进程是有限的)

    5、进程创建

    创建task_struct进程地址空间mm_struct创建页表,构建映射关系,加载代码和数据

    6、进程终止

    释放进程申请的相关数据结构和对应的数据和代码

    7、进程终止常见的方式

    1、代码跑完,结果正确

    2、代码跑完,结果错误

    3、代码没有跑完,程序崩溃

    8、main函数返回值的意义?

    我们经常在C\C++程序中写return 0,但是return 0是什么意思?可以返回其它值吗?

    main的返回值是进程退出码

    可以使用echo $?来获取最近一个进程执行完毕的退出码

    如果退出码是0结果是正确,非0代表运行结果不正确

    1. 1 #include
    2. 2 #include
    3. 3
    4. 4 int main()
    5. 5 {
    6. 6 printf("Hello World\n");
    7. 7
    8. 8 return 0;
    9. 9 }

     因为我们的代码在main中return的是0,所以test的退出码就是0

    如果我们return 10  时看看会发生什么?

    它的退出码就是10


    main函数的返回值的意义是返回给上一级进程用来评判该进程执行的结果。

    1. 1 #include
    2. 2 #include
    3. 3
    4. 4 int sum(int top)
    5. 5 {
    6. 6 int ans = 0;
    7. 7 for(int i = 0; i <= top; i++)
    8. 8 {
    9. 9 ans += i;
    10. 10 }
    11. 11
    12. 12 return ans;
    13. 13 }
    14. 14
    15. 15 int main()
    16. 16 {
    17. 17 int ret = 0;
    18. 18 int ans = sum(100);
    19. 19 if(ans != 5050)
    20. 20 {
    21. 21 ret = 1;
    22. 22 }
    23. 23 return ret;
    24. 24 }

     我们知道从1一直加到100的和是5050,我们就可以通过返回值来判断结果是否正确。

    如果返回0就代表执行结果正确,返回1就代表结果不正确。

     它的退出码是0,代表结果正确,如果我们故意在for中去掉=


    非0值有无数个,不同的非0值可以表示不同的错误原因

    给我们的程序在运行结束之后,结果是不正确的,方便定位错误的原因细节。

    退出码只是针对代码跑完了,结果不正确的情况,注意一定是代码跑完了。


    我们也可以使用库函数strerror来打印具体的退出码的含义

    1. 1 #include
    2. 2 #include
    3. 3
    4. 4 int main()
    5. 5 {
    6. 6 for(int i = 0; i < 150; i++)
    7. 7 {
    8. 8 printf("%d: %s\n", i, strerror(i));
    9. 9 }
    10. 10
    11. 11 return 0;
    12. 12 }

    打印的结果是?

    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表示成功,非0表示错误

    既然我们已经知道了不同退出码的含义但是它们是正确的吗?

    我们使用ls命令去查看一个不存在的目录,它的退出码是2, 我们通过和上面的退出码结果比对

    2: No such file or directory答案是正确的。

    我们再看一个例子:

    我们使用9号信号杀掉一个根本不存在的进程,它的退出码是1 ,可是退出码1是1: Operation not permitted,与我们的预期不一样,这是因为不同的程序可以自定义退出码,但是退出码0一定是程序正常运行完,且答案正确。

    我们再做一个实验

    1. int* ptr = NULL;
    2. *ptr = 30;

    我们知道它会出现野指针问题,那么它的退出码是什么呢?


     

     我们一看是139,可是可是根本没有139号退出码,这是怎么回事?

    程序崩溃的时候,退出码无意义,一般而言退出码对应的return语句并没有被执行就被操作系统将进程杀掉。


    9、用代码如何退出一个进程?

    在main函数内,我们可以return + 进程退出码 来退出进程。

    而在其他函数中要使用exit函数来退出

    exit的参数status就是退出码。exit在任何地方调用都直接表示终止进程。

    1. 1 #include
    2. 2 #include
    3. 3
    4. 4 int sum(int top)
    5. 5 {
    6. 6 int ans = 0;
    7. 7 for(int i = 0; i <= top; i++)
    8. 8 {
    9. 9 ans += i;
    10. W> 10 }
    11. 11
    12. 12 exit(1);
    13. 13 }
    14. 14
    15. 15 int main()
    16. 16 {
    17. 17 int ans = sum(100);
    18. 18 return 0;
    19. 19 }

     

    退出码是1跟我们所想的一致。

    还有一个函数也可以退出进程,但是它们之间略有差别

     

    _exit,它也可以退出进程,我们将上面的exit全部换成_exit它们的现象是一样的

     

    结论是一样的,但是他们之间还是有不同的。

    1. 1 #include
    2. 2 #include
    3. 3 #include
    4. 4
    5. 5 int main()
    6. 6 {
    7. 7 printf("you can see me");
    8. 8
    9. 9 sleep(1);
    10. 10 exit(1);
    11. 11
    12. 12 return 0;
    13. 13 }

     这段代码比较简单,我们知道它会打印you can see me,然后光标闪1秒

    但是如果我们去掉\n会发生什么呢?

    发现好像跟加了\n就只有换行的区别

    但是如果我们将exit换成_exit时会发生什么呢?

     

    发现光标闪动1秒然后没有打印出语句。

    这是因为调用_exit程序是直接退出的,并没有刷新缓冲区


     

     这里也就引出来一个问题,printf——\n数据是保存在缓冲区中的,请问这个缓冲区在哪里?缓冲区是谁维护的?

    这个缓冲区一定不在操作系统内部,它是由C标准库所维护的

    10、僵尸进程的回收机制

    子进程退出,父进程不管子进程,子进程要处于僵尸状态,会导致内存泄漏

    父进程创建了子进程,是要让子进程办事,那么子进程把任务完成的怎么样?父进程需要关心,那么如何得知?

    子进程完成任务就三种情况:

    代码跑完,结果正确

    代码跑完,结果不正确

    代码没有跑完,程序崩溃

    父进程就通过进程等待的方式来获取子进程的相关信息。

    11、进程等待的必要性

    之前讲过,子进程退出,父进程如果不管不顾,就可能造成僵尸进程的问题,进而造成内存泄漏。
    另外,进程一旦变成僵尸状态,那就刀枪不入,杀人不眨眼kill -9 也无能为力,因为谁也没有办法
    杀死一个已经死去的进程。
    最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,
    或者是否正常退出。
    父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息

    接下来我们创建一个子进程,然后让子进程变成僵尸进程

    1. 1 #include
    2. 2 #include
    3. 3 #include
    4. 4
    5. 5 int main()
    6. 6 {
    7. 7 pid_t id = fork();
    8. 8
    9. 9 if(id < 0)
    10. 10 {
    11. 11 perror("fork");
    12. 12 exit(1);
    13. 13 }
    14. 14 else if(id == 0)
    15. 15 {
    16. 16 //子进程
    17. 17 int cnt = 5;
    18. 18 while(cnt--)
    19. 19 {
    20. 20 printf("I am child %d pid: %d ppid: %d \n", cnt, getpid(), getppid());
    21. 21 }
    22. 22
    23. 23 }
    24. 24 else
    25. 25 {
    26. 26 //父进程
    27. 27 while(1)
    28. 28 {
    29. 29
    30. 30 printf("I am father pid: %d ppid: %d \n", getpid(), getppid());
    31. 31 }
    32. 32 }
    33. 33
    34. 34 return 0;
    35. 35 }

    我们可以使用wait和waitpid来回收僵尸进程

     

    wait有一个参数status,它的含义与waitpid的status一样,在后面会讲解

    它会监测子进程,直到子进程的进程状态发生变化

    如果成功会返回子进程的pid,如果失败会返回-1

    1. 1 #include
    2. 2 #include
    3. 3 #include
    4. 4 #include
    5. 5 #include
    6. 6
    7. 7 int main()
    8. 8 {
    9. 9 pid_t id = fork();
    10. 10
    11. 11 if(id < 0)
    12. 12 {
    13. 13 perror("fork");
    14. 14 exit(1);
    15. 15 }
    16. 16 else if(id == 0)
    17. 17 {
    18. 18 //子进程
    19. 19 int cnt = 5;
    20. 20 while(cnt--)
    21. 21 {
    22. 22 printf("I am child %d pid: %d ppid: %d \n", cnt, getpid(), getppid());
    23. 23 sleep(1);
    24. 24 }
    25. 25
    26. 26 }
    27. 27 else
    28. 28 {
    29. 29 //父进程
    30. 30
    31. 31 pid_t ret = wait(NULL);
    32. 32 if(ret > 0)
    33. 33 {
    34. 34 printf("等待子进程成功,ret: %d",ret);
    35. 35 }
    36. 36 }
    37. 37 return 0;

     

    这样就回收了子进程这个僵尸进程。

    接下来我们再看一下waitpid这个函数

     

    waitpid这个函数它有三个参数

    第一个参数是pid 我们通常会给它赋两种值 -1 等待任意子进程与wait等效:pid > 0等待其进程id与pid相等的子进程

     第三个参数options默认为0,表示阻塞式等待

    第二个参数是status:输出型参数,标识子进程的退出结果


    status并不是按照整数整体来使用的

    按照比特位的方式,将32个比特位进行划分

    正常终止时,我们看退出状态也就是退出码,通过次低8位

    被信号所杀我们看低7位

    那么如何拿到退出码呢?

    (status  >> 8) & 0xFF就获取了退出码

    被信号所杀

    status & 0x7F 


    进程异常退出,或者崩溃本质是操作系统杀掉了你的进程

    操作系统通过发送信号的方式杀掉你的进程

    进程异常退出或者崩溃使用status的低7位来查看

    如果收到的信号是0,那么进程是正常的,这时退出码才会有意义


    程序异常不光是内部代码有问题,也可能是外力直接杀掉,程序可能跑完了,可能没有跑完,所以退出码是无意义的

    12、父进程通过wait/waitpid可以拿到子进程的退出结果,为什么要用wait/waitpid?直接使用全局变量不行吗?

    进程具有独立性,那么数据要发生写时拷贝,父进程无法拿到子进程全局变量的值

    13、既然进程是独立的,进程退出码,不也是子进程的数据吗?父进程凭什么能够拿到?

    wait/waitpid是有这个权力的,它通过系统调用,来获取子进程的task_struct结构体里面的exit_code和exit_signal来获取


    总结


    以上就是今天要讲的内容,本文仅仅简单介绍了fork及僵尸进程。

  • 相关阅读:
    Tuxera NTFS2023Mac读写ntfs磁盘工具
    关于Vue使用props传值遇到的一些问题
    Linux安装
    浅谈Maven以及在项目中的应用
    开篇-开启全新的.NET现代应用开发体验
    SQL 行转列
    基于 MATLAB 的电力系统动态分析研究【IEEE9、IEEE68系节点】
    28图图解Raft协议,so easy~~
    a标签_文件下载(download属性)
    (一). 贝叶斯滤波器
  • 原文地址:https://blog.csdn.net/m0_62179366/article/details/126879546