第五章:Linux进程控制

系列文章目录



前言

进程控制是操作系统的一个重要功能。


进程创建

fork函数初识

在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。

#include <unistd.h>
pid_t fork(void);
返回值:自进程中返回0,父进程返回子进程id,出错返回-1

进程调用fork,当控制转移到内核中的fork代码后,内核做:

分配新的内存块和内核数据结构给子进程
将父进程部分数据结构内容拷贝至子进程
添加子进程到系统进程列表
当中fork返回,开始调度器调度

在这里插入图片描述

fork写时拷贝

通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。具体见下图:

在这里插入图片描述

fork常规用法

  • 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。

  • 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。

fork调用失败的原因

  • 系统中有太多的进程

  • 实际用户的进程数超过了限制

进程终止

进程退出场景

  • 正常执行完了

    • 代码运行完毕,结果正确

    • 代码运行完毕,结果不正确

  • 代码异常终止

    • 进程因为某些原因,导致进程收到了来自操作系统的信号

进程的退出码

int main()
{
    return 0;//退出码
}

退出码:用来表示进程执行完后,结果是否是正确的;0就是正常,非0就是不正常

echo $?
//查看最新执行完的进程的退出码
//$?只会保存最近一次的进程的退出码

系统自带的退出码

strerror

将错误码给转化成错误信息

STRERROR(3)    Linux Programmer's Manual    STRERROR(3)                                              

NAME
       strerror, strerror_r - return string describing error number

SYNOPSIS
       #include <string.h>

       char *strerror(int errnum);

       int strerror_r(int errnum, char *buf, size_t buflen);
                   /* XSI-compliant */

       char *strerror_r(int errnum, char *buf, size_t buflen);
                   /* GNU-specific */

   Feature Test Macro Requirements for glibc (see feature_test_macros(7)):

       The XSI-compliant version of strerror_r() is provided if:
       (_POSIX_C_SOURCE >= 200112L || _XOPEN_SOURCE >= 600) && ! _GNU_SOURCE
       Otherwise, the GNU-specific version is provided.

DESCRIPTION
       The  strerror() function returns a pointer to a string that describes the error code passed in         
       the argument errnum, possibly using the LC_MESSAGES
       part of the current locale to select the appropriate language.  (For example, if errnum is              
       EINVAL, the returned description  will  "Invalid  argu‐
       ment".)   This  string  must  not  be  modified  by the application, but may be modified by a 
       subsequent call to strerror().  No library function,
       including perror(3), will modify this string.

       The strerror_r() function is similar to strerror(), but is thread safe.  This function is 
       available in  two  versions:  an  XSI-compliant  version
       specified  in  POSIX.1-2001  (available  since glibc 2.3.4, but not POSIX-compliant until glibc 
       2.13), and a GNU-specific version (available since
       glibc 2.0).  The XSI-compliant version is provided with the feature test macros settings shown 
       in the SYNOPSIS; otherwise the GNU-specific version
       is provided.  If no feature test macros are explicitly defined, then (since glibc 2.4) 
       _POSIX_SOURCE is defined by default with the value 200112L,
       so that the XSI-compliant version of strerror_r() is provided by default.

       The XSI-compliant strerror_r() is preferred for portable applications.  It returns the error 
       string in the  user-supplied  buffer  buf  of  length
       buflen.

       The GNU-specific strerror_r() returns a pointer to a string containing the error message.  This 
       may be either a pointer to a string that the func‐
       tion stores in buf, or a pointer to some (immutable) static string (in which case buf is 
       unused).  If the function stores a string in buf, then at
       most buflen bytes are stored (the string may be truncated if buflen is too small and errnum is 
       unknown).  The string always includes a terminating
       null byte ('\0').
RETURN VALUE
       The strerror() and the GNU-specific strerror_r() functions return the appropriate error 
       description string, or an "Unknown error nnn"  message  if
       the error number is unknown.

       POSIX.1-2001  and  POSIX.1-2008  require that a successful call to strerror() shall leave errno 
       unchanged, and note that, since no function return
       value is reserved to indicate an error, an application that wishes to check for errors should 
       initialize errno to zero before the call,  and  then
       check errno after the call.

       The  XSI-compliant  strerror_r()  function  returns  0  on  success.  On error, a (positive) 
       error number is returned (since glibc 2.13), or -1 is
       returned and errno is set to indicate the error (glibc versions before 2.13).

ERRORS
       EINVAL The value of errnum is not a valid error number.

       ERANGE Insufficient storage was supplied to contain the error description string.

C语言提供的退出码

int main()
{
   for(int i = 0; i<=134; i++)
   {
       printf("%d : %s\n", i, strerror(i));
   }
}


//结果
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

进程退出深度理解

os内少了一个进程,os就要释放进程对应的内核数据结构+代码和(如果有独立)数据

进程常见退出方法

正常退出

  1. main函数return:其他函数退出仅仅是函数退出,所以进程执行本质是main执行流执行

  2. exit函数(C库函数):exit(int code) code就是代表退出码,等价于main return code;在代码任意地方调用该函数都表示进程退出

  3. _exit函数(系统调用):看似等价于exit,但exit比_exit多做了一些事情

在这里插入图片描述

int main()
{
	printf("hello");
	exit(0);
}
//结果
[root@localhost linux] # . / a.out
hello[root@localhost linux]#
int main()
{
	printf("hello");
	_exit(0);
}
//结果
[root@localhost linux] # . / a.out
[root@localhost linux]#

缓冲区

exit(int code)
{//终止进程只能由OS来执行,所以用户想要终止进程必定有系统调用
    //冲刷缓冲区
    _exit(code)
}

缓冲区一定不在操作系统内核中,因为都使用了系统调用但并没有刷新缓冲区

进程等待

进程等待必要性

  • 之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏

  • 另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程

  • 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,
    或者是否正常退出

父进程通过进程等待的方式,回收子进程资源,即父进程可以通过信号+退出码的方式,获取子进程退出信息
所谓进程等待就是通过系统调用,获取子进程退出码或退出信号的方式,顺便释放内存

进程等待的方法

wait

父进程等待子进程状态改变,回收子进程

#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int* status);
返回值:
	成功返回被等待进程pid,失败返回 - 1。
参数:
	输出型参数,获取子进程退出状态, 不关心则可以设置成为NULL
WAIT(2)    Linux Programmer's Manual    WAIT(2)

NAME
       wait, waitpid, waitid - wait for process to change state

SYNOPSIS
       #include <sys/types.h>
       #include <sys/wait.h>

       pid_t wait(int *status);

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

       int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);

   Feature Test Macro Requirements for glibc (see feature_test_macros(7)):

       waitid():
           _SVID_SOURCE || _XOPEN_SOURCE >= 500 || _XOPEN_SOURCE && _XOPEN_SOURCE_EXTENDED
           || /* Since glibc 2.12: */ _POSIX_C_SOURCE >= 200809L

