unix高级编程笔记

unix进程的环境

当内核启动一个c程序时,在main函数之前会先调用启动例程,由启动例程做一些处理工作然后才调用main函数,该启动例程至少要设置命令行参数和环境变量。
unix进程退出的5种方式:

  1. 在main函数中return。
  2. 调用exit函数,一般在main函数返回后也会调用exit函数。
  3. 调用_exit函数。
  4. 调用abort。
  5. 由一个信号终止。情况4是本情况的特殊情况。

exit和_exit函数的区别
exit位于头文件:
_exit位于头文件: 是一个系统调用函数,用于处理unix特定细节。
_exit直接进入内核,而exit函数首先执行终止处理程序并关闭所有标准io流(调用fclose函数刷新缓冲区),然后进入内核(一般会调用_exit函数)。

function: void atexit(void (*func)(void));
位于头文件:
ANSI C规定一个进程可登记多达32个终止处理程序,由exit函数调用,并以注册相反顺序调用。

存储空间布局

c程序的存储空间布局
系统为每个进程分配了虚拟进程空间,存储空间布局建立在该虚拟进程空间中。虚拟进程空间映射到物理内存空间是由操作系统完成,相关概念有分页机制,分段机制,页交换机制等等,此处暂且略过。
由低地址到高地址,一般按照以下顺序来分配:
正文段:执行的机器指令部分,只读,可共享。
初始化数据段
非初始化数据段:也称为bss段,由exec置0,并不需要被放在磁盘文件中。

堆内存分配:
void* malloc(size_t size);
分配指定字节数的动态内存,内容未指定。
void* calloc(size_t obj, size_t size);
为指定长度的对象,分配能容纳指定个数的存储空间,每一位被置0。
void realloc(void ptr, size_t new_size);
更改已分配的存储区大小,可能会将以前的内容移动到更大的存储区,新增的区域内容未确定。
若出错则返回nullptr指针。这三个函数返回的指针一定是适当内存对其的,满足最严苛的对齐要求。
void free(void* ptr);

命令行参数和环境变量:

环境变量

char getenv(const char name);
位于头文件:
如果name不存在则返回nullptr。

进程控制

0号进程是调度进程。1号进程是init进程,在自举过程结束时由内核调用,是一个用户进程,拥有超级用户特权,成为所有孤儿进程的父进程。
进程的六个标识符。

关于实际用户/组id,有效用户/组id和存储用户/组id的记录与理解。
一个进程,其实际的用户/组id由启动这个进程的用户决定,我们将其称为ruid和rgid。
进程本身也是一个文件,那么它必定拥有文件所有者id和对应的组id,我们将其称为st_uid和st_gid。
有效用户/组id是在程序执行时才被指定的一种id,默认的会被指定为实际用户/组id,这个id是用来程序执行时检测相关权限的,为什么我们不直接用实际用户/组id来检测权限呢?因为程序的所有者和使用者可能不是同一个用户,用户a写的程序,其中设计到访问资源需要用户a自身的某些权限,但是程序被其他用户执行时,不一定有对应的权限。所以我们提供有效用户/组id来执行权限检测,然后以合理的方式提供修改程序执行时有效用户/组id的机制。这种机制必须保证安全性,如果我们允许任何用户随意指定执行程序的有效用户id和有效组id,那么就毫无安全性可言,我们还要权限机制做什么呢?
所以这种机制必须由程序的编写者提供,即用户b想要使用用户a的程序,必须由用户a通过某种机制,让其他用户在执行自己的程序时,能暂时获得自己的权限。这就是设置位和有效用户/组id存在的意义。程序的编写者通过设置对应的设置位,使得本程序被其他用户使用时,将有效用户/组id设置为本程序的st_uid和st_gid。
那么存储用户/组id的作用是什么呢?是用来存储有效用户/组id的,有时候程序中需要多次调整权限,某时刻用程序的实际用户/组id来设置有效用户/组id,之后又需要之前的有效用户/组id来进行权限检测,这意味着我们需要把有效用户/组id存储在一个位置,当有效用户/组id被修改为其他值后,还能在这个存储位置找到初始设置的值。
两个综合函数,用来读取/设置这三个值getresuid和getresgid。当然,并不是任何一个程序都能随意设置这三个值。没有超级用户权限的用户不能设置实际用户/组id,并且只能将有效用户/组id设置为实际用户/组id或者存储用户/组id,这很好理解,提供给普通用户适当的修改机制以完成上述功能。
完事收工。
参考文章:https://blog.csdn.net/hubinbi...

