• 进程控制~


    一、进程标识

    1. 每个进程都有一个非负整型表示的唯一进程ID。因为进程ID标识符总是唯 一的,常将其用作其他标识符的一部分以保证其唯一性。进程唯一但可复用。

    2. ID为0表示调度进程,被称为交换进程。

    3. ID为1表示init进程,不会终止,普通用户进程以超级用户特权运行。

    4. UNIX系统实现都有一天提供操作系统服务的内核进程,如:ID2为页守护进程,支持虚拟存存储器系统的分页。

    //用于返回进程其他标识符:
    #include  
    pid_t getpid(void); //返回值:调用进程的进程ID 
    pid_t getppid(void); //返回值:调用进程的父进程ID 
    uid_t getuid(void); //返回值:调用进程的实际用户ID 
    uid_t geteuid(void);// 返回值:调用进程的有效用户ID
    gid_t getgid(void); //返回值:调用进程的实际组ID
    gid_t getegid(void);//返回值:调用进程的有效组ID
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    二、函数vfork

    由于frok会对父进程的数据段、堆和栈设行严格的复杂,造成浪费。所有引入vfork系统调用,效率更高,但UNIX采用写时复制技术实现fork(),效率提供了很多,进而将vfork()的需求剔除殆尽。

    
    #include
    pid vfork(void);
    
    • 1
    • 2
    • 3

    vfork相当于fork区别和特性:

    1. 无需为子进程复制虚拟内存页或页表。相反,子进程共享父进程的内存,直至其成功执行了exec()或调用_exit()退出。
    2. 在子进程调用exec()或_exit()之前,将暂停执行父进程。
    3. vfrok执行调用后,系统保证子进程优先于父进程获得调度以使用CPU。
    4. 由于子进程使用父进程的内存,因此子进程对数据段、对或栈的任何改变将在父进程恢复执行时为其所见。
    5. 子进程在vfork与后续的exec或_exit()之间执行了函数返回,也会影响父进程。
    6. vfork()之后调用exec()。exec()执行失败,子进程应调用_exit()退出。
    //子进程共享父进程的内存,父进程会一直挂起直到子进程终止或调用exec。
    int main(int argc, char *argv[])
    {
        int istack = 222;
    
        switch (vfork()) {
        case -1:
            errExit("vfork");
    
        case 0:             /* 子进程首先在父进程的内存空间中执行*/
            sleep(3);                   
                                          
            write(STDOUT_FILENO, "Child executing\n", 16);
            istack *= 3;                //更改
            _exit(EXIT_SUCCESS);
    
        default:            /* 父进程被阻止,直到子级退出 */
            write(STDOUT_FILENO, "Parent executing\n", 17);
            printf("istack=%d\n", istack);
            exit(EXIT_SUCCESS);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    三、函数wait3和wait4

    //提供的功能比wait、waitpid和waitid所提供功能的要多一个,
    //附加参数:内核返回由终止进程及其所有子进程使用的资源概况。
    #include 
    #include  
    #include  
    #include 
    pid_t wait3(int *statloc, int options, struct rusage *rusage); //等待任意子进程
    pid_t wait4(pid_t pid, int *statloc, int options, struct rusage *rusage); //选择等待
    //两个函数返回值:若成功,返回进程ID;若出错,返回−1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    资源统计信息包括用户CPU时间总量、系统CPU时间总量、缺页次数、接 收到信号的次数等。

    在这里插入图片描述

    四、竞争条件

    1. 调用fork()后,无法确定父、子进程谁将率先访问CPU,多个进程都企图对共享数据进行某种处理,而最后的结果又取决于进程运行的顺序时,会发生进程条件。

    2. 如果在 fork 之后的某种逻辑显式或隐式地依赖于在fork 之后是父进程先运行还是子进程先运行,那么 fork 函数就会是竞争条件活跃的滋生地。

    3. 若一个进程希望等待一个子进程终止,必须调用wait函数中一个。如果一个进程要等待父进程终止,则使用循环while(getppid() !=1)sleep(1);,这种形式循环称为轮询。但这种方法浪费CPU时间,因为每隔一秒就被唤醒。

    4. 为了避免竞争条件轮询,在多个进程之间需要有某种形式的信号发送和接收的方法。在UNIX中可以使用信号机制,各种形式的进程间通信(IPC)也可使用。

    例:输出两个字符串,一个由子进程输出,另一个由父进程输出。输出依赖内核使这两个进程运行的实现和每个进程运行的时间长度,存在竞争条件。

    
    #include "apue.h"
    static void charatatime(char *);
    
    int main(void)
    {
    	pid_t	pid;
    
    	if ((pid = fork()) < 0) {
    		err_sys("fork error");
    	} else if (pid == 0) {
    		charatatime("output from child\n");
    	} else {
    		charatatime("output from parent\n");
    	}
    	exit(0);
    }
    
    
    static void charatatime(char *str)
    {
    	char	*ptr;
    	int		c;
    
    	setbuf(stdout, NULL);			/*设置无缓冲 */
    	for (ptr = str; (c = *ptr++) != 0; )
    		putc(c, stdout);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    1. 输出设置为不带缓冲的,于是每个字符输出都需调用一次 write

    2. 本例的目的是使内核能尽可能多次地在两个进程之间进行切换,以便演 示竞争条件

    3. 结果确没有错误但并不意外着竞争条件不存在,只是意味着在此特定的系统 上未能见到它

    4. 下面的实际输出说明该程序的运行结果是会改变的。

    在这里插入图片描述
    5 个例程 TELLWAIT、TELL PARENT、TELL_CHILD、WAIT_PARENT以及 WAIT_CHILD可以是宏,也可以是函数。使用TELL和WAIT函数修改后:两个进程的输出不再交叉混合,使父进程先运行。

    #include "apue.h"
    static int	pfd1[2], pfd2[2];
    static void charatatime(char *);
    
    
    void TELL_WAIT(void)
    {
    	if (pipe(pfd1) < 0 || pipe(pfd2) < 0)
    		err_sys("pipe error");
    }
    
    /*
    void TELL_PARENT(pid_t pid)
    {
    	if (write(pfd2[1], "c", 1) != 1)
    		err_sys("write error");
    }
    */
    
    void WAIT_PARENT(void)
    {
    	char	c;
    
    	if (read(pfd1[0], &c, 1) != 1)
    		err_sys("read error");
    
    	if (c != 'p')
    		err_quit("WAIT_PARENT: incorrect data");
    }
    
    void TELL_CHILD(pid_t pid)
    {
    	if (write(pfd1[1], "p", 1) != 1)
    		err_sys("write error");
    }
    
    
    /*
    void WAIT_CHILD(void)
    {
    	char	c;
    
    	if (read(pfd2[0], &c, 1) != 1)
    		err_sys("read error");
    
    	if (c != 'c')
    		err_quit("WAIT_CHILD: incorrect data");
    }
    */
    
    
    int main(void)
    {
    	pid_t	pid;
    
    	TELL_WAIT();
    
    	if ((pid = fork()) < 0) {
    		err_sys("fork error");
    	} else if (pid == 0) {
    		WAIT_PARENT();		/* parent goes first */
    		charatatime("output from child\n");
    	} else {
    		charatatime("output from parent\n");
    		TELL_CHILD(pid);
    	}
    //修改为下面形式则先运行子进程:
    /*
    else if (pid == 0)
     { 
     charatatime("output from child\n"); 
     TELL_PARENT(getppid()); 
     }else {
     WAIT_CHILD(); 
     charatatime("output from parent\n");
      }
      */
      
    	exit(0);
    }
    
    
    
    static void charatatime(char *str)
    {
    	char	*ptr;
    	int		c;
    
    	setbuf(stdout, NULL);			/* set unbuffered */
    	for (ptr = str; (c = *ptr++) != 0; )
    		putc(c, stdout);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92

    五、更改用户ID和更改组ID

    1. 特权以及访问控制是基于用户ID和组ID。

    2. 当程序需要增加特权或需要访问当前不允许访问的资源时,需要更换用户ID或组ID,使得新ID具有合适的特权或访问权限。

    3. 当程序需要降低其特权或阻止 对某些资源的访问时,也需要更换用户ID或组ID,新ID不具有相应特权或访问 这些资源的能力。

    4. 设计应用时,使用最小特权模 型,依照此模型,我们的程序应当只具有为完成给定任务所需的最小特权。降低了由恶意用户试图哄骗我们的程序以未预料的方式使用特权造成的安全性 风险。

    #include 
    int setuid(uid_t uid); //设置实际用户ID和有效用户ID。
    int setgid(gid_t gid); //setgid函数设置实际组ID合有效组ID。
    //两个函数返回值:若成功,返回0;若出错,返回−1
    
    • 1
    • 2
    • 3
    • 4

    更改用户ID的规则:

    1. 若进程具有超级用户特权,则setuid函数将实际用户ID、有效用户ID 以及保存的设置用户ID设置为uid。

    2. 若进程没有超级用户特权,但是uid等于实际用户ID或保存的设置用 户ID,则setuid只将有效用户ID设置为uid。不更改实际用户ID和保存的设置用 户ID。

    3. 如果上面两个条件都不满足,则errno设置为EPERM,并返回−1。

    4. 假定_POSIX_SAVED_IDS 为真。如果没有提供这种功能,则上面所说 的关于保存的设置用户ID部分都无效。

    内核维护的3个用户ID注意事项:

    • 只有超级用户进程可以更改实际用户ID。

      • 仅当对程序文件设置了设置用户ID位时,exec函数才设置有效用户 ID。若设置用户ID位没有设置,exec函数不会改变有效用户ID,而将维持其 现有值。任何时候都可以调用setuid,将有效用户ID设置为实际用户ID或保存的设置用户ID。不能将有效用户ID设置为任一随机值。

        • 保存的设置用户ID是由exec复制有效用户ID而得到的。

    更改3个用户ID的不同方法:
    在这里插入图片描述
    注:getuid和geteuid函数只能获得实际用户ID和有效用户 ID的当前值,没有可移植的方法去获得保存的设置用户ID的当前值。

    1.函数setreuid和setregid

    //是交换实际用户ID和有效用户ID的值。
    #include 
     int setreuid(uid_t ruid, uid_t euid); 
     int setregid(gid_t rgid, gid_t egid); 
     //其中任一参数的值为−1,则表示相应的ID应当保持不变。
     //两个函数返回值:若成功,返回0;若出错,返回-1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    1. 一个非特权用户总能交换实际用户ID和有效用户ID。这就允 许一个设置用户ID程序交换成用户的普通权限,以后又可再次交换回设置用户 ID权限
    2. POSIX.1引进了保存的设置用户ID特性后,其规则也相应加强,它允 许一个非特权用户将其有效用户ID设置为保存的设置用户ID。

    2.函数seteuid和setegid

    //类似于setuid和setgid,但只更改有效用户ID和有效组ID。
    #include  
    int seteuid(uid_t uid); 
    int setegid(gid_t gid); 
    //两个函数返回值:若成功,返回0;若出错,返回−1
    //一个非特权用户可将其有效用户ID设置为其实际用户ID或其保存的设置用户ID。
    //对于特权用户则可将有效用户ID设置为uid。(区别于setuid函数,它更改所有3个用户ID。)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    更改3个不同用户ID的各个函数,如图所示:
    在这里插入图片描述

    3.组ID

    前面中所说明的一切都以类似方式适用于各个组 ID。附属组 ID 不受 setgid、setregid和setegid函数的影响。

    六、 解释器文件

    解释器文件称文本文件,能够读取并执行文本格式命令的程序。其起始行的形式是:

    #! pathname [ optional-argument ]
    //pathname通常是绝对路径名。
    //常见的解释器文件以下列行开始:
    #! /bin/sh
    
    //exec系统识别这种文件。
    //exec函数的进程实际执行的并不是该解释器文件,而是在该解释器文件第一行中pathname所指定的文件。
    //一定要将解释器文件(文本文件,它以#!开头)和解释器(由该解释器文件第一行中的pathname指定)区分开来。
    //很多系统对解释器文件第一行有长度限制。这包括#!、pathname、可选参数、终止换行符以及空格数
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    七、 函数system

    执行任意的shell命令,如将时间和日期放到某一个文件中,可以使用:

    system("data>file");
    
    • 1
    
    #include
    int system(const char *cmdstring);
    //cmdstring是一个空指针,则仅当命令处理程序可用时,system返回非0值。
    
    
    • 1
    • 2
    • 3
    • 4
    • 5

    system在其实现中调用了fork、exec和waitpid,因此有3种返回值:

    1. fork失败或者waitpid返回除EINTR之外的出错,则system返回−1,并 且设置errno以指示错误类型。
    2. 如果 exec失败(表示不能执行 shell),则其返回值如同 shell执行了 exit(127)一样。
    3. 否则所有3个函数(fork、exec和waitpid)都成功,那么system的返回 值是shell的终止状态,其格式已在waitpid中说明。

    例:system函数实现,没有对信号进行处理。

    #include "apue.h"
    #include 
    
    
    void pr_exit(int status)
    {
    	if (WIFEXITED(status))
    		printf("normal termination, exit status = %d\n",
    				WEXITSTATUS(status));
    	else if (WIFSIGNALED(status))
    		printf("abnormal termination, signal number = %d%s\n",
    				WTERMSIG(status),
    #ifdef	WCOREDUMP
    				WCOREDUMP(status) ? " (core file generated)" : "");
    #else
    				"");
    #endif
    	else if (WIFSTOPPED(status))
    		printf("child stopped, signal number = %d\n",
    				WSTOPSIG(status));
    }
    
    
    
    int main(void)
    {
    	int		status;
    
    	if ((status = system("date")) < 0)
    		err_sys("system() error");
    
    	pr_exit(status);
    
    	if ((status = system("nosuchcommand")) < 0)
    		err_sys("system() error");
    
    	pr_exit(status);
    
    	if ((status = system("who; exit 44")) < 0)
    		err_sys("system() error");
    
    	pr_exit(status);
    
    	exit(0);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

    使用system而不是直接使用fork和exec的优点是:system进行了所需的各种 出错处理以及各种信号处理。

    1.设置用户ID程序

    不应该用户ID程序调用system,调用存在安全性问题。

    //只对其命令行参数调用system函数。
    #include "apue.h"
    
    int main(int argc, char *argv[])
    {
    	int		status;
    
    	if (argc < 2)
    		err_quit("command-line argument required");
    
    	if ((status = system(argv[1])) < 0)
    		err_sys("system() error");
    
    	pr_exit(status);
    
    	exit(0);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    //打印实际用户ID和有效ID:
    
    int main(void)
    {
    	printf("real uif = %d,effective uid = %d\n",getuid(),geteuid());
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    同时运行两个程序:

    在这里插入图片描述

    1. 给予tsys程序的超级用户权限在system中执行了fork和exec之后仍被保持下来。

    2. 一个进程正以特殊的权限(设置用户ID或设置组ID)运行,它又想生成另一个进程执行另一个程序,则它应当直接使用fork和exec,而且在fork之 后、exec之前要更改回普通权限。

    3. 设置用户ID或设置组ID程序决不应调用 system函数。

    八、 进程会计

    • UNIX系统提供了一个选项以进行进程会计处理。启用该选项后,每当进程结束时内核就写一个会计记录。
      • 典型的会计记录 包含总量较小的二进制数据,一般包括命令名、所使用的CPU时间总量、用户 ID和组ID、启动时间等。
        • 函数acct启用和禁用进程会计。唯一使用这一函数 的是accton(8)命令。。超 级用户执行一个带路径名参数的accton命令启用会计处理。

        • 会计记录对应于进程而不是程序。

    会计记录结构定义在头文件中,

    typedef u_short comp_t; 
     struct acct { 
    char ac_flag; 
    char ac_stat; 
    uid_t ac_uid; 
    gid_t ac_gid; 
    dev_t ac_tty; 
    time_t ac_btime; 
    comp_t ac_utime; 
    comp_t ac_stime; 
    comp_t ac_etime;
    comp_t ac_mem; 
    comp_t ac_io; 
    comp_t ac_rw; 
    char ac_comm[8];  
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    ac_flag记录进程执行期间的某些事件:
    在这里插入图片描述
    会计记录所需的各个数据(各CPU时间、传输的字符数等)都由内核保存 在进程表中,并在一个新进程被创建时初始化(如fork之后在子进程中)。进程 终止时写一个会计记录。这产生两个后果。

    1. 不能获取永远不终止的进程的会计记录。

    2. 不能获取永远不终止的进程的会计记录。

    九、用户标识

    任一进程都可以得到其实际用户ID和有效用户ID及组ID。使用getpwuid(getuid)获取用户登录名,用户有多个登录,对应同一个ID,系统通常记录用户登录使用的名字。

    //获取登录名
    #include
    char *getlogin(void);
    //返回值:若成功,返回指向登录名字符串的指针;若出错,返回NULL
    //调用此函数的进程没有连接到用户登录时所用的终端,则函数会失败。称这些进程为守护进程(daemon)。
    //给出了登录名,可用getpwnam在口令文件中查找用户的相应记录,从而确定其登录shell等。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    十、进程调度

    调 度策略和调度优先级是由内核确定的。进程可以通过调整nice值选择以更低优先级运行(通过调整nice值降低它对CPU的占有,因此该进程是“友好的”)。只有特权进程允许提高调度权限。nice值越小,优先级越高。NZERO是系统默认 的nice值。

    //获取或更改它的nice值
    //进程只能影响自己的nice值,不能影响任何其他进程的nice值。
    #include 
     int nice(int incr);
    //incr参数被增加到调用进程的nice值上。
    //incr太大,系统直接把它降到 最大合法值,不给出提示。
    //incr太小,系统也会无声息地把它提高到最小合法值。
     //由于−1是合法的成功返回值,在调用nice函数之前需要清楚 errno, 在nice函数返回−1时,需要检查它的值。
     //nice调用成功,并且返回值为−1,那么errno仍然为0。如果errno不为0,说明nice调用失败。
     //返回值:若成功,返回新的nice值NZERO;若出错,返回−1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    //像nice函数那样用于获取进程的nice值,但还可以获取一组相关进程的nice值。
    #include 
     int getpriority(int which, id_t who);
    //which参数:PRIO_PROCESS 表示进程,PRIO_PGRP表示进程组,PRIO_USER表示用户ID。
    //who参数选择感兴趣的一个或多个进程。
    //who参数为0,表示调用进程、进程组或者用户(取决于which参数的值)。
    //当which设为 PRIO_USER并且who为0时,使用调用进程的实际用户ID。
    //果which参数作用 于多个进程,则返回所有作用进程中优先级最高的(最小的nice值)。
    // 返回值:若成功,返回-NZERO~NZERO-1之间的nice值;若出错,返回−1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    //为进程、进程组和属于特定用户ID的所有进程设置优先级。
    #include  
    int setpriority(int which, id_t who, int value);
    //参数which和who与getpriority函数中相同。
    //value增加到NZERO上,然后变 为新的nice值。
    // 返回值:若成功,返回0;若出错,返回−1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    例:下面程序度量了调整进程nice值的效果。

    • 两个进程并行运行,各自增加 自己的计数器。

      • 父进程使用了默认的nice值,子进程以可选命令参数指定的调整 后的nice值运行。运行10 s后,两个进程都打印各自的计数值并终止。

        • 通过比较 不同nice值的进程的计数值的差异,我们可以了解nice值时如何影响进程调度 的。
    #include "apue.h"
    #include 
    #include 
    
    #if defined(MACOS)
    #include 
    #elif defined(SOLARIS)
    #include 
    #elif defined(BSD)
    #include 
    #endif
    
    unsigned long long count;
    struct timeval end;
    
    void checktime(char *str)
    {
    	struct timeval	tv;
    
    	gettimeofday(&tv, NULL);
    	if (tv.tv_sec >= end.tv_sec && tv.tv_usec >= end.tv_usec) {
    		printf("%s count = %lld\n", str, count);
    		exit(0);
    	}
    }
    
    int main(int argc, char *argv[])
    {
    	pid_t	pid;
    	char	*s;
    	int		nzero, ret;
    	int		adj = 0;
    
    	setbuf(stdout, NULL);
    #if defined(NZERO)
    	nzero = NZERO;
    #elif defined(_SC_NZERO)
    	nzero = sysconf(_SC_NZERO);
    #else
    #error NZERO undefined
    #endif
    	printf("NZERO = %d\n", nzero);
    	if (argc == 2)
    		adj = strtol(argv[1], NULL, 10);
    	gettimeofday(&end, NULL);
    	end.tv_sec += 10;	/* run for 10 seconds */
    
    	if ((pid = fork()) < 0) {
    		err_sys("fork failed");
    	} else if (pid == 0) {	/* child */
    		s = "child";
    		printf("current nice value in child is %d, adjusting by %d\n",
    		  nice(0)+nzero, adj);
    		errno = 0;
    		if ((ret = nice(adj)) == -1 && errno != 0)
    			err_sys("child set scheduling priority");
    		printf("now child nice value is %d\n", ret+nzero);
    	} else {		/* parent */
    		s = "parent";
    		printf("current nice value in parent is %d\n", nice(0)+nzero);
    	}
    	for(;;) {
    		if (++count == 0)
    			err_quit("%s counter wrap", s);
    		checktime(s);
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67

    执行该程序两次:一次用默认的nice值,另一次用最高有效nice值(最低调 度优先级)。在这里插入图片描述

    1. 当两个进程的nice值相同时,父进程占用50.2%的CPU,子进程占用49.8% 的CPU。可以看到,两个进程被有效地进行了平等对待。百分比并不完全相 同,是因为进程调度并不精确,而且子进程和父进程在计算结束时间和处理循 环开始时间之间执行了不同数量的处理。

    2. 相比之下,当子进程有最高可能nice值(最低优先级)时,我们看到父进程 占用98.5%的CPU,而子进程只占用1.5%的 CPU。这些值取决于进程调度程序 如何使用 nice 值,因此不同的 UNIX系统会产生不同的CPU占用比。

    十一、实时进程调用API

    进程调度API的各个系统调用,这些系统调用允许控制进程调度策略和优先级·。

    1.实时优先级范围

    下列函数返回一个调度策略的优先级取值范围。

    #include
    int sched_get_priority_min(int policy);//返回最小优先级
    int sched_get_priority_max(int policy);//返回最大优先级
    
    • 1
    • 2
    • 3
    1. 参数policy指定了需要获取哪种调度策略的信息,取值范围一般是SCHED_RR或SCHED_FIFO

    2. SCHED_RR策略中最低的优先级应该是sched_get_min(SCHED_FIFO),比它高一级的优先级是sched_get_priority_min(SCHED_FIFO)+1

    2、修改和获取策略和优先级

    此函数调用修改进程ID为pid的进程的调度策略和优先级。pid为0,那么将会修改调用进程的特性。

    #inclue<sched.h>
    int sched_setcheduler(pid_t pid,int policy,const strcut sched_param *param);
    
    //param:指下列结构体指针:
    struct sched_param{
    		int sched_priority;//指定调度策略。
    		//对于SCHED_RR何SCHED_FIFO来说这个字段值必须位于 sched_get_priority_min和 sched_get_priority_max规定范围内:对于其他策略,优先级必须为0。
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    参数poliy属性值:

    在这里插入图片描述

    1. 成功调用此函数会将pid指定的进程移到与其优先级级别对应的队列的队尾。

    2. 成功调用该函数返回0,一个可移植性的应用程序应该通过检查返回值是否为-1来判定调用是否成功。

    3. 通过fork()创建的子进程会继承父进程的调度策略和优先级,并且在exec()调用中会报错这些信息。

    sched_setparam()系统调用提供了sched_setcheduler()函数的一个功能子集。修改一个仅的调度策略但不会修改其优先级。

    #include
    int sched_setparam(pid_t pid,const struct sched_parm *param);
    //pid和param参数和sched_setcheduler()一样。
    //成功调用此函数会将pid指定的进程移到与其优先级相同的对应的队列队尾。
    
    • 1
    • 2
    • 3
    • 4

    3.修改进程的调度策略和优先级

    使用sched_setscheduler函数设置由命令行参数指定的进程的策略和优先级。第一个参数指定调度策略的字母,第二个是该整数优先级,剩下的参数是需修改调度特性的进程的进程ID。

    #include 
    
    int main(int argc, char *argv[])
    {
        int j, pol;
        struct sched_param sp;
    
        if (argc < 3 || strchr("rfobi", argv[1][0]) == NULL)
            usageErr("%s policy priority [pid...]\n"
                    "    policy is 'r' (RR), 'f' (FIFO), "
    #ifdef SCHED_BATCH              /* Linux-specific */
                    "'b' (BATCH), "
    #endif
    #ifdef SCHED_IDLE               /* Linux-specific */
                    "'i' (IDLE), "
    #endif
                    "or 'o' (OTHER)\n",
                    argv[0]);
    
        pol = (argv[1][0] == 'r') ? SCHED_RR :
                    (argv[1][0] == 'f') ? SCHED_FIFO :
    #ifdef SCHED_BATCH
                    (argv[1][0] == 'b') ? SCHED_BATCH :
    #endif
    #ifdef SCHED_IDLE
                    (argv[1][0] == 'i') ? SCHED_IDLE :
    #endif
                    SCHED_OTHER;
    
        sp.sched_priority = getInt(argv[2], 0, "priority");
    
        for (j = 3; j < argc; j++)
            if (sched_setscheduler(getLong(argv[j], 0, "pid"), pol, &sp) == -1)
                errExit("sched_setscheduler");
    
        exit(EXIT_SUCCESS);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    4.获取调度策略和优先级

    下例函数用于获取进程的调度策略和优先级。

    #include
    int sched_getscheduler(pid_t pid);
    int sched_getparam(pid_t pid,struct sched_param *param);
    //pid:指定程序的进程ID,pid为0,查询调用进程的信息。
    
    • 1
    • 2
    • 3
    • 4

    sched_getparam函数调用返回由param指向的sched_param结构中sched_priority字段指定的进程的实时优先级。

    函数执行成功,返回如下图中一个策略:
    在这里插入图片描述

    例:使用sched_getscheduler和sched_getparam函数获取进程ID为命令行参数指定的数值的进程的策略和优先级。

    #include
    #include 
    int main(int argc, char *argv[])
    {
        int j, pol;
        struct sched_param sp;
    
        for (j = 1; j < argc; j++) {
            pol = sched_getscheduler(getLong(argv[j], 0, "pid"));
            if (pol == -1)
                errExit("sched_getscheduler");
    
            if (sched_getparam(getLong(argv[j], 0, "pid"), &sp) == -1)
                errExit("sched_getparam");
    
            printf("%s: %-5s ", argv[j],
                    (pol == SCHED_OTHER) ? "OTHER" :
                    (pol == SCHED_RR) ? "RR" :
                    (pol == SCHED_FIFO) ? "FIFO" :
    #ifdef SCHED_BATCH              /* Linux-specific */
                    (pol == SCHED_BATCH) ? "BATCH" :
    #endif
    #ifdef SCHED_IDLE               /* Linux-specific */
                    (pol == SCHED_IDLE) ? "IDLE" :
    #endif
                    "???");
            printf("%2d\n", sp.sched_priority);
        }
    
        exit(EXIT_SUCCESS);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    在这里插入图片描述

    十二、释放CPU

    实时进程可以通过两种方式自愿释放CPU:通过调用阻塞进程的系统调用(从终端read())或调用sched_yield()。

    #include
    int sched_yield(void);
    
    • 1
    • 2
    1. 若存在与调用进程的优先级相同的其他排队的可运行进程,那么调用进程和被放在队列的队尾,队列在队头的进程被调度使用CPU。
    2. 若在该优先级队列不存在可运行的进程,那么此函数不做任何事,调用进程继续使用CPU。

    十四、SCHED_RR时间片

    此函数用于找出SCHED_RR进程在每次被授权使用CPU时分配到的时间片的长度。

    #include
    int sched_rr_get_interval(pid_t pid,struct timespec *tp);
    //pid:需查询信息的进程,pid为0表示调用进程。
    //tp指向下列结构
    struct timespec{
    	time_t tv_sec;
    	long tv_nsec;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    十五、CPU亲和力

    设置CPU亲和力的原因:

    1. 可以避免由使高速缓冲器中的数据失效所带来的性能影响。
    2. 如果多个线程(或进程)访问同样数据,那么当将它们限制在同样的CPU上的话可能带来性能提升,因为无需竞争数据并且也不存在由此产生的高速缓冲器未命中。
    3. 对于时间关键的应用程序来说,可能需要为此应用程序预留一个或更多CPU,而将系统中大多数进程限制在其他CPU上。
    //设置pid指定进程的CPU亲和力,pid为0,那么调用进程的CPU亲和力就会被修改。
    #include
    int sched_setaffinity(pid_t pid,size_t len,cpu_set_t *set);
    //set指定的CPU都不匹配则返回EINVAL错误。
    
    //赋给进程CPU亲和力由set指向的cpu_set_t结构指定。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    cpu_set_t数据类型实现为一个位掩码,但应该将其看成一个不透明的结构。所有对这个结构操作都应该使用宏CPU_ZERO()、CPU_SET()、CPU_CLR()和CPU_ISSET()来完成。

    #include
    void CPU_ZERO(cpu_set_t *set);
    void CPU_SET(int cpu,cpu_set_t *set);
    void CPU_CLR(int cpu,cpu_set_t *set);
    int CPU_ISSET(int cpu,cpu_set_t *set);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    下面这些宏操作set指向的CPU集合:

    1. CPU_ZERO()将set初始化为空。

    2. CPU_SET()将CPU cpu添加到set中。

    3. CPU_CLR()从set中删除CPU cpu。

    4. CPU_ISSET()在CPU cpu是set的一个成员时返回true。

    5. CPU集合中的CPU从0开始编号。头文件定义了CPU_SETSIZE(1024),它是从cpu_set_t变量能够表示的最大CPU编号还要大的一个数字。

    传递给sched_setaffinity函数的len参数应该知道参数中字节数(sizeof(cpu_set_t))。

    例:将pid标识的进程限制在四处理器系统上除第一个CPU之外的任意CPU上运行:

    cpu_set_t set;
    CPU_ZERO(&set):
    CPU_SET(1,&set):
    CPU_SET(2,&set);
    CPU_SET(3,&set);
    sched_setaffinity(pid,CPU_SETSIZE,&set);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 运行调用进程的CPU不包含在set中,那么进程会被迁移到set中的一个CPU上。

      • 非特权进程只有在有效用户ID与目标进程的真实或有效用户ID匹配时才能够设置进程的CPU亲和力。特权(CAP_SYS_NICE)进程可以设置进程的CPU亲和力。
    //获取pid指定的进程的CPU亲和力掩码。pid为0那么就返回调用进程的CPU亲和力掩码。
    #include
    int sched_getaffinity(pid_t pid,size_t len,cpu_set_t *set);
    
    • 1
    • 2
    • 3
    1. 返回的CPU亲和力掩码位于set指向的cpu_set_t结构中,同时应该len参数设置为结构中包含的字节数,即sizeof(cpu_set_t)。使用CPU_ISSET()宏能够确定那些CPU位于set中。

    2. 若目标进程的CPU亲和力掩码并没有被修改,那么sched_getaffinity()返回包含系统中所有CPU集合。

    3. 此函数执行时不会进行权限检查,非特权进程能够获取系统上所有进程的CPU亲和力掩码。

    4. 通过fork()创建的子进程会继承其父进程的CPU亲和力掩码并且在exec调用之间掩码会得以保留。

    十三、进程时间

    墙上时钟时间、用户CPU时间 和系统CPU时间。任一进程都可调用times函数获得它自己以及已终止子进程的 上述值。

    #include  
    clock_t times(struct tms *buf)); 
    //返回值:若成功,返回流逝的墙上时钟时间(以时钟滴答数为单位);
    //出错,返回-1
    //
    
    //由buf指向的tms结构,该结构定义如下: 
    struct tms { 
    clock_t tms_utime; /* user CPU time */ 
    clock_t tms_stime; /* system CPU time */
    clock_t tms_cutime; /* user CPU time,terminated children */
    clock_t tms_cstime; /* system CPU time,terminated children */
     }; 
    
    //此结构没有包含墙上时钟时间。
    //times函数返回墙上时钟时间作为函数值。
    //此值是相对于过去的某一时刻度量的,所以不能用其绝对值而必须使 用其相对值。
    //该结构中两个针对子进程的字段包含了此进程用本章开始部分的wait函数族已等待到的各子进程的值。
    //所有由此函数返回的clock_t值都用_SC_CLK_TCK转换成秒数。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    //将每个命令行参数作为shell命令串执行,对每个命令计时,并打印从tms结构取得的值。
    #include "apue.h"
    #include 
    
    static void	pr_times(clock_t, struct tms *, struct tms *);
    static void	do_cmd(char *);
    
    int main(int argc, char *argv[])
    {
    	int		i;
    
    	setbuf(stdout, NULL);
    	for (i = 1; i < argc; i++)
    		do_cmd(argv[i]);	/* once for each command-line arg */
    	exit(0);
    }
    
    static void do_cmd(char *cmd)		/* execute and time the "cmd" */
    {
    	struct tms	tmsstart, tmsend;
    	clock_t		start, end;
    	int			status;
    
    	printf("\ncommand: %s\n", cmd);
    
    	if ((start = times(&tmsstart)) == -1)	/* starting values */
    		err_sys("times error");
    
    	if ((status = system(cmd)) < 0)			/* execute command */
    		err_sys("system() error");
    
    	if ((end = times(&tmsend)) == -1)		/* ending values */
    		err_sys("times error");
    
    	pr_times(end-start, &tmsstart, &tmsend);
    	pr_exit(status);
    }
    
    static void pr_times(clock_t real, struct tms *tmsstart, struct tms *tmsend)
    {
    	static long		clktck = 0;
    
    	if (clktck == 0)	/* fetch clock ticks per second first time */
    		if ((clktck = sysconf(_SC_CLK_TCK)) < 0)
    			err_sys("sysconf error");
    
    	printf("  real:  %7.2f\n", real / (double) clktck);
    	printf("  user:  %7.2f\n",
    	  (tmsend->tms_utime - tmsstart->tms_utime) / (double) clktck);
    	printf("  sys:   %7.2f\n",
    	  (tmsend->tms_stime - tmsstart->tms_stime) / (double) clktck);
    	printf("  child user:  %7.2f\n",
    	  (tmsend->tms_cutime - tmsstart->tms_cutime) / (double) clktck);
    	printf("  child sys:   %7.2f\n",
    	  (tmsend->tms_cstime - tmsstart->tms_cstime) / (double) clktck);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
  • 相关阅读:
    Ubuntu-虚拟机常见问题
    Java基于PHP+MySQL干洗店管理系统的设计与实现
    VISUAL STUDIO调试器指南---断点和跟踪点
    【Android学习】日期和时间选择对话框
    神经网络输出层的作用,神经网络输出表达式
    生态系统NPP及碳源、碳汇模拟实践技术应用
    原生小程序小话题——数据绑定、列表渲染和条件渲染
    C++STL-string类的使用
    rocketmq消息发送能不能接收,具体什么情况下能接收,代码演示答疑解惑
    Vue理解01
  • 原文地址:https://blog.csdn.net/weixin_50866517/article/details/126913715