Linux--进程控制

一、进程创建

fork函数

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

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

1)分配新的内存块和pcb给子进程

2)将父进程部分pcb内容拷贝到子进程

3)添加子进程到系统进程列表中

4)fork函数返回,开始调度器调度

fork函数调用失败的原因:①系统中有太多的进程,系统资源不足以创建新的子进程②实际用户的进程数量超过了限制

 写时拷贝

 一般情况下,父子代码共享,父子在不写入的时候数据也是共享的。当任意一方需要进行写入,则以写时拷贝的方式对需要的部分进行拷贝。

为什么采用写时拷贝,而不是直接复制一份代码运行?

写时拷贝本质上是一种按需申请资源的策略,当一个子进程创建,如果子进程并不需要新的物理内存空间,则与父进程共用同一块空间。减少资源的浪费,操作系统不允许资源的低利用率。

  • 创建子进程时,将父进程的 虚拟内存 与 物理内存 映射关系复制到子进程中,并将内存设置为只读(设置为只读是为了当对内存进行写操作时触发 缺页异常)。
  • 当子进程或者父进程对内存数据进行修改时,便会触发 写时拷贝 机制:将原来的内存页复制一份新的,并重新设置其内存映射关系,将父子进程的内存读写权限设置为可读写

二、进程终止

简单来讲,程序运行就是进程。进程终止有两种情况①程序正常执行完毕(执行结果正确/执行结果错误)②程序运行崩溃,也就是进程异常

退出码 

在 Unix 和 Linux 系统中,程序可以在执行终止后传递值给其父进程,这个值被称为退出码(exit code)或退出状态(exit status)。 

一个成功结束的命令的退出状态码是0,如果一个命令结束时有错误,退出状态码就是一个正数值(1-255)。

int main()
{
    return 0;
}

这里的 0 就是进程的退出码。正常执行完毕,结果正确 返回0;结果错误 返回其他值,不同的数值标识不同的错误原因,用于用户进行进程退出健康状况的判断 

#include<stdio.h>

int add_totop(int top)
{
  int sum=0;
  for(int i=0;i<top;i++)
  {
    sum+=i;
  }
  return sum;
}
int main()
{
  int result=add_totop(100);
  if(result==5050) return 0;
  else return 1;//计算结果不正确

}

通过进程的退出码可以判断程序是否正常执行,echo $?打印进程的退出码并且只显示最近一次进程的退出码

进程终止也就是操作系统内部减少一个进程,操作系统在进程结束后会将进程对应的pcb、代码和独立数据释放

exit函数

Linux上执行exit可使shell以指定的状态值退出。若不设置状态值参数,则shell以预设值退出。状态值0代表执行成功,其他值代表执行失败。

在代码的任意地方调用exit函数,都表示进程的退出

exit函数是一个C语言库函数,而_exit函数是一个系统函数调用。其功能和作用并没有差别

exit()与_exit()函数最大的区别在于:

exit()函数在调用exit系统之前要检查文件的打开情况,把文件缓冲区的内容写回文件。由于Linux标准函数中,“缓冲I/O”的操作,其特征即对应每一个打开的文件,在内存中都有一片缓冲区。每次读文件时,会连续读出若干条记录,在下次读文件时就可以直接从内存的缓冲区读取;同样每次写文件的时候也仅仅是写入内存的缓冲区,等满足了一定的条件(如达到了一定数量或遇到特定字符等),再将缓冲区中的内容一次性写入文件。这种技术大大增加了文件读写的速度。

_exit()函数直接终止函数

exit函数调用相当于:执行清理函数 -> 关闭所有打开的流,写入所有缓冲数据 -> _exit

二、进程等待 

运行状态是进程获得cpu使用权,正在执行代码的状态; 等待状态是阻塞状态 

进程等待:就是通过系统调用,获取子进程退出码或者退出信号的方式,同时释放内存

进程等待的必要性:

①子进程退出,父进程没有读取子进程的退出码,造成僵尸进程问题,导致内存泄漏

②进程如果变成僵尸进程,则属于一种“假死”状态,kill -9也无法将其释放,因为该进程以及终止

③子进程承担部分父进程功能,父进程需要知道子进程处理结果,也就是父进程需要子进程的退出码信息

④父进程通过进程等待,回收子进程资源,获取子进程退出信息

进程运行完毕的结果通过退出码展现,代码运行异常通过信号展现。因此采用信号+退出码的方案

wait函数

#include <sys/types.h>
#include <sys/wait.h>
int wait(int *status)

//参数status用来保存被收集进程退出时的一些状态,它是一个指向int类型的指针。
//但如果不关心子进程如何死掉,只想消灭僵尸进程,我们就可以设定这个参数为NULL

wait函数功能是:父进程一旦调用了wait就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。

wait()要与fork()配套出现,如果在使用fork()之前调用wait(),wait()的返回值则为-1,正常情况下wait()的返回值为子进程的PID

waitpid函数

#include <sys/types.h> 
#include <sys/wait.h>
pid_t waitpid(pid_t pid,int *status,int options);

1)pid_t pid:

pid>0等待进程号为pid的子进程。
pid=-1等待任何子进程,此时的waitpid()函数就退化成了普通的wait()函数。

2)int *status

这个参数将保存子进程的状态信息,有了这个信息父进程就可以了解子进程为什么会推出,是正常退出还是出现错误。如果status不是空指针,则写入状态信息.