DESCRIPTION
       All of these system calls are used to wait for state changes in a child of the calling process, 
       and obtain information about the child whose state
       has changed.  A state change is considered to be: the child terminated; the child was stopped 
       by a signal; or the child was resumed by  a  signal.
       In  the  case of a terminated child, performing a wait allows the system to release the 
       resources associated with the child; if a wait is not per‐
       formed, then the terminated child remains in a "zombie" state (see NOTES below).

       If a child has already changed state, then these calls return immediately.  Otherwise they 
       block until either a child changes state  or  a  signal
       handler  interrupts  the  call  (assuming  that  system  calls are not automatically restarted 
       using the SA_RESTART flag of sigaction(2)).  In the
       remainder of this page, a child whose state has changed and which has not yet been waited upon 
       by one of these system calls is termed waitable.

RETURN VALUE
       wait(): on success, returns the process ID of the terminated child; on error, -1 is returned.

       waitpid():  on success, returns the process ID of the child whose state has changed; if WNOHANG 
       was specified and one or more child(ren) specified
       by pid exist, but have not yet changed state, then 0 is returned.  On error, -1 is returned.

       waitid(): returns 0 on success or if WNOHANG was specified and no child(ren) specified by id 
       has yet changed state;  on  error,  -1  is  returned.
       Each of these calls sets errno to an appropriate value in the case of an error.

ERRORS
       ECHILD (for wait()) The calling process does not have any unwaited-for children.

       ECHILD (for  waitpid()  or  waitid()) The process specified by pid (waitpid()) or idtype and id 
       (waitid()) does not exist or is not a child of the
              calling process.  (This can happen for one's own child if the action for SIGCHLD is set 
              to SIG_IGN.  See also the Linux Notes section about
              threads.)

       EINTR  WNOHANG was not set and an unblocked signal or a SIGCHLD was caught; see signal(7).

       EINVAL The options argument was invalid.
int main()
{

    pid_t id = fork();
    if(id == 0)
    {
        //子进程
        int cnt = 5;
        while(cnt)
        {
            printf("我是子进程,我还活着呢,我还有%dS, pid: %d, ppid%d\n", cnt--, getpid(), getppid());
            sleep(1);
        }
        exit(111);
    }

    //父进程
    pid_t ret_id = wait(NULL);
    printf("我是父进程,等待子进程成功, pid: %d, ppid: %d, ret_id: %d\n", getpid(), getppid(), ret_id);
    sleep(10);

}

wait函数在子进程没终止时,使得父进程一直在等子进程

waitpid

