UNIX环境高级编程读书笔记

第1章UNIX基础知识

每个程序都会有唯一的数字标识符,称为进程ID,进程ID总是一个非负整数

getpid会返回一个pid_t的数据类型,pid_t最大数据范围是长整型

fork对父进程返回新的子进程的ID(一个非负整数),对子进程返回0.因为fork创建一个进程,所以说它被调用一次,但返回两次(分别在父子进程中)

与进程相同,线程也用ID表示,但线程ID只在它所属的线程内起作用。一个进程中的线程ID在另一个进程中没有意义

用户ID和组ID:通常每一个用户都有唯一一个用户ID

信号:用于通知进程发生某种状态,进程有以下三种处理信号方式

  • 忽略信号:对信号不做处理,假装看不见
  • 按系统默认方式处理,对于除以0系统默认方式是终止进程
  • 捕捉信号:捕捉响应的信号,进行函数处理。

文件描述符和文件指针区分
文件描述符:在linux系统中打开文件就会获得文件描述符,它是个很小的正整数。每个进程在PCB(Process Control Block)中保存着一份文件描述符表,文件描述符就是这个表的索引,每个表项都有一个指向已打开文件的指针。

文件指针:C语言中使用文件指针做为I/O的句柄。文件指针指向进程用户区中的一个被称为FILE结构的数据结构。FILE结构包括一个缓冲区和一个文件描述符。而文件描述符是文件描述符表的一个索引,因此从某种意义上说文件指针就是句柄的句柄(在Windows系统上,文件描述符被称作文件句柄)。

第三章文件I/O

文件描述符0与进程的标准输入关联,文件描述符1与进程输出相关联,文件描述符2与进程的标准错误输出相关联
open函数
lseek函数
write函数
read函数
dup2dup函数都可以复制一个现有的文件描述符

#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);

dup函数用来复制参数oldfd所指的文件描述符。当复制成功是,返回最小的尚未被使用过的文件描述符,若有错误则返回-1.错误代码存入errno中返回的新文件描述符和参数oldfd指向同一个文件,这两个描述符共享同一个数据结构,共享所有的锁定,读写指针和各项全现或标志位。

dup2函数,dup2与dup区别是dup2可以用参数newfd指定新文件描述符的数值。若参数newfd已经被程序使用,则系统就会将newfd所指的文件关闭,若newfd等于oldfd,则返回newfd,而不关闭newfd所指的文件。dup2所复制的文件描述符与原来的文件描述符共享各种文件状态。共享所有的锁定,读写位置和各项权限或flags等。

funcl函数可以改变文件打开的属性

第八章进程控制

每个进程都有一个非负数整型表示的唯一进程ID
系统中有一些专用进程,ID为0的进程通常是调度进程,常常被称为交换进程。进程ID 1通常是init进程,init进程决不会终止,它是一个普通的用户进程,但它以一个超级用户特权运行。

  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
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h> 
#include<stdlib.h>
int globvar = 6;
char buf[] = "a write to stdout\n";
int main(){
    int var;
    pid_t pid;
    var = 88;
    if(write(STDOUT_FILENO,buf,sizeof(buf)-1) != sizeof(buf)-1){
        exit(1);
    }
    printf("before fork\n");
    if((pid = fork())< 0){
        perror("fork");
    }else if(pid == 0){
        globvar++;
        var++;
    }else{
        sleep(2);
    }
    printf("pid = %ld, glob = %d,var = %d\n",(long)getpid(),globvar,var);
    exit(0);
}

输出
在这里插入图片描述
如果去掉before fork的回车
在这里插入图片描述
或者不去掉回车但输出到文件里
在这里插入图片描述
原因:write函数是不带缓冲的。因为在fork之前调用write,所以其数据写到标准输出一次。但是,标准I/O函数库是带缓冲的。如果标准输出连到终端设备,则它是行缓冲;否则它是全缓冲的,当以交互方式运行程序时,只得到该printf输出的行一次,其原因是标准输出缓冲区有换行符冲洗,但是当将标准输出重定向一个文件时,却得到printf输出两行,其原因是,在fork之前调用一次printf一次,但当调用fork时,该行数据仍在缓冲区中,此时父进程和子进程各自有了带有该行内容的缓冲区。在exit之前的第二个printf将其数据追加到已有缓冲区中。当进程终止时,其缓冲区中的内容都被写到相应文件中。

