linux内核学习-进程-进程控制

进程标识

每一个linux都有唯一一个进程标识,且为非负数。进程的其他参数可以通过相应的函数获得,函数声明在头文件“unistd.h”中。

函数声明功能
pid_t getpid(id)获得进程ID
pid_t getppid(id)获得父进程ID
pid_t getuid(id)获得进程实际用户ID
pid_t getcuid(id)获得进程有效用户ID
pid_t getgid(id)获得进程实际组ID
pid_t getegid(id)获得进程有效组ID

实际用户:标识运行进程的用户
有效用户:程序以怎么的身份运行程序
实际组:实际用户所属的组
有效组:有效用户所属的组

linux进程的结构

在这里插入图片描述
代码段存放程序可执行代码。
数据段存放程序全局变量、常量、静态变量
堆:存放动态分配的内存变量
栈:存放函数调用

linux进程状态

  1. 运行状态
  2. 可中断等待状态
  3. 不可中断等待状态
  4. 僵死状态
  5. 停止状态

用ps命令可以查看进程的状态。运行状态R,可中断等待状态S,不可中断等待状态D,僵死状态Z,停止状态为T。

在这里插入图片描述

进程的控制

用来对进程进行控制的主要系统调用如下
fork:用于创建一个新进程
exit:用于终止进程
exec:用于执行一个应用程序
wait:将父进程挂起,等待子进程终止
getpid:获取当前进程的进程ID
nice:改变进程的优先级

进程的PID

PID_MAX=0x8000(可改),因此进程号的最大值为0x7fff,即32767。其中0-299保留给daemon进程。可以通过/proc/sys/kernel/pid_max修改。
内核的PID号是会一直往上增长的,一般不会使用旧的PID号。但如果PID号达到最大值,内核就会重新开始找小的,没有使用的PID号。

查看进程的详细信息

查看进程详细信息的常用三种方法有ps命令,top命令和cat /proc/pid/status。其中ps和top的显示比较直观,下面把cat /proc/pid/status的方法详细说下,这里是内核创建进程后存放进程信息最原始的地方。
1.先用ps命令找到你关心的进程的pid号,如我想看bash进程的pid号

ps -aux |grep bash
然后
cat /proc/pid/status  这里的pid要用你 查到的pid号代替。如cat /proc/8311/status 

输出如下

svauto@ubuntua:/proc/8311$ cat /proc/8311/status 
Name:	bash
State:	S (sleeping)
Tgid:	8311
Ngid:	0
Pid:	8311
PPid:	2632
TracerPid:	0
Uid:	1001	1001	1001	1001
Gid:	1001	1001	1001	1001
FDSize:	256
Groups:	27 1001 
NStgid:	8311
NSpid:	8311
NSpgid:	8311
NSsid:	8311
VmPeak:	   24908 kB
VmSize:	   24844 kB
VmLck:	       0 kB
VmPin:	       0 kB
VmHWM:	    6004 kB
VmRSS:	    5972 kB
VmData:	    2068 kB
VmStk:	     136 kB
VmExe:	     976 kB
VmLib:	    2308 kB
VmPTE:	      68 kB
VmPMD:	      12 kB
VmSwap:	       0 kB
HugetlbPages:	       0 kB
Threads:	1
SigQ:	0/15584
SigPnd:	0000000000000000
ShdPnd:	0000000000000000
SigBlk:	0000000000000000
SigIgn:	0000000000380004
SigCgt:	000000004b817efb
CapInh:	0000000000000000
CapPrm:	0000000000000000
CapEff:	0000000000000000
CapBnd:	0000003fffffffff
CapAmb:	0000000000000000
Seccomp:	0
Cpus_allowed:	ffffffff,ffffffff,ffffffff,ffffffff
Cpus_allowed_list:	0-127
Mems_allowed:	00000000,00000001
Mems_allowed_list:	0
voluntary_ctxt_switches:	2897
nonvoluntary_ctxt_switches:	520

各字段解释