pid_ t waitpid(pid_t pid, int* status, int options);
返回值:
	当正常返回的时候waitpid返回收集到的子进程的进程ID;
	如果设置了选项WNOHANG, 而调用中waitpid发现没有已退出的子进程可收集, 则返回0;
	如果调用中出错, 则返回 - 1, 这时errno会被设置成相应的值以指示错误所在;
参数:
	pid:
	    pid = -1, 等待任一个子进程。与wait等效。
	    pid > 0.等待其进程ID与pid相等的子进程。
    status :
	    WIFEXITED(status) : 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
	    WEXITSTATUS(status) : 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
options :
	    WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进
程的ID。
WAIT(2)    Linux Programmer's Manual    WAIT(2)

NAME
       wait, waitpid, waitid - wait for process to change state

SYNOPSIS
       #include <sys/types.h>
       #include <sys/wait.h>

       pid_t wait(int *status);

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

       int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);

   Feature Test Macro Requirements for glibc (see feature_test_macros(7)):

       waitid():
           _SVID_SOURCE || _XOPEN_SOURCE >= 500 || _XOPEN_SOURCE && _XOPEN_SOURCE_EXTENDED
           || /* Since glibc 2.12: */ _POSIX_C_SOURCE >= 200809L

DESCRIPTION
       All of these system calls are used to wait for state changes in a child of the calling process, 
       and obtain information about the child whose state
       has changed.  A state change is considered to be: the child terminated; the child was stopped 
       by a signal; or the child was resumed by  a  signal.
       In  the  case of a terminated child, performing a wait allows the system to release the 
       resources associated with the child; if a wait is not per‐
       formed, then the terminated child remains in a "zombie" state (see NOTES below).

       If a child has already changed state, then these calls return immediately.  Otherwise they 
       block until either a child changes state  or  a  signal
       handler  interrupts  the  call  (assuming  that  system  calls are not automatically restarted 
       using the SA_RESTART flag of sigaction(2)).  In the
       remainder of this page, a child whose state has changed and which has not yet been waited upon 
       by one of these system calls is termed waitable.
RETURN VALUE
       wait(): on success, returns the process ID of the terminated child; on error, -1 is returned.

       waitpid():  on success, returns the process ID of the child whose state has changed; if WNOHANG 
       was specified and one or more child(ren) specified
       by pid exist, but have not yet changed state, then 0 is returned.  On error, -1 is returned.

       waitid(): returns 0 on success or if WNOHANG was specified and no child(ren) specified by id 
       has yet changed state;  on  error,  -1  is  returned.
       Each of these calls sets errno to an appropriate value in the case of an error.

ERRORS
       ECHILD (for wait()) The calling process does not have any unwaited-for children.

       ECHILD (for  waitpid()  or  waitid()) The process specified by pid (waitpid()) or idtype and id 
       (waitid()) does not exist or is not a child of the calling process.  (This can happen for one's 
       own child if the action for SIGCHLD is set to SIG_IGN.  See also the Linux Notes section about
       threads.)

       EINTR  WNOHANG was not set and an unblocked signal or a SIGCHLD was caught; see signal(7).

       EINVAL The options argument was invalid.
int main()
{

    pid_t id = fork();
    if(id == 0)
    {
        //子进程
        int cnt = 5;
        while(cnt)
        {
            printf("我是子进程,我还活着呢,我还有%dS, pid: %d, ppid: %d\n", cnt--, getpid(), getppid());
            sleep(1);
        }
        exit(111);
    }

    //父进程
    //pid_t ret_id = wait(NULL);
    int status;
    pid_t ret_id = waitpid(id , &status, 0);
    printf("我是父进程,等待子进程成功, pid: %d, ppid: %d, ret_id: %d, child exit status: %d\n", getpid(), getppid(), ret_id, status);
    sleep(10);

}

在这里插入图片描述

信号

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
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

#define SIGHUP 1

输出型参数与位图

  1. int* status 是输出性参数,用来获取子进程终止后的状态

  2. status不能简单的当作整形来看待,可以当作位图来看待,,即将退出码+信号看做位图,具体细节如下图(只研究status低16比特位)

在这里插入图片描述

  1. 0~6位表示信号,如果都为0表示没有收到信号进程正常退出

  2. 8~15位表示退出码,如果没有收到信号,则用退出码表示进程结果

通过status获取退出码和信号

int main()
{

    pid_t id = fork();
    if(id == 0)
    {
        //子进程
        int cnt = 1;
        while(cnt)
        {
            printf("我是子进程,我还活着呢,我还有%dS, pid: %d, ppid: %d\n", cnt--, getpid(), getppid());
            sleep(1);
        }
        exit(111);
    }

    //父进程
    //pid_t ret_id = wait(NULL);
    int status;
    pid_t ret_id = waitpid(id , &status, 0);
    printf("我是父进程,等待子进程成功, pid: %d, ppid: %d, ret_id: %d, child exit code: %d, child exit signal: %d\n", \
    getpid(), getppid(), ret_id, (status>>8)&0xFF, status & 0x7F);

}
//结果
我是子进程,我还活着呢,我还有1S, pid: 1406, ppid: 1405
我是父进程,等待子进程成功, pid: 1405, ppid: 14530, ret_id: 1406, child exit code: 111, child exit signal: 0


