Linux进程控制
1.进程创建
1.1fork函数
在Linux中fork函数,从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
返回值:子进程中返回0,父进程返回子进程id,出错返回-1
进程调用fork,当控制转移到内核中的fork代码后,内核做的事:
- 分配新的内存块和内核数据结构给子进程
- 将父进程部分数据结构内容拷贝至子进程
- 添加子进程到系统进程列表当中
- fork返回,开始调度器调度
当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。
1.2写时拷贝
父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。
2.进程终止
2.1进程退出场景
- 代码运行完毕,结果正确
- 代码运行完毕,结果不正确
- 代码异常终止
2.2进程退出方法
正常终止(可以通过 echo $? 查看进程退出码)
- main函数的return返回
- 调用exit
- 调用_exit
异常退出:
ctrl+c,信号终止
2.3_exit函数和exit函数的区别
- exit是库函数,_exit是系统调用
- exit终止进程的时候,会自动刷新缓存区。_exit终止进程的时候,不会自动刷新缓冲区。
return退出是一种常见的退出进程方法。执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返回值当作exit的参数
3.进程等待
通过wait/waitpid的方式,让父进程对子进程进行资源回收的等待过程
解决子进程僵尸问题带来的内存泄露问题
父进程为什么要创建子进程?要让子进程来完成的如何,父进程要不要知道?要知道----需要通过进程等待的方式,获取子进程退出的信息—两个数字!—不是必须的,但是操作系统需要提供这样的基础功能!
wait的作用
能让进程等待能回收子进程僵尸状态,Z->x
如果子进程根本没有退出,父进程必须在wait上进行阻塞等待,直到子进程僵尸,wait自动回收,返回!
3.1进程等待的方法
wait方法
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status);
//返回值:
// 成功返回被等待进程pid,失败返回-1。
//参数:
// 输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
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/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退
出信息。
如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
如果不存在该子进程,则立即出错返回。
3.2获取子进程status
wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
如果传递NULL,表示不关心子进程的退出状态信息。
否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特
位)
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
//int status = 0;
void Worker(int number)
{
int *p = NULL;
int cnt = 10;
while(cnt)
{
printf("I am child process, pid: %d, ppid: %d, cnt: %d, number: %d\n", getpid(), getppid(), cnt--, number);
sleep(1);
//*p = 100;
//int a = 10;
//a /= 0;
}
}
const int n = 10;
int main()
{
for(int i = 0;i < n; i++)
{
pid_t id = fork();
if(id == 0)
{
Worker(i);
//status = i;
exit(0);
}
}
//等待多个子进程?
for(int i = 0; i < n; i++)
{
int status = 0;
pid_t rid = waitpid(-1, &status, 0); // pid>0, -1:任意一个退出的子进程
if(rid > 0){
printf("wait child %d success, exit code: %d\n", rid, WEXITSTATUS(status));
}
}
return 0;
}
3.3父进程等待子进程的状态
options有两种阻塞方式:
- 0:阻塞等待
- WNOHANG:等待的时候,以非阻塞的方式等待!
阻塞式调用,子进程不退出,wait不返回
如果我们等待的条件不满足,wait/waitpid不阻塞,而是立即返回!!非阻塞等待,往往要进行重复调用,轮询+非阻塞方案==非阻塞轮询方案,进行进程等待!!—好处?当我们进行等待的过程中,可以顺便做一下自己的事,占据时间并不多的事情!!!
4.进程程序替换
4.1替换原理
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
环境变量被子进程继承下去是一种默认行为,不受程序替换的影响,其原因是通过地址空间可以让子进程继承父进程的环境变量数据,程序替换只替换新程序的代码和数据,环境变量不会被替换
子进程执行的时候,获得的环境变量有以下三种情况:
- 将父进程的环境变量原封不动传递给子进程
- 可以直接构造环境变量表,给子进程传递
- 新增传递
4.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 execve(const char *path, char *const argv[], char *const envp[]);
l(list) : 表示参数采用列表
v(vector) : 参数用数组
p(path) : 有p自动搜索环境变量PATH
e(env) : 表示自己维护环境变量
这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
如果调用出错则返回-1
所以exec函数只有出错的返回值而没有成功的返回值。
#include <unistd.h>
int main()
{
char *const argv[] = {"ps", "-ef", NULL};
char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};
execl("/bin/ps", "ps", "-ef", NULL);
// 带p的,可以使用环境变量PATH,无需写全路径
execlp("ps", "ps", "-ef", NULL);
// 带e的,需要自己组装环境变量
execle("ps", "ps", "-ef", NULL, envp);
//带v的需要自己组装命令行参数数组
execv("/bin/ps", argv);
// 带p的,可以使用环境变量PATH,无需写全路径
execvp("ps", argv);
// 带e的,需要自己组装环境变量
execve("/bin/ps", argv, envp);
exit(0);
}
5.简易的shell
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#define NUM 1024
#define SIZE 64
#define SEP " "
//#define Debug 1
char cwd[1024];
char enval[1024]; // for test
int lastcode = 0;
char *homepath()
{
char *home = getenv("HOME");
if(home) return home;
else return (char*)".";
}
const char *getUsername()
{
const char *name = getenv("USER");
if(name) return name;
else return "none";
}
const char *getHostname()
{
const char *hostname = getenv("HOSTNAME");
if(hostname) return hostname;
else return "none";
}
const char *getCwd()
{
const char *cwd = getenv("PWD");
if(cwd) return cwd;
else return "none";
}
int getUserCommand(char *command, int num)
{
printf("[%s@%s %s]# ", getUsername(), getHostname(), getCwd());
char *r = fgets(command, num, stdin); // 最终你还是会输入\n
if(r == NULL) return -1;
// "abcd\n" "\n"
command[strlen(command) - 1] = '\0';
return strlen(command);
}
void commandSplit(char *in, char *out[])
{
int argc = 0;
out[argc++] = strtok(in, SEP);
while( out[argc++] = strtok(NULL, SEP));
#ifdef Debug
for(int i = 0; out[i]; i++)
{
printf("%d:%s\n", i, out[i]);
}
#endif
}
int execute(char *argv[])
{
pid_t id = fork();
if(id < 0) return -1;
else if(id == 0) //child
{
// exec command
execvp(argv[0], argv); // cd ..
exit(1);
}
else // father
{
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid > 0){
lastcode = WEXITSTATUS(status);
}
}
return 0;
}
void cd(const char *path)
{
chdir(path);
char tmp[1024];
getcwd(tmp, sizeof(tmp));
sprintf(cwd, "PWD=%s", tmp); // bug
putenv(cwd);
}
// 什么叫做内键命令: 内建命令就是bash自己执行的,类似于自己内部的一个函数!
// 1->yes, 0->no, -1->err
int doBuildin(char *argv[])
{
if(strcmp(argv[0], "cd") == 0)
{
char *path = NULL;
if(argv[1] == NULL) path=homepath();
else path = argv[1];
cd(path);
return 1;
}
else if(strcmp(argv[0], "export") == 0)
{
if(argv[1] == NULL) return 1;
strcpy(enval, argv[1]);
putenv(enval); // ???
return 1;
}
else if(strcmp(argv[0], "echo") == 0)
{
if(argv[1] == NULL){
printf("\n");
return 1;
}
if(*(argv[1]) == '$' && strlen(argv[1]) > 1){
char *val = argv[1]+1; // $PATH $?
if(strcmp(val, "?") == 0)
{
printf("%d\n", lastcode);
lastcode = 0;
}
else{
const char *enval = getenv(val);
if(enval) printf("%s\n", enval);
else printf("\n");
}
return 1;
}
else {
printf("%s\n", argv[1]);
return 1;
}
}
else if(0){}
return 0;
}
int main()
{
while(1){
char usercommand[NUM];
char *argv[SIZE];
// 1. 打印提示符&&获取用户命令字符串获取成功
int n = getUserCommand(usercommand, sizeof(usercommand));
if(n <= 0) continue;
// 2. 分割字符串
// "ls -a -l" -> "ls" "-a" "-l"
commandSplit(usercommand, argv);
// 3. check build-in command
n = doBuildin(argv);
if(n) continue;
// 4. 执行对应的命令
execute(argv);
}
}