pid_t fork(void)
位于头文件:
返回值子进程中为0,父进程为子进程id,出错则返回-1。
fork被用来创建一个新进程,子进程获得父进程的数据空间,堆和栈的复制品。注意fork和io函数之间的关系,所有被父进程打开的描述符都被复制到子进程中,父子进程相同的描述符共享一个文件表项。这种共享方式使得父子进程对共享的文件使用同一个偏移量。这种情况下如果不对父子进程做同步处理,则对同一个文件的输出是混合在一起的。因此要么父进程等待子进程执行完毕,要么互不干扰对方使用的文件描述符。

函数vfork和fork的区别:vfork也创建一个子进程,但其不复制父进程的信息而是和其共享,vfork保证子进程先执行,并且只有在子进程调用exec或者exit函数之后父进程才可以继续运行。vfork就是为了exec而生,因为其避免了子进程拷贝父进程的各种信息--但是如今fork函数一般会采用写时复制的方法,因此fork+exec的开销会小很多。
注:在vfork中调用return会导致父进程一起挂掉,因为其共享父进程的信息,return会触发mian函数的局部变量析构并弹栈,而直接使用exit函数则不会发生这种情况。
关于fork和vfork的区别见链接:https://www.cnblogs.com/19322...

exec系列函数
exec系列函数提供了在进程中切换为另一个进程的方法,该系列函数一共有六个,在提供的接口上有一些不同,但最终是通过调用execve系统调用完成。对打开文件的处理与每个描述符的exec关闭标志值有关,进程中每个文件描述符有一个exec关闭标志(FD_CLOEXEC),若此标志设置,则在执行exec时关闭该描述符,否则该描述符仍打开。除非特地用fcntl设置了该标志,否则系统的默认操作是在exec后仍保持这种描述符打开,利用这一点可以实现I/O重定向。
见链接:https://blog.csdn.net/amoscyk...

文件io

int dup(int fd);
int dup2(int fd1, int fd2);
位于头文件:
这两个函数可用来复制一个现存的文件描述符,dup返回新的描述符值,一定是当前可用最小的,dup2允许你指定新描述符的值,即将fd1复制到fd2处,如果原先fd2处有文件打开,则关闭,如果fd1等于fd2,则不执行关闭。
返回值,成功则返回新的文件描述符,失败则返回-1.

进程间通信

1.管道 pipe

两点限制:pipe是单向的,且只能在具有公共祖先进程的进程之间使用。
example:

#include 
#include 
#include 
#include 
#include 
#include 
#include 

typedef  void Sigfunc(int);
Sigfunc* signaler(int signo, Sigfunc *func) {
  struct sigaction act, oact;
  act.sa_handler = func;
  sigemptyset(&act.sa_mask);
  act.sa_flags = 0;
  if(sigaction(signo, &act, &oact) < 0) {
    return SIG_ERR;
  }
  return oact.sa_handler;
}

void child_handle(int) {
    pid_t pid;
    int stat;
    while((pid = waitpid(-1, &stat, WNOHANG)) > 0) {
        ;
    }
}

int main()
{
    signaler(SIGPIPE, SIG_IGN);
    signaler(SIGCHLD, child_handle);

    int fd[2];
    int result = pipe(fd);
    assert(result == 0);

    pid_t pid = fork();
    assert(pid >= 0);
    if(pid == 0) {
        close(fd[1]);
        if(fd[0] != STDIN_FILENO) {
          int result = dup2(fd[0], STDIN_FILENO);
          assert(result >= 0);
        }
        int n = -1;
        char buf[64];
        while((n = read(STDIN_FILENO, buf, sizeof(buf) - 1)) > 0) {
            buf[n] = '\0';
            std::cout << buf;
        }
    }
    else {
        //parent;
        const char* ptr = "hello world! \n";
        int result = close(fd[0]);

        int n = write(fd[1], ptr, strlen(ptr));
    }
    return 0;
}