int main()
{

    pid_t id = fork();
    if(id == 0)
    {
        //子进程
        int cnt = 1;
        while(cnt)
        {
            printf("我是子进程,我还活着呢,我还有%dS, pid: %d, ppid: %d\n", cnt--, getpid(), getppid());
            sleep(1);
            int n = 10;
            n /= 0;
        }
        exit(111);
    }

    //父进程
    //pid_t ret_id = wait(NULL);
    int status;
    pid_t ret_id = waitpid(id , &status, 0);
    printf("我是父进程,等待子进程成功, pid: %d, ppid: %d, ret_id: %d, child exit code: %d, child exit signal: %d\n", \
    getpid(), getppid(), ret_id, (status>>8)&0xFF, status & 0x7F);

}

//结果
我是子进程,我还活着呢,我还有1S, pid: 1645, ppid: 1644
我是父进程,等待子进程成功, pid: 1644, ppid: 14530, ret_id: 1645, child exit code: 0, child exit signal: 8

第一个进程是正常退出,返回了退出码;第二个进程是异常终止,返回了信号

进程等待的实现

阻塞等待

在这里插入图片描述

子进程的PCB会存有进程退出的退出码或信号,父进程可以在子进程退出后通过wait/waitpid系统调用来获得操作系统内核中子进程PCB的属性
阻塞等待:在子进程没有退出的时候,父进程只能一直在调用waitpid/wait进行等待,所以父进程一直处于阻塞状态

在这里插入图片描述

非阻塞等待

父进程不一直等待子进程,而是采用轮询访问的方式,在子进程未退出时,可以继续执行代码

int main()
{

    pid_t id = fork();
    if(id == 0)
    {
        //子进程
        int cnt = 5;
        while(cnt)
        {
            printf("我是子进程,我还活着呢,我还有%dS, pid: %d, ppid: %d\n", cnt--, getpid(), getppid());
            sleep(1);
        }
        exit(0);
    }

    //父进程
    //pid_t ret_id = wait(NULL);
    while(1)
    {
        int status = 0;
        pid_t ret_id = waitpid(id, status, WNOHANG); //夯住了
        if(ret_id < 0)
        {
            printf("waitpid error\n");
        }
        else if(ret_id == 0)
        {
            printf("子进程还没退出,父进程做其他事情\n");
            sleep(1);
            continue;
        }
        else
        {
            printf("我是父进程,等待子进程成功, pid: %d, ppid: %d, ret_id: %d, child exit code: %d, child exit signal: %d\n", \
            getpid(), getppid(), ret_id, (status>>8)&0xFF, status & 0x7F);
            break;
        }

    }
    
}

//结果
子进程还没退出,父进程做其他事情
我是子进程,我还活着呢,我还有5S, pid: 10391, ppid: 10390
子进程还没退出,父进程做其他事情
我是子进程,我还活着呢,我还有4S, pid: 10391, ppid: 10390
子进程还没退出,父进程做其他事情
我是子进程,我还活着呢,我还有3S, pid: 10391, ppid: 10390
子进程还没退出,父进程做其他事情
我是子进程,我还活着呢,我还有2S, pid: 10391, ppid: 10390
子进程还没退出,父进程做其他事情
我是子进程,我还活着呢,我还有1S, pid: 10391, ppid: 10390
子进程还没退出,父进程做其他事情
我是父进程,等待子进程成功, pid: 10390, ppid: 14530, ret_id: 10391, child exit code: 0, child exit signal: 0

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>

#define TASK_NUM 10

// 预设一批任务
void sync_disk()
{
    printf("这是一个刷新数据的任务!\n");
}

void sync_log()
{
    printf("这是一个同步日志的任务!\n");
}

void net_send()
{
    printf("这是一个进行网络发送的任务!\n");
}

// 要保存的任务相关的
typedef void (*func_t)(); //函数指针类型
func_t other_task[TASK_NUM] = {NULL};  //函数指针数组

int LoadTask(func_t func)
{
    int i = 0;
    for(; i < TASK_NUM; i++)
    {
        if(other_task[i] == NULL) break;
    }
    if(i == TASK_NUM) return -1;
    else other_task[i] = func;

    return 0;
}

void InitTask() 
{
    for(int i = 0; i < TASK_NUM; i++) other_task[i] = NULL;
    LoadTask(sync_disk);
    LoadTask(sync_log);
    LoadTask(net_send);
}

void RunTask()
{
    for(int i = 0; i < TASK_NUM; i++)
    {
        if(other_task[i] == NULL) continue;
        other_task[i]();
    }
}

