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、没有新东西,见课本即可,例子可参考上面的程序

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值