2.FIFO

相比于管道pipe,FIFO不是只有共同祖先进程的进程之间才能够使用。常数PIPE_BUF说明了可被原子的写到FIFO的最大数据量。

3.三种IPC

1.没有访问记数,无法及时删除。
2.不按名字为文件系统所知,不能被多路转接函数所使用。
3.标识其结构的内核id不易共享,要么由系统分配然后通过文件传输,要么显示指定,但是可能指定一个之前被分配过的id,要么通过函数ftok生成。
4.具有消息信息+优先权的优点和面向记录的优点。
消息队列
具体使用方法见:https://www.jianshu.com/p/7e3...
信号量
具体使用方法见:
https://blog.csdn.net/xiaojia...
信号量本质上是一个计数器,用于多进程对于共享数据的保护。信号量的理论模型并不算复杂,但是linux中的实现却实在过于繁琐。
共享内存
允许两个或多个进程共享同一块存储区,不具有同步机制,一般配合信号量一起使用。
https://blog.csdn.net/qq_2766...
unix域套接字

struct sockaddr_un {
sa_family_t sun_family; //AF_LOCAL
char sun_path[104];
}

位于头文件:

#include 
#include 
#include 
#include 
#include 
#include 
int main()
{
    sockaddr_un server_addr;
    bzero(&server_addr, sizeof(server_addr));
    server_addr.sun_family = AF_LOCAL;
    strncpy(server_addr.sun_path, "/home/pn/unix222", sizeof(server_addr.sun_path) - 1);

    int fd = socket(AF_LOCAL, SOCK_STREAM, 0);
    assert(fd >= 0);
    socklen_t len = sizeof(server_addr);
    int result = bind(fd, (struct sockaddr*)&server_addr, len);
    assert(result == 0);

    sockaddr_un server2;
    socklen_t llen = sizeof(server2);
    result = getsockname(fd, (struct sockaddr*)&server2, &llen);
    assert(result == 0);
    std::cout << "name : " << server2.sun_path;
    return 0;
}

信号

信号是异步的,对于进程而言信号是随机出现的
进程可以设置3种方式处理发生的信号:(1)忽略此信号,SIGKILL和SIGSTOP不能被忽略。(2)提供信号处理函数,指定在信号发生时调用此函数。(3)执行默认动作,终止进程或者忽略信号。

早期的不可靠信号机制

typedef void Sigfunc(int);
Sigfunc *signal(int, Sigfunc*);
返回值:若成功则返回以前的信号处理配置,若失败返回SIG_ERR。
第一个参数是设置的信号,第二个参数可以是:SIG_IGN,SIG_DFL或者自定义的信号处理函数。
特点1:每次处理信号时,将该信号复置为默认值。如果在信号处理函数中重新调用signal函数设置处理程序,在进入处理函数到调用signal之间如果产生信号,则执行默认动作。另外,如果想要在信号处理函数中通过设置变量,然后在普通程序中根据该变量的值识别是否有该信号产生,这种机制也是有漏洞的。
总而言之,纯异步的机制必须配套以完备合理的同步方式,才能正常工作,显然signal函数没有达到这个要求。

信号的产生会中断某些低速系统调用,如果在进程执行一个低速系统调用而阻塞期间捕捉到一个信号,则该系统调用就被中断不再继续执行。该系统调用返回出错,其errno设置为EINTR。这样处理的理由是:因为一个信号发生了,进程捕捉到了它,这意味着已经发生了某种事情,所以是个好机会应当唤醒阻塞的系统调用。(摘自unix环境高级编程)在网络编程中典型的有connect函数和accept函数。早期的signal函数对于重启系统调用的细节在各个平台上各不相同,总之是很混乱的。

可靠信号机制