int main()
{

    pid_t id = fork();
    if(id == 0)
    {
        //子进程
        int cnt = 5;
        while(cnt)
        {
            printf("我是子进程,我还活着呢,我还有%dS, pid: %d, ppid: %d\n", cnt--, getpid(), getppid());
            sleep(1);
        }
        exit(0);
    }

    //父进程
    //pid_t ret_id = wait(NULL);
    InitTask();
    while(1)
    {
        int status = 0;
        pid_t ret_id = waitpid(id, status, WNOHANG); //夯住了
        if(ret_id < 0)
        {
            printf("waitpid error\n");
        }
        else if(ret_id == 0)
        {
            //printf("子进程还没退出,父进程做其他事情\n");
            RunTask();
            sleep(1);
            continue;
        }
        else
        {
            printf("我是父进程,等待子进程成功, pid: %d, ppid: %d, ret_id: %d, child exit code: %d, child exit signal: %d\n", \
            getpid(), getppid(), ret_id, (status>>8)&0xFF, status & 0x7F);
            break;
        }

    }
    
}

进程替换

创建子进程的目的

  1. 让子进程执行一部分父进程的代码

  2. 让子进程执行一个全新的代码(进程的程序替换)

替换原理

有六种以exec开头的函数,统称exec函数

#include <unistd.h>
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ...,char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);

int execve(const char *filename, char *const argv[], char *const envp[]);

EXEC(3)    Linux Programmer's Manual    EXEC(3)

NAME
       execl, execlp, execle, execv, execvp, execvpe - execute a file

SYNOPSIS
       #include <unistd.h>

       extern char **environ;

       int execl(const char *path, const char *arg, ...);
       int execlp(const char *file, const char *arg, ...);
       int execle(const char *path, const char *arg,
                  ..., char * const envp[]);
       int execv(const char *path, char *const argv[]);
       int execvp(const char *file, char *const argv[]);
       int execvpe(const char *file, char *const argv[],
                   char *const envp[]);

   Feature Test Macro Requirements for glibc (see feature_test_macros(7)):

       execvpe(): _GNU_SOURCE

DESCRIPTION
       The  exec()  family  of  functions  replaces the current process image with a new process 
       image.  The functions described in this manual page are
       front-ends for execve(2).  (See the manual page for execve(2) for further details about the 
       replacement of the current process image.)

       The initial argument for these functions is the name of a file that is to be executed.

       The const char *arg and subsequent ellipses in the execl(), execlp(), and execle() functions  
       can  be  thought  of  as  arg0,  arg1,  ...,  argn.
       Together  they describe a list of one or more pointers to null-terminated strings that 
       represent the argument list available to the executed pro‐
       gram.  The first argument, by convention, should point to the filename associated with the file 
       being executed.  The list of  arguments  must  be
       terminated by a NULL pointer, and, since these are variadic functions, this pointer must be 
       cast (char *) NULL.

       The execv(), execvp(), and execvpe() functions provide an array of pointers to null-terminated 
       strings that represent the argument list available
       to the new program.  The first argument, by convention, should point to the filename associated 
       with the  file  being  executed.   The  array  of
       pointers must be terminated by a NULL pointer.

       The  execle()  and execvpe() functions allow the caller to specify the environment of the 
       executed program via the argument envp.  The envp argu‐
       ment is an array of pointers to null-terminated strings and must be terminated by a NULL 
         pointer.  The other functions take the  environment  for
       the new process image from the external variable environ in the calling process.

   Special semantics for execlp() and execvp()
       The  execlp(), execvp(), and execvpe() functions duplicate the actions of the shell in 
       searching for an executable file if the specified filename
       does not contain a slash (/) character.  The file is sought in the colon-separated list of 
       directory pathnames specified in the PATH  environment
       variable.   If this variable isn't defined, the path list defaults to the current directory 
         followed by the list of directories returned by conf‐
       str(_CS_PATH).  (This confstr(3) call typically returns the value "/bin:/usr/bin".)

       If the specified filename includes a slash character, then PATH is ignored, and the file at the 
       specified pathname is executed.

       In addition, certain errors are treated specially.

       If permission is denied for a file (the attempted execve(2) failed with the error EACCES), 
       these functions will continue searching  the  rest  of
       the search path.  If no other file is found, however, they will return with errno set to 
       EACCES.

       If  the  header  of  a  file  isn't  recognized  (the  attempted execve(2) failed with the 
       error ENOEXEC), these functions will execute the shell
       (/bin/sh) with the path of the file as its first argument.  (If this attempt fails, no further 
       searching is done.)

RETURN VALUE
       The exec() functions return only if an error has occurred.  The return value is -1, and errno 
       is set to indicate the error.

ERRORS
       All of these functions may fail and set errno for any of the errors specified for execve(2).