关于上述缓冲区问题原文

什么是缓冲区

缓冲区又称为缓存,它是内存空间的一部分。也就是说,在内存空间中预留了一定的存储空间,这些存储空间用来缓冲输入或输出的数据,这部分预留的空间就叫做缓冲区。缓冲区根据其对应的是输入设备还是输出设备,分为输入缓冲区和输出缓冲区。

为什么要引入缓冲区

我们为什么要引入缓冲区呢?

比如我们从磁盘里取信息,我们先把读出的数据放在缓冲区,计算机再直接从缓冲区中取数据,等缓冲区的数据取完后再去磁盘中读取,这样就可以减少磁盘的读写次数,再加上计算机对缓冲区的操作大大快于对磁盘的操作,故应用缓冲区可大大提高计算机的运行速度。

又比如,我们使用打印机打印文档,由于打印机的打印速度相对较慢,我们先把文档输出到打印机相应的缓冲区,打印机再自行逐步打印,这时我们的CPU可以处理别的事情。现在您基本明白了吧,缓冲区就是一块内存区,它用在输入输出设备和CPU之间,用来缓存数据。它使得低速的输入输出设备和高速的CPU能够协调工作,避免低速的输入输出设备占用CPU,解放出CPU,使其能够高效率工作。

缓冲区的类型

缓冲区 分为三种类型:全缓冲、行缓冲和不带缓冲。

1、全缓冲

在这种情况下,当填满标准I/O缓存后才进行实际I/O操作。全缓冲的典型代表是对磁盘文件的读写。

2、行缓冲

在这种情况下,当在输入和输出中遇到换行符时,执行真正的I/O操作。这时,我们输入的字符先存放在缓冲区,等按下回车键换行时才进行实际的I/O操作。典型代表是键盘输入数据。

3、不带缓冲

也就是不进行缓冲,标准出错情况stderr是典型代表,这使得出错信息可以直接尽快地显示出来。
缓冲区的刷新

下列情况会引发缓冲区的刷新:

1、缓冲区满时;

2、执行flush语句;

3、执行endl语句;

4、关闭文件。

可见,缓冲区满或关闭文件时都会刷新缓冲区,进行真正的I/O操作。另外,在C++中,我们可以使用flush函数来刷新缓冲区(执行I/O操作并清空缓冲区),如:cout << flush; //将显存的内容立即输出到显示器上进行显示

endl控制符的作用是将光标移动到输出设备中下一行开头处,并且清空缓冲区。

cout < < endl;

相当于

cout < < ”\n”< < flush;

文件共享
如果父进程和子进程写同一描述符所指向的文件(假定所用文件描述符是在fork之前打开)
在fork之后处理文件描述符有以下两种情况:
1)父进程等待子进程完成。在这种情况下,父进程无需对其描述符做任何处理。当子进程终止后,它曾进行读、写操作的任一共享描述符的文件偏移量已做了相应更新。
2)父进程和子进程各自执行不同程序段。在这种情况下,在fork之后,父进程和子进程各自关闭它们不需要使用的文件描述符,这样就不会干扰对方使用的文件描述符。这种方法是网络服务进程经常使用的。

wait函数

#include<sys/types.h>
#include<sys/wait.h>
pid_t wait (int * status);

参数 status 是一个整形指针。如果status不是一个空指针,则终止进程的终止状态将存储在该指针所指向的内存单元中。如果不关心终止状态,可以将 status参数设置为NULL。
status 不是NULL时子进程的结束状态值会由参数 status 返回,而子进程的进程识别码作为函数返回值返回。
调用 wait 函数时,调用进程将会出现下面的情况:
· 如果其所有子进程都还在运行,则阻塞。
· 如果一个子进程已经终止,正等待父进程获取其终止状态,则获取该子进程的终止状态然后立即返回。
· 如果没有任何子进程,则立即出错返回。
如果执行成功则返回子进程识别码(PID),如果有错误发生则返回-1。失败原因存于errno 中。

如果子进程已经终止,并且是一个僵尸进程,则wait立即返回并取得该子进程的状态,否则wait使其调用者阻塞,直到一个子进程终止。如果调用者阻塞而且它有多个子进程,则在其某个子进程终止时,wait函数就立即返回。要想处理多个僵尸进程则调用多次wait函数