我们需要定义一些在讨论信号时用到的术语:

  1. 当造成信号的事件发生时,为进程产生一个信号。在产生了信号时,内核通常在进程表中设置某种形式的标志,当对信号做了这种动作时,我们说向进程递送了一个信号。在信号产生和递送之间的时间间隔内,称信号未决。
  2. 进程可以选择信号递送阻塞,如果为进程产生了一个设置为阻塞的信号,且对该信号的处理方式是默认或者捕捉该信号,则此信号保持未决状态。直到进程对该信号解除阻塞或者设置为忽略该信号。进程在信号递送给它之前可以改变对它的动作,通过调用sigpending函数将指定的信号设置为阻塞。很短时间内发生多次的信号不会被排队,即各个unix内核对信号是不排队的。
  3. 每个进程维护一个信号屏蔽字结构,该结构记录了进程阻塞的信号集合。

kill函数将信号发送给进程或者进程组。
int kill(pid_t pid, int signo);
位于头文件:
若成功返回0,出错返回-1。

POSIX.1 定义了类型sigset_t以包含一个信号集, 并定义了5个处理信号集的函数,两个用来初始化,两个用来set和clear,一个用来check。

sigprocmask函数用来检测或者更改进程的信号屏蔽字。
int sigprocmask(int how, const sigset_t* set, sigset_t* oset);
位于头文件:
首先,oset是非空指针,进程的当前信号屏蔽字通过oset返回。其次,若set是一个非空指针,则参数how指示如何修改当前信号屏蔽字。

sigpending函数用来返回当前由于阻塞而没有递交,保持未决状态的信号集。
int sigpending(sigset_t* set);
若成功返回0,出错返回-1。

sigaction函数代替了之前的signal函数,用来检测或者修改与指定信号相关联的处理动作。
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
成功返回0,出错返回-1。
如果act指针非空,则要修改信号signum的处理动作,如果oldact指针非空,则要返回之前的处理动作。
关于结构sigaction:

struct sigaction {
  void (*sa_handler)();
  sigset_t sa_mask;
  int sa_flags;
}

sa_handler成员要么是处理函数,要么是SIG_IGN或者SIG_DFL,当是处理函数时,sa_mask表示的信号集会在处理函数调用前设置为当前进程的屏蔽信号集,在处理函数返回后再设置回为old屏蔽信号集。这样就可以在处理某些信号时阻塞某些信号,默认的,正在被投递的信号被阻塞。
这种设置方式是长期有效的,除了再用sigaction函数改变它,这与早期的不可靠机制不同。sa_flags字段包含了用来对信号进行处理的各个选项,详见unix环境高级编程10.14节。注意一个选项:SA_RESTART。由此信号中断的系统调用自动再启动。
对信号的介绍到此为止,之后的内容用得到在仔细学吧,信号这一章的内容我看到烦躁,太繁琐了。

多线程时代的信号处理

如果是异常产生的信号(比如程序错误,像SIGPIPE、SIGEGV这些),则只有产生异常的线程收到并处理。
如果是用pthread_kill产生的内部信号,则只有pthread_kill参数中指定的目标线程收到并处理。
如果是外部使用kill命令产生的信号,通常是SIGINT、SIGHUP等job control信号,则会遍历所有线程,直到找到一个不阻塞该信号的线程,然后调用它来处理。(一般从主线程找起),注意只有一个线程能收到。

其次,每个线程都有自己独立的signal mask,但所有线程共享进程的signal action。这意味着,你可以在线程中调用pthread_sigmask(不是sigmask)来决定本线程阻塞哪些信号。但你不能调用sigaction来指定单个线程的信号处理方式。如果在某个线程中调用了sigaction处理某个信号,那么这个进程中的未阻塞这个信号的线程在收到这个信号都会按同一种方式处理这个信号。另外,注意子线程的mask是会从主线程继承而来的。
见链接:https://www.cnblogs.com/codin...
可见,标准对于多线程时代下的信号处理有如下原则:信号来源或者指定明确的,则精确调用对应线程的信号处理函数;信号来源或指定是以进程为目标的,尽量交付于未阻塞该信号的线程让它处理,而且只交付于一个线程。

另:内核提供了signalfd,将信号抽象成一种文件,信号的产生意味着文件可读,这样就可以用io复用来一起处理文件fd和信号fd。详情见:
http://www.man7.org/linux/man...
中文翻译:https://blog.csdn.net/yusiguy...
个人比较喜欢这种处理方法。

你可能感兴趣的:(unix)