int main()
{
    printf("begin...\n");
    printf("begin...\n");
    printf("begin...\n");
    printf("begin...\n");
    printf("我已经是一个进程了,我都PID:%d\n",getpid());
    execl("/bin/ls", "ls", "-a", "-l", NULL);

    printf("end...\n");
    printf("end...\n");
    printf("end...\n");
    printf("end...\n");
}
//结果
begin...
begin...
begin...
begin...
我已经是一个进程了,我都PID:884
total 44
drwxrwxr-x  4 admin1 admin1 4096 Jul 24 20:34 .
drwxrwxr-x 12 admin1 admin1 4096 Jul 22 00:02 ..
drwxrwxr-x  2 admin1 admin1 4096 Jul 18 13:50 exec
-rw-rw-r--  1 admin1 admin1   65 Jul 24 20:33 Makefile
-rwxrwxr-x  1 admin1 admin1 8512 Jul 24 20:34 myproc
-rw-rw-r--  1 admin1 admin1 1255 Jul 24 20:34 myproc.c
drwxrwxr-x  2 admin1 admin1 4096 Jul 18 13:50 myshell
-rw-rw-r--  1 admin1 admin1   33 Jul 18 13:50 shell.sh
-rwxrwxr-x  1 admin1 admin1   43 Jul 18 13:50 test.py

在这里插入图片描述
在这里插入图片描述

让进程执行另一个在磁盘中的程序
程序替换并没有创建新的进程(PCB),进程id并灭有改变
从程序角度看是这个程序被加载到了内存
既然我们可以加载程序,那么操作系统肯定也可以加载程序
当创建进程的时候,是先有数据结构(PCB),然后再加载代码和数据

多进程:使用独有程序替换的接口

程序替换是整体替换,不能局部替换
程序替换只会影响调用进程,进程具有独立性
加载新程序是需要程序替换的,即代码区也要发生写时拷贝

int main()
{
  
  pid_t id = fork();
  if(id == 0)
  {
    //child
    printf("我是子进程:%d\n", getpid());
    execl("/bin/lss","lss","hello.txt",NULL);
    //execl:如果替换成功,不会有返回值;如果替换失败,必定有返回值
    //所以不用对这个函数返回值做判断,只要原程序代码继续执行,那该函数一定失败了
    exit(1);
  }
  sleep(5);

  //father
    int status = 0;
    printf("我是父进程:%d\n",getpid());
    waitpid(id,&status,0);
    printf("child exit code: %d\n",WEXITSTATUS(status));
    return 0;
}
//结果
我是子进程:10569
我是父进程:10568
child exit code: 1

execl

int execl(const char *path, const char *arg, ...);
在这里插入图片描述

执行一个程序:

  1. 找到它,用路径找到
  2. 加载执行它,如何加载执行,用命令行所有参数:ls -al => ls -a -l
  3. 参数列表最后参数要以以NULL结尾

execv

int execv(const char *path, char *const argv[]);

在这里插入图片描述

用字符串指针数组存放命令行所有参数

execlp

int execlp(const char *file, const char *arg, ...);

在这里插入图片描述

执行程序只需指定程序名,系统会自动在环境变量PATH中进行查找

execvp

int execvp(const char *file, char *const argv[]);

在这里插入图片描述

执行程序只需指定程序名,系统会自动在环境变量PATH中进行查找

execle

int execle(const char *path, const char *arg, ...,char *const envp[]);

在这里插入图片描述

自定义环境变量会覆盖之前的环境变量
子进程继承父进程的环境变量就是可以考execle()这个函数

putenv

PUTENV(3)    Linux Programmer's Manual    PUTENV(3)

NAME
       putenv - change or add an environment variable

SYNOPSIS
       #include <stdlib.h>

       int putenv(char *string);

   Feature Test Macro Requirements for glibc (see feature_test_macros(7)):

       putenv(): _SVID_SOURCE || _XOPEN_SOURCE

DESCRIPTION
       The putenv() function adds or changes the value of environment variables.  The argument string 
       is of the form name=value.  If name does not already exist in the envi‐
       ronment, then string is added to the environment.  If name does exist, then the value of name 
       in the environment is changed to value.  The string pointed to by string
       becomes part of the environment, so altering the string changes the environment.

RETURN VALUE
       The putenv() function returns zero on success, or nonzero if an error occurs.  In the event of 
       an error, errno is set to indicate the cause.

ERRORS
       ENOMEM Insufficient space to allocate new environment.

将环境变量传入环境变量列表

execvpe

int execvpe(const char *file, char *const argv[], char *const envp[]);

在这里插入图片描述

execve

int execve(const char *filename, char *const argv[], char *const envp[]);

EXECVE(2)    Linux Programmer's Manual    EXECVE(2)

NAME
       execve - execute program

SYNOPSIS
       #include <unistd.h>

       int execve(const char *filename, char *const argv[],
                  char *const envp[]);