参数解释
Name应用程序或命令的名字
State任务的状态,运行/睡眠/僵死/
SleepAVG任务的平均等待时间(以nanosecond为单位),交互式任务因为休眠次数多、时间长,它们的 sleep_avg 也会相应地更大一些,所以计算出来的优先级也会相应高一些。
Tgid线程组号
Pid任务ID
Ppid父进程ID
TracerPid接收跟踪该进程信息的进程的ID号
UidUid euid suid fsuid
GidGid egid sgid fsgid
FDSize文件描述符的最大个数,file->fds
Groups
VmSize(KB)任务虚拟地址空间的大小 (total_vm-reserved_vm),其中total_vm为进程的地址空间的大小,reserved_vm:进程在预留或特殊的内存间的物理页
VmLck(KB)任务已经锁住的物理内存的大小。锁住的物理内存不能交换到硬盘 (locked_vm)
VmRSS(KB)应用程序正在使用的物理内存的大小,就是用ps命令的参数rss的值 (rss)
VmData(KB)程序数据段的大小(所占虚拟内存的大小),存放初始化了的数据; (total_vm-shared_vm-stack_vm)
VmStk(KB)任务在用户态的栈的大小 (stack_vm)
VmExe(KB)程序所拥有的可执行虚拟内存的大小,代码段,不包括任务使用的库 (end_code-start_code)
VmLib(KB)被映像到任务的虚拟内存空间的库的大小 (exec_lib)
VmPTE该进程的所有页表的大小,单位:kb
Threads共享使用该信号描述符的任务的个数,在POSIX多线程序应用程序中,线程组中的所有线程使用同一个信号描述符。
SigQ待处理信号的个数
SigPnd屏蔽位,存储了该线程的待处理信号
ShdPnd屏蔽位,存储了该线程组的待处理信号
SigBlk存放被阻塞的信号
SigIgn存放被忽略的信号
SigCgt存放被俘获到的信号
CapInh Inheritable能被当前进程执行的程序的继承的能力
CapPrm Permitted进程能够使用的能力,可以包含CapEff中没有的能力,这些能力是被进程自己临时放弃的,CapEff是CapPrm的一个子集,进程放弃没有必要的能力有利于提高安全性
CapEff Effective进程的有效能力

创建进程

创建进程可以由“操作系统”和“父进程”创建。操作系统创建的进程没有继承关系。父进程创建的子进程,可以继承父进程的几乎所有资源。

一、fork函数创建进程
需要包含头文件

#include <unistd.h>

fork函数的示例代码如下

#include "stdio.h"
#include <sys/types.h>
#include <unistd.h>

int main(void)
{
	pid_t pid;
	
	printf("Process Creating study!\n");
	pid = fork();
	printf("point 1");
	switch(pid)
	{
	case 0:
		printf("Child process is running ,CurPid is %d, ParentId is %d\n",pid, getppid());
		break;
	case -1:
		printf("Process creating faild\n");
		break;
	default:
		printf("Parent process is running ,CurPid is %d, ParentId is %d\n",pid, getpid());
	}
	
	
	printf("exit\n");		
}

很多书会说fork在使用的时候,会返回两次。但是我觉得这样的描述是不妥的。其实fork并没有返回两次,看到有两次是因为fork会创建新的一个进程,且子进程的代码和父进程的代码是一样的。并且进程的运行位置都是一样的,有区别的是,子进程得到了0的返回值。父进程得到了子进程的PID号。

程序运行结果:
在这里插入图片描述
fork函数创建进程的特点
1.子进程和父进程是两个独立的程序。他们互不干扰的运行。
2.子进程有自己唯一的PID
3.父进程退出后,子进程继承给祖先进程,一级一级的上升,直到继承给init进程。
4.子进程共享父进程打开的文件描述符,但是父进程对文件描述符的修改,不会影响子进程
5.子进程不继承父进程设置的警告
6.子进程的未决信号集被清空。

