系列文章目录
文章目录
- 系列文章目录
- 前言
- 进程创建
- fork函数初识
- fork写时拷贝
- fork常规用法
- fork调用失败的原因
- 进程终止
- 进程退出场景
- 进程的退出码
- 系统自带的退出码
- strerror
- C语言提供的退出码
- 进程退出深度理解
- 进程常见退出方法
- 正常退出
- 缓冲区
- 进程等待
- 进程等待必要性
- 进程等待的方法
- wait
- waitpid
- 信号
- 输出型参数与位图
- 通过status获取退出码和信号
- 进程等待的实现
- 阻塞等待
- 非阻塞等待
- 进程替换
- 创建子进程的目的
- 替换原理
- 多进程:使用独有程序替换的接口
- execl
- execv
- execlp
- execvp
- execle
- putenv
- execvpe
- execve
- 测试exec系列函数
- 简易版shell
- fgets
- 简易版shell
- 总结
前言
进程控制是操作系统的一个重要功能。
进程创建
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就要释放进程对应的内核数据结构+代码和(如果有独立)数据
进程常见退出方法
正常退出
-
main函数return:其他函数退出仅仅是函数退出,所以进程执行本质是main执行流执行
-
exit函数(C库函数):exit(int code) code就是代表退出码,等价于main return code;在代码任意地方调用该函数都表示进程退出
-
_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
输出型参数与位图
-
int* status 是输出性参数,用来获取子进程终止后的状态
-
status不能简单的当作整形来看待,可以当作位图来看待,,即将退出码+信号看做位图,具体细节如下图(只研究status低16比特位)
-
0~6位表示信号,如果都为0表示没有收到信号进程正常退出
-
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;
}
}
}
进程替换
创建子进程的目的
-
让子进程执行一部分父进程的代码
-
让子进程执行一个全新的代码(进程的程序替换)
替换原理
有六种以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, ...);
执行一个程序:
- 找到它,用路径找到
- 加载执行它,如何加载执行,用命令行所有参数:ls -al => ls -a -l
- 参数列表最后参数要以以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中要用户自己来进行维护,不要用一个经常被覆盖的缓冲区来保护环境变量
让父进程执行的命令,我们称为内建命令/内置命令
总结
进程管理是操作系统的功能之一,要学会如何让管理进程。
人的一生注定会遇到两个人,一个惊艳了时光,一个温柔了岁月。——张爱玲