目录
一.进程的相关概念
1.什么是程序,什么是进程,有什么区别?
1.程序是静态的概念, eg:gcc xxx.c-o pro。
2.磁盘中生成pro文件,就叫做程序。
3. 进程是程序的一次运行活动,通俗意思就是程序跑起来了,系统就会多一个进程。
2.如何查看系统中有那些进程
1.使用ps指令查看;实际工作中,配合grep来查找程序中是否存在某一进程。
eg:ps -aux|grep ./a.out
2. 使用top指令查看,类似windows任务管理器。
**直接输入top指令就可以看到创建的进程。
二.进程标识符
1.什么是进程标识符
1.每个进程都有一个非负整数表示的唯一ID,叫做pid,类似身份证。
2.pid = 0(称为交换进程)swapper
**作用——进程调度
3.pid = 1 (init进程)
**作用——系统初始化
3.编程调用getpid函数获取自身的进程标识符;getpid获取父进程的进程标识符。
1.2.案例分析
首先定义两个pid号,分别打印出来fork函数创建之前的函数和之后的函数,并通过两个pid号的判断,就可以确定哪一个是父进程,哪一个是子进程。(其中的关键就是判断pid号)
#include<sys/types.h>
#include<unistd.h>
#include<stdio.h>
int main()
{
pid_t pid;
pid_t pid2;
pid = getpid();
printf("before fork: pid = %d\n",pid);
fork();
pid2 = getpid();
printf("after fork: pid = %d\n",pid2);
if(pid == pid2)
{
printf("this is father \n");
}else{
printf("this is child print, child pid = %d\n",getpid());
}
return 0;
}
三.fork和vfork函数
1.对于fork函数的理解
1.一个进程,包括代码、数据和分配给进程的资源。fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程,也就是两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同,两个进程也可以做不同的事。
2.一个进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值不同。相当于克隆了一个自己。
2.fork创建一个子进程的一般目的
1.一个父进程希望复制自己,使父、子进程同时执行不同的代码段,这在网络服务进程中是最常见的——父进程等待客户端的服务请求;当这种请求到达时,父进程调用fork,使子进程处理此请求、父进程则继续等待下一个服务请求到达。
2.1案例分析
1.通过父、子两个进程,并且定义一个数据变量为10的一个变量;当父进程收到数据为1的时候启用fork函数创建一个子进程,是的子进程继续执行下面的请求;如果收到了除了1以外的数字将不会继续执行一下的请求。
#include<sys/types.h>
#include<unistd.h>
#include<stdio.h>
//demo7
int main()
{
pid_t pid;
pid_t pid2;
int data = 10;
while(1){
printf("please input a data\n");
scanf("%d\n",&data);
if(data == 1){
pid = fork();
if(pid > 0){
}else if(pid == 0){
while(1){
printf("do net request net%d\n",getpid());
sleep(3);
}
}
}else{
printf("wait , do nothing\n");
}
}
return 0;
}
2.一个进程执行一个不同的程序,这对shell是常见的情况;在这种情况下,子进程从fork返回后立即调用exec函数。
3.使用fork函数创建一个进程
1.fork函数调用成功,返回两次
2.返回值为0,代表当前进程是子进程
3.返回值非负数,代表当前进程为父进程
4.调用失败,返回-1
3.1案列分析
通过定义一个retpid这样一个pid号,在fork函数创建一个新进程的时候并不清楚创建的父、子哪一个进程,就需要把这个信息传递给这个retpid来并接收到这个进程的id号,并且从打印的信息中就可以判断出来,该进程是子进程还是父进程。
#include<sys/types.h>
#include<unistd.h>
#include<stdio.h>
int main()
{
pid_t pid;
pid_t pid2;
pid_t retpid; //定义一个返回的进程
pid = getpid();
printf("before fork: pid = %d\n",pid);
retpid = fork(); //
pid2 = getpid();
printf("after fork: pid = %d\n",pid2);
if(pid == pid2)
{
printf("this is father print: retpid=%d \n",retpid);
}else{
printf("this is child print, retpid=%d, child pid = %d\n",retpid, getpid());
}
return 0;
}
4.vfork函数也可以创建进程,与fork有什么不同?
1.关键区别一:vfork直接使用父进程存储空间,不拷贝。
2.关键区别一:vfork保证子进程先运行,当子进程调用exit退出后,父进程才执行。
4.1案例分析
1.首先定义一个pid和一个计数变量cnt,当vfork创建子进程的时候,这个计算变量设置为三次,运行三次以后,子进程调用exit退出,父进程接下来将会执行;
2.并且只会使用父进程这一个存储空间,不需要像fork函数那样进行拷贝操作。
#include<sys/types.h>
#include<unistd.h>
#include<stdio.h>
//demo10.c
int main()
{
pid_t pid;
int cnt = 0;
pid = vfork();
if(pid > 0)
{
while(1){
printf("cnt = %d\n",cnt);
printf("this is father print, pid = %d\n",getpid());
sleep(1);
}
}
else if(pid == 0){
while(1){
printf("this is child print, child pid = %d\n",getpid());
sleep(1);
cnt++;
if(cnt == 3){
_exit(0);
break;
}
}
}
return 0;
}
四.进程退出
1.正常退出
1.main函数调用return.
2.进程调用_exit()或者_Exit,属于系统调用。
3.进程调用exit(),标准c库。
4.进程最后一个线程返回。
5.最后一个线程调用pthread_exit.
2.异常退出
1.调用abort.
2.当进程收到某些信号时,如ctrl+c.
3.最后一个线程对取消(cancellation)请求做出响应。
4.不管进程如何终止,最后都会执行内核中的同一段代码;这段代码为相应进程关闭所有打开描述符,释放它所使用的存储器。
5.对上述任意一种终止情况,都希望终止进程能够通知其父进程它是如何终止的,对于三个终止函数(-exit、exit、和Exit)实现这一点的方法是,将其退出状态(exit、status)作为参数传送给函数;在任意一种情况下,该终止进程的父进程都能用wait或waitpid函数取得终止状态。
3.案例分析
主要要了解退出的三个终止函数-exit、exit、和Exit,其中exit是另外两个函数的一个封装,需要使用那个函数就手动处理即可,但推荐大家使用exit函数。
#include<sys/types.h>
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
//demo11.c
int main()
{
pid_t pid;
int cnt = 0;
pid = vfork();
if(pid > 0)
{
while(1){
printf("cnt = %d\n",cnt);
printf("this is father print, pid = %d\n",getpid());
sleep(1);
}
}
else if(pid == 0){
while(1){
printf("this is child print, child pid = %d\n",getpid());
sleep(1);
cnt++;
if(cnt == 3){
// _exit(0);
exit(0);
// _Exit(0);
}
}
}
return 0;
}
五.等待进程
1.为什么要等待子进程退出
1.首先是创建子进程的目的——让子进程进行干活。
2.其次判断子进程是否干完活。
(1)干完活则exit(0,1,2——输出退出码)。
(2)没有干完活则判断是abort或者是被kill了。
3.手机子进程的退出状态,才能检验出子进程是以那种形式退出,才能知道干活的状态。
1.父进程等待子进程退出并收集子进程的退出状态。
2.子进程退出状态不被收集,变成僵尸进程。
2.相关函数
2.1wait函数
这个就是在man手册里面查询到的wait函数的原函数。
NAME
wait, waitpid, waitid - wait for process to change state
SYNOPSIS
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *wstatus);
pid_t waitpid(pid_t pid, int *wstatus, int options);
int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);
2.2waitpid参数解释
对于waitpid函数中pid参数的作用解释如下:
1.pid==-1
**等待任一子进程,就这一方面而言,waitpid与wait等效。
2.pid>0
** 等待其进程ID与pid相等的子进程。
3.pid=0
**等待其组ID等于调用进程组ID的任一子进程。
4.pid<-1
等待其组ID等于pid绝对值的任一子进程。
1.区别:waip使调用者阻塞,waitpid有一个选项
2.status参数:是一个整型数指针。
3.非空:子进程退出状态放在它所指向的地址中。
4.空:不关心退出状态。
2.3 waitpid的options常量
常量 |
|
---|---|
WCONTINUED |
|
WNOHANG |
|
WUNTRACED |
|
2.3.1.案例分析
了解waitpid的用法,以及此刻的在waitpid中的参数中的pid这是子进程的pid号,所以不能和父进程的pid这个两个pid混淆,这其中的前提是父进程首先要先收集子进程的退出状态,通过这个代码主要了解子进程的退出状态
#include<sys/types.h>
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
#include <sys/wait.h>
//demo14
int main()
{
pid_t pid;
int cnt = 0;
int status = 10;
pid = fork();
if(pid > 0)
{
// wait(&status);
waitpid(pid,&status,WNOHANG);
printf("child quit, child status = %d\n",WEXITSTATUS(status));
while(1){
printf("cnt = %d\n",cnt);
printf("this is father print, pid = %d\n",getpid());
sleep(1);
}
}
else if(pid == 0){
while(1){
printf("this is child print, pid = %d\n",getpid());
sleep(1);
cnt++;
if(cnt == 5){
exit(3);
}
}
}
return 0;
}
2.4.wait和waitpid返回终止状态的宏
这个宏相当于可以看成一种权限去使用,操作说明看图片说明即可
2.4.1.案例分析
1.这个案例主要说明了wait函数中的status参数如何使用,相当于在执行的时候返回到这个地址中,来收集子进程的退出状态;
2.exit的退出码与child status打印出来的值相同是因为返回状态的宏有关的。
#include<sys/types.h>
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
#include <sys/wait.h>
//demo15.c
int main()
{
pid_t pid;
int cnt = 0;
int status = 10;
pid = fork();
if(pid > 0)
{
wait(&status);
printf("child quit, child status = %d\n",WEXITSTATUS(status));
while(1){
printf("cnt=%d\n",cnt);
printf("this is father print, pid = %d\n",getpid());
sleep(1);
}
}
else if(pid == 0){
while(1){
printf("this is child print, pid = %d\n",getppid());
sleep(1);
cnt++;
if(cnt == 5){
exit(3);
}
}
}
return 0;
}
六.孤儿进程
1.解释
1.父进程如果不等待子进程退出,在子进程之前就来了自己的“生命”,此时子进程叫做孤儿进程。
2.Linux避免系统存在过多孤儿进程,init进程收留孤儿进程,变成孤儿进程的父进程。
七.exec族函数
1.基本函数概念
1.有六种以exec开头的函数,统称exec函数:
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[]);
1.1 exec族函数返回值
1.exec函数族的函数执行成功后不会返回,调用失败时,会设置errno并返回-1,然后从原程序的调用点接着往下执行。
1.2 参数说明
path: 可执行文件的路径名字。
arg: 可执行程序所带的参数,第一个参数为可执行文件名字,没有带路径且arg必须以NULL结束。
file: 如果参数file中包含/,则就将其视为路径名,否则就按 PATH环境变量,在它所指定的各目录中搜寻可执行文件。
exec族函数参数极难记忆和分辨,函数名中的字符会给我们一些帮助:
l : 使用参数列表
p: 使用文件名,并从PATH环境进行寻找可执行文;path的作用就是找到一些可执行的文件;查看patn路径:echo $PATH
。
v: 应先构造一个指向各参数的指针数组,然后将该数组的地址作为这些函数的参数。
e: 多了envp[]数组,使用新的环境变量代替调用进程的环境变量
1.3 案例分析(execl函数)
1.通过exec族函数来调用echoarg.c文件
include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
//函数原型:int execl(const char *path, const char *arg, ...);
int main(void)
{
printf("before execl\n");
if(execl("./echoarg","echoarg","abc",NULL) == -1)//阅读1.2参数说明
{
printf("execl failed!\n");
perror("why");//阅读返回值的那一段话
}
printf("after execl\n");
return 0;
}
2.文件 echoarg.c
#include <stdio.h>
// echoarg.c
int main(int argc,char *argv[])
{
int i = 0;
for(i = 0; i < argc; i++)
{
printf("argv[%d]: %s\n",i,argv[i]);
}
return 0;
}
1.4 对案例的解释说明
(1)首先使用gcc编译echoarg.c文件,生成可执行文件echoarg并放在当前路径bin目录下。
(2)文件echoarg的作用是打印命令行参数,然后再编译execl.c并执行execl可执行文件。
(3)同时一定要清楚的明白这个文件是在相互进行的。
(4)用execl 找到并执行echoarg,将当前进程main替换掉,所以”after execl” 没有在终端被打印出来。
(5)exec函数的参数结尾必须为空,并且在出现错误一下返回值为-1;如果成功的话继续执行下面的程序。
1.5 perror的用法
1.如果在使用exec族函数的时候不确定这个路径下是否存在这个文件的时候就可以通过perror这个函数来观察这个提示信息,就可以判断除当前目录或者文件夹下面是否存在这个文件。
2.务必去观察和上一个代码不同之处。
#include <stdlib.h>
#include <unistd.h>
//函数原型:int execl(const char *path, const char *arg, ...);
int main(void)
{
printf("before execl\n");
if(execl("./bin/echoarg","echoarg","abc",NULL) == -1)
{
printf("execl failed!\n");
perror("why");
}
printf("after execl\n");
return 0;
}
2.运行完以后就会出现这样一种调试信息
1.6 实际操作execl(ls,pwd,data)
1.通过该操作打开在该目录下的所有文件,记得总共有四个参数,那个参数不需要的话则要写成NULL的形式。
2.如何找到系统目录下的路径(eg:ls、data)
whereis ls
whereis data
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
//函数原型:int execl(const char *path, const char *arg, ...);
//demo18.c
int main(void)
{
printf("before execl\n");
if(execl("/bin/ls","ls","-l",NULL) == -1)
{
printf("execl failed!\n");
perror("why");
}
printf("after execl\n");
return 0;
}
1.7 如何使用带v不带l的一类exec函数
(1)如execv、execvp、execve, 应先构造一个指向各参数的指针数组,然后将该数组的地址作为这些函数的参数。
(2)像char *arg[]这种形式,且arg最后一个元素必须是NULL.
例如:char *arg[] = {“ls”,”-l”,NULL};
1.8 以execvp函数为例说明:
这段代码结合上一段代码execl即可。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
//函数原型:int execl(const char *path, const char *arg, ...);
//demo21.c
int main(void)
{
printf("this pro get system date:\n");
char *argv[] = {"ps,",NULL,NULL};
//if(execvp("ps",argv) == -1 );
if(execv("/bin/ps",argv) == -1)
{
printf("execl failed!\n");
perror("why");
}
printf("after execl\n");
return 0;
}
1.8 以execv函数实例:
此段代码需要注意的是运行ps的时候需要加上绝对路径。
//dmoc 20
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
//函数原型:int execl(const char *path, const char *arg, ...);
int main(void)
{
printf("this pro get system date:\n");
char *argv[] = {"ps",NULL,NULL};
if(execv("/bin/ps",argv)== -1)
{
printf("execl failed!\n");
perror("why");
}
printf("after execl\n");
return 0;
}
2.为什么要用exec族函数,有什么用?(很重要)
1.fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。
2.当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。
3.调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
2.将当前进程的.text、.data替换为所要加载的程序的.text、.data,然后让进程从新的.text第一条指令开始执行,但进程ID不变,换核不换壳
3.exec配合fork使用
1.实现功能,当父进程检测输入为1的时候,创建子进程把配置文件的字段值修改掉。
八.C程序的存储空间是如何分配?
1.进程创建后发生的事情
1.在进程还没有被fork函数运行是,这个进程里面的需要被拷贝的部分如下图所示,当fork函数执行后包括前一个进程里面的数据段、代码段、栈、堆,都会被新进程复制过来进行运行,而这发生的一切活动相当于是一个全拷贝。
2.其中的变量在拷贝过去以后编程共享这个变量,只有那个进程对这个变量开始改变的时候才会发生改变。
4.在子进程下改变变量的值,这个值只会在子进程下进行改变,父进程下并不会影响初始变量的值。
#include<sys/types.h>
#include<unistd.h>
#include<stdio.h>
//demo7
int main()
{
pid_t pid;
pid_t pid2;
int data = 10;
pid = getpid();
printf("before fork: pid = %d\n",pid);
fork();
pid2 = getpid();
printf("after fork: pid = %d\n",pid2);
if(pid == pid2)
{
printf("this is father \n");
}else{
printf("this is child print, child pid = %d\n",getpid());
data = data +10;
}
printf("data = %d\n",data);
return 0;
}
5.下面的程序则是给大家简要说明一下,代码段和数据段配合这图片进行理解。
#include <stdio.h>
int var = 10; //初始化的数据
int b; //未初始化的数据
int array[100]; //未初始化的数据
int main(int argc, char **argv)
{
int a = 0; //初始化的数据
if(a==0){ //if函数的一下代码统称为代码段
printf("a=0\n");
}else{
printf("a!=0\n");
}
return 0;
}
九.system函数
1.函数原型
NAME
system - execute a shell command
SYNOPSIS
#include <stdlib.h>
int system(const char *command);
2.system函数的返回值
(1)成功,则返回进程的状态值.
(2)当sh不能执行时,返回127.
(3)失败返回-1 .
3.实例分析
system函数不想exec族函数那样有多重参数,直接简单粗暴就可以完成任务了。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
//函数原型:int execl(const char *path, const char *arg, ...);
//demo28.c
int main(void)
{
printf("before execl\n");
if(system("ps") == -1)
{
printf("execl failed!\n");
perror("why");
}
printf("after execl\n");
return 0;
}
3.1运行结果
十.popen函数
1. 函数原型
NAME
popen, pclose - pipe stream to or from a process
SYNOPSIS
#include <stdio.h>
FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);
2.参数解释
(1)command: 是一个指向以 NULL 结束的 shell 命令字符串的指针。这行命令将被传到 bin/sh 并使用 -c 标志,shell 将执行这个命令。
(2)type: 只能是读或者写中的一种,得到的返回值(标准 I/O 流)也具有和 mode 相应的只读或只写类型。如果 type 是 “r” 则文件指针连接到 command 的标准输出;如果 type 是 “w” 则文件指针连接到 command 的标准输入。
3.实例分析
popen() 函数用于创建一个管道:其内部实现为调用 fork 产生一个子进程,在创建的这个管道里面写入数据并进行收集后输出结果;这也相当于popen函数的结果。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
//函数原型:int execl(const char *path, const char *arg, ...);
//size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
//demo26.c
int main(void)
{
char ret[1024] = {0};
FILE *fp;
fp = popen("ps","r");
int nread = fread(ret,1,1024,fp); //每次运行1次开辟1024个的空间
printf("read ret %d byte, ret = %s\n",nread, ret);
return 0;
}