当内核启动一个c程序时,在main函数以前会先调用启动例程,由启动例程作一些处理工做而后才调用main函数,该启动例程至少要设置命令行参数和环境变量。
unix进程退出的5种方式:html
exit和_exit函数的区别
exit位于头文件:<stdlib.h>
_exit位于头文件:<unistd.h> 是一个系统调用函数,用于处理unix特定细节。
_exit直接进入内核,而exit函数首先执行终止处理程序并关闭全部标准io流(调用fclose函数刷新缓冲区),而后进入内核(通常会调用_exit函数)。linux
function: void atexit(void (*func)(void));
位于头文件:<stdlib.h>
ANSI C规定一个进程可登记多达32个终止处理程序,由exit函数调用,并以注册相反顺序调用。ios
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);
栈:
命令行参数和环境变量:c++
char getenv(const char name);
位于头文件:<stdlib.h>
若是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)
位于头文件:<unistd.h>
返回值子进程中为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...socket
int dup(int fd);
int dup2(int fd1, int fd2);
位于头文件:<unistd.h>
这两个函数可用来复制一个现存的文件描述符,dup返回新的描述符值,必定是当前可用最小的,dup2容许你指定新描述符的值,即将fd1复制到fd2处,若是原先fd2处有文件打开,则关闭,若是fd1等于fd2,则不执行关闭。
返回值,成功则返回新的文件描述符,失败则返回-1.
两点限制:pipe是单向的,且只能在具备公共祖先进程的进程之间使用。
example:
#include <iostream> #include <unistd.h> #include <cassert> #include <sys/wait.h> #include <sys/signal.h> #include <cstring> #include <cstdio> 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; }
相比于管道pipe,FIFO不是只有共同祖先进程的进程之间才可以使用。常数PIPE_BUF说明了可被原子的写到FIFO的最大数据量。
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]; }
位于头文件:<sys/un.h>
#include <iostream> #include <sys/socket.h> #include <sys/un.h> #include <cstring> #include <cassert> #include <error.h> 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函数对于重启系统调用的细节在各个平台上各不相同,总之是很混乱的。
咱们须要定义一些在讨论信号时用到的术语:
kill函数将信号发送给进程或者进程组。
int kill(pid_t pid, int signo);
位于头文件:<signal.h>
若成功返回0,出错返回-1。
POSIX.1 定义了类型sigset_t以包含一个信号集, 并定义了5个处理信号集的函数,两个用来初始化,两个用来set和clear,一个用来check。
sigprocmask函数用来检测或者更改进程的信号屏蔽字。
int sigprocmask(int how, const sigset_t* set, sigset_t* oset);
位于头文件:<signal.h>
首先,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...我的比较喜欢这种处理方法。