DESCRIPTION
       execve() executes the program pointed to by filename.  filename must be either a binary 
       executable, or a script starting with a line of the form:

           #! interpreter [optional-arg]

       For details of the latter case, see "Interpreter scripts" below.

       argv  is an array of argument strings passed to the new program.  By convention, the first of 
       these strings should contain the filename associated with the file being
       executed.  envp is an array of strings, conventionally of the form key=value, which are passed 
       as environment to the new program.  Both argv and envp must  be  termi‐
       nated by a NULL pointer.  The argument vector and environment can be accessed by the called 
       program's main function, when it is defined as:

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

       execve() does not return on success, and the text, data, bss, and stack of the calling process 
       are overwritten by that of the program loaded.

       If the current program is being ptraced, a SIGTRAP is sent to it after a successful execve().

       If  the set-user-ID bit is set on the program file pointed to by filename, and the underlying 
       file system is not mounted nosuid (the MS_NOSUID flag for mount(2)), and
       the calling process is not being ptraced, then the effective user ID of the calling process is 
       changed to that of the owner of the program file.  Similarly, when  the
       set-group-ID bit of the program file is set the effective group ID of the calling process is 
       set to the group of the program file.
         
RETURN VALUE
       On success, execve() does not return, on error -1 is returned, and errno is set appropriately.

在这里插入图片描述

execve是真系统调用

测试exec系列函数

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>

int main()
{
  extern char **environ;
  pid_t id = fork();
  if(id == 0)
  {
    //child
    printf("我是子进程:%d\n", getpid());

    //execl("/bin/ls","-a","-ln", NULL);
    //execl:如果替换成功,不会有返回值;如果替换失败,必定有返回值
    //所以不用对这个函数返回值做判断,只要原程序代码继续执行,那该函数一定失败了

    // char* const myargv[] ={
    //   "ls",
    //   "-a",
    //   "-l",
    //   "-n",
    //   NULL
    // };
    // execv("/bin/ls", myargv);

    //execlp("ls", "ls", "-a", "-l", "-n", NULL);

    //execvp("ls", myargv);

    // char *const myenv[] = {
    //   "MYENV=YOUCANSEEME",
    //   NULL 
    // };
    putenv("MYENV=YOUCANSEEME");
    execle("./exec/otherproc", "otherproc", NULL, environ);
    exit(1);
  }
  sleep(1);

  //father
    int status = 0;
    printf("我是父进程:%d\n",getpid());
    waitpid(id,&status,0);
    printf("child exit code: %d\n",WEXITSTATUS(status));
    return 0;
//  printf("begin...\n");
//  printf("begin...\n");
//  printf("begin...\n");
//  printf("begin...\n");
//  printf("我已经是一个进程了,我都PID:%d\n",getpid());
//  execl("/bin/ls", "ls", "-a", "-l", NULL);

//  printf("end...\n");
//  printf("end...\n");
//  printf("end...\n");
//  printf("end...\n");

}

#include <iostream>
#include <unistd.h>
#include <stdlib.h>

using namespace std;

int main()
{
  for(int i = 0;i < 5;i++)
  {
    cout << "-------------------------------------------------------" << endl;
    cout << "我是另一个程序,我的pid是:" << getpid() <<  endl;
    cout << "MYENV: " <<(getenv("MYENV") == NULL?"NULL":getenv("MYENV")) <<endl;
    cout << "PATH: " <<(getenv("PATH") == NULL?"NULL":getenv("PATH")) <<endl;
    cout << "-------------------------------------------------------" << endl;
    cout << endl;
    sleep(1);
  }
  return 0;
}

简易版shell

fgets

GETS(3)    Linux Programmer's Manual    GETS(3)

NAME
       fgetc, fgets, getc, getchar, gets, ungetc - input of characters and strings

SYNOPSIS
       #include <stdio.h>

       int fgetc(FILE *stream);

       char *fgets(char *s, int size, FILE *stream);

       int getc(FILE *stream);

       int getchar(void);

       char *gets(char *s);

       int ungetc(int c, FILE *stream);

DESCRIPTION
       fgetc() reads the next character from stream and returns it as an unsigned char cast to an int, 
       or EOF on end of file or error.

       getc() is equivalent to fgetc() except that it may be implemented as a macro which evaluates 
       stream more than once.

       getchar() is equivalent to getc(stdin).

       gets()  reads  a  line  from stdin into the buffer pointed to by s until either a terminating 
       newline or EOF, which it replaces with a null byte ('\0').  No check for
       buffer overrun is performed (see BUGS below).

       fgets() reads in at most one less than size characters from stream and stores them into the 
       buffer pointed to by s.  Reading stops after an EOF or a  newline.   If  a
       newline is read, it is stored into the buffer.  A terminating null byte ('\0') is stored after 
       the last character in the buffer.

       ungetc()  pushes  c  back  to stream, cast to unsigned char, where it is available for 
       subsequent read operations.  Pushed-back characters will be returned in reverse
       order; only one pushback is guaranteed.

       Calls to the functions described here can be mixed with each other and with calls to other 
       input functions from the stdio library for the same input stream.

       For nonlocking counterparts, see unlocked_stdio(3).
         
