LINUX系统编程--4 进程
四 进程
基本内容
1 进程标识符pid
2 进程的产生fork()、fork()
3 进程的消亡以及释放资源
4 exec函数族
5 用户权限和组权限
6 观摩课:解释器文件
7 system函数
8 进程会计
9 进程时间
10 守护进程
11 系统日志文件
1 进程标识符pid
1、类型pid_t
2、shell命令:ps命令!重点。要熟悉常用的几种ps命令。在man中学。
比如:ps axf 、 ps axm、ps ax -L、
3、进程号是顺次向下使用,不是使用当前最小的!
4、getpid、getppid
2 进程的产生
1、fork()
执行一次返回两次。注意理解关键字 duplicating 意味着拷贝 克隆 一模一样。
fork 后父子进程的区别 : fork 的返回值不一样 ,pid不同 ,ppid也不同;未决信号与文件锁不继承;资源利用量清0 。
init进程 :是所有进程的祖先进程 pid == 1 。
例子:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
printf("%d start !\n",getpid());
fflush(NULL);//记得刷新 否则begin放到缓冲区 父子进程的缓冲区里各有一句begin。
//有换行符的话,在终端下是不需要这句的。要是没有换行符,必须有这个,但是若是输出到文件,那么必须要有这个。
//(所以不管是什么场景,记得都加这个!)
pid_t pid = fork();
if (pid == 0){
printf("child %d\n",getpid());
}else{
printf("parent %d\n",getpid());
}
getchar();
printf("pid %d end\n",getpid());
return 0;
}
补充:
- 关于ps -axf的使用。得到的是进程树。其中顶格写的的父进程是init进程。其他的都以树状形式展示父子进程。
- 注意fflush的重要性。
实例:
找质数,下面的程序是并发版本的(并且还是子进程不回收版本)。(单任务版本的简单,就省略了)
//primer1.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#define LEFT 2
#define RIGHT 200
int main()
{
pid_t pid = 0;
int i,j,mark;
for (i = LEFT;i <= RIGHT;i++){
pid = fork();
if (pid == 0){
mark = 1;
for (j = 2;j < i/2;j++){
if (i%j == 0){
mark = 0;
break;
}
}
if (mark) {
printf("%d is a primer\n",i);
}
exit(0);//至关重要,必须要有
}
}
getchar();
exit(0);
}
对上面程序的几点说明:
- 如果查看多任务版本的和单任务版本的运行时间,可以发现多任务版本的运行时间是明显小于单任务版本的的。
- 在新终端用ps -axf查看进程信息可以看到,下面有很多僵尸进程,这是因为父进程在阻塞等待(getchar),没有对子进程进行回收
2、vfork()与fork()的写时复制的区别
见课本
简单地来讲,现在的fork都用了写时复制技术,也就是说,子进程并不是简单的复制父进程的地址空间,而是只有读的时候才复制相应的数据块!这就避免了子进程用了很大力气复制了父进程的所有的地址空间,却什么也不做的现象!
而vfork也是同样的道理,并不复制父进程的地址空间,因为vfork后一般是调用exec函数,即使是复制了也是浪费时间,因为用不到。vfork后必须调用exec函数,如果vfork后的子进程试图修改数据、进行其他函数调用或者没有调用exec就返回,都会带来不可预知的结果!
3 进程的消亡以及释放资源
wait和waitpid
见书,没有其他特殊的内容
实战:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <wait.h>
#define N 3
#define LEFT 100000002
#define RIGHT 100000200
//交叉算法计算 池类算法涉及到竞争
int main()
{
printf("[%d] start !\n",getpid());
fflush(NULL);//记得刷新 否则begin放到缓冲区 父子进程的缓冲区里各有一句begin
pid_t pid = 0;
int i,j,mark;
for (int n = 0;n < N;n++){
pid = fork();
if (pid < 0){
perror("fork");
for (int k = 0;k < n;k++){
wait(NULL);
}
exit(1);
}
if (pid == 0){
for (i = LEFT+n;i <= RIGHT;i+=N){
mark = 1;
for (j = 2;j <= i/2;j++){
if (i%j == 0){
mark = 0;
break;
}
}
if (mark) {
printf("%d is a primer\n",i);
}
}
printf("[%d] exit\n",n);
exit(0);
}
}
int st,n;
for (n =0 ;n < N;n++){
wait(&st);
printf("%d end\n",st);
}
exit(0);
}
4 exec函数族
exec 替换 当前进程映像
extern char **environ
execl
execlp
execle
execv
execvpa
注意:fflush的使用!!
例子1:
打印时间戳
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <shadow.h>
#include <string.h>
#include<time.h>
int main()
{
puts("begin()!");
fflush(NULL);//一定要有这个! 防止重定向到文件时,不显示
execl("/bin/date","date","+%s",NULL);
perror("execl error");
exit(1);
puts("end!");
exit(0);
}
例二(exec和fork一起使用)
下面的程序有几个需要注意的点
- 第一个点是fflush的使用,以后的程序在这个位置都不要忘记加fflush;
- 第二个点是父进程的wait的回收的子进程是哪个,实际上,父进程等待回收的子进程的唯一标志是子进程的pid,虽然子进程被exec替换掉了,但是exec后的进程的同样还是那个fork后的pid。所以exec后不管是子进程被替换成了哪个程序映像,只要这个是这个pid的进程结束了,父进程就会wait回收掉!!
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <shadow.h>
#include <string.h>
#include<time.h>
int main()
{
pid_t pid;
puts("Begin()!");
fflush(NULL);//一定要有这个! 防止重定向到文件时,不显示
pid=fork();
if(pid<0)
{
perror("fork()");
eixt(1);
}
if(pid==0)
{
execl("/bin/date","date","+%s",NULL);//相当于在shell中输入date +%s
perror("execl()");
exit(1);
}
wait(NULL);
puts("end!");
exit(0);
}
5 shell命令的实现
以ls为例,当我们在shell中输入ls时,实际上就是shell fork了一个子进程,然后这个子进程exec了ls这个程序,子进程打印相关信息结束后,shell父进程完成对这个子进程的回收,然后回到父进程的shell。至于两个shell为啥共用同一个终端,这是因为子进程复制了父进程的文件描述符表,所以两个进程共同打开同一个终端,所以共用同一个终端。
6 模拟实现shell
实战:
模拟一个简单的shell(仅仅模拟外部命令)!
这一部分是重点!
用到的主要的函数:glob!用这个函数来解析命令行参数!
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <wait.h>
#include <glob.h>
#include <string.h>
#define BUFSIZE 1024
#define DELIMS " \t\n"
extern char **environ;
static int cd(char *path){
int ret = chdir(path);
if (ret == -1){
perror("chdir");
}
return ret;
}
static void readrc(char *name){
FILE *fp;
fp = fopen(name,"r+");
//处理文件内容
fclose(fp);
}
static void prompt()
{
char pwd[BUFSIZE];
char name[BUFSIZE];
getcwd(pwd,BUFSIZE);
getlogin_r(name,BUFSIZE);
printf("%s %s $ ",name,pwd);
}
static int parse(char *linebuf,glob_t *globres){
char *tok;
int flag = 0;
while (1){
tok = strsep(&linebuf,DELIMS);
if (tok == NULL){
break;
return -1;
}else if(strcmp(tok,"cd") == 0){
char *path = strsep(&linebuf,DELIMS);
return cd(path);
}else if(tok[0] == '\0'){
continue;
}
glob(tok,GLOB_NOCHECK|GLOB_APPEND*flag,NULL,globres);//第一次不能append glob_argv中是随机值 GLOB_NOCHECK | (GLOB_APPEND*flag)==0 第一次不append
flag = 1;
}
return 1;
}
//之后记得 将 ctrl+c 转为 stdout:\n 将ctrl+d 转为 退出+再见标语
int main()
{
printf("This is YSHELL\n");
pid_t pid;
char *linebuf = NULL;
size_t lienbuf_size = 0;
glob_t globres;//解析命令行
//读取配置文件
char *yshrc = "/home/yixingwei/.yshrc";//填一个绝对路径
readrc(yshrc);
while(1){
prompt();
//获取命令
getline(&linebuf,&lienbuf_size,stdin);
//解析命令
int ret = parse(linebuf,&globres);
if (ret == -1){
}else if (ret == 0){//内部命令
}else if (ret == 1){//外部命令
fflush(NULL);
pid = fork();
if (pid < 0){
perror("fork()");
exit(1);
}else if(pid == 0){
execvp(globres.gl_pathv[0],globres.gl_pathv);
perror("execl()");
exit(1);
}
}
waitpid(pid,NULL,0);
}
exit(0);
}
7 用户权限和组权限!!!
shell的passwd命令:用来更改使用者的密码。(本质上是修改shadow中的内容)
1、下面着重将一下用户权限,以及文件身份、与进程身份的关系:
首先要明白的是,一个文件主要有两个身份,分别是所有者,所在组(实际上这两个属性,就是,谁创建了这个文件,这个文件的所有者就是谁,所在组就是这个创建者所在的组),一个文件也有对应的九个访问权限标志位,分别说明了文件所有者,组内成员以及其他人的访问权限。所以进程在访问文件的时候,要进行进程身份的校验,拿进程的身份(ID以及组ID)来和文件访问权限位做检验,校验符合,进程才能访问文件。
所以系统内的一切身份的来源,就是进程自己的身份,进程的身份比较复杂,有六个身份标志(实际用户(组)ID-代表我们实际是谁,有效用户(组)ID,保存的设置用户(组)ID),进程创建的文件的文件所有者,就是有效用户ID代表的用户(课本P80)!
进程的身份又是怎么来的呢,进程的身份的由来要从系统登陆和bash讲起!
首先系统开始只有init进程,此时的用户是root(ruid、euid和suid都是0),然后init fork和exec出一个getey进程,这个进程提示输入用户名(假设我们登陆Devin用户),输入完成后,这个进程exec一个login进程,提示输入密码,这个进程负责校验用户名字和密码,到这里,进程的身份(ruid、euid以及suid等)都是root。输入完密码验证成功后,login fork然后exec出一个shelll进程,这个shelll的身份就是上面我们输入验证的身份,此时shell进程的六个身份(以ruid、ruid和suid为例),就是我们输入的身份的一整套。以后在shell中打开其他文件(包括生成进程的可执行文件)的时候,校验的是shell的身份以及文件的权限位。
下面举个例子说明一下:
下面假设系统登陆的用户是devin。
我们在修改密码的时候,实际上是修改的/etc/shadow这个文件的内容,而这个文件的访问权限是-rw-r-----,很明显我们是没有这个范围权限的。
我们的做法是通过passwd命令来修改密码。在shell中输入passwd,实际上就是闲fork一个子进程,然后exec一个可执行文件(/usr/bin/passwd)生成一个子进程。fork的子进程在没有exec之前的身份信息是和父进程一样的,/usr/bin/passwd可执行文件的访问权限是-rwsr-xr-x,这个文件的所有者和所有组都是root,我们是以Devin(普通用户)的身份访问的,而这个可执行文件中设置用户ID位有效,所以在exec这个passwd可执行文件后的子进程中,原先Devin的有效用户ID会被设置为root的ID,这样,这个子进程就会获得root的权限,这样在这个子进程中访问shadow文件就是合理的。
上面的例子需要注意的是:
- 对一个可执行文件而言,如果执行了这个文件成为了一个进程,这个进程的ruid是其父进程的ruid(其他的id身份信息也是)!!!! 而不是这个文件所有者的身份!!举个例子,一个进程fork并exec了一个由root创建的可执行文件,那么这个子进程的身份信息与父进程是一样的,而不是root!
- 也就是说,在shell下执行任何一个执行文件,生成的子进程的身份信息与父进程都是一样的。要想改变进程的身份信息,就要使用下面的setuid等等的命令。
- 文件检查访问这个的进程的身份时,检查的是他的有效用户ID与有效组ID,将这两个ID与文件的那九个权限访问位作比对!!
- 每个进程的身份信息都是以euid,ruid,suid等标志位记录的(可以看做一个结构体),很明显,这些信息里面,起码有效用户ID是可以更改的!
再举一个例子:
ubuntu下的sudo命令,实际上就是将该进程的有效用户ID设置为了root的ID!
下面这个图不准确,但是可以作为参考。
2、几个函数
上面的是在shell中进行的,下面看在代码中用到的几个函数。
getuid:返回当前进程的真实用户ID
geteuid:返回当前进程的有效用户ID
getgid:返回当前进程的真实组ID
getegid:返回当前进程的有效组ID
setuid:设置有效用户ID
setgid:设置有效组ID
setreuid:交换真实用户ID和有效用户ID
setregid:交换真实组ID和有效组ID
seteuid:设置有效用户ID
setegid:设置有效组ID
setuid(uid)首先请求内核将本进程的[真实uid],[有效uid]和[被保存的uid]都设置成函数指定的uid, 若权限不够则请求只将effective uid设置成uid, 再不行则调用失败。
seteuid(uid)仅请求内核将本进程的[有效uid]设置成函数指定的uid。、
例子:实现mysudo
要实现的是:
sudo +系统内其他用户ID+命令(可执行文件)
就是使用指定的其他的用户ID,使用其他用户的权限来执行后面的命令。
解析:在不切换到其他用户的前提下,要想使用其他用户的权限,就是将当前进程的有效用户ID改为所指定的用户的实际用户ID。(因为去执行可执行文件时,可执行文件校验的是当前进程的有效用户ID与有效组ID!)
//mysu.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <wait.h>
int main(int argc,char **argv)
{
if (argc < 3){
fprintf(stderr,"Useage");
exit(1);
}
pid_t pid;
pid = fork();
if (pid == 0){
setuid(atoi(argv[1]));
execvp(argv[2],argv+2);
perror("execvp()");
exit(1);
}else {
wait(NULL);
}
exit(0);
}
编译后执行:
./a.out 0 cat /etc/shadow
会报权限不够的错。
原因在于:
我们是在ubuntu用户(普通用户)去执行的这个可执行文件。下面看看由mysu.c编译而成的./a.out的可执行文件的文件属性信息:
这是个很简单的可执行文件,甚至很多自己编译的可执行文件都是这种权限:rwxrwxr-x,当我们用ubuntu用户执行这个文件时,(shell 先fork一个子进程,然后exec这个./a.out可执行文件),./a.out运行成为一个子进程,这个进程的实际用户ID和有效用户ID都是ubuntu(与父进程一样),至于保存的设置用户ID,如P205所说,是从有效用户复制过来的,所以也是ubuntu!这个进程运行到setuid这个函数的时候,setuid的参数是0,也就是root的ID号,如课本P204所说,这个ID即不是实际用户ID,也不是保存的设置用户ID,所以这个函数会出错,并返回-1 。也就是说,这个进程并没有完成我们期待的任务:更改进程的euid!!!
解决方法:
如上分析。要想完成我们的期待的任务,就要使./a.out这个进程要么具有ROOT特权,要么就是将这个进程的实际用户ID或者保存的设置用户ID设为root。
所以可以这样做:将./a.out的文件所有者改为root,然后设置文件的设置用户ID位,这样当ubuntu用户执行这个文件时,产生的子进程的实际用户ID是ubuntu的ID,有效用户ID是0(root的ID),保存的设置用户ID也是0(见P205),所在这个进程在执行到setuid时,会成功更改有效用户ID(这个例子不会改,因为一样)。
修改后的运行结果:
8 解释器文件(脚本文件)
一个小例子:
#! /bin/bash
ls
whoami
cat /etc/shadow
ps
假设这个文件的名字是mysh.sh。(一般监本后缀为sh,但是这不是必须的,unix中没有后缀而言)
没有执行权限,执行chmod u+x mysh.sh (chmod要求进程的有效用户ID等于文件的所有者ID)
然后./mysh.sh,得到:
9 补充
top命令:查看系统的资源、进程、磁盘内存占用率等
10 守护进程
1、进程组和会话的概念
2、进程组和会话的几个函数
pid_t getpgrp(void)
pid_t getpgid(pid_t pid)
int setgpid(pid_t pid,pid_t pgid)
3、例子
实现的功能:守护进程不断的向某一个文件写内容。
#include <stdio.h>
#include <stdlib.h>
#include <sys/syslog.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <syslog.h>
#define FNAME "/tmp/out"
static int deamonize(){
int fd;
pid_t pid;
pid = fork();
if (pid < 0){
return -1;
}
if (pid > 0){
exit(0);
}
fd = open("/dev/null",O_RDWR);//输出都忽略
if (fd < 0){
return -1;
}
if (pid == 0){
printf("test");
fflush(NULL);
dup2(fd,0);
dup2(fd,1);
dup2(fd,2);
if (fd > 2){
close(fd);
}
setsid();//脱离终端
//umask();
chdir("/");
}
return 0;
}
int main()
{
FILE* fp;
//开启日志服务
openlog("print i",LOG_PID,LOG_DAEMON);
if (deamonize()){
syslog(LOG_ERR,"init failed!");
}else{
syslog(LOG_INFO,"successded!");
}
fp = fopen(FNAME,"w+");
if (fp == NULL){
syslog(LOG_ERR,"write file failed!");
exit(1);
}
syslog(LOG_INFO,"%s opened",FNAME);
for(int i = 0; ;i++){
fprintf(fp,"%d\n",i);
fflush(NULL);
syslog(LOG_DEBUG,"%d 写入",i);
sleep(1);
}
closelog();
fclose(fp);
exit(0);
}
4、单实例守护进程,见课本
5、守护进程的启动脚本文件:/etc/rc*…,里面可以自己加想成为守护进程(开机启动)的程序文件。
11 系统日志
1、系统的日志的存放地方是/var/log/
2、实际上,实际上的日志是由syslogd这个系统服务去写的,所有写系统日志的用户都将所要写的日志提交给这个服务,由这个服务统一去写。
3、日志相关的函数
openlog
syslog
4、没有新东西,见课本即可,例子可参考上面的程序