exec函数
用fork函数创建子进程后,子进程往往要调用一种exec函数用以执行另一个程序,当程序调用一种exec函数时,该进程执行的程序完全替代为新程序,而新程序则从其main函数开始执行。因为调用exec函数并不是创建新进程,所以前后的进程ID并未改变。exec只是用磁盘上的一个新程序替换了当前进程的正文段、数据段、堆段和栈段。
用fork函数可以创建新进程,用exec函数可以初始执行的新程序。exit函数和wait函数处理终止和等待终止。

头文件:
#include <unistd.h>
原型:
int execl(const char *path, const char arg, … / (char *) NULL */);
int execle(const char *path, const char arg, … /, (char *) NULL, char * const envp[] */);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
int execlp(const char *file, const char arg, … / (char *) NULL */);
参数:
path:要执行的程序路径。可以是绝对路径或者是相对路径。在execv、execve、execl和execle这4个函数中,使用带路径名的文件名作为参数。
file:要执行的程序名称。如果该参数中包含“/”字符,则视为路径名直接执行;否则视为单独的文件名,系统将根据PATH环境变量指定的路径顺序搜索指定的文件。
argv:命令行参数的矢量数组。
envp:带有该参数的exec函数可以在调用时指定一个环境变量数组。其他不带该参数的exec函数则使用调用进程的环境变量。
arg:程序的第0个参数,即程序名自身。相当于argv[O]。
…:命令行参数列表。调用相应程序时有多少命令行参数,就需要有多少个输入参数项。注意:在使用此类函数时,在所有命令行参数的最后应该增加一个空的参数项(NULL),表明命令行参数结束。
返回值:一1表明调用exec失败,无返回表明调用成功。

第十章信号

进程调度kill函数可将任意信号发送给另一个进程或进程组。自然,对此有所限制:接受信号进程和发送信号进程所有者必须相同,或发送信号进程的所有者必须是超级用户。

第十一章线程

一个进程的所有信息对该进程的所有线程是共享的,包括可执行程序的代码、程序的全局内存和堆内存、堆内存、栈以及文件描述符
进程ID在整个系统中是唯一的,但线程ID不同,线程ID只有在它所属的进程上下文中才有意义
当主线程退出时,其它线程会被终止
线程可以通过调用pthread_self函数获取自身线程ID

#include<pthread.h>
pthread_t pthread_self(void);
返回值:调用线程的线程ID

线程创建

#include<pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

thread 指向线程标识符(ID)的指针。
attr 用来设置线程属性,一般为NULL
start_routine 线程运行函数的起始地址。
arg 运行函数的参数。

线程终止
如果进程中的任意线程调用exit、_Exit或者_exit,那么整个进程就会终止

#include <pthread.h>
void pthread_exit(void *retval);

线程通过调用pthread_exit函数终止执行,就如同进程在结束时调用exit函数一样。
在main线程中调用pthread_exit会起到只让main线程退出,但是保留进程资源,供其他由main创建的线程使用,直至所有线程都结束,但在其他线程中不会有这种效果

#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);

pthread_join()函数,以阻塞的方式等待thread指定的线程结束。当函数返回时,被等待线程的资源被收回。如果线程已经结束,那么该函数会立即返回。并且thread指定的线程必须是joinable的。

pthread_join函数与pthread_exit函数区别
pthread_join一般是主线程来调用,用来等待子线程退出,因为是等待,所以是阻塞的,一般主线程会依次join所有它创建的子线程。
pthread_exit一般是子线程调用,用来结束当前线程。
子线程可以通过pthread_exit传递一个返回值,而主线程通过pthread_join获得该返回值,从而判断该子线程的退出是正常还是异常。
互斥量

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
如成功返回0,否则,返回错误编号
要用默认属性初始化互斥量,只需把attr设为NULL。

互斥变量用pthread_mutex_t数据类型表示,在使用互斥变量以前,必须首先对它进行初始化,可以调用pthread_mutex_init函数进行初始化,如果动态分配互斥量(例如,通过malloc函数),在释放内存前需要调用pthread_mutex_destroy

对互斥量进行加锁,需要调用pthread_mutex_lock,如果互斥量已经上锁,调用线程将阻塞直到互斥量被解锁。对互斥量解锁,需要调用pthread_mutex_unlock

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:若成功返回0,否则返回错误编号

发布了46 篇原创文章 · 获赞 64 · 访问量 8777

猜你喜欢

转载自blog.csdn.net/qq_43799957/article/details/105334739
今日推荐