这里并不能将status视为一个简单的整数,而是看作一个位图,status长度为32个比特位,对于高位的16个比特位(16-31)不做研究,第8-15位表示进程的退出状态(也就是exit(status),退出码),第0-7位为进程终止信号。

15-8比特位7-0比特位
正常退出退出状态0(退出码)
程序错误未使用终止信号
说明
WIFEXITED(status)如果子进程正常结束,它就返回真;否则返回假
WEXITSTATUS(status)如果WIFEXITED(status)为真,则可以用该宏取得子进程exit()返回的结束代码
WIFSIGNALED(status)如果子进程因为一个未捕获的信号而终止,它就返回真;否则返回假
WTERMSIG(status)如果WIFSIGNALED(status)为真,则可以用该宏获得导致子进程终止的信号代码
WIFSTOPPED(status) 如果当前子进程被暂停了,则返回真;否则返回假
WSTOPSIG(status)如果WIFSTOPPED(status)为真,则可以使用该宏获得导致子进程暂停的信号代码

3)int options

参数options提供了一些另外的选项来控制waitpid()函数的行为。如果不想使用这些选项,则可以把这个参数设为0

参数说明
WNOHANG如果pid指定的子进程没有结束,则waitpid()函数立即返回0,而不是阻塞在这个函数上等待;如果结束了,则返回该子进程的进程号。
WUNTRACED如果子进程进入暂停状态,则马上返回。

在子进程没有退出的时候,父进程只能一直调用waitpid进行等待,这种状况叫做阻塞等待。这种等待并不是运行状态,也不在运行队列中,而是在阻塞队列中

阻塞

A、阻塞

阻塞调用是指调用结果返回之前,当前线程会被挂起。函数只有在得到结果之后才会返回。有人也许会把阻塞调用和同步调用等同起来,实际上他是不同的。对于同步调用来说,很多时候当前线程还是激活的,只是从逻辑上当前函数没有返回而已。

socket 接收数据函数 recv 是一个阻塞调用的例子。当 socket 工作在阻塞模式的时候, 如果没有数据的情况下调用该函数,则当前线程就会被挂起,直到有数据为止。

B、非阻塞

非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。

三、进程程序替换

替换原理

在父进程中调用fork函数创建子进程,在子进程创建之前父进程的代码就已经存在,子进程执行的代码与父进程相同。如果子进程想要执行不同于父进程的程序,就需要程序替换。

所以进程需要调用exec族中的某一个函数来进行程序替换, 让一个进程来执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不会创建新进程,所以调用exec前后被替换进程的id并未改变。

程序替换就是让一个进程执行另一个在磁盘当中的程序,进程的PCB与虚拟内存不变,只改变代码内容

依据计算机的冯诺依曼体系结构,CPU只能从内存中读取数据。然而程序编写完成后是在磁盘当中,需要通过程序替换将其加载到内存当中。当一个进程创建的时候,操作系统先创建进程的数据结构以及PCB,当调用的时候才加载代码和数据

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

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");
}

由于程序替换,之后的代码都不会执行,也就没有打印end。因此程序替换是整体替换,而不是局部替换。

程序替换只影响调用进程,子进程不影响父进程(进程具有独立性)由于此时子进程发生了写操作,所以父子进程发生写时拷贝,父进程保留源代码,子进程进行程序替换

替换函数

#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[]);

exec族函数的区分方法:① l (list) : 表示参数采用列表 ② v (vector) : 参数用数组 ③ p (path) : 有p自动搜索环境变量PATH ④ e (env) : 表示自己维护环境变量

这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回;如果调用出错则返回-1

所以exec函数只有出错的返回值而没有成功的返回值

函数参数说明
参数名称作用
path标识想要执行程序的位置
arg选择执行方法(比如ls、ls -a、ls -a -l),最后必须以NULL结尾
argv[]以数组方式传参,方式与arg相同
函数说明
函数名参数个数是否带路径是否使用当前环境变量
execl列表不是
execlp列表

execle列表不是不是,需要自己组装环境变量
execv数组不是
execvp数组
execve数组不是不是,需要自己组装环境变量

事实上,只有execve是真正的系统调用,其它五个函数最终都调用 execve

简易shell

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

#define MAX 1024
#define ARGC 64
#define SEP " "
int split(char* commandstr,char* argv[])
{
  assert(commandstr);
  assert(argv);

  argv[0]=strtok(commandstr,SEP);
  int i=1;
  while((argv[i++]=strtok(NULL,SEP)))
  //while(1)
  //{
  //  argv[i]=strtok(NULL,SEP);
  //  if(argv[i]==NULL) break;
  //  i++;
  //}
  return 0;
}
int main()
{
  while(1)
  {
    char commandstr[MAX]={0};
    char* argv[ARGC]={NULL};
    printf("[rxy@my_machine path]# ");
    char *s=fgets(commandstr,sizeof(commandstr)-1,stdin);
    assert(s);
    (void)s;//保证在release方式使用时,由于断言导致没有使用而带来编译报警。这条语句并无实际用处,只是充当一次使用
    commandstr[strlen(commandstr)-1]='\0';//避免由于输入完命令后按下回车导致的换行,这里使用\0替换\n
    //printf("%s\n",commandstr);
    

    int n=split(commandstr,argv);//将一个字符串根据空格切割成多个
    if(n!=0) continue;//当n为0时对字符串的切割成功
    
    pid_t id=fork();
    assert(id>=0);
    (void)id;
    if(id==0)
    {
      //子进程
      execvp(argv[0],argv);
      exit(-1);
    }
    int status=0;
    waitpid(id,&status,0);
  }
}

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值