RETURN VALUE
       fgetc(), getc() and getchar() return the character read as an unsigned char cast to an int or 
       EOF on end of file or error.

       gets() and fgets() return s on success, and NULL on error or when end of file occurs while no 
       characters have been read.

       ungetc() returns c on success, or EOF on error.

指定的流 stream 读取一行,并把它存储在 str 所指向的字符串内。当读取 (n-1) 个字符时,或者读取到换行符时,或者到达文件末尾时,它会停止,具体视情况而定

简易版shell

#include <stdio.h>
#include <unistd.h>
#include <assert.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>

#define MAX 1024
#define ARGC 64
#define SEP " "

// 一般用户自定义的环境变量,在bash中要用户自己来进行维护,不要用一个经常被覆盖的缓冲区来保护环境变量


int split(char *commandstr, char *argv[])
{//切割字符串
  assert(commandstr);
  assert(argv);

  
  argv[0] = strtok(commandstr, SEP);
  if(argv[0] == NULL) return -1;
  int i= 1;
  while((argv[i++] = strtok(NULL, SEP)));
 // while(1)
 // {
 //   argv[i] = strtok(NULL, SEP);
 //   if(argv[i] == NULL) break;
 //   i++;
 // }
  return 0;
}

void debugPrint(char * argv[])
{
  for(int i = 0;argv[i];i++)
  {
    printf("%d: %s\n", i, argv[i]);
  }
}

void showEnv()
{
    extern char **environ;
    for(int i = 0; environ[i]; i++) printf("%d:%s\n", i, environ[i]);
}

int main()
{
  int last_exit = 0;
  char myenv[32][256];
  int env_index = 0;
  while(1)
  {
    char commandstr[MAX] = {0};
    char *argv[ARGC] = {NULL};

    printf("[admin1@mymachine currpath]# ");
    fflush(stdout);
    
    //读取命令行
    char* s = fgets(commandstr, sizeof(commandstr),stdin);
    assert(s);
    (void)s; //保证在release方式发布的时候,因为去掉assert了,所以s就没有使用,而带来的编译告警,什么都没做,但是充当一次使用
    // abcd\n\0
    commandstr[strlen(commandstr) -1] = '\0';
    
    //切割字符串
    int n = split(commandstr, argv);
    if(n != 0) continue; 

    if(strcmp(argv[0], "ls") == 0)
    {
        int pos = 0;
        while(argv[pos])pos++;
        argv[pos++] = (char*)"--color=auto";
        argv[pos] = NULL;
    }
    //debugPrint(argv);
    // cd .. :让bash自己执行的命令,我们称之为内建命令/内置命令
    if(strcmp(argv[0], "cd") == 0)
    {
        if(argv[1] != NULL) chdir(argv[1]);
        continue;
    }
    //增加父进程环境变量
    if(strcmp(argv[0], "export") == 0) //其实我们之气那学习到所有的(几乎)环境变量的命令都是内建命令
    {
      if(argv[1] != NULL)
      {
        strcpy(myenv[env_index], argv[1]);
        putenv(myenv[env_index++]);
      }
      continue;
    }
    //读取父进程环境变量
    if(strcmp(argv[0], "env") == 0)
    {
      showEnv();
      continue;
    }

    if(strcmp(argv[0], "echo") == 0)
    {
        // echo $PATH
        const char *target_env = NULL;
        if(argv[1][0] == '$') 
        {
            if(argv[1][1] == '?'){
                printf("%d\n", last_exit);
                continue;
            }
            else target_env = getenv(argv[1]+1); // "abcdefg
            
            if(target_env != NULL) printf("%s=%s\n", argv[1]+1, target_env);
        }
        continue;
    }

    pid_t id = fork();
    assert(id >= 0);
    (void)id;

    if(id == 0)
    {
      //child
      execvp(argv[0], argv);
      exit(0);    
    }
    int status = 0;
    pid_t ret = waitpid(id, &status, 0);
    if(ret > 0)
    {
      last_exit = WEXITSTATUS(status);
    }
    //printf("%s\n", commandstr);

  }
}

一般用户自定义的环境变量,在bash中要用户自己来进行维护,不要用一个经常被覆盖的缓冲区来保护环境变量
让父进程执行的命令,我们称为内建命令/内置命令


总结

进程管理是操作系统的功能之一,要学会如何让管理进程。
人的一生注定会遇到两个人,一个惊艳了时光,一个温柔了岁月。——张爱玲

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值