二、vfork函数创建进程
vfork的本质也是最终调用fork来进程,但是还是有细微的差别,如下
1.使用fork创建的父子两个进程是完全独立的。但是vfork创建的子进程共享父进程的地址空间。因此子进程改了数据,父进程是可以看见的。
2.fork创建的父子进程执行先后顺序由内核进程调度机制决定。vfork创建的子进程需要调用exec或exit之后,父进程才能继续执行。

fork和vfork创建进程的方法是一样的。如下代码所示

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

int globVar =5;

int main(void)
{
	pid_t pid;
	int var =1,i;
	
	printf("fork is diffirent with vfork!\n");
	printf("now globVar =%d,var=%d\r\n",globVar,var);
	
	pid = vfork();
	//pid = fork();  //解开注释就能使用任意一个
	
	switch(pid)
	{
	case 0:
		i=2;
		while(i-->0)
		{
			printf("Child process is running \n");
			globVar++;
			var++;
			sleep(1);
		}
		printf("child globvar=%d,var=%d\r\n",globVar,var);
		break;
	case -1:
		printf("Process creating faild\n");
		break;
	default:
		i=2;
		while(i-->0)
		{
			printf("Parent process is running \n");
			globVar++;
			var++;
			sleep(2);
		}
		printf("parent gloVar =%d,var=%d\n",globVar,var);
		break;
	}
	
	exit(0);
		
}

这个程序,把下面代码任意选择一个编译并运行。就可以看到vfork的地址空间父子进程是共享的。

pid = vfork();
	//pid = fork();  //解开注释就能使用任意一个

如下vfork的执行结果,刚开始全局变量globVar=5,局部变量var=1.子进程对局部变量和全局变量都做了修改globVar=7,var=3.,然后父进程继续做两次+1操作。最终globVar=9,var=5.说明子进程的操作对父进程有影响。
在这里插入图片描述

以下是fork的结果,可以看出子进程和父进程的结果互不影响,各算各的,执行顺序也是无法预计。
在这里插入图片描述

三、创建守护进程

守护进程就是在后台运行的进程。用来做服务进程非常有用。创建步骤比较多。但都是套路,直接写上去就行了。
守护进程一般在系统系统的时候自动启动。因此都是修改启动脚本(如/etc/rc.d)来完成启动。
创建守护进程需要有如下特性:

  1. 进程后台运行,fork一个子进程,然后父进程退出。
  2. 将进程变成会话组长,调用setsid。
  3. 禁止进程重新打开控制终端。可以用fork创建一个子进程,然后父进程退出
  4. 关闭不再需要的文件描述符。避免浪费资源或造成文件系统无法卸载。
  5. 更改当前目录为根目录。
  6. 设置屏蔽字为0,使用umask(0)。
  7. 处理SIGCHLD信号。这一步不是必须的。避免出现僵尸进程。

代码如下

#include "stdio.h"
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <sys/param.h>
#include <sys/stat.h>
#include <time.h>
#include <syslog.h>
#include <stdlib.h>

int init_daemon(void)
{
	int pid;
	int i;
	
	signal(SIGTTOU,SIG_IGN);
	signal(SIGTTIN,SIG_IGN);
	signal(SIGTSTP,SIG_IGN);
	signal(SIGHUP,SIG_IGN);
	
	
	pid = fork();
	
	if(pid>0)
	{
		exit(0);
	}
	else if(pid <0)
	{
		return -1;
	}
	
	setsid();
	
	pid = fork();
	if(pid >0)
	{
		exit(0);
	}
	else if(pid <0)
	{
		return -1;
	}
	
	for(i=0;i<NOFILE;close(i++));
	
	chdir("/");
	
	umask(0);
	
	return 0;	
}


int main()
{
	time_t now;
	init_daemon();
	syslog(LOG_USER|LOG_INFO,"守护程序\n");
	
	while(1)
	{
		sleep(8);
		time(&now);
		syslog(LOG_USER|LOG_INFO,"系统时间:\t%s\t\n",ctime(&now));
	}
	
}

编译运行

gcc -o daemon daemon.c
./daemon
ps -aux |grep daemon
或者ps -ef

查看进程,有如下输出

