第七章:进程环境
7.2 main函数
main函数的原型是
int main(int argc, char * argv) ;
#argc是命令行参数的个数,argv是指向参数的各个指针构成的数组
- 当内核调用C程序时,在调用main 之前先调用一个启动例程,可执行文件将此启动例程指定为程序的起始地址。启动例程从内核取得命令行参数和环境变量的值。然后为调用main函数做好准备。
7.3 进程终止
- 进程终止有八种方式,其中五种为正常终止
- 正常终止
- 从main返回
- 调用exit
- 调用_exit或_Exit
- 最后一个线程从其启动例程返回
- 最后一个线程调用pthread_exit
- 异常终止
- 调用abort
- 接到一个信号
- 最后一个线程对取消请求作出响应
- 正常终止
- 退出函数
-三个函数用于正常终止一个程序#include <stdlib.h> void exit(int status) ; #先调用终止处理程序,然后关闭所有打开流,后退出 void _Exit(int status) ; #立即进入内核 #include<unistd.h> void _exit(int status) ; #立即进入内核
- 由于历史原因,exit函数总是执行一个标准io库的清理关闭操作,对所有打开流调用fclose函数,这将造成输出缓冲中所有的数据都被冲洗到文件上
- 三个退出函数都带一个整形参数,称为
终止状态或退出状态
, - 检查进程终止状态的方法
- 如果调这些函数时不带终止壮态或main执行一个无返回的return,或main没有声明返回类型为整形,则该进程终止状态是为定义的
- 若main的返回类型是整型并且main执行到最后一条语句时返回,那么终止状态为0
- main函数返回一个整型值和用该值调用exit是等价的。
- 函数atexit
- atexit()函数可以把一些函数注册为退出函数
- 把函数指针传递给atexit()函数时,它会把指针保存起来留给将来引用。当程序将要正常终止时(或者由于调用exit,或者由于main函数返回),退出函数将被调用。退出函数不能接受任何参数。
- 一个进程可以登记至多32个函数,这些函数将自动有exit调用,我们称这些函数为终止处理程序,并调用atexit函数登记这些函数。
- exit调用这些函数的的顺序与他们登记的顺序刚好相反,同一函数如果登记多次,也会被调用多次。
- 终止处理函数程序每登记一次,就会被调用一次。
static void my_exit1(void) ;
static void my_exit2(void) ;
int main(){
atexit(my_exit2) ;
atexit(my_exit1) ;
atexit(my_exit1) ;
print("main is done") ;
return 0 ;
}
output:
main is done
first exit handler
first exit handler
second exit handler
- return和exit的区别
- exit用于结束正在运行的整个程序,它将参数返回给OS,把控制权交给操作系统;而return 是退出当前函数,返回函数值,把控制权交给调用函数。
- exit是
系统调用
级别,它表示一个进程的结束;而return 是语言级别
的,它表示调用堆栈的返回。 -
在main函数结束时,会隐式地调用exit函数,所以一般程序执行到main()结尾时,则结束主进程。exit将删除进程使用的内存空间,同时把错误信息返回给父进程
。 - void exit(int status); 一般status为0,表示正常退出,非0表示非正常退出。
- 内核使程序执行的唯一方法是调用一个exec函数,进程自愿终止的唯一方法是显示或隐式(调用exit)的调用_exit或_Exit进程也可以做自愿由一个函数终止
7.4 命令行参数
- 当执行一个程序时,调用exec进程可将命令行参数传递给该新程序
#include <stdio.h>
int main(int argc, char *argv[])
{
int i;
for (i=0;i<argc;i++)
{
printf("argv[%d]:%s \n", i, argv[i]);
}
}
7.5 环境表
- 每个程序都
接收
到一张环境表,环境表也是一个字符指针数组,每个指针包含一个以null结束的C字符串的地址。 - 全局变量environ包含了该
指针数组
的地址extern char **environ ;
- 每个字符串的结尾处都显式有一个null字节
- 我们称environ为
环境指针
,指针数组为环境表,其中各指针指向的字符串为环境字符串 - 使用getenv和putenv函数来访问特定的环境变量
7.6 C程序的内存布局
- C程序由一下几部分组成
- 正文段
- CPU执行的机器指令部分。正文段共享,通常是只读的
- 初始化数据段
- 包含程序中明确赋值的变量
- 未初始化数据段(bss段)
- 在程序开始之前,内核将此段中的数据初始化为0或nullptr
- 栈
- 自动变量以及每次函数调用需要保存的信息放在此段中。栈在内存高位,往下增长。
- 堆
- 堆用于动态内存分配,
- 正文段
- size命令返回正文段,数据段,bss段的长度,以字节为单位
size /usr/bin/cc
7.7 共享库
- 大多数unix系统支持共享库
- 共享库使得可执行文件中不在需要包含公用库函数,而只需要在所有进程都可以引用的存储区中保存这种例程的一个副本,程序第一次执行或第一次调用的时候某个库函数的时候,用动态链接的方法与共享库链接,减少每个可执行文件的大小,但增加一次运行时间开销,这种时间开销发生在第一此被执行的时候或每个共享库第一次被调用时。
- 共享库的另一个优点是可以用库函数的新版本来代替老版本而不需要对使用该库的程序重新链接编译
7.8 存储空间的分配
- malloc 分配指定字节数的存储区,此存储区中的初始值不确定
- calloc 为指定数量指定长度的对象分配存储空间,该空间中每一位都初始化为0
- realloc 增加或减少以前分配区的长度,当增加长度时,可能需要将以前分配区的内容转移到另一个足够大的区域,以便在末尾提供增加的存储区,而新增区域内的初始值不确定
#incluede <stdlib.h>
void *malloc(size_t size) ;
void *calloc(size_t nobj, size_t size) ;
void *realloc(void *ptr, size_t newsize) ;
// 成功返回非空指针,出错返回NULL
void free(void *ptr) ;
- 这三个分配函数所返回的指针一定是适当对齐的,使其可以用于任何数据对象。
- 这三个函数都返回通用指针void *, 当我们将这些函数返回的指针赋予一个不同类型的指针的时候,就不需要显式的执行强制类型转换。
- 释放的空间可供分配再分配,但将他们保持在malloc内存池中而不是返回个内核
- 大多数实现所分配的存储空间比所要求的要稍大一些,额外的空间用来记 录管理信息——分配块的长度,指向下一个分配块的指针等等。这就意味着如果写过一个已分 配区的尾端,则会改写后一块的管理信息。这种类型的错误是灾难性的,但是因为这种错误不 会很快就暴露出来,所以也就很难发现。将指向分配块的指针向后移动也可能会改写本块的管 理信息
- ==可能致命的错误==
- 释放一个已经释放了的块
- 调用free时所用的指针不是3个alloca函数返回的值
- 调用了alloca函数但是没有free
7.9 环境变量
- unix 内核并不查看这些字符串,它们的解释完全取决于各个应用
- 当在一个进程中改变环境变量的值的时候,我们能影响的只是当前进程及其后生成和调用的任何子进程的环境,但不能影响父进程的环境
- putenv和setenv函数用来设置环境变量
#include<stdlib.h>
int putenv(char *str)
// 成功返回0失败返回非0
int setenv(const char *name, const char*value, int rewrite)
int unsetenv(const char *name) ;
// 成功返回0,出错返回-1
- 函数在修改环境表时是如何操作的
- 环境表和环境字符串通常存放在进程存储空间的顶部(栈之上)
- 删除字符串--简单
- 只要在该环境表中找到该指针,然后将所有后续的指针都向环境表收不顺次移动一个位置。
- 增加一个或者修改现有字符串--困难
- 由于环境表使用的是进程地址空间的顶部,不能再向上扩展,同时也不能移动在它之下的各栈帧,所以两者组合是的该空间的长度不能再增加
- 修改一个现有name
- 增加一个新name
7.10 函数setjmp和longjmp
- c语言中的goto语句是不能跨越函数的
- setjmp和longjmp可以实现跨越函数的跳转
- 非局部goto setjmp和longjmp
- 非局部指的是这不是由普通的c语言goto语句在一个函数内实施的跳转,而是在栈上跳过若干调用帧,返回到当前函数调用路径上的某一个函数中去
#include<setjmp.h> int setjmp(jmp_buf env); //直接调用返回0,若conglongjmp返回,则为非0 void longjmp(jmp_buf, int val) ;
7.11 函数getrlimit和setrlimit
- 每个进程都有一组资源限制,其中一些可以用getrlimit和setrlimt查询和修改
#include <sys/resource.h>
int getrlimit(int resource, struct rlimit *rlptr) ;
int setrlimit(int resource, const struce rlimit *rlptr)
成功返回0,出错返回非0
- 对这两个函数的每一次调用都指定一个资源以及一个指向下列结构的指针
struct rlimit{
rlim_t rlim_cur ;
rlim_t rlim_max ;
}
- 更改资源的时候,要遵循下了三条规则
- 任何一个进程都可以将一个软限制值更改为小于或等于其限制值
- 任何一个进程都可以降低其硬限制值,但它必须大于或等于其软限制值。
- 只有超级用户进程可以提高硬限制值。
- 常量
RLIM_INFINITY
指定了一个无限量的限制 - 这两个函数的resource参数取下列值之一
- RLIMIT_AS
- 进程总的可用存储空间的最大长度
- RLIMIT_CORE
- core文件最大字节数,为0阻止创建core文件
- RLIMIT_CPU
- CPU时间的最大量值
- RLIMIT_DATA
- 数据段最大字节数
- RLIMIT_FSIZE
- 可以创建文件最大字节数
- RLIMIT_MEMLOCK
- 一个进程使用mlock能够锁定在存储空间的最大字节长度
- RLIMIT_NICE
- 为了影响进程调度优先级,nice值可以设置的最大限制
- RLIMIT_MSGQUEUE
- 进程为posix消息队列可分配的最大存储字节数
- RLIMIT_NOFILE
- RLIMIT_NPROC
- RLIMIT_NPTS
- RLIMIT_RSS
- RLIMIT_SBSIZE
- RLIMIT_SIGPENDING
- RLIMIT_STACK
- RLIMIT_SWAP
- RLIMIT_VMEM
- RLIMIT_AS
第8章 进程控制
8.2 进程标识
- 每个进程都有一个非负整数表示的唯一进程id,进程id是可复用的,复用使用延迟复用算法,防止新进程是原来的进程
- 每个进程在进程中有一个pcb来维护进程相关信息,LinuxPCB由 task_struct结构体来描述。PCB中有如下几个结构比较重要
- 进程id,用于标志进程唯一性的值
- 进程状态:就绪,运行,挂起,停止等
- 进程切换时需要保存和恢复的cpu寄存器
- 虚拟地址空间的信息(每个进程都有虚拟地址空间)
- 进程资源上限
- ID为0的进程叫调度进程,也称为交互进程。该进程是内核的一部分,不执行任何磁盘上的程序,因此被称为系统进程
- ID为1的进程叫init进程,早期位于/etc/init,该进程负责在内核自检后启动一个unix系统。init通常读取与系统有关的初始化文件,并将系统引导到一个状态。init进程不会终止。是一个普通的用户进程(与交互进程不同,他不是内核中的系统进程),但是它以超级用户特权运行
- 除了进程id,每个进程还有其他一些标识符,下列函数返回这些标识符
#include<unistd.h>
pid_t getpid(void) ;
pid_t getppid(void) ;
uid_t getuid(void) ;
uid_t geteuid(void) ;
gid_t getgid(void) ;
gid_t getegid(void) ;
//返回调用进程的各种id
- 进程状态
- 初始态
- 就绪态
- 运行态
- 挂起态(没有执行权限,没有执行资格,sleep)
- 终止态
8.3 函数fork
一个现有进程可以调用fork函数创建一个新进程
#include<unistd.h>
pid_t fork(void) ;
//子进程返回0,父进程返回进程id,出错返回-1
- 由fork创建的进程称为子进程
fork函数调用一次返回两次,两次返回的区别是子进程的返回值是0,父进程的返回值则是新建子进程的id
- 将子进程id返回给父进程的理由
- 因为一个进程的子进程可以有多个,并且没有一个函数使一个进程可以获得其所有子进程的进程id
- 使子进程得到返回值0的理由是:
- 一个进程只会有一个父进程,所以子进程总是可以调用getppid得到父进程的id
- 父进程和父进程继续执行fork调用后的指令。
- 子进程是父进程的副本。但是并不共享这些存储空间部分,但是共享正文段
- 由于在fork之后通常会exec,所以现在很多实现并不完全拷贝副本,而是使用
写时复制
,即这些区域由父子进程共享,如果由其中一个进程修改某个区域,则内核只为修改区域的那块内存制作一个副本。
#include <iostream>
#include "apue.h"
#include <dirent.>h
#include <fcntl.h>
int gloable = 6 ;
char buf[] = "a write to stdou \n" ;
int main(int argc, char * argv[]) {
int var ;
int pid ;
var = 88 ;
if ((write(STDOUT_FILENO,buf, sizeof(buf)-1))!= sizeof(buf)-1){
printf("error write") ;
}
printf("before") ;
if ((pid = fork())<0){
printf("fork error") ;
} else if(pid ==0){
gloable++ ;
var++ ;
} else{
sleep(2) ;
}
printf("pid=%ld, glob=%d, var=%d\n" ,(long)getpid(), gloable, var) ;
exit(0) ;
}
output:
a write to stdou
beforepid=15211, glob=7, var=89
beforepid=15210, glob=6, var=88
- 进程中的注意事项
- fork 之后子进程先执行还是父进程先执行是不确定的。取决于内核使用的调度算法
- write函数是不带缓冲的,但是printf标准输出连到终端设备是行缓冲的,否则是全缓冲的
- 当以交互方式运行程序的时候,printf函数只执行了一次,原因是标准输出缓冲区被换行符冲洗
- 当将输出重定向到文件的时候,printf会输出两次,原因是在fork之前调用printf一次,但当调用fork时,该行数据仍然在缓冲区中,再将父进程的数据空间赋值到子进程的过程中,该缓冲区数据也被复制到了子进程中,此时父进程和子进程各自有该行内容的缓冲区。
- 文件共享(但是内存空间是独立的,不过内容是拷贝自父进程)
- 在重定向父进程的标准输出的时候,子进程的标注输出也被重定向。实际上,
fork的一个特性是父进程打开的所有文件描述符都被复制到子进程中。父进程和子进程每个相同的打开描述符共享一个文件表项
- 如果父子进程打开同一个文件,那么父进程和子进程共享同一个文件偏移量。
- 如果父进程和子进程写同一描述符指向的文件,但又没有任何形式的同步,那么他们的输出就会互相混合。
- fork之后处理文件通常有一下两种情况
- 父进程等待子进程完成
- 父进程和子进程各自执行不同的程序段
- 除了打开的文件之外,父进程的很多其他属性也由子进程继承
- 实际用户id,实际组id,有效用户id,有效组id
- 附属组id
- 进程组id
- 会话id
- 控制终端
- 设置用户id标志和设置组id标志
- 当前工作目录
- 根目录
- 文件模式创建屏蔽字
- 信号屏蔽和安排
- 对任一打开的文件描述符的执行时关闭
- 环境
- 连接的共享存储段
- 存储印象
- 资源限制
- 父进程和子进程的区别如下
- fork的返回值不同
- 进程id
- 这两个进程的父进程id
- 子进程的tms_utime,tms_stime,tms_cutime,tms_ustime
- 子进程不继承父进程设置的文件锁
- 子进程的未处理闹钟被清除
- 子进程的未处理信号集被设置为空集
- 在重定向父进程的标准输出的时候,子进程的标注输出也被重定向。实际上,
- fork失败的原因有下面几种
- 系统中已经有太多进程
- 该实际用户id的进程总数超过了系统限制
- fork有一下两种用法
- 一个父进程希望复制自己
- 在网络请求中常见
- 一个进程要执行一个不同的程序
- 在shell中常见
- 一个父进程希望复制自己
8.4 函数vfork
- vfork返回值和调用序列和fork相同,但是语义不同
8.5 函数exit
- 进程有5中正常终止的方式
- 在main函数内执行return语句
- 调用exit函数
- 调用_exit和_Exit函数
- 进程的最后一个线程在其启动例程中执行return语句
- 进程的最后一个线程调用pthread_exit函数
- 三种异常终止如下
- 调用abort,它产生SIGABRT信号
- 当进程函数接收到某些信号时
- 最后一个线程对取消(cancellation)请求做出响应
- 任何一种终止都希望进程能通知父进程
- 三种终止函数实现这一点的方法是
- 将其==退出状态==作为参数传递给函数
- 在异常终止的状态下
- 内核(不是进程本身)产生一个指示其异常终止原因的==终止状态==
- 三种终止函数实现这一点的方法是
- 在任何一种情况下,该终止进程的父进程都能够用wait或waitpid取得其终止状态
- ==退出状态==是向三个终止函数传递的参数,或main的返回值,==和终止状态有区别==
- 在最后调用_exit的时候,内核将退出状态转换为终止状态。
- 如如果父进程在子进程之前终止,那么原来父进程的所有子进程都将由init进程收养。
- 操作过程如下
- 在一个进程终止时,内核逐个检查活动进程,判断它是否是正要终止的进程的子进程
- 如果是,则该进程的父进程改为1
- 保证每一个进程都由一个父进程
- 操作过程如下
- 子进程在父进程之前终止,父进程如何做相应的检查得到子进程的状态?
- 内核为每个终止子进程保存了一定量的信息,所以当终止进程的父进程调用wait或waitpid的时候,可以得到这些信息
- 内核可以释放终止进程所使用的进程所有存储区,关闭其所有打开的文件。
- 在unix中,一个已经终止但是其父进程尚未对其进行善后处理(获取子进程相关信息,释放仍然占用的资源)的进程被称为僵尸进程zombie
- init被编写成只要有一个子进程终止,他就调用wait函数取得其终止状态。防止系统中塞满僵尸进程
8.6 函数wait和waitpid
- 当一个进程正常或者异常终止时,内核就向其父进程发送
SIGCHLD
信号 - 调用wait或waitpid会发生如下情况
- 如果其所有子进程都还在运行,则阻塞
- 如果一个子进程已经终止,正等待父进程获取其终止状态,则取得改子进程的终止状态后马上返回
- 如果没有任何子进程,则立即出错返回-1
- wait 的参数status表示终止状态(表示是怎么终止的)
#include <sys/wait.h>
// statloc是一个整形指针,如果statloc不是一个控指针,则终止进程的终止状态就存放在它所指向的单元内。如果不关心终止状态,则可以将参数指定未空指针。
pid_t wait(int *statloc) ;
//waitpid可以等待指定的进程终止
//pid参数的作用解释如下:
//pid==-1 等待任意子进程
//pid>0 等待进程id和pid相等的子进程
//pid==0 等待组id等于调用组id的任一子进程
//pid<-1 等待组id等于pid绝对值的任一子进程
//options参数可以进一步控制进程
pid_t waitpid(pid_t pid, int *statloc, int options) ;
//成功返回进程id,失败返回0或-1
- 在一个子进程终止前,wait使其调用者阻塞,而waitpid有一个选项,可使调用者不阻塞
- waitpid并不等待在其调用之后的第一个终止子进程,它有若干各选项,可以控制它所等待的进程
#include <iostream>
#include "apue.h"
#include <dirent.h>
#include <fcntl.h>
#include <sys/wait.h>
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\n", WTERMSIG(status));
} else if(WIFSTOPPED(status)){
printf("child stopped ,signal number = %d\n",WSTOPSIG(status));
}
}
int main(int argc, char * argv[]) {
int pid ;
int status ;
if ((pid = fork()) < 0) //
printf("fork error") ;
else if(pid == 0) //fork后子进程得到的pid的值是0
exit(7) ;
if (wait(&status) !=pid)
printf("wait error") ;
pr_exit(status) ;
if ((pid = fork()) < 0)
printf("fork error") ;
else if(pid == 0)
abort() ;
if (wait(&status) !=pid)
printf("wait error") ;
pr_exit(status) ;
if ((pid = fork()) < 0)
printf("fork error") ;
else if(pid == 0)
status /=0 ;
if (wait(&status) !=pid)
printf("wait error") ;
pr_exit(status) ;
exit(0) ;
}
- 如果一个进程fork一个子进程,但不要它等待子进程终止,也不希望子进程处于僵尸状态知道父进程终止,实现这一诀窍的方法是调用fork两次
#include <iostream>
#include "apue.h"
#include <dirent.h>
#include <fcntl.h>
#include <sys/wait.h>
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\n", WTERMSIG(status));
} else if(WIFSTOPPED(status)){
printf("child stopped ,signal number = %d\n",WSTOPSIG(status));
}
}
int main(int argc, char * argv[]) {
int pid ;
if ((pid = fork()) <0)
printf("error forked") ;
else if (pid == 0){ //第一个子进程
if ((pid =fork())<0){
printf("error froked") ;
} else if (pid > 0){ //第一个子进程,让他正常退出
exit(0);
}
sleep(2);
printf("second child ,parent pid = %ld \n", (long)getppid());
exit(0); //第二个子进程让他退出
}
if (waitpid(pid, NULL, 0)!= pid)
printf("waitpid error") ;
exit(0) ;
}
- 僵尸进程是已经退出的进程,是无法被kill的
- 当僵尸进程的父进程终止后,僵尸进程被托管为init进程,init进程回收僵尸进程的资源
8.7 函数waitid
- 这是另一个取得进程终止状态的函数waitid
#include <sys/wait.h>
int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options) ;
//成功返回0出错返回-1
8.8 函数wait3和wait4
- 这个函数也用于返回进程终止状态
- 但是这两个函数有一个附加参数,允许内核返回由终止进程及其所有子进程使用的资源概况
8.9 竞争条件
- 竞争条件:当多个进程企图对共享数据进行某种处理,而最后的结果取决于进程运行的顺序时,我们认为发生了竞争条件。
- 进程运行的顺序依赖于系统负载和调度算法
- 如果一个进程希望等待一个子进程终止,则它必须调用wait函数中的一个
- 如果一个进程要等待其父进程终止,则可以使用下面的循环
while (getppid !=1) sleep() //这种形式的循环称为轮询(polling),它会浪费CPU时间
- 避免竞争条件和轮询,可以使用信号机制或者IPC
8.10 函数exec
- 当使用fork创建新的子进程后,子进程往往需要调用一种exec函数以执行另一个程序。当进程调用一种exec函数的时候,
该进程执行的程序完全替换未新程序
。 - 新程序从自己的main函数开始执行
-
因为调用exec并不创建新进程,所以前后的进程id并不改变
。 exec只是使用磁盘上的一个新程序替换了当前进程的正文段,数据段,堆段和栈段
- 进程控制原语
- 使用fork创建新进程
- 用exec出事执行新的程序
- exit和wait函数处理终止和等待终止。
#include<unistd.h>
//使用完整路径做参数
int execl(const char * pathname, const char *arg0, ...);
int execv(const char *pathname, char *const argv[]) ;
int execle(const char *pathname, const char *arg0) ;
int execve(const char *pathname, char *const argv[],char *const envp[]) ;
//使用PATH环节变量中的文件(或fd)做参数
int execlp(const char *filename, char *arg0, ...) ;
int execvp(const char *filename, char *const argv[]);
int fexecve(int fd, char *const argv[], char *const envp[]) ;
//出错返回-1,成功无返回
- 当使用filenname做参数的时候
- 如果filename包含/,则将其视为路径名
- 否则就按PATH环境变量,在它所指定的目录中搜索可执行文件
- 如果execlp或execvp使用路径前缀中的一个找到了一个可执行文件,但是该文件不是由连接编辑器产生的机器可执行文件,则就认为该文件是一个shell脚本,于是试着调用sh并以该filename作为输入
第九章 进程关系
9.2 终端登录
- unix系统如何经由终端登陆系统
- 系统管理者创建名为/etc/ttys的文件,每个终端一行,每行说明设备名和传到gettty程序的参数
- 当系统自检,内核创建init进程,init进程使系统进入多用户模式,init读取/etc/ttys,对每个允许登陆的终端设备,init调用一次fork,它所生成的子进程执行exec gettty程序
- gettty程序对终端设备
- 终端是一种字符型设备,通常使用tty来统称各种类型的终端设备,现在的大多使用tty虚拟设备支持虚拟控制台
- 其实起初终端和控制台都不是个人电脑的概念,而是多人共用的小型中型大型计算机上的概念。
- 终端为主机提供了人机接口,每个人都通过终端使用主机的资源。终端有字符终端和图形终端两种。一台主机可以连很多终端。
- 控制台是一种特殊的人机接口, 是人控制主机的第一人机接口。而主机对于控制台的信任度高于其他终端。
- 个人计算机只有控制台,没有终端。当然愿意的话,可以在串口上连一两台字符哑终端。但是linux按POSIX标准把个人计算机当成小型机来用,在控制台上通过getty软件虚拟了六个字符哑终端(或者叫虚拟控制台终端tty1-tty6)(数量可以在/etc/inittab里自己调整)和一个图型终端, 在虚拟图形终端中又可以通过软件(如rxvt)再虚拟无限多个伪终端(pts/0等)。但这全是虚拟的,虽然用起来一样,但实际上没有物理实体。所以在个人计算机上,只有一个实际的控制台,没有终端,所有终端都是在控制台上用软件模拟的。要把个人计算机当主机再通过串口或网卡外连真正的物理终端也可以,论成本,谁会怎么做呢。
- 一旦设备被打开,则文件描述符0,1,2就被设置到该设备
- 因为最初的init进程具有超级用户特权,所以init进程fork的进程都有超级用户特权。
- login能处理多项工作,因为它得到了用户名,调用getpwnam取得相应用户的口令文件登录项
- 现代unix系统支持PAM(可插入的身份验证模块),允许管理人员配置使用何种身份验证方法来访问哪些使用PAM库编写的服务
- 如果用户登陆正确,login完成如下工作
- 将当前用户的工作目录该为该用户的起始目录
- 调用chown更改终端的所有权,是登录者成为所有者
- 将对该终端设备的访问权改成用户读和写
- 调用setgid以及initgroups设置进程的组id
- 用login得到所有初始化环境:HOME,shell,用户名,一起系统默认PATH
- login今年初更改登录用户的用户ID并调用该用户的登录shell。
- login调用setuid更改用户的所有用户id
- 然后==登录shell读取其启动文件.profile==,这些启动文件通常更改某些环境变量并增加很多环境变量
9.3 网络登录
通过串行终端和网络登录到系统的主要区别是:
- 网络登录时,在终端和计算机之间的连接不再是点到点的
- 网络登录情况下,login仅仅是一种可用的服务
- 为了使用户既能处理终端程序,又能处理网络登录程序,系统使用了一种伪终端的软件驱动程序,仿真串行终端的运行行为,并将终端操作映射为网络操作。
- BSD网络登录
- 在BSD中,有一个inetd进程,它等待大多数网络连接
- 作为系统启动的一部分,init调用一个shell,使其执行sh脚本/etc/rc,由此,shell脚本启动一个inetd。一旦此shell脚本终止,inetd的父进程就变成了init。
- inet等待tcp/ip连接请求到达主机,而当一次连接请求到达主机的时候,他执行一次fork,生成的子进程执行适当的程序。
- 假定对于一个telnet服务进程的tcp请求连接到达,启动客户进程的用户现在登录到了服务进程所在的主机
- telnet进程打开一个伪终端设备,并用fork分成两个进程。父进程处理网络连接,子进程执行login程序,父子进程通过伪终端连接
- 当通过终端或网络登录时,我们得到一个登录shell其标准输入标准输出,标准错误要么连接到一个终端设备,要么连接到一个伪终端设备。这个登录shell是一个session的开始,这个终端或者伪终端设备则是会话的控制终端
9.4 进程组
- 进程组使一个或者多个进程的组合,通常他们是在一个作业中被组合起来的
- 同一进程组中的各进程接收到来自同一终端的各种信号
#include<unistd.h>
pid_t getpgrp(void) ;
//返回进程的进程组id
- 每个进程组有一个组长id,组长进程的进程组id等于其进程id
- 进程组组长id可以创建一个进程组,创建该组中的进程
- 进程可以调用setpgid加入一个现有的进程组或者新建一个进程组
#include<unistd.h>
int setpgid(pid_t pid, pid_t pgid) ;
成功返回0,出错返回-1
- 一个进程只能为它自或它的子进程设置组id,在他的子进程调用了exec后,他就不在更改该子进程的进程组id
9.5会话
- session 是一个多个或多个进程组的集合,通常是由shell管道讲几个进程编程一组的
- 进程调用setsid(void)创建一个新会话
#include<unistd.h>
pid_t setsid(void) ;
- 如果调用此函数不是一个进程组的组长,则此函数创建一个新会话。
- 该进程变成新会话的会话首进程(创建该会话的进程),此时,该进程是新会话中的唯一进程
- 该进程成为一个新进程的组长进程,新进程组id是该调用进程的进程ID
- 该进程没有控制终端
9.6 控制终端
- 会话和进程组还有一些其他特性
- 一个会话可以有一个控制终端
- 建立与控制终端连接的会话首进程被称为控制进程
- 一个会话中的几个进程组可被分成一个前台进程组和一个后台进程组
- 无论何时键入终端的中断键,都会讲中断信号发送至前台进程组的所有进程
- 无论何时键入终端的退出键,都会讲退出信号发送至前台进程组的所有进程
- 如果终端接口检测到终端断开链接,则将挂断信号发送至终端控制进程
9.7 函数tcgetpgrp,tcsetpgrp和tcgetsid
- 需要有一种方法来通知内核哪个进程组是前台进程组,哪个是后台,这样就知道将终端输入和终端产生的信号发送到何处
#Include <unistd.h>
pid_t tcgetpgrp(int fd) ;
//若成功,返回前台进程组id,该id和在fd上打开的终端相关联,出错返回-1,
int tcsetpgrp(int fd, pid_t pgrpid) ;
9.8作业控制
- 作业控制允许在一个终端上启动多个作业(进程组),它控制哪一个作业可以访问该终端以及哪些作业可以在后台运行。
- 作业控制有三种形式的支持
- 支持作业控制的shell
- 内核中的终端控制程序必须支持作业控制
- 内核必须提供对某些作业控制信号的支持