svauto   103299  0.0  0.0   4352    80 ?        S    14:56   0:00 ./daemon

查看系统日志。在/var/log目录下的sudo tail syslog。结果如下图。
在这里插入图片描述

四、程序退出

1.正常退出。可用方法

  • 在main中return
  • 调用exit
  • 调用_exit

2.异常退出。可用方法

  • 调用about函数
  • 进程收到某个信号,而该信号终止进程

五、执行新进程

执行新程序的函数
在这里插入图片描述
这几个函数的区别与环境变量和传参方法有关。

函数传参
execve通过路劲方式调用,argv,envp对应新程序的argv ,envp
execv通过路劲方式调用,argv对应新程序的argv
execl和execv类似,不过argv参数数量不确定
execle和execl类似,但是要指定环境变量
execvp和execv类似,但可以在环境变量中找可执行程序
execlp类似execle , 但可以在环境变量中找可执行程序

exec函数族除了execve是系统调用,其他都是库函数。

exec函数返回错误表
在这里插入图片描述
实例代码
程序1,用来被调用

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

int main(int argc,char *argv[],char **environ)
{
	int i;
	
	printf("i am a process image!\n");
	printf("My pid = %d, parentpid = %d\n",getpid(),getppid());
	printf("uid = %d,gid  = %d\n",getuid(),getgid());
	
	for(i= 0 ;i<argc;i++)
	{
		printf("argv[%d]:%s\n",i,argv[i]);
	}
}

程序2:

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

int main(int argc,char *argv[],char ** environ)
{
	pid_t pid;
	int stat_val;
	
	printf("Exec example !\n");
	pid = fork();
	switch(pid)
	{
	case -1:
		perror("process creation failed\n");
		exit(1);
	case 0:
		printf("Child process is running\n");
		printf("My pid =%d,parentpid=%d\n",getpid(),getppid());
		execve("processimage",argv,environ);
		printf("process never go to here!\n");
		exit(0);
	default:
		printf("Parent process is running !\n");	
		break;
	
	}
	
	wait(&stat_val);
	exit(0);
}

运行程序
在这里插入图片描述
做个测试,如果把execveDemo删除,运行程序就会报错,如下
在这里插入图片描述
六、等待进程结束wait 和waitpid

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

int main()
{
	pid_t pid;
	char *msg;
	int k;
	int exit_code;
	
	printf("Study how to get exit code\n");
	pid =fork();
	switch(pid)
	{
	case 0:
		msg = "Child process is running";
		k = 5;
		exit_code = 37;
		break;
	case 1:
		perror("process create faild\n");
		exit(1);
	default:
		exit_code= 0;
		break;
		
	}
	
	//父进程会执行这段代码,
	if(pid!=0)
	{
		int stat_val;
		pid_t child_pid;
		
		child_pid = wait(&stat_val);
		
		printf("Child process has exited,pid = %d\n",child_pid);
		if(WIFEXITED(stat_val))
			printf("child exited with code %d\n",WEXITSTATUS(stat_val));
		else 
			printf("child exited abnormallly\n");
			
	}
	else
	{//子进程暂停5秒,在这个时候可以运行ps -aux查看父进程状态
		while(k-->0)
		{
			puts(msg);
			sleep(1);
		}
		
	}
	
	exit(exit_code);
}

运行程序输出
在这里插入图片描述
可以看出父进程在执行wait后,就处于等待状态,知道子进程退出后,父进程再继续执行。

如果子进程比父进程提前结束,子进程就会成为僵尸进程,占用着资源,无法释放。所以在子进程结束之前,需要让父进程调用wait。僵尸进程只有父进程结束了,才能释放资源。

八、常见的进程的其他操作

函数功能
getpid获取进程ID
setuid设置实际用户id
setgid设置有效用户ID
nice改变进程优先级
getpriority获取优先级
setpriority设置优先级

遇到的错误总结:

如下图错误,程序语法是没有问题的,但是在最后链接的时候,因为找不到函数,就会出现链接错误,如下图,这时候需要增